Skip to content

Commit 65a64a5

Browse files
authored
Image manager (#3)
* Export images to rootfs with docker * docker client dependency injection * Fix permissions during extraction * Don't skip docker in tests * extraction test passing but seems too complicated * Use umoci for image to rootfs conversion * Update docs * Update README * Move OCI cache dir * Tweak disk settings * Async API * Simplify async API: remove sse for now * Avoid rate limit in CI * Add API level image test * Check queuing works * Test failure on invalid tag * Fix image filesystem layout * image name validation * Update README * Clean up error * Use erofs * API layer depends on domain cmd/api/api (handlers) ↓ depends on lib/images, lib/instances, lib/volumes (domain) ↓ independent of lib/oapi (API types) * Idempotency and race conditions * Add erofs to ci * OCI client internal to image manager * Disambiguate digests and tags * WIP: figuring out error handling registry errors * Switch to more lightweight library for interacting with registry * Handle 404 * Use docker auth config file * Add timeout on resolve * Update README * Poll by digest * Tweak readme * caching test * More comprehensive basic test * 404 error mapping
1 parent 98c320d commit 65a64a5

31 files changed

+2747
-332
lines changed

.air.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
55
[build]
66
args_bin = []
77
bin = "./tmp/main"
8-
cmd = "go build -o ./tmp/main ./cmd/api"
8+
cmd = "go build -tags containers_image_openpgp -o ./tmp/main ./cmd/api"
99
delay = 1000
1010
exclude_dir = ["assets", "tmp", "vendor", "testdata", "bin", "scripts", "data", "kernel"]
1111
exclude_file = []

.github/workflows/test.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@ jobs:
1515
go-version: '1.25'
1616

1717
- name: Install dependencies
18-
run: go mod download
18+
run: |
19+
set -xe
20+
sudo apt-get install -y erofs-utils
21+
go mod download
22+
23+
# Avoids rate limits when running the tests
24+
# Tests includes pulling, then converting to disk images
25+
- name: Login to Docker Hub
26+
uses: docker/login-action@v3
27+
with:
28+
username: ${{ secrets.DOCKERHUB_USERNAME }}
29+
password: ${{ secrets.DOCKERHUB_PASSWORD }}
1930

2031
- name: Run tests
2132
run: make test

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ generate-all: oapi-generate generate-wire
4343

4444
# Build the binary
4545
build: | $(BIN_DIR)
46-
go build -o $(BIN_DIR)/hypeman ./cmd/api
46+
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api
4747

4848
# Run in development mode with hot reload
4949
dev: $(AIR)
5050
$(AIR) -c .air.toml
5151

5252
# Run tests
5353
test:
54-
go test -v -timeout 30s ./...
54+
go test -tags containers_image_openpgp -v -timeout 30s ./...
5555

5656
# Clean generated files and binaries
5757
clean:

README.md

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,40 @@ Run containerized workloads in VMs, powered by [Cloud Hypervisor](https://github
88

99
### Prerequisites
1010

11-
**Cloud Hypervisor** - [Installation guide](https://www.cloudhypervisor.org/docs/prologue/quick-start/#use-pre-built-binaries)
12-
```bash
13-
cloud-hypervisor --version # Verify
14-
ch-remote --version
15-
```
11+
**Go 1.25.4+**, **Cloud Hypervisor**, **KVM**, **erofs-utils**
1612

17-
**containerd** - [Installation guide](https://github.com/containerd/containerd/blob/main/docs/getting-started.md)
1813
```bash
19-
containerd --version # Verify
14+
cloud-hypervisor --version
15+
mkfs.erofs --version
2016
```
2117

22-
**Go 1.25.4+** and **KVM**
23-
2418
### Configuration
2519

20+
#### Environment variables
21+
2622
```bash
2723
cp .env.example .env
2824
# Edit .env and set JWT_SECRET
2925
```
3026

27+
#### Data directory
28+
29+
Hypeman stores data in a configurable directory. Configure permissions for this directory.
30+
31+
```bash
32+
sudo mkdir /var/lib/hypeman
33+
sudo chown $USER:$USER /var/lib/hypeman
34+
```
35+
36+
#### Dockerhub login
37+
38+
Requires Docker Hub authentication to avoid rate limits when running the tests:
39+
```bash
40+
docker login
41+
```
42+
43+
Docker itself isn't required to be installed. `~/.docker/config.json` is a standard used for handling registry authentication.
44+
3145
### Build
3246

3347
```bash

cmd/api/api/api_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ func newTestService(t *testing.T) *ApiService {
1616
DataDir: t.TempDir(),
1717
}
1818

19+
imageMgr, err := images.NewManager(cfg.DataDir, 1)
20+
if err != nil {
21+
t.Fatalf("failed to create image manager: %v", err)
22+
}
23+
1924
return &ApiService{
2025
Config: cfg,
21-
ImageManager: images.NewManager(cfg.DataDir),
26+
ImageManager: imageMgr,
2227
InstanceManager: instances.NewManager(cfg.DataDir),
2328
VolumeManager: volumes.NewManager(cfg.DataDir),
2429
}
@@ -27,4 +32,3 @@ func newTestService(t *testing.T) *ApiService {
2732
func ctx() context.Context {
2833
return context.Background()
2934
}
30-

cmd/api/api/images.go

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,45 @@ import (
99
"github.com/onkernel/hypeman/lib/oapi"
1010
)
1111

12-
// ListImages lists all images
1312
func (s *ApiService) ListImages(ctx context.Context, request oapi.ListImagesRequestObject) (oapi.ListImagesResponseObject, error) {
1413
log := logger.FromContext(ctx)
1514

16-
imgs, err := s.ImageManager.ListImages(ctx)
15+
domainImages, err := s.ImageManager.ListImages(ctx)
1716
if err != nil {
1817
log.Error("failed to list images", "error", err)
1918
return oapi.ListImages500JSONResponse{
2019
Code: "internal_error",
2120
Message: "failed to list images",
2221
}, nil
2322
}
24-
return oapi.ListImages200JSONResponse(imgs), nil
23+
24+
oapiImages := make([]oapi.Image, len(domainImages))
25+
for i, img := range domainImages {
26+
oapiImages[i] = imageToOAPI(img)
27+
}
28+
29+
return oapi.ListImages200JSONResponse(oapiImages), nil
2530
}
2631

27-
// CreateImage creates a new image from an OCI reference
2832
func (s *ApiService) CreateImage(ctx context.Context, request oapi.CreateImageRequestObject) (oapi.CreateImageResponseObject, error) {
2933
log := logger.FromContext(ctx)
3034

31-
img, err := s.ImageManager.CreateImage(ctx, *request.Body)
35+
domainReq := images.CreateImageRequest{
36+
Name: request.Body.Name,
37+
}
38+
39+
img, err := s.ImageManager.CreateImage(ctx, domainReq)
3240
if err != nil {
3341
switch {
34-
case errors.Is(err, images.ErrAlreadyExists):
42+
case errors.Is(err, images.ErrInvalidName):
3543
return oapi.CreateImage400JSONResponse{
36-
Code: "already_exists",
37-
Message: "image already exists",
44+
Code: "invalid_name",
45+
Message: err.Error(),
46+
}, nil
47+
case errors.Is(err, images.ErrNotFound):
48+
return oapi.CreateImage404JSONResponse{
49+
Code: "not_found",
50+
Message: "image not found",
3851
}, nil
3952
default:
4053
log.Error("failed to create image", "error", err, "name", request.Body.Name)
@@ -44,46 +57,44 @@ func (s *ApiService) CreateImage(ctx context.Context, request oapi.CreateImageRe
4457
}, nil
4558
}
4659
}
47-
return oapi.CreateImage201JSONResponse(*img), nil
60+
return oapi.CreateImage202JSONResponse(imageToOAPI(*img)), nil
4861
}
4962

50-
// GetImage gets image details
5163
func (s *ApiService) GetImage(ctx context.Context, request oapi.GetImageRequestObject) (oapi.GetImageResponseObject, error) {
5264
log := logger.FromContext(ctx)
5365

54-
img, err := s.ImageManager.GetImage(ctx, request.Id)
66+
img, err := s.ImageManager.GetImage(ctx, request.Name)
5567
if err != nil {
5668
switch {
57-
case errors.Is(err, images.ErrNotFound):
69+
case errors.Is(err, images.ErrInvalidName), errors.Is(err, images.ErrNotFound):
5870
return oapi.GetImage404JSONResponse{
5971
Code: "not_found",
6072
Message: "image not found",
6173
}, nil
6274
default:
63-
log.Error("failed to get image", "error", err, "id", request.Id)
75+
log.Error("failed to get image", "error", err, "name", request.Name)
6476
return oapi.GetImage500JSONResponse{
6577
Code: "internal_error",
6678
Message: "failed to get image",
6779
}, nil
6880
}
6981
}
70-
return oapi.GetImage200JSONResponse(*img), nil
82+
return oapi.GetImage200JSONResponse(imageToOAPI(*img)), nil
7183
}
7284

73-
// DeleteImage deletes an image
7485
func (s *ApiService) DeleteImage(ctx context.Context, request oapi.DeleteImageRequestObject) (oapi.DeleteImageResponseObject, error) {
7586
log := logger.FromContext(ctx)
7687

77-
err := s.ImageManager.DeleteImage(ctx, request.Id)
88+
err := s.ImageManager.DeleteImage(ctx, request.Name)
7889
if err != nil {
7990
switch {
80-
case errors.Is(err, images.ErrNotFound):
91+
case errors.Is(err, images.ErrInvalidName), errors.Is(err, images.ErrNotFound):
8192
return oapi.DeleteImage404JSONResponse{
8293
Code: "not_found",
8394
Message: "image not found",
8495
}, nil
8596
default:
86-
log.Error("failed to delete image", "error", err, "id", request.Id)
97+
log.Error("failed to delete image", "error", err, "name", request.Name)
8798
return oapi.DeleteImage500JSONResponse{
8899
Code: "internal_error",
89100
Message: "failed to delete image",
@@ -93,3 +104,29 @@ func (s *ApiService) DeleteImage(ctx context.Context, request oapi.DeleteImageRe
93104
return oapi.DeleteImage204Response{}, nil
94105
}
95106

107+
func imageToOAPI(img images.Image) oapi.Image {
108+
oapiImg := oapi.Image{
109+
Name: img.Name,
110+
Digest: img.Digest,
111+
Status: oapi.ImageStatus(img.Status),
112+
QueuePosition: img.QueuePosition,
113+
Error: img.Error,
114+
SizeBytes: img.SizeBytes,
115+
CreatedAt: img.CreatedAt,
116+
}
117+
118+
if len(img.Entrypoint) > 0 {
119+
oapiImg.Entrypoint = &img.Entrypoint
120+
}
121+
if len(img.Cmd) > 0 {
122+
oapiImg.Cmd = &img.Cmd
123+
}
124+
if len(img.Env) > 0 {
125+
oapiImg.Env = &img.Env
126+
}
127+
if img.WorkingDir != "" {
128+
oapiImg.WorkingDir = &img.WorkingDir
129+
}
130+
131+
return oapiImg
132+
}

0 commit comments

Comments
 (0)