Skip to content

Commit 693e051

Browse files
committed
feat: support for real docker image downloads & test
Implemented: * A Registry interface with default DockerHubRegistry * The `Pull` function to fetch manifests and layers using the `Registry` interface * `run` will use new `Pull` Unit tests for `FetchManifest` and `FetchLayer` methods using HTTP server (mock)
1 parent 1523990 commit 693e051

File tree

3 files changed

+164
-97
lines changed

3 files changed

+164
-97
lines changed

image.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"io"
7+
"net/http"
68
"os"
79
"os/exec"
810
"path/filepath"
@@ -44,6 +46,55 @@ type Registry interface {
4446
FetchLayer(repo, digest string) (io.ReadCloser, error)
4547
}
4648

49+
// DockerHubRegistry is a default implementation of the Registry interface for Docker Hub.
50+
type DockerHubRegistry struct {
51+
BaseURL string
52+
}
53+
54+
// NewDockerHubRegistry creates a new instance of DockerHubRegistry.
55+
func NewDockerHubRegistry() *DockerHubRegistry {
56+
return &DockerHubRegistry{
57+
BaseURL: "https://registry-1.docker.io/v2/",
58+
}
59+
}
60+
61+
// FetchManifest fetches the manifest for a given repository and tag.
62+
func (r *DockerHubRegistry) FetchManifest(repo, tag string) (*Manifest, error) {
63+
url := fmt.Sprintf("%s%s/manifests/%s", r.BaseURL, repo, tag)
64+
resp, err := http.Get(url)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to fetch manifest: %w", err)
67+
}
68+
defer resp.Body.Close()
69+
70+
if resp.StatusCode != http.StatusOK {
71+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
72+
}
73+
74+
var manifest Manifest
75+
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
76+
return nil, fmt.Errorf("failed to decode manifest: %w", err)
77+
}
78+
79+
return &manifest, nil
80+
}
81+
82+
// FetchLayer fetches a specific layer by its digest.
83+
func (r *DockerHubRegistry) FetchLayer(repo, digest string) (io.ReadCloser, error) {
84+
url := fmt.Sprintf("%s%s/blobs/%s", r.BaseURL, repo, digest)
85+
resp, err := http.Get(url)
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to fetch layer: %w", err)
88+
}
89+
90+
if resp.StatusCode != http.StatusOK {
91+
resp.Body.Close()
92+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
93+
}
94+
95+
return resp.Body, nil
96+
}
97+
4798
// Manifest represents the structure of an image manifest
4899
type Manifest struct {
49100
Config struct {

image_test.go

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"path/filepath"
66
"strings"
77
"testing"
8+
"net/http"
9+
"net/http/httptest"
10+
"io/ioutil"
811
)
912

1013
// Test Scenarios Documentation
@@ -13,8 +16,16 @@ import (
1316
// - Verifies that the ListImages function correctly lists available images.
1417
// - Setup: Creates a temporary directory and a mock image directory.
1518
// - Expected Outcome: The output of ListImages should include the mock image name.
16-
17-
// Additional test scenarios can be documented here as new tests are added.
19+
//
20+
// TestDockerHubRegistry_FetchManifest:
21+
// - Verifies the FetchManifest method of DockerHubRegistry using a mock HTTP server.
22+
// - Setup: Creates a mock server to simulate Docker Hub API responses.
23+
// - Expected Outcome: The manifest returned by FetchManifest should match the mock data.
24+
//
25+
// TestDockerHubRegistry_FetchLayer:
26+
// - Verifies the FetchLayer method of DockerHubRegistry using a mock HTTP server.
27+
// - Setup: Creates a mock server to simulate Docker Hub API responses.
28+
// - Expected Outcome: The layer content returned by FetchLayer should match the mock data.
1829

1930
func TestListImages(t *testing.T) {
2031
baseDir := filepath.Join(os.TempDir(), "basic-docker")
@@ -60,4 +71,79 @@ func captureOutput(f func()) string {
6071

6172
func contains(output, substring string) bool {
6273
return strings.Contains(output, substring)
74+
}
75+
76+
// TestDockerHubRegistry_FetchManifest tests the FetchManifest method of DockerHubRegistry
77+
func TestDockerHubRegistry_FetchManifest(t *testing.T) {
78+
// Mock server to simulate Docker Hub API
79+
handler := http.NewServeMux()
80+
handler.HandleFunc("/v2/library/busybox/manifests/latest", func(w http.ResponseWriter, r *http.Request) {
81+
w.Header().Set("Content-Type", "application/json")
82+
w.WriteHeader(http.StatusOK)
83+
w.Write([]byte(`{
84+
"config": {"digest": "sha256:configdigest"},
85+
"layers": [
86+
{"digest": "sha256:layer1digest"},
87+
{"digest": "sha256:layer2digest"}
88+
]
89+
}`))
90+
})
91+
92+
server := httptest.NewServer(handler)
93+
defer server.Close()
94+
95+
// Create a DockerHubRegistry instance with the mock server URL
96+
registry := &DockerHubRegistry{BaseURL: server.URL + "/v2/"}
97+
98+
// Call FetchManifest
99+
manifest, err := registry.FetchManifest("library/busybox", "latest")
100+
if err != nil {
101+
t.Fatalf("FetchManifest failed: %v", err)
102+
}
103+
104+
// Verify the manifest content
105+
if manifest.Config.Digest != "sha256:configdigest" {
106+
t.Errorf("Expected config digest 'sha256:configdigest', got '%s'", manifest.Config.Digest)
107+
}
108+
if len(manifest.Layers) != 2 {
109+
t.Errorf("Expected 2 layers, got %d", len(manifest.Layers))
110+
}
111+
if manifest.Layers[0].Digest != "sha256:layer1digest" {
112+
t.Errorf("Expected first layer digest 'sha256:layer1digest', got '%s'", manifest.Layers[0].Digest)
113+
}
114+
if manifest.Layers[1].Digest != "sha256:layer2digest" {
115+
t.Errorf("Expected second layer digest 'sha256:layer2digest', got '%s'", manifest.Layers[1].Digest)
116+
}
117+
}
118+
119+
// TestDockerHubRegistry_FetchLayer tests the FetchLayer method of DockerHubRegistry
120+
func TestDockerHubRegistry_FetchLayer(t *testing.T) {
121+
// Mock server to simulate Docker Hub API
122+
handler := http.NewServeMux()
123+
handler.HandleFunc("/v2/library/busybox/blobs/sha256:layer1digest", func(w http.ResponseWriter, r *http.Request) {
124+
w.WriteHeader(http.StatusOK)
125+
w.Write([]byte("layer1content"))
126+
})
127+
128+
server := httptest.NewServer(handler)
129+
defer server.Close()
130+
131+
// Create a DockerHubRegistry instance with the mock server URL
132+
registry := &DockerHubRegistry{BaseURL: server.URL + "/v2/"}
133+
134+
// Call FetchLayer
135+
reader, err := registry.FetchLayer("library/busybox", "sha256:layer1digest")
136+
if err != nil {
137+
t.Fatalf("FetchLayer failed: %v", err)
138+
}
139+
defer reader.Close()
140+
141+
// Verify the layer content
142+
content, err := ioutil.ReadAll(reader)
143+
if err != nil {
144+
t.Fatalf("Failed to read layer content: %v", err)
145+
}
146+
if string(content) != "layer1content" {
147+
t.Errorf("Expected layer content 'layer1content', got '%s'", string(content))
148+
}
63149
}

main.go

Lines changed: 25 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -306,119 +306,49 @@ func printSystemInfo() {
306306
fmt.Printf(" - Filesystem isolation: true\n")
307307
}
308308

309+
// Update the run function to use the new Pull logic
309310
func run() {
310-
// Adjust the path to include the mock image directory during testing
311-
if os.Getenv("TEST_ENV") == "true" {
312-
os.Setenv("PATH", os.Getenv("PATH")+":"+imagesDir)
311+
if len(os.Args) < 3 {
312+
fmt.Println("Error: Image name required for run")
313+
os.Exit(1)
313314
}
314315

315-
// Generate a container ID
316-
containerID := fmt.Sprintf("container-%d", time.Now().Unix())
317-
fmt.Printf("Starting container %s\n", containerID)
318-
319-
// Check if the image exists before proceeding
320316
imageName := os.Args[2]
321-
imagePath := filepath.Join(imagesDir, imageName)
322-
if _, err := os.Stat(imagePath); os.IsNotExist(err) {
323-
fmt.Printf("Image '%s' not found. Fetching the image...\n", imageName)
324-
if err := fetchImage(imageName); err != nil {
325-
fmt.Printf("Error: Failed to fetch image '%s': %v\n", imageName, err)
326-
os.Exit(1)
327-
}
328-
fmt.Printf("Image '%s' fetched successfully.\n", imageName)
317+
registry := NewDockerHubRegistry()
318+
319+
fmt.Printf("Fetching image '%s'...\n", imageName)
320+
image, err := Pull(registry, imageName)
321+
if err != nil {
322+
fmt.Printf("Error: Failed to fetch image '%s': %v\n", imageName, err)
323+
os.Exit(1)
329324
}
325+
fmt.Printf("Image '%s' fetched successfully.\n", imageName)
330326

331327
// Create rootfs for this container
328+
containerID := fmt.Sprintf("container-%d", time.Now().Unix())
332329
rootfs := filepath.Join(baseDir, "containers", containerID, "rootfs")
333330

334-
// Instead of calling createMinimalRootfs directly:
335-
// 1. Create a base layer if it doesn't exist
336-
baseLayerID := "base-layer"
337-
baseLayerPath := filepath.Join(baseDir, "layers", baseLayerID)
338-
339-
if _, err := os.Stat(baseLayerPath); os.IsNotExist(err) {
340-
// Create the base layer
341-
if err := os.MkdirAll(baseLayerPath, 0755); err != nil {
342-
fmt.Printf("Error creating base layer directory: %v\n", err)
343-
os.Exit(1)
344-
}
345-
346-
// Initialize the base layer with minimal rootfs content
347-
must(initializeBaseLayer(baseLayerPath))
348-
349-
// Save layer metadata
350-
layer := ImageLayer{
351-
ID: baseLayerID,
352-
Created: time.Now(),
353-
BaseLayerPath: baseLayerPath,
354-
}
355-
356-
if err := saveLayerMetadata(layer); err != nil {
357-
fmt.Printf("Warning: Failed to save layer metadata: %v\n", err)
358-
}
359-
}
360-
361-
// Fix permission issue by ensuring correct ownership and permissions for the base layer
362-
if err := os.Chmod(baseLayerPath, 0755); err != nil {
363-
fmt.Printf("Error setting permissions for base layer: %v\n", err)
331+
if err := os.MkdirAll(rootfs, 0755); err != nil {
332+
fmt.Printf("Error: Failed to create rootfs for container '%s': %v\n", containerID, err)
364333
os.Exit(1)
365334
}
366335

367-
// 2. Create an app layer for this specific container (optional)
368-
appLayerID := "app-layer-" + containerID
369-
appLayerPath := filepath.Join(baseDir, "layers", appLayerID)
370-
371-
// Use the appLayerID variable to log its creation
372-
fmt.Printf("App layer created with ID: %s\n", appLayerID)
373-
374-
// You could add container-specific files to the app layer here
375-
// For now, we'll just use the base layer
376-
377-
// Save layer metadata including app layer path
378-
layer := ImageLayer{
379-
ID: appLayerID,
380-
Created: time.Now(),
381-
BaseLayerPath: baseLayerPath,
382-
AppLayerPath: appLayerPath,
383-
}
384-
385-
if err := saveLayerMetadata(layer); err != nil {
386-
fmt.Printf("Warning: Failed to save layer metadata: %v\n", err)
387-
}
388-
389-
// 3. Mount the layers to create the container rootfs
390-
layers := []string{baseLayerID} // Add appLayerID if you created one
391-
must(mountLayeredFilesystem(layers, rootfs))
392-
393-
// Write the PID of the current process to a file
394-
pidFile := filepath.Join(baseDir, "containers", containerID, "pid")
395-
fmt.Printf("Debug: Writing PID file for container %s at %s\n", containerID, pidFile)
396-
fmt.Printf("Debug: Current process PID is %d\n", os.Getpid())
397-
if err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil {
398-
fmt.Printf("Error: Failed to write PID file for container %s: %v\n", containerID, err)
336+
if err := copyDir(image.RootFS, rootfs); err != nil {
337+
fmt.Printf("Error: Failed to copy rootfs for container '%s': %v\n", containerID, err)
399338
os.Exit(1)
400339
}
401340

402-
// Fix deadlock by adding a timeout mechanism
403-
if len(os.Args) < 4 {
404-
fmt.Println("No command provided. Keeping the container process alive with a timeout.")
405-
timeout := time.After(10 * time.Minute) // Set a timeout of 10 minutes
406-
select {
407-
case <-timeout:
408-
fmt.Println("Timeout reached. Exiting container process.")
409-
os.Exit(0)
410-
}
411-
}
341+
fmt.Printf("Starting container %s\n", containerID)
412342

413-
// Update the fallback logic to avoid using unshare entirely in limited isolation
414-
if hasNamespacePrivileges && !inContainer {
415-
// Full isolation approach for pure Linux environments
416-
runWithNamespaces(containerID, rootfs, os.Args[2], os.Args[3:])
417-
} else {
418-
runWithoutNamespaces(containerID, rootfs, os.Args[2], os.Args[3:])
343+
// Execute the command in the container
344+
if len(os.Args) < 4 {
345+
fmt.Println("Error: Command required for run")
346+
os.Exit(1)
419347
}
420348

421-
fmt.Printf("Container %s exited\n", containerID)
349+
command := os.Args[3]
350+
args := os.Args[4:]
351+
runWithoutNamespaces(containerID, rootfs, command, args)
422352
}
423353

424354
func initializeBaseLayer(baseLayerPath string) error {

0 commit comments

Comments
 (0)