this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

mod/modregistry: metadata support

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>

+134 -5
+25 -5
mod/modregistry/client.go
··· 210 210 211 211 // putCheckedModule is like [Client.PutModule] except that it allows the 212 212 // caller to do some additional checks (see [CheckModule] for more info). 213 - func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule) error { 213 + func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule, meta *Metadata) error { 214 + var annotations map[string]string 215 + if meta != nil { 216 + annotations0, err := meta.annotations() 217 + if err != nil { 218 + return fmt.Errorf("invalid metadata: %v", err) 219 + } 220 + annotations = annotations0 221 + } 214 222 loc, err := c.resolve(m.mv) 215 223 if err != nil { 216 224 return err ··· 241 249 MediaType: moduleFileMediaType, 242 250 Size: int64(len(m.modFileContent)), 243 251 }}, 252 + Annotations: annotations, 244 253 } 245 254 246 255 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[0], io.NewSectionReader(m.blobr, 0, m.size)); err != nil { ··· 262 271 // PutModule puts a module whose contents are held as a zip archive inside f. 263 272 // It assumes all the module dependencies are correctly resolved and present 264 273 // inside the cue.mod/module.cue file. 265 - // 266 - // TODO check deps are resolved correctly? Or is that too domain-specific for this package? 267 - // Is it a problem to call zip.CheckZip twice? 268 274 func (c *Client) PutModule(ctx context.Context, m module.Version, r io.ReaderAt, size int64) error { 275 + return c.PutModuleWithMetadata(ctx, m, r, size, nil) 276 + } 277 + 278 + // PutModuleWithMetadata is like [Client.PutModule] except that it also 279 + // includes the given metadata inside the module's manifest. 280 + // If meta is nil, no metadata will be included, otherwise 281 + // all fields in meta must be valid and non-empty. 282 + func (c *Client) PutModuleWithMetadata(ctx context.Context, m module.Version, r io.ReaderAt, size int64, meta *Metadata) error { 269 283 cm, err := checkModule(m, r, size) 270 284 if err != nil { 271 285 return err 272 286 } 273 - return c.putCheckedModule(ctx, cm) 287 + return c.putCheckedModule(ctx, cm, meta) 274 288 } 275 289 276 290 // checkModule checks a module's zip file before uploading it. ··· 360 374 } 361 375 defer r.Close() 362 376 return io.ReadAll(r) 377 + } 378 + 379 + // Metadata returns the metadata associated with the module. 380 + // If there is none, it returns (nil, nil). 381 + func (m *Module) Metadata() (*Metadata, error) { 382 + return newMetadataFromAnnotations(m.manifest.Annotations) 363 383 } 364 384 365 385 // GetZip returns a reader that can be used to read the contents of the zip
+51
mod/modregistry/client_test.go
··· 127 127 qt.Assert(t, qt.DeepEquals(tags, []string{"v0.5.100"})) 128 128 } 129 129 130 + func TestPutWithMetadata(t *testing.T) { 131 + const testMod = ` 132 + -- cue.mod/module.cue -- 133 + module: "foo.com/bar@v0" 134 + language: version: "v0.8.0" 135 + 136 + -- x.cue -- 137 + package bar 138 + ` 139 + ctx := context.Background() 140 + mv := module.MustParseVersion("foo.com/bar@v0.5.100") 141 + c := newTestClient(t) 142 + zipData := createZip(t, mv, testMod) 143 + meta := &Metadata{ 144 + VCSType: "git", 145 + VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae", 146 + VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC), 147 + } 148 + err := c.PutModuleWithMetadata(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData)), meta) 149 + qt.Assert(t, qt.IsNil(err)) 150 + 151 + m, err := c.GetModule(ctx, mv) 152 + qt.Assert(t, qt.IsNil(err)) 153 + 154 + gotMeta, err := m.Metadata() 155 + qt.Assert(t, qt.IsNil(err)) 156 + qt.Assert(t, qt.DeepEquals(gotMeta, meta)) 157 + } 158 + 159 + func TestPutWithInvalidMetadata(t *testing.T) { 160 + const testMod = ` 161 + -- cue.mod/module.cue -- 162 + module: "foo.com/bar@v0" 163 + language: version: "v0.8.0" 164 + 165 + -- x.cue -- 166 + package bar 167 + ` 168 + ctx := context.Background() 169 + mv := module.MustParseVersion("foo.com/bar@v0.5.100") 170 + c := newTestClient(t) 171 + zipData := createZip(t, mv, testMod) 172 + meta := &Metadata{ 173 + // Missing VCSType field. 174 + VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae", 175 + VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC), 176 + } 177 + err := c.PutModuleWithMetadata(ctx, mv, bytes.NewReader(zipData), int64(len(zipData)), meta) 178 + qt.Assert(t, qt.ErrorMatches(err, `invalid metadata: empty metadata value for field "org.cuelang.vcs-type"`)) 179 + } 180 + 130 181 func TestGetModuleWithManifest(t *testing.T) { 131 182 const testMod = ` 132 183 -- cue.mod/module.cue --
+58
mod/modregistry/metadata.go
··· 1 + package modregistry 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "time" 7 + ) 8 + 9 + // Metadata holds extra information that can be associated with 10 + // a module. It is stored in the module's manifest inside 11 + // the annotations field. All fields must JSON-encode to 12 + // strings. 13 + type Metadata struct { 14 + VCSType string `json:"org.cuelang.vcs-type"` 15 + VCSCommit string `json:"org.cuelang.vcs-commit"` 16 + VCSCommitTime time.Time `json:"org.cuelang.vcs-commit-time"` 17 + } 18 + 19 + func newMetadataFromAnnotations(annotations map[string]string) (*Metadata, error) { 20 + // TODO if this ever turned out to be a bottleneck we could 21 + // improve performance by avoiding the round-trip through JSON. 22 + raw, err := json.Marshal(annotations) 23 + if err != nil { 24 + // Should never happen. 25 + return nil, err 26 + } 27 + var m Metadata 28 + if err := json.Unmarshal(raw, &m); err != nil { 29 + return nil, err 30 + } 31 + return &m, nil 32 + } 33 + 34 + func (m *Metadata) annotations() (map[string]string, error) { 35 + // The "is-empty" checks don't work for time.Time 36 + // so check explicitly. 37 + if m.VCSCommitTime.IsZero() { 38 + return nil, fmt.Errorf("no commit time in metadata") 39 + } 40 + // TODO if this ever turned out to be a bottleneck we could 41 + // improve performance by avoiding the round-trip through JSON. 42 + data, err := json.Marshal(m) 43 + if err != nil { 44 + // Should never happen. 45 + return nil, err 46 + } 47 + var annotations map[string]string 48 + if err := json.Unmarshal(data, &annotations); err != nil { 49 + // Should never happen. 50 + return nil, err 51 + } 52 + for field, val := range annotations { 53 + if val == "" { 54 + return nil, fmt.Errorf("empty metadata value for field %q", field) 55 + } 56 + } 57 + return annotations, nil 58 + }