Skip to content

Commit

Permalink
mod/modregistry: metadata support
Browse files Browse the repository at this point in the history
This adds support for including VCS metadata
in the module manifest when publishing a module.

For #3034.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: Ia4066eff076988a4f1f4666238fd068443687233
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1193705
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
  • Loading branch information
rogpeppe committed Apr 24, 2024
1 parent f2980b7 commit cf2551f
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 5 deletions.
30 changes: 25 additions & 5 deletions mod/modregistry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,15 @@ type checkedModule struct {

// putCheckedModule is like [Client.PutModule] except that it allows the
// caller to do some additional checks (see [CheckModule] for more info).
func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule) error {
func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule, meta *Metadata) error {
var annotations map[string]string
if meta != nil {
annotations0, err := meta.annotations()
if err != nil {
return fmt.Errorf("invalid metadata: %v", err)
}
annotations = annotations0
}
loc, err := c.resolve(m.mv)
if err != nil {
return err
Expand Down Expand Up @@ -241,6 +249,7 @@ func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule) error {
MediaType: moduleFileMediaType,
Size: int64(len(m.modFileContent)),
}},
Annotations: annotations,
}

if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[0], io.NewSectionReader(m.blobr, 0, m.size)); err != nil {
Expand All @@ -262,15 +271,20 @@ func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule) error {
// PutModule puts a module whose contents are held as a zip archive inside f.
// It assumes all the module dependencies are correctly resolved and present
// inside the cue.mod/module.cue file.
//
// TODO check deps are resolved correctly? Or is that too domain-specific for this package?
// Is it a problem to call zip.CheckZip twice?
func (c *Client) PutModule(ctx context.Context, m module.Version, r io.ReaderAt, size int64) error {
return c.PutModuleWithMetadata(ctx, m, r, size, nil)
}

// PutModuleWithMetadata is like [Client.PutModule] except that it also
// includes the given metadata inside the module's manifest.
// If meta is nil, no metadata will be included, otherwise
// all fields in meta must be valid and non-empty.
func (c *Client) PutModuleWithMetadata(ctx context.Context, m module.Version, r io.ReaderAt, size int64, meta *Metadata) error {
cm, err := checkModule(m, r, size)
if err != nil {
return err
}
return c.putCheckedModule(ctx, cm)
return c.putCheckedModule(ctx, cm, meta)
}

// checkModule checks a module's zip file before uploading it.
Expand Down Expand Up @@ -362,6 +376,12 @@ func (m *Module) ModuleFile(ctx context.Context) ([]byte, error) {
return io.ReadAll(r)
}

// Metadata returns the metadata associated with the module.
// If there is none, it returns (nil, nil).
func (m *Module) Metadata() (*Metadata, error) {
return newMetadataFromAnnotations(m.manifest.Annotations)
}

// GetZip returns a reader that can be used to read the contents of the zip
// archive containing the module files. The reader should be closed after use,
// and the contents should not be assumed to be correct until the close
Expand Down
51 changes: 51 additions & 0 deletions mod/modregistry/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,57 @@ x: a.foo + something.bar
qt.Assert(t, qt.DeepEquals(tags, []string{"v0.5.100"}))
}

func TestPutWithMetadata(t *testing.T) {
const testMod = `
-- cue.mod/module.cue --
module: "foo.com/bar@v0"
language: version: "v0.8.0"
-- x.cue --
package bar
`
ctx := context.Background()
mv := module.MustParseVersion("foo.com/bar@v0.5.100")
c := newTestClient(t)
zipData := createZip(t, mv, testMod)
meta := &Metadata{
VCSType: "git",
VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae",
VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC),
}
err := c.PutModuleWithMetadata(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData)), meta)
qt.Assert(t, qt.IsNil(err))

m, err := c.GetModule(ctx, mv)
qt.Assert(t, qt.IsNil(err))

gotMeta, err := m.Metadata()
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.DeepEquals(gotMeta, meta))
}

func TestPutWithInvalidMetadata(t *testing.T) {
const testMod = `
-- cue.mod/module.cue --
module: "foo.com/bar@v0"
language: version: "v0.8.0"
-- x.cue --
package bar
`
ctx := context.Background()
mv := module.MustParseVersion("foo.com/bar@v0.5.100")
c := newTestClient(t)
zipData := createZip(t, mv, testMod)
meta := &Metadata{
// Missing VCSType field.
VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae",
VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC),
}
err := c.PutModuleWithMetadata(ctx, mv, bytes.NewReader(zipData), int64(len(zipData)), meta)
qt.Assert(t, qt.ErrorMatches(err, `invalid metadata: empty metadata value for field "org.cuelang.vcs-type"`))
}

func TestGetModuleWithManifest(t *testing.T) {
const testMod = `
-- cue.mod/module.cue --
Expand Down
58 changes: 58 additions & 0 deletions mod/modregistry/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package modregistry

import (
"encoding/json"
"fmt"
"time"
)

// Metadata holds extra information that can be associated with
// a module. It is stored in the module's manifest inside
// the annotations field. All fields must JSON-encode to
// strings.
type Metadata struct {
VCSType string `json:"org.cuelang.vcs-type"`
VCSCommit string `json:"org.cuelang.vcs-commit"`
VCSCommitTime time.Time `json:"org.cuelang.vcs-commit-time"`
}

func newMetadataFromAnnotations(annotations map[string]string) (*Metadata, error) {
// TODO if this ever turned out to be a bottleneck we could
// improve performance by avoiding the round-trip through JSON.
raw, err := json.Marshal(annotations)
if err != nil {
// Should never happen.
return nil, err
}
var m Metadata
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
return &m, nil
}

func (m *Metadata) annotations() (map[string]string, error) {
// The "is-empty" checks don't work for time.Time
// so check explicitly.
if m.VCSCommitTime.IsZero() {
return nil, fmt.Errorf("no commit time in metadata")
}
// TODO if this ever turned out to be a bottleneck we could
// improve performance by avoiding the round-trip through JSON.
data, err := json.Marshal(m)
if err != nil {
// Should never happen.
return nil, err
}
var annotations map[string]string
if err := json.Unmarshal(data, &annotations); err != nil {
// Should never happen.
return nil, err
}
for field, val := range annotations {
if val == "" {
return nil, fmt.Errorf("empty metadata value for field %q", field)
}
}
return annotations, nil
}

0 comments on commit cf2551f

Please sign in to comment.