Skip to content

Commit acb0ba9

Browse files
authored
Merge branch 'main' into codex/guestmemory-reclaim
2 parents 17a7064 + b7a4031 commit acb0ba9

46 files changed

Lines changed: 1425 additions & 363 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,19 @@ jobs:
3838
sudo apt-get install -y erofs-utils e2fsprogs iptables
3939
fi
4040
go mod download
41-
41+
42+
- name: Verify Linux test toolchain
43+
run: |
44+
set -euo pipefail
45+
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
46+
for bin in mkfs.erofs mkfs.ext4 iptables; do
47+
if ! sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin' >/dev/null"; then
48+
echo "missing required binary under sudo PATH: $bin"
49+
exit 1
50+
fi
51+
sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin'"
52+
done
53+
4254
# Avoids rate limits when running the tests
4355
# Tests includes pulling, then converting to disk images
4456
- name: Login to Docker Hub

cmd/api/api/builds.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/kernel/hypeman/lib/images"
1414
"github.com/kernel/hypeman/lib/logger"
1515
"github.com/kernel/hypeman/lib/oapi"
16+
"github.com/kernel/hypeman/lib/tags"
1617
)
1718

1819
// ListBuilds returns all builds
@@ -28,9 +29,12 @@ func (s *ApiService) ListBuilds(ctx context.Context, request oapi.ListBuildsRequ
2829
}, nil
2930
}
3031

31-
oapiBuilds := make([]oapi.Build, len(domainBuilds))
32-
for i, b := range domainBuilds {
33-
oapiBuilds[i] = buildToOAPI(b)
32+
oapiBuilds := make([]oapi.Build, 0, len(domainBuilds))
33+
for _, b := range domainBuilds {
34+
if b == nil || !matchesMetadataFilter(b.Metadata, request.Params.Metadata) {
35+
continue
36+
}
37+
oapiBuilds = append(oapiBuilds, buildToOAPI(b))
3438
}
3539

3640
return oapi.ListBuilds200JSONResponse(oapiBuilds), nil
@@ -46,6 +50,7 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe
4650
var timeoutSeconds, memoryMB, cpus int
4751
var isAdminBuild bool
4852
var secrets []builds.SecretRef
53+
var metadata map[string]string
4954

5055
for {
5156
part, err := request.Body.NextPart()
@@ -169,6 +174,22 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe
169174
}, nil
170175
}
171176
imageName = string(data)
177+
case "metadata":
178+
data, err := io.ReadAll(part)
179+
if err != nil {
180+
return oapi.CreateBuild400JSONResponse{
181+
Code: "invalid_request",
182+
Message: "failed to read metadata field",
183+
}, nil
184+
}
185+
parsed, err := parseMetadataJSON(string(data))
186+
if err != nil {
187+
return oapi.CreateBuild400JSONResponse{
188+
Code: "invalid_request",
189+
Message: "metadata must be a JSON object with string key-value pairs",
190+
}, nil
191+
}
192+
metadata = parsed
172193
}
173194
part.Close()
174195
}
@@ -203,6 +224,7 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe
203224
IsAdminBuild: isAdminBuild,
204225
GlobalCacheKey: globalCacheKey,
205226
ImageName: imageName,
227+
Metadata: metadata,
206228
}
207229

