Skip to content

Commit 10245f0

Browse files
Add quotas test with bytes enabled
1 parent 67c5aea commit 10245f0

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

internal/api/keppel/quotas_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,219 @@ func TestQuotasAPI(t *testing.T) {
263263
).ExpectText(t, http.StatusUnprocessableEntity, "requested manifest quota (5) is below usage (10)\n")
264264
s.Auditor.ExpectEvents(t /*, nothing */)
265265
}
266+
267+
func TestQuotasAPIWithBytes(t *testing.T) {
268+
// NOTE: This tests both the Keppel-native quota API and the LIQUID API which accesses the same logic.
269+
s := test.NewSetup(t,
270+
test.WithKeppelAPI,
271+
test.WithBytesQuotas,
272+
test.WithAccount(models.Account{Name: "test1", AuthTenantID: "tenant1"}),
273+
)
274+
ctx := t.Context()
275+
276+
var infoVersion int64
277+
278+
s.RespondTo(ctx, "GET /liquid/v1/info",
279+
withPerms("viewquota:tenant1"),
280+
).ExpectJSON(t, http.StatusOK, jsonmatch.Object{
281+
"capacityMetricFamilies": nil,
282+
"categories": nil,
283+
"displayName": "Container Image Registry",
284+
"rates": nil,
285+
"resources": jsonmatch.Object{
286+
"capacity": jsonmatch.Object{
287+
"displayName": "Capacity",
288+
"hasCapacity": false,
289+
"hasQuota": true,
290+
"needsResourceDemand": false,
291+
"topology": "flat",
292+
"unit": "B",
293+
},
294+
"images": jsonmatch.Object{
295+
"displayName": "Images",
296+
"hasCapacity": false,
297+
"hasQuota": true,
298+
"needsResourceDemand": false,
299+
"topology": "flat",
300+
},
301+
},
302+
"usageMetricFamilies": nil,
303+
"version": jsonmatch.CaptureField(&infoVersion),
304+
})
305+
306+
// GET on auth tenant without more specific configuration shows default values
307+
s.RespondTo(ctx, "GET /keppel/v1/quotas/tenant1", withPerms("viewquota:tenant1")).
308+
ExpectJSON(t, http.StatusOK, jsonmatch.Object{
309+
"bytes": jsonmatch.Object{"quota": 0, "usage": 0},
310+
"manifests": jsonmatch.Object{"quota": 0, "usage": 0},
311+
})
312+
buildLiquidResponse := func(capacityQuota, capacityUsage, imagesQuota, imagesUsage uint64) jsonmatch.Object {
313+
return jsonmatch.Object{
314+
"infoVersion": infoVersion,
315+
"resources": jsonmatch.Object{
316+
"capacity": jsonmatch.Object{
317+
"forbidden": false,
318+
"quota": capacityQuota,
319+
"perAZ": jsonmatch.Object{
320+
"any": jsonmatch.Object{
321+
"usage": capacityUsage,
322+
},
323+
},
324+
},
325+
"images": jsonmatch.Object{
326+
"forbidden": false,
327+
"quota": imagesQuota,
328+
"perAZ": jsonmatch.Object{
329+
"any": jsonmatch.Object{
330+
"usage": imagesUsage,
331+
},
332+
},
333+
},
334+
},
335+
}
336+
}
337+
338+
s.RespondTo(ctx, "POST /liquid/v1/projects/tenant1/report-usage",
339+
withPerms("viewquota:tenant1"),
340+
httptest.WithJSONBody(map[string]any{"allAZs": []string{"dummy"}}),
341+
).ExpectJSON(t, http.StatusOK, buildLiquidResponse(0, 0, 0, 0))
342+
343+
// PUT happy case with native API
344+
for _, pass := range []int{1, 2, 3} {
345+
s.RespondTo(ctx, "PUT /keppel/v1/quotas/tenant1",
346+
withPerms("changequota:tenant1"),
347+
httptest.WithJSONBody(map[string]any{
348+
"bytes": map[string]any{"quota": 5_000_000},
349+
"manifests": map[string]any{"quota": 50},
350+
}),
351+
).ExpectJSON(t, http.StatusOK, jsonmatch.Object{
352+
"bytes": jsonmatch.Object{"quota": 5_000_000, "usage": 0},
353+
"manifests": jsonmatch.Object{"quota": 50, "usage": 0},
354+
})
355+
356+
// only the first pass should generate an audit event
357+
if pass == 1 {
358+
s.Auditor.ExpectEvents(t, cadf.Event{
359+
RequestPath: "/keppel/v1/quotas/tenant1",
360+
Action: cadf.UpdateAction,
361+
Outcome: "success",
362+
Reason: test.CADFReasonOK,
363+
Target: cadf.Resource{
364+
TypeURI: "docker-registry/project-quota",
365+
ID: "tenant1",
366+
ProjectID: "tenant1",
367+
Attachments: []cadf.Attachment{
368+
{
369+
Name: "payload-before",
370+
TypeURI: "mime:application/json",
371+
Content: `{"manifests":0}`,
372+
},
373+
{
374+
Name: "payload",
375+
TypeURI: "mime:application/json",
376+
Content: `{"bytes":5000000,"manifests":50}`,
377+
},
378+
},
379+
},
380+
})
381+
} else {
382+
s.Auditor.ExpectEvents(t /*, nothing */)
383+
}
384+
}
385+
386+
// PUT happy case with LIQUID API
387+
for _, pass := range []int{1, 2, 3} {
388+
s.RespondTo(ctx, "PUT /liquid/v1/projects/tenant1/quota",
389+
withPerms("changequota:tenant1"),
390+
httptest.WithJSONBody(map[string]any{
391+
"resources": map[string]any{
392+
"capacity": map[string]any{"quota": 50_000_000},
393+
"images": map[string]any{"quota": 100},
394+
},
395+
}),
396+
).ExpectStatus(t, http.StatusNoContent)
397+
398+
// only the first pass should generate an audit event
399+
if pass == 1 {
400+
s.Auditor.ExpectEvents(t, cadf.Event{
401+
RequestPath: "/liquid/v1/projects/tenant1/quota",
402+
Action: cadf.UpdateAction,
403+
Outcome: "success",
404+
Reason: test.CADFReasonOK,
405+
Target: cadf.Resource{
406+
TypeURI: "docker-registry/project-quota",
407+
ID: "tenant1",
408+
ProjectID: "tenant1",
409+
Attachments: []cadf.Attachment{
410+
{
411+
Name: "payload-before",
412+
TypeURI: "mime:application/json",
413+
Content: `{"bytes":5000000,"manifests":50}`,
414+
},
415+
{
416+
Name: "payload",
417+
TypeURI: "mime:application/json",
418+
Content: `{"bytes":50000000,"manifests":100}`,
419+
},
420+
},
421+
},
422+
})
423+
} else {
424+
s.Auditor.ExpectEvents(t /*, nothing */)
425+
}
426+
}
427+
428+
// reflects changes
429+
s.RespondTo(ctx, "GET /keppel/v1/quotas/tenant1", withPerms("viewquota:tenant1")).
430+
ExpectJSON(t, http.StatusOK, jsonmatch.Object{
431+
"bytes": jsonmatch.Object{"quota": 50_000_000, "usage": 0},
432+
"manifests": jsonmatch.Object{"quota": 100, "usage": 0},
433+
})
434+
s.RespondTo(ctx, "POST /liquid/v1/projects/tenant1/report-usage",
435+
withPerms("viewquota:tenant1"),
436+
httptest.WithJSONBody(map[string]any{"allAZs": []string{"dummy"}}),
437+
).ExpectJSON(t, http.StatusOK, buildLiquidResponse(50_000_000, 0, 100, 0))
438+
439+
// put some blobs in the DB, check that GET reflects higher usage
440+
repo := models.Repository{
441+
AccountName: "test1",
442+
Name: "foo",
443+
}
444+
for idx := 1; idx <= 10; idx++ {
445+
image := test.GenerateImage(
446+
test.GenerateExampleLayer(int64(idx)),
447+
test.GenerateExampleLayer(int64(idx+1)),
448+
)
449+
image.MustUpload(t, s, repo, "latest")
450+
}
451+
s.Auditor.IgnoreEventsUntilNow()
452+
453+
s.RespondTo(ctx, "GET /keppel/v1/quotas/tenant1", withPerms("viewquota:tenant1")).
454+
ExpectJSON(t, http.StatusOK, jsonmatch.Object{
455+
"bytes": jsonmatch.Object{"quota": 50_000_000, "usage": 11558149},
456+
"manifests": jsonmatch.Object{"quota": 100, "usage": 10},
457+
})
458+
s.RespondTo(ctx, "POST /liquid/v1/projects/tenant1/report-usage",
459+
withPerms("viewquota:tenant1"),
460+
httptest.WithJSONBody(map[string]any{"allAZs": []string{"dummy"}}),
461+
).ExpectJSON(t, http.StatusOK, buildLiquidResponse(50000000, 11558149, 100, 10))
462+
463+
// PUT error cases
464+
s.RespondTo(ctx, "PUT /keppel/v1/quotas/tenant1",
465+
withPerms("viewquota:tenant1"),
466+
httptest.WithJSONBody(map[string]any{
467+
"bytes": map[string]any{"bytes": 50_000_000, "quota": 5_000_000},
468+
"manifests": map[string]any{"bytes": 10000, "quota": 100},
469+
}),
470+
).ExpectStatus(t, http.StatusForbidden)
471+
s.Auditor.ExpectEvents(t /*, nothing */)
472+
473+
s.RespondTo(ctx, "PUT /keppel/v1/quotas/tenant1",
474+
withPerms("changequota:tenant1"),
475+
httptest.WithJSONBody(map[string]any{
476+
"bytes": map[string]any{"quota": 5_000_000},
477+
"manifests": map[string]any{"quota": 5},
478+
}),
479+
).ExpectText(t, http.StatusUnprocessableEntity, "requested manifest quota (5) is below usage (10)\n")
480+
s.Auditor.ExpectEvents(t /*, nothing */)
481+
}

0 commit comments

Comments
 (0)