Skip to content

Commit 122313b

Browse files
committed
chain: 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. Tests are formulated based on pre-calculation of chain identifiers to ensure correctness. Signed-off-by: Stephen J Day <[email protected]>
1 parent 656fb2f commit 122313b

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

specs-go/chain/chainid.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package chain
2+
3+
import "github.com/docker/distribution/digest"
4+
5+
// ChainID takes a slice of digests and returns the ChainID corresponding to
6+
// the last entry. Typically, these are a list of layer DiffIDs, with the
7+
// result providing the ChainID identifying the result of sequential
8+
// application of the preceeding layers.
9+
func ChainID(dgsts []digest.Digest) digest.Digest {
10+
chainIDs := make([]digest.Digest, len(dgsts))
11+
copy(chainIDs, dgsts)
12+
ChainIDs(chainIDs)
13+
14+
if len(chainIDs) == 0 {
15+
return ""
16+
}
17+
return chainIDs[len(chainIDs)-1]
18+
}
19+
20+
// ChainIDs calculates the recursively applied chain id for each identifier in
21+
// the slice. The result is written direcly back into the slice such that the
22+
// ChainID for each item will be in the respective position.
23+
//
24+
// By definition of ChainID, the zeroth element will always be the same before
25+
// and after the call.
26+
//
27+
// As an exmaple, given the chain of ids `[A, B, C]`, the result `[A,
28+
// ChainID(A|B), ChainID(A|B|C)]` will be written back to the slice.
29+
//
30+
// The input is provided as a return value for convenience.
31+
//
32+
// Typically, these are a list of layer DiffIDs, with the
33+
// result providing the ChainID for each the result of each layer application
34+
// sequentially.
35+
func ChainIDs(dgsts []digest.Digest) []digest.Digest {
36+
if len(dgsts) < 2 {
37+
return dgsts
38+
}
39+
40+
parent := digest.FromBytes([]byte(dgsts[0] + " " + dgsts[1]))
41+
next := dgsts[1:]
42+
next[0] = parent
43+
ChainIDs(next)
44+
45+
return dgsts
46+
}

specs-go/chain/chainid_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package chain
2+
3+
import (
4+
_ "crypto/sha256" // required to install sha256 digest support
5+
"reflect"
6+
"testing"
7+
8+
"github.com/docker/distribution/digest"
9+
)
10+
11+
func TestChainID(t *testing.T) {
12+
// To provide a good testing base, we define the individual links in a
13+
// chain recursively, illustrating the calculations for each chain.
14+
//
15+
// Note that we use invalid digests for the unmodified identifiers here to
16+
// make the computation more readable.
17+
chainDigestAB := digest.FromString("sha256:a" + " " + "sha256:b") // chain for A|B
18+
chainDigestABC := digest.FromString(chainDigestAB.String() + " " + "sha256:c") // chain for A|B|C
19+
20+
for _, testcase := range []struct {
21+
Name string
22+
Digests []digest.Digest
23+
Expected []digest.Digest
24+
}{
25+
{
26+
Name: "nil",
27+
},
28+
{
29+
Name: "empty",
30+
Digests: []digest.Digest{},
31+
Expected: []digest.Digest{},
32+
},
33+
{
34+
Name: "identity",
35+
Digests: []digest.Digest{"sha256:a"},
36+
Expected: []digest.Digest{"sha256:a"},
37+
},
38+
{
39+
Name: "two",
40+
Digests: []digest.Digest{"sha256:a", "sha256:b"},
41+
Expected: []digest.Digest{"sha256:a", chainDigestAB},
42+
},
43+
{
44+
Name: "three",
45+
Digests: []digest.Digest{"sha256:a", "sha256:b", "sha256:c"},
46+
Expected: []digest.Digest{"sha256:a", chainDigestAB, chainDigestABC},
47+
},
48+
} {
49+
t.Run(testcase.Name, func(t *testing.T) {
50+
t.Log("before", testcase.Digests)
51+
52+
var ids []digest.Digest
53+
54+
if testcase.Digests != nil {
55+
ids = make([]digest.Digest, len(testcase.Digests))
56+
copy(ids, testcase.Digests)
57+
}
58+
59+
ids = ChainIDs(ids)
60+
t.Log("after", ids)
61+
if !reflect.DeepEqual(ids, testcase.Expected) {
62+
t.Errorf("unexpected chain: %v != %v", ids, testcase.Expected)
63+
}
64+
65+
if len(testcase.Digests) == 0 {
66+
return
67+
}
68+
69+
// Make sure parent stays stable
70+
if ids[0] != testcase.Digests[0] {
71+
t.Errorf("parent changed: %v != %v", ids[0], testcase.Digests[0])
72+
}
73+
74+
// make sure that the ChainID function takes the last element
75+
id := ChainID(testcase.Digests)
76+
if id != ids[len(ids)-1] {
77+
t.Errorf("incorrect chain id returned from ChainID: %v != %v", id, ids[len(ids)-1])
78+
}
79+
})
80+
}
81+
}

0 commit comments

Comments
 (0)