208230
// Apply build policy if any field was provided
@@ -223,6 +245,11 @@ func (s *ApiService) CreateBuild(ctx context.Context, request oapi.CreateBuildRe
223245
build, err := s.BuildManager.CreateBuild(ctx, domainReq, sourceData)
224246
if err != nil {
225247
switch {
248+
case errors.Is(err, tags.ErrInvalidMetadata):
249+
return oapi.CreateBuild400JSONResponse{
250+
Code: "invalid_request",
251+
Message: err.Error(),
252+
}, nil
226253
case errors.Is(err, builds.ErrDockerfileRequired):
227254
return oapi.CreateBuild400JSONResponse{
228255
Code: "dockerfile_required",
@@ -359,6 +386,7 @@ func buildToOAPI(b *builds.Build) oapi.Build {
359386
oapiBuild := oapi.Build{
360387
Id: b.ID,
361388
Status: oapi.BuildStatus(b.Status),
389+
Metadata: toOAPIMetadata(b.Metadata),
362390
QueuePosition: b.QueuePosition,
363391
ImageDigest: b.ImageDigest,
364392
ImageRef: b.ImageRef,

cmd/api/api/devices.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/kernel/hypeman/lib/devices"
88
"github.com/kernel/hypeman/lib/oapi"
9+
"github.com/kernel/hypeman/lib/tags"
910
)
1011

1112
// ListDevices returns all registered devices
@@ -18,9 +19,12 @@ func (s *ApiService) ListDevices(ctx context.Context, request oapi.ListDevicesRe
1819
}, nil
1920
}
2021

21-
result := make([]oapi.Device, len(deviceList))
22-
for i, d := range deviceList {
23-
result[i] = deviceToOAPI(d)
22+
result := make([]oapi.Device, 0, len(deviceList))
23+
for _, d := range deviceList {
24+
if !matchesMetadataFilter(d.Metadata, request.Params.Metadata) {
25+
continue
26+
}
27+
result = append(result, deviceToOAPI(d))
2428
}
2529

2630
return oapi.ListDevices200JSONResponse(result), nil
@@ -53,11 +57,17 @@ func (s *ApiService) CreateDevice(ctx context.Context, request oapi.CreateDevice
5357
req := devices.CreateDeviceRequest{
5458
Name: name,
5559
PCIAddress: request.Body.PciAddress,
60+
Metadata: toMapMetadata(request.Body.Metadata),
5661
}
5762

5863
device, err := s.DeviceManager.CreateDevice(ctx, req)
5964
if err != nil {
6065
switch {
66+
case errors.Is(err, tags.ErrInvalidMetadata):
67+
return oapi.CreateDevice400JSONResponse{
68+
Code: "invalid_request",
69+
Message: err.Error(),
70+
}, nil
6171
case errors.Is(err, devices.ErrInvalidName):
6272
return oapi.CreateDevice400JSONResponse{
6373
Code: "invalid_name",
@@ -142,6 +152,7 @@ func deviceToOAPI(d devices.Device) oapi.Device {
142152
Id: d.Id,
143153
Name: &d.Name,
144154
Type: deviceType,
155+
Metadata: toOAPIMetadata(d.Metadata),
145156
PciAddress: d.PCIAddress,
146157
VendorId: d.VendorID,
147158
DeviceId: d.DeviceID,

cmd/api/api/images.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/kernel/hypeman/lib/logger"
99
mw "github.com/kernel/hypeman/lib/middleware"
1010
"github.com/kernel/hypeman/lib/oapi"
11+
"github.com/kernel/hypeman/lib/tags"
1112
)
1213

1314
func (s *ApiService) ListImages(ctx context.Context, request oapi.ListImagesRequestObject) (oapi.ListImagesResponseObject, error) {
@@ -22,24 +23,32 @@ func (s *ApiService) ListImages(ctx context.Context, request oapi.ListImagesRequ
2223
}, nil
2324
}
2425

25-
oapiImages := make([]oapi.Image, len(domainImages))
26-
for i, img := range domainImages {
27-
oapiImages[i] = imageToOAPI(img)
26+
oapiImages := make([]oapi.Image, 0, len(domainImages))
27+
for _, img := range domainImages {
28+
if !matchesMetadataFilter(img.Metadata, request.Params.Metadata) {
29+
continue
30+
}
31+
oapiImages = append(oapiImages, imageToOAPI(img))
2832
}
29-
3033
return oapi.ListImages200JSONResponse(oapiImages), nil
3134
}
3235

3336
func (s *ApiService) CreateImage(ctx context.Context, request oapi.CreateImageRequestObject) (oapi.CreateImageResponseObject, error) {
3437
log := logger.FromContext(ctx)
3538

3639
domainReq := images.CreateImageRequest{
37-
Name: request.Body.Name,
40+
Name: request.Body.Name,
41+
Metadata: toMapMetadata(request.Body.Metadata),
3842
}
3943

4044
img, err := s.ImageManager.CreateImage(ctx, domainReq)
4145
if err != nil {
4246
switch {
47+
case errors.Is(err, tags.ErrInvalidMetadata):
48+
return oapi.CreateImage400JSONResponse{
49+
Code: "invalid_request",
50+
Message: err.Error(),
51+
}, nil
4352
case errors.Is(err, images.ErrInvalidName):
4453
return oapi.CreateImage400JSONResponse{
4554
Code: "invalid_name",
@@ -117,6 +126,9 @@ func imageToOAPI(img images.Image) oapi.Image {
117126
if len(img.Env) > 0 {
118127
oapiImg.Env = &img.Env
119128
}
129+
if len(img.Metadata) > 0 {
130+
oapiImg.Metadata = toOAPIMetadata(img.Metadata)
131+
}
120132
if img.WorkingDir != "" {
121133
oapiImg.WorkingDir = &img.WorkingDir
122134
}

cmd/api/api/ingress.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ func (s *ApiService) ListIngresses(ctx context.Context, request oapi.ListIngress
2323
}, nil
2424
}
2525

26-
oapiIngresses := make([]oapi.Ingress, len(ingresses))
27-
for i, ing := range ingresses {
28-
oapiIngresses[i] = ingressToOAPI(ing)
26+
oapiIngresses := make([]oapi.Ingress, 0, len(ingresses))
27+
for _, ing := range ingresses {
28+
if !matchesMetadataFilter(ing.Metadata, request.Params.Metadata) {
29+
continue
30+
}
31+
oapiIngresses = append(oapiIngresses, ingressToOAPI(ing))
2932
}
3033

3134
return oapi.ListIngresses200JSONResponse(oapiIngresses), nil
@@ -37,8 +40,9 @@ func (s *ApiService) CreateIngress(ctx context.Context, request oapi.CreateIngre
3740

3841
// Convert OAPI request to domain request
3942
domainReq := ingress.CreateIngressRequest{
40-
Name: request.Body.Name,
41-
Rules: make([]ingress.IngressRule, len(request.Body.Rules)),
43+
Name: request.Body.Name,
44+
Metadata: toMapMetadata(request.Body.Metadata),
45+
Rules: make([]ingress.IngressRule, len(request.Body.Rules)),
4246
}
4347

4448
for i, rule := range request.Body.Rules {
@@ -180,6 +184,7 @@ func ingressToOAPI(ing ingress.Ingress) oapi.Ingress {
180184
return oapi.Ingress{
181185
Id: ing.ID,
182186
Name: ing.Name,
187+
Metadata: toOAPIMetadata(ing.Metadata),
183188
Rules: rules,
184189
CreatedAt: ing.CreatedAt,
185190
}

cmd/api/api/instances.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (s *ApiService) ListInstances(ctx context.Context, request oapi.ListInstanc
3434
filter.State = &state
3535
}
3636
if request.Params.Metadata != nil {
37-
filter.Metadata = *request.Params.Metadata
37+
filter.Metadata = toMapMetadata(request.Params.Metadata)
3838
}
3939
}
4040

@@ -127,7 +127,7 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
127127

128128
metadata := make(map[string]string)
129129
if request.Body.Metadata != nil {
130-
metadata = *request.Body.Metadata
130+
metadata = toMapMetadata(request.Body.Metadata)
131131
}
132132

133133
// Parse network enabled (default: true)
@@ -288,6 +288,11 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
288288
Code: "insufficient_resources",
289289
Message: err.Error(),
290290
}, nil
291+
case errors.Is(err, instances.ErrInvalidRequest):
292+
return oapi.CreateInstance400JSONResponse{
293+
Code: "invalid_request",
294+
Message: err.Error(),
295+
}, nil
291296
default:
292297
log.ErrorContext(ctx, "failed to create instance", "error", err, "image", request.Body.Image)
293298
return oapi.CreateInstance500JSONResponse{
@@ -791,23 +796,21 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
791796
uploadBwStr = &s
792797
}
793798

794-
// Build network object with ip/mac and bandwidth nested inside
795-
netObj := &struct {
796-
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
797-
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
798-
Enabled *bool `json:"enabled,omitempty"`
799-
Ip *string `json:"ip"`
800-
Mac *string `json:"mac"`
801-
Name *string `json:"name,omitempty"`
802-
}{
803-
Enabled: lo.ToPtr(inst.NetworkEnabled),
804-
BandwidthDownload: downloadBwStr,
805-
BandwidthUpload: uploadBwStr,
799+
// Build network payload as JSON to avoid compile-time coupling to
800+
// generated anonymous struct tags in oapi.Instance.Network.
801+
networkPayload := map[string]any{
802+
"enabled": inst.NetworkEnabled,
803+
}
804+
if downloadBwStr != nil {
805+
networkPayload["bandwidth_download"] = *downloadBwStr
806+
}
807+
if uploadBwStr != nil {
808+
networkPayload["bandwidth_upload"] = *uploadBwStr
806809
}
807810
if inst.NetworkEnabled {
808-
netObj.Name = lo.ToPtr("default")
809-
netObj.Ip = lo.ToPtr(inst.IP)
810-
netObj.Mac = lo.ToPtr(inst.MAC)
811+
networkPayload["name"] = "default"
812+
networkPayload["ip"] = inst.IP
813+
networkPayload["mac"] = inst.MAC
811814
}
812815

813816
// Convert hypervisor type
@@ -831,7 +834,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
831834
OverlaySize: lo.ToPtr(overlaySizeStr),
832835
Vcpus: lo.ToPtr(inst.Vcpus),
833836
DiskIoBps: diskIoBpsStr,
834-
Network: netObj,
837+
Network: nil,
835838
CreatedAt: inst.CreatedAt,
836839
StartedAt: inst.StartedAt,
837840
StoppedAt: inst.StoppedAt,
@@ -840,6 +843,10 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
840843
Hypervisor: &hvType,
841844
}
842845

846+
if b, err := json.Marshal(networkPayload); err == nil {
847+
_ = json.Unmarshal(b, &oapiInst.Network)
848+
}
849+
843850
if inst.ExitMessage != "" {
844851
oapiInst.ExitMessage = lo.ToPtr(inst.ExitMessage)
845852
}
@@ -849,7 +856,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
849856
}
850857

851858
if len(inst.Metadata) > 0 {
852-
oapiInst.Metadata = &inst.Metadata
859+
oapiInst.Metadata = toOAPIMetadata(inst.Metadata)
853860
}
854861

855862
// Convert volume attachments

cmd/api/api/metadata.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/kernel/hypeman/lib/oapi"
8+
"github.com/kernel/hypeman/lib/tags"
9+
)
10+
11+
func toMapMetadata(metadata *oapi.MetadataTags) map[string]string {
12+
if metadata == nil {
13+
return nil
14+
}
15+
return tags.Clone(map[string]string(*metadata))
16+
}
17+
18+
func toOAPIMetadata(metadata map[string]string) *oapi.MetadataTags {
19+
if len(metadata) == 0 {
20+
return nil
21+
}
22+
cloned := oapi.MetadataTags(tags.Clone(metadata))
23+
return &cloned
24+
}
25+
26+
func matchesMetadataFilter(metadata map[string]string, filter *oapi.MetadataTags) bool {
27+
if filter == nil {
28+
return true
29+
}
30+
return tags.Matches(metadata, map[string]string(*filter))
31+
}
32+
33+
func parseMetadataJSON(raw string) (map[string]string, error) {
34+
if raw == "" {
35+
return nil, nil
36+
}
37+
var metadata map[string]string
38+
if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
39+
return nil, fmt.Errorf("parse metadata JSON: %w", err)
40+
}
41+
return metadata, nil
42+
}

0 commit comments

Comments
 (0)