-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mirror: keep order of oci image index manifests and their annotations…
… consistent during marshaling Signed-off-by: Maxim Vasilenko <[email protected]>
- Loading branch information
Showing
6 changed files
with
219 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package layouts | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"sort" | ||
|
||
v1 "github.com/google/go-containerregistry/pkg/v1" | ||
"github.com/google/go-containerregistry/pkg/v1/layout" | ||
"github.com/google/go-containerregistry/pkg/v1/types" | ||
"golang.org/x/exp/maps" | ||
) | ||
|
||
// ociIndexManifest represents an OCI image index. | ||
type ociIndexManifest struct { | ||
SchemaVersion int64 `json:"schemaVersion"` | ||
MediaType types.MediaType `json:"mediaType,omitempty"` | ||
Manifests []v1.Descriptor `json:"manifests"` | ||
Annotations indexManifestAnnotations `json:"annotations,omitempty"` | ||
Subject *v1.Descriptor `json:"subject,omitempty"` | ||
} | ||
|
||
// indexManifestAnnotations is a map of image annotations that marshals to JSON form while keeping keys ordered alphabetically. | ||
type indexManifestAnnotations map[string]string | ||
|
||
// MarshalJSON marshals go map while keeping keys ordered alphabetically in resulting JSON. | ||
func (a indexManifestAnnotations) MarshalJSON() ([]byte, error) { | ||
names := maps.Keys(a) | ||
sort.Strings(names) | ||
|
||
buf := bytes.Buffer{} | ||
buf.WriteRune('{') | ||
for _, key := range names { | ||
buf.WriteRune('"') | ||
buf.WriteString(key) | ||
buf.Write([]byte(`": "`)) | ||
buf.WriteString(a[key]) | ||
buf.Write([]byte(`",`)) | ||
} | ||
js := buf.Bytes() | ||
js[len(js)-1] = '}' | ||
return js, nil | ||
} | ||
|
||
func SortIndexManifests(l layout.Path) error { | ||
index, err := l.ImageIndex() | ||
if err != nil { | ||
return fmt.Errorf("Read image index: %w", err) | ||
} | ||
|
||
rawManifest, err := index.RawManifest() | ||
if err != nil { | ||
return fmt.Errorf("Read index manifest: %w", err) | ||
} | ||
|
||
indexManifest := &ociIndexManifest{} | ||
if err = json.Unmarshal(rawManifest, indexManifest); err != nil { | ||
return fmt.Errorf("Parse index manifest: %w", err) | ||
} | ||
|
||
sort.Slice(indexManifest.Manifests, func(i, j int) bool { | ||
ref1 := indexManifest.Manifests[i].Annotations["org.opencontainers.image.ref.name"] | ||
ref2 := indexManifest.Manifests[j].Annotations["org.opencontainers.image.ref.name"] | ||
return ref1 < ref2 | ||
}) | ||
|
||
rawManifest, err = json.MarshalIndent(indexManifest, "", " ") | ||
if err != nil { | ||
return fmt.Errorf("Marshal image index manifest: %w", err) | ||
} | ||
if err = l.WriteFile("index.json", rawManifest, os.ModePerm); err != nil { | ||
return fmt.Errorf("Write image index manifest: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package layouts | ||
|
||
import ( | ||
"sort" | ||
"testing" | ||
|
||
v1 "github.com/google/go-containerregistry/pkg/v1" | ||
"github.com/google/go-containerregistry/pkg/v1/layout" | ||
"github.com/google/go-containerregistry/pkg/v1/random" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestSortIndexManifests(t *testing.T) { | ||
const imagesCount = 25 | ||
l := createEmptyOCILayout(t) | ||
for i := 0; i < imagesCount; i++ { | ||
img, err := random.Image(512, 4) | ||
require.NoError(t, err, "Images should be generated without problems") | ||
|
||
digest, err := img.Digest() | ||
require.NoError(t, err, "Digest should be a resolved") | ||
imageRef := "localhost/repo/image:" + digest.Hex | ||
|
||
require.NoError(t, l.AppendImage( | ||
img, | ||
layout.WithPlatform(v1.Platform{Architecture: "amd64", OS: "linux"}), | ||
layout.WithAnnotations(map[string]string{ | ||
"org.opencontainers.image.ref.name": imageRef, | ||
"io.deckhouse.image.short_tag": digest.Hex, | ||
})), "Images should be added to layout") | ||
} | ||
|
||
err := SortIndexManifests(l) | ||
require.NoError(t, err, "Should be able to sort index manifests without failures") | ||
index, err := l.ImageIndex() | ||
require.NoError(t, err, "Should be able to read index") | ||
indexManifest, err := index.IndexManifest() | ||
require.NoError(t, err, "Should be able to parse index manifest") | ||
require.Len(t, indexManifest.Manifests, imagesCount, "Number of images should not be changed after sorting") | ||
require.True(t, sort.SliceIsSorted(indexManifest.Manifests, func(i, j int) bool { | ||
ref1 := indexManifest.Manifests[i].Annotations["org.opencontainers.image.ref.name"] | ||
ref2 := indexManifest.Manifests[j].Annotations["org.opencontainers.image.ref.name"] | ||
return ref1 < ref2 | ||
}), "Index manifests should be sorted by image references") | ||
} | ||
|
||
func Test_indexManifestAnnotations_MarshalJSON(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
a indexManifestAnnotations | ||
want []byte | ||
}{ | ||
{ | ||
name: "one key", | ||
a: indexManifestAnnotations{ | ||
"org.opencontainers.image.ref.name": "registry.com/foo:bar", | ||
}, | ||
want: []byte(`{"org.opencontainers.image.ref.name": "registry.com/foo:bar"}`), | ||
}, | ||
{ | ||
name: "multiple keys", | ||
a: indexManifestAnnotations{ | ||
"org.opencontainers.image.ref.name": "registry.com/foo:bar", | ||
"short_tag": "bar", | ||
}, | ||
want: []byte(`{"org.opencontainers.image.ref.name": "registry.com/foo:bar","short_tag": "bar"}`), | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := tt.a.MarshalJSON() | ||
require.NoError(t, err) | ||
|
||
// JSONEq validates that JSON has valid structure, Equal validates order of fields in JSON. | ||
require.JSONEq(t, string(tt.want), string(got)) | ||
require.Equal(t, tt.want, got) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters