From a4785a0b7a7d0979ed89dc8101a9784856b7a6f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:13:41 +0000 Subject: [PATCH 1/9] Use GHCR as default container registry with explicit registry parsing Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/63030a95-e379-4aa4-81af-d71cc883fe9c Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- image.go | 12 ++-- main.go | 188 ++++++++++++++++++++++++++++----------------------- main_test.go | 60 ++++++++++++++-- 3 files changed, 164 insertions(+), 96 deletions(-) diff --git a/image.go b/image.go index 157369a..347e8f3 100644 --- a/image.go +++ b/image.go @@ -59,9 +59,9 @@ func calculateDirSize(dirPath string) (int64, error) { // Image represents a container image type Image struct { - Name string - RootFS string - Layers []string + Name string + RootFS string + Layers []string } // Registry represents a generic interface for interacting with container registries @@ -70,7 +70,7 @@ type Registry interface { FetchLayer(repo, digest string) (io.ReadCloser, error) } -// DockerHubRegistry is a default implementation of the Registry interface for Docker Hub or custom registries. +// DockerHubRegistry is a default implementation of the Registry interface for GHCR or custom registries. type DockerHubRegistry struct { BaseURL string } @@ -78,7 +78,7 @@ type DockerHubRegistry struct { // NewDockerHubRegistry creates a new instance of DockerHubRegistry with an optional custom registry URL. func NewDockerHubRegistry(customURL string) *DockerHubRegistry { if customURL == "" { - customURL = "https://registry-1.docker.io/v2/" + customURL = "https://ghcr.io/v2/" } return &DockerHubRegistry{ BaseURL: customURL, @@ -216,4 +216,4 @@ func LoadImageFromTar(tarFilePath string, imageName string) (*Image, error) { RootFS: rootfs, Layers: []string{"base"}, }, nil -} \ No newline at end of file +} diff --git a/main.go b/main.go index b6e72f9..963de8c 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,15 @@ import ( "encoding/json" "fmt" "io" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" "syscall" "time" - "runtime" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Environment detection @@ -146,7 +146,7 @@ func addDockerResourceCapsule(capsuleName, capsuleVersion, capsulePath string) e fmt.Printf("[Docker] Verification output:\n%s\n", string(output)) - // Show docker ps output + // Show docker ps output psCmd := exec.Command("docker", "ps", "-a") psOutput, psErr := psCmd.CombinedOutput() if psErr != nil { @@ -185,7 +185,7 @@ func addKubernetesResourceCapsule(capsuleName, capsuleVersion, capsulePath strin // Determine if we should create a ConfigMap or Secret based on the file content // For this example, we'll create a ConfigMap if it's text data, Secret if binary isTextData := isTextFile(capsuleData) - + if isTextData { // Create as ConfigMap data := map[string]string{ @@ -230,18 +230,18 @@ func isTextFile(data []byte) bool { if len(data) == 0 { return true } - + sample := data if len(data) > 512 { sample = data[:512] } - + for _, b := range sample { if b == 0 { return false // null byte suggests binary } } - + return true } @@ -509,7 +509,7 @@ func printSystemInfo() { fmt.Printf("Running in container: %v\n", inContainer) fmt.Printf("Namespace privileges: %v\n", hasNamespacePrivileges) fmt.Printf("Cgroup access: %v\n", hasCgroupAccess) - + // Display cgroup details if cgroupInfo.Available { cgroupVersionStr := "unknown" @@ -526,7 +526,7 @@ func printSystemInfo() { } else if cgroupInfo.ErrorMessage != "" { fmt.Printf("Cgroup error: %s\n", cgroupInfo.ErrorMessage) } - + fmt.Println("Available features:") fmt.Printf(" - Process isolation: %v\n", hasNamespacePrivileges) fmt.Printf(" - Network isolation: %v\n", hasNamespacePrivileges) @@ -549,14 +549,7 @@ func run() { fmt.Printf("Using locally loaded image '%s'.\n", imageName) } else { fmt.Printf("Fetching image '%s' from registry...\n", imageName) - // Extract registry URL and repository from image name - parts := strings.SplitN(imageName, "/", 2) - registryURL := "https://registry-1.docker.io/v2/" // Default to Docker Hub - repo := imageName - if len(parts) > 1 { - registryURL = fmt.Sprintf("http://%s/v2/", parts[0]) - repo = parts[1] - } + registryURL, repo := resolveRegistry(imageName) registry := NewDockerHubRegistry(registryURL) image, err := Pull(registry, repo) @@ -615,6 +608,29 @@ func run() { runWithoutNamespaces(containerID, rootfs, command, args) } +func resolveRegistry(imageName string) (string, string) { + registryURL := "https://ghcr.io/v2/" + repo := imageName + + parts := strings.SplitN(imageName, "/", 2) + if len(parts) == 2 { + host := parts[0] + if host == "localhost" || strings.Contains(host, ".") || strings.Contains(host, ":") { + registryURL = registryURLForHost(host) + repo = parts[1] + } + } + + return registryURL, repo +} + +func registryURLForHost(host string) string { + if host == "localhost" || strings.HasPrefix(host, "localhost:") || host == "[::1]" || host == "::1" || strings.HasPrefix(host, "127.") { + return fmt.Sprintf("http://%s/v2/", host) + } + return fmt.Sprintf("https://%s/v2/", host) +} + func initializeBaseLayer(baseLayerPath string) error { // Create essential directories in the base layer dirs := []string{"/bin", "/dev", "/etc", "/proc", "/sys", "/tmp"} @@ -754,7 +770,7 @@ func runWithNamespaces(containerID, rootfs, command string, args []string) { // Reintroduce runWithoutNamespaces for simplicity and modularity func runWithoutNamespaces(containerID, rootfs, command string, args []string) { fmt.Println("Warning: Namespace isolation is not permitted. Executing without isolation.") - + // Update state to running startedAt := time.Now() UpdateContainerState(containerID, func(m *ContainerMetadata) { @@ -762,14 +778,14 @@ func runWithoutNamespaces(containerID, rootfs, command string, args []string) { m.StartedAt = &startedAt m.PID = os.Getpid() }) - + // Set up cgroups if available if hasCgroupAccess { if err := SetupCgroupsWithDetection(containerID, 100*1024*1024); err != nil { fmt.Printf("Warning: Failed to setup cgroups: %v\n", err) } } - + // Set up log file logFile := filepath.Join(baseDir, "containers", containerID, "stdout.log") logFd, err := os.Create(logFile) @@ -778,10 +794,10 @@ func runWithoutNamespaces(containerID, rootfs, command string, args []string) { } else { defer logFd.Close() } - + cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin - + // Use MultiWriter to send output to both console and log file if logFd != nil { cmd.Stdout = io.MultiWriter(os.Stdout, logFd) @@ -790,15 +806,15 @@ func runWithoutNamespaces(containerID, rootfs, command string, args []string) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr } - + err = cmd.Run() - + // Update state to exited or failed finishedAt := time.Now() exitCode := 0 state := StateExited errorMsg := "" - + if err != nil { state = StateFailed errorMsg = err.Error() @@ -809,7 +825,7 @@ func runWithoutNamespaces(containerID, rootfs, command string, args []string) { } fmt.Printf("Error: %v\n", err) } - + UpdateContainerState(containerID, func(m *ContainerMetadata) { m.State = state m.FinishedAt = &finishedAt @@ -1239,7 +1255,7 @@ func handleKubernetesCapsuleCommand() { } command := os.Args[3] - + kcm, err := NewKubernetesCapsuleManager("default") if err != nil { fmt.Printf("Error: Failed to create Kubernetes client: %v\n", err) @@ -1256,20 +1272,20 @@ func handleKubernetesCapsuleCommand() { name := os.Args[4] version := os.Args[5] filePath := os.Args[6] - + err := AddResourceCapsule("kubernetes", name, version, filePath) if err != nil { fmt.Printf("Error: Failed to create Kubernetes capsule: %v\n", err) os.Exit(1) } - + case "list": err := kcm.ListCapsules() if err != nil { fmt.Printf("Error: Failed to list capsules: %v\n", err) os.Exit(1) } - + case "get": if len(os.Args) < 6 { fmt.Println("Usage: basic-docker k8s-capsule get ") @@ -1277,7 +1293,7 @@ func handleKubernetesCapsuleCommand() { } name := os.Args[4] version := os.Args[5] - + // Try ConfigMap first configMap, err := kcm.GetConfigMapCapsule(name, version) if err == nil { @@ -1285,7 +1301,7 @@ func handleKubernetesCapsuleCommand() { fmt.Printf("Data keys: %v\n", getKeys(configMap.Data)) return } - + // Try Secret secret, err := kcm.GetSecretCapsule(name, version) if err == nil { @@ -1293,10 +1309,10 @@ func handleKubernetesCapsuleCommand() { fmt.Printf("Data keys: %v\n", getKeysBytes(secret.Data)) return } - + fmt.Printf("Error: Capsule %s:%s not found\n", name, version) os.Exit(1) - + case "delete": if len(os.Args) < 6 { fmt.Println("Usage: basic-docker k8s-capsule delete ") @@ -1304,13 +1320,13 @@ func handleKubernetesCapsuleCommand() { } name := os.Args[4] version := os.Args[5] - + err := kcm.DeleteCapsule(name, version) if err != nil { fmt.Printf("Error: Failed to delete capsule: %v\n", err) os.Exit(1) } - + default: fmt.Printf("Error: Unknown command '%s'\n", command) os.Exit(1) @@ -1334,10 +1350,10 @@ func handleCapsuleBenchmark(environment string) { // runDockerCapsuleBenchmark runs benchmarks for Docker-based Resource Capsules func runDockerCapsuleBenchmark() { fmt.Println("=== Docker Resource Capsule Benchmark ===") - + cm := NewCapsuleManager() cm.AddCapsule("benchmark-capsule", "1.0", "/tmp/benchmark-file") - + // Create a test file testFile := "/tmp/benchmark-file" err := os.WriteFile(testFile, []byte("benchmark data"), 0644) @@ -1346,7 +1362,7 @@ func runDockerCapsuleBenchmark() { return } defer os.Remove(testFile) - + // Benchmark capsule access iterations := 10000 start := time.Now() @@ -1358,7 +1374,7 @@ func runDockerCapsuleBenchmark() { } } duration := time.Since(start) - + fmt.Printf("Docker Capsule Access: %d iterations in %v\n", iterations, duration) fmt.Printf("Average per operation: %v\n", duration/time.Duration(iterations)) } @@ -1366,27 +1382,27 @@ func runDockerCapsuleBenchmark() { // runKubernetesCapsuleBenchmark runs benchmarks for Kubernetes-based Resource Capsules func runKubernetesCapsuleBenchmark() { fmt.Println("=== Kubernetes Resource Capsule Benchmark ===") - + kcm, err := NewKubernetesCapsuleManager("default") if err != nil { fmt.Printf("Error: Failed to create Kubernetes client: %v\n", err) return } - + // Create a test capsule testData := map[string]string{ "benchmark-file": "benchmark data", } - + err = kcm.CreateConfigMapCapsule("benchmark-capsule", "1.0", testData) if err != nil { fmt.Printf("Error: Failed to create test capsule: %v\n", err) return } - + // Clean up after benchmark defer kcm.DeleteCapsule("benchmark-capsule", "1.0") - + // Benchmark capsule access iterations := 100 // Lower iterations for K8s API calls start := time.Now() @@ -1398,7 +1414,7 @@ func runKubernetesCapsuleBenchmark() { } } duration := time.Since(start) - + fmt.Printf("Kubernetes Capsule Access: %d iterations in %v\n", iterations, duration) fmt.Printf("Average per operation: %v\n", duration/time.Duration(iterations)) } @@ -1476,7 +1492,7 @@ func handleKubernetesCRDCommand() { fmt.Printf("ResourceCapsule CRD: %s\n", name) fmt.Printf("Namespace: %s\n", resourceCapsule.GetNamespace()) - + spec, found, _ := unstructured.NestedMap(resourceCapsule.Object, "spec") if found { if version, found, _ := unstructured.NestedString(spec, "version"); found { @@ -1486,7 +1502,7 @@ func handleKubernetesCRDCommand() { fmt.Printf("Type: %s\n", capsuleType) } } - + status, found, _ := unstructured.NestedMap(resourceCapsule.Object, "status") if found { if phase, found, _ := unstructured.NestedString(status, "phase"); found { @@ -1585,20 +1601,20 @@ func handleMonitoringCommand() { fmt.Printf("Error: Invalid PID '%s': %v\n", os.Args[3], err) return } - + pm := NewProcessMonitor(pid) metrics, err := pm.GetMetrics() if err != nil { fmt.Printf("Error getting process metrics: %v\n", err) return } - + jsonData, err := json.MarshalIndent(metrics, "", " ") if err != nil { fmt.Printf("Error formatting metrics: %v\n", err) return } - + fmt.Printf("Process Metrics (PID %d):\n", pid) fmt.Println(string(jsonData)) @@ -1608,20 +1624,20 @@ func handleMonitoringCommand() { return } containerID := os.Args[3] - + cm := NewContainerMonitor(containerID) metrics, err := cm.GetMetrics() if err != nil { fmt.Printf("Error getting container metrics: %v\n", err) return } - + jsonData, err := json.MarshalIndent(metrics, "", " ") if err != nil { fmt.Printf("Error formatting metrics: %v\n", err) return } - + fmt.Printf("Container Metrics (%s):\n", containerID) fmt.Println(string(jsonData)) @@ -1632,20 +1648,20 @@ func handleMonitoringCommand() { fmt.Printf("Error getting host metrics: %v\n", err) return } - + jsonData, err := json.MarshalIndent(metrics, "", " ") if err != nil { fmt.Printf("Error formatting metrics: %v\n", err) return } - + fmt.Println("Host Metrics:") fmt.Println(string(jsonData)) case "all": aggregator := NewMonitoringAggregator() aggregator.AddMonitor(NewHostMonitor()) - + // Add container monitors for all existing containers containerDir := filepath.Join(baseDir, "containers") if entries, err := os.ReadDir(containerDir); err == nil { @@ -1655,13 +1671,13 @@ func handleMonitoringCommand() { } } } - + metricsStr, err := aggregator.GetFormattedMetrics() if err != nil { fmt.Printf("Error getting aggregated metrics: %v\n", err) return } - + fmt.Println("Complete System Monitoring (All Levels):") fmt.Println(metricsStr) @@ -1669,7 +1685,7 @@ func handleMonitoringCommand() { // Perform gap analysis aggregator := NewMonitoringAggregator() aggregator.AddMonitor(NewHostMonitor()) - + // Add container monitors containerDir := filepath.Join(baseDir, "containers") if entries, err := os.ReadDir(containerDir); err == nil { @@ -1679,20 +1695,20 @@ func handleMonitoringCommand() { } } } - + metrics, err := aggregator.GetAllMetrics() if err != nil { fmt.Printf("Error getting metrics for gap analysis: %v\n", err) return } - + gap := AnalyzeMonitoringGap(metrics) gapData, err := json.MarshalIndent(gap, "", " ") if err != nil { fmt.Printf("Error formatting gap analysis: %v\n", err) return } - + fmt.Println("Monitoring Gap Analysis:") fmt.Println("========================") fmt.Println("This analysis identifies gaps in monitoring coverage between") @@ -1707,7 +1723,7 @@ func handleMonitoringCommand() { return } containerID := os.Args[3] - + showMonitoringCorrelation(containerID) default: @@ -1721,7 +1737,7 @@ func showMonitoringCorrelation(containerID string) { fmt.Printf("Monitoring Correlation Analysis for Container: %s\n", containerID) fmt.Println("=" + strings.Repeat("=", len(containerID)+41)) fmt.Println() - + // Get container metrics cm := NewContainerMonitor(containerID) containerMetrics, err := cm.GetMetrics() @@ -1729,7 +1745,7 @@ func showMonitoringCorrelation(containerID string) { fmt.Printf("Error getting container metrics: %v\n", err) return } - + // Get host metrics hm := NewHostMonitor() hostMetrics, err := hm.GetMetrics() @@ -1737,31 +1753,31 @@ func showMonitoringCorrelation(containerID string) { fmt.Printf("Error getting host metrics: %v\n", err) return } - + // Display correlation table as per problem statement fmt.Println("Level Correlation Table (Based on Docker Monitoring Problem):") fmt.Println("-------------------------------------------------------------") fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "Aspect", "Process", "Container", "Host") fmt.Println(strings.Repeat("-", 80)) - + if cMetrics, ok := containerMetrics.(ContainerMetrics); ok { if hMetrics, ok := hostMetrics.(HostMetrics); ok { // Spec line - fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", + fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "Spec", "Source", "Dockerfile", "Kickstart") - + // On disk line - fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", + fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "On disk", ".TEXT", cMetrics.DockerPath, "/") - + // In memory line processInfo := "N/A" if len(cMetrics.Processes) > 0 { processInfo = fmt.Sprintf("PID %d", cMetrics.Processes[0].PID) } - fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", + fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "In memory", processInfo, cMetrics.ContainerID, hMetrics.Hostname) - + // In network line networkInfo := "Socket" if len(cMetrics.Processes) > 0 { @@ -1775,27 +1791,27 @@ func showMonitoringCorrelation(containerID string) { if len(hMetrics.NetworkInterfaces) > 0 { ethInfo = hMetrics.NetworkInterfaces[0].Name } - fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", + fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "In network", networkInfo, vethInfo, ethInfo) - + // Runtime context line - fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", + fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "Runtime context", "server core", "host", hMetrics.RuntimeContext) - + // Isolation line - fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", + fmt.Printf("%-15s | %-20s | %-20s | %-20s\n", "Isolation", "moderate", "private OS view", "full") } } - + fmt.Println() fmt.Println("Detailed Metrics:") fmt.Println("-----------------") - + // Container details containerData, _ := json.MarshalIndent(containerMetrics, "", " ") fmt.Printf("Container Metrics:\n%s\n\n", string(containerData)) - + // Host summary (subset of metrics) if hMetrics, ok := hostMetrics.(HostMetrics); ok { fmt.Printf("Host Summary:\n") @@ -1823,12 +1839,12 @@ func showLogs(containerID string) { fmt.Printf("Error: %v\n", err) os.Exit(1) } - + if logs == "" { fmt.Println("No logs available for this container") return } - + fmt.Print(logs) } @@ -1839,12 +1855,12 @@ func inspectContainer(containerID string) { fmt.Printf("Error: %v\n", err) os.Exit(1) } - + data, err := json.MarshalIndent(metadata, "", " ") if err != nil { fmt.Printf("Error formatting container data: %v\n", err) os.Exit(1) } - + fmt.Println(string(data)) } diff --git a/main_test.go b/main_test.go index f4807e0..0ffb429 100644 --- a/main_test.go +++ b/main_test.go @@ -1,11 +1,11 @@ package main import ( - "os" - "testing" "fmt" - "path/filepath" + "os" "os/exec" + "path/filepath" + "testing" ) // Test Scenarios Documentation @@ -109,6 +109,58 @@ func TestGetContainerStatus(t *testing.T) { } } +func TestResolveRegistry(t *testing.T) { + tests := []struct { + name string + imageName string + wantRegistry string + wantRepository string + }{ + { + name: "default ghcr for short image", + imageName: "alpine:latest", + wantRegistry: "https://ghcr.io/v2/", + wantRepository: "alpine:latest", + }, + { + name: "explicit ghcr host", + imageName: "ghcr.io/j143/basic-docker-engine:latest", + wantRegistry: "https://ghcr.io/v2/", + wantRepository: "j143/basic-docker-engine:latest", + }, + { + name: "explicit docker host", + imageName: "docker.io/library/busybox:latest", + wantRegistry: "https://docker.io/v2/", + wantRepository: "library/busybox:latest", + }, + { + name: "local registry over http", + imageName: "localhost:5000/alpine:latest", + wantRegistry: "http://localhost:5000/v2/", + wantRepository: "alpine:latest", + }, + { + name: "loopback local registry over http", + imageName: "127.0.0.1:5000/alpine:latest", + wantRegistry: "http://127.0.0.1:5000/v2/", + wantRepository: "alpine:latest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRegistry, gotRepository := resolveRegistry(tt.imageName) + if gotRegistry != tt.wantRegistry { + t.Fatalf("registry mismatch: got %q, want %q", gotRegistry, tt.wantRegistry) + } + if gotRepository != tt.wantRepository { + t.Fatalf("repository mismatch: got %q, want %q", gotRepository, tt.wantRepository) + } + }) + } +} + // TestCapsuleManager: // - Verifies the CapsuleManager's functionality, including adding, retrieving, and attaching Resource Capsules. // - Setup: Initializes a CapsuleManager instance. @@ -348,4 +400,4 @@ func TestNetworkPingCLI(t *testing.T) { if err == nil { t.Errorf("Expected CLI ping to fail for non-existent network, but it succeeded") } -} \ No newline at end of file +} From d22da255a7db930be07256c3ddd80cdb8bcc6e12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:15:06 +0000 Subject: [PATCH 2/9] Refine loopback registry handling after review feedback Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/63030a95-e379-4aa4-81af-d71cc883fe9c Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 963de8c..5a823e1 100644 --- a/main.go +++ b/main.go @@ -625,7 +625,7 @@ func resolveRegistry(imageName string) (string, string) { } func registryURLForHost(host string) string { - if host == "localhost" || strings.HasPrefix(host, "localhost:") || host == "[::1]" || host == "::1" || strings.HasPrefix(host, "127.") { + if host == "localhost" || strings.HasPrefix(host, "localhost:") || host == "[::1]" || strings.HasPrefix(host, "127.") { return fmt.Sprintf("http://%s/v2/", host) } return fmt.Sprintf("https://%s/v2/", host) From eb6be8a15c7826f56a9cff79243e33470c3c6cde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:16:50 +0000 Subject: [PATCH 3/9] Harden registry host parsing for loopback addresses Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/63030a95-e379-4aa4-81af-d71cc883fe9c Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main.go | 21 +++++++++++++++++++-- main_test.go | 12 ++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 5a823e1..8167ce5 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "net" "os" "os/exec" "path/filepath" @@ -615,7 +616,7 @@ func resolveRegistry(imageName string) (string, string) { parts := strings.SplitN(imageName, "/", 2) if len(parts) == 2 { host := parts[0] - if host == "localhost" || strings.Contains(host, ".") || strings.Contains(host, ":") { + if strings.Contains(host, ".") || strings.Contains(host, ":") || host == "localhost" { registryURL = registryURLForHost(host) repo = parts[1] } @@ -625,12 +626,28 @@ func resolveRegistry(imageName string) (string, string) { } func registryURLForHost(host string) string { - if host == "localhost" || strings.HasPrefix(host, "localhost:") || host == "[::1]" || strings.HasPrefix(host, "127.") { + if isLocalRegistryHost(host) { return fmt.Sprintf("http://%s/v2/", host) } return fmt.Sprintf("https://%s/v2/", host) } +func isLocalRegistryHost(host string) bool { + if host == "localhost" || strings.HasPrefix(host, "localhost:") { + return true + } + + normalizedHost := host + if parsedHost, _, err := net.SplitHostPort(host); err == nil { + normalizedHost = parsedHost + } + normalizedHost = strings.TrimPrefix(normalizedHost, "[") + normalizedHost = strings.TrimSuffix(normalizedHost, "]") + + ip := net.ParseIP(normalizedHost) + return ip != nil && ip.IsLoopback() +} + func initializeBaseLayer(baseLayerPath string) error { // Create essential directories in the base layer dirs := []string{"/bin", "/dev", "/etc", "/proc", "/sys", "/tmp"} diff --git a/main_test.go b/main_test.go index 0ffb429..5be0eb0 100644 --- a/main_test.go +++ b/main_test.go @@ -122,6 +122,12 @@ func TestResolveRegistry(t *testing.T) { wantRegistry: "https://ghcr.io/v2/", wantRepository: "alpine:latest", }, + { + name: "short image with dot still uses default ghcr", + imageName: "my.image:latest", + wantRegistry: "https://ghcr.io/v2/", + wantRepository: "my.image:latest", + }, { name: "explicit ghcr host", imageName: "ghcr.io/j143/basic-docker-engine:latest", @@ -146,6 +152,12 @@ func TestResolveRegistry(t *testing.T) { wantRegistry: "http://127.0.0.1:5000/v2/", wantRepository: "alpine:latest", }, + { + name: "ipv6 loopback local registry over http", + imageName: "[::1]:5000/alpine:latest", + wantRegistry: "http://[::1]:5000/v2/", + wantRepository: "alpine:latest", + }, } for _, tt := range tests { From 91706e076d656d8b37b0f689df960d57d16de4e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:18:05 +0000 Subject: [PATCH 4/9] Normalize import grouping in main registry logic Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/63030a95-e379-4aa4-81af-d71cc883fe9c Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 8167ce5..ab12ec4 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "net" "os" "os/exec" @@ -14,6 +13,8 @@ import ( "strings" "syscall" "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Environment detection From ffd1c980eda20060f32a6fda3f77e80057f2ef2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 11:47:38 +0000 Subject: [PATCH 5/9] Add simple AKS deployment and verification workflow Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/d8915ba8-c732-47bd-bfb1-a2e12067f631 Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- .github/workflows/azure-aks-verify.yml | 148 +++++++++++++++++++++++++ README.md | 26 +++++ 2 files changed, 174 insertions(+) create mode 100644 .github/workflows/azure-aks-verify.yml diff --git a/.github/workflows/azure-aks-verify.yml b/.github/workflows/azure-aks-verify.yml new file mode 100644 index 0000000..6c70bf9 --- /dev/null +++ b/.github/workflows/azure-aks-verify.yml @@ -0,0 +1,148 @@ +name: Deploy and Verify on Azure AKS + +on: + workflow_dispatch: + inputs: + resource_group: + description: Azure resource group containing AKS + required: true + type: string + aks_cluster: + description: AKS cluster name + required: true + type: string + +permissions: + id-token: write + contents: read + +jobs: + deploy-and-verify: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + NAMESPACE: capsule-test-${{ github.run_id }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '^1.24' + cache: true + + - name: Build binary + run: | + go build -v -o basic-docker . + chmod +x basic-docker + sudo mv basic-docker /usr/local/bin/ + which basic-docker + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Set AKS context + run: | + az aks get-credentials \ + --resource-group "${{ inputs.resource_group }}" \ + --name "${{ inputs.aks_cluster }}" \ + --overwrite-existing + kubectl cluster-info + kubectl get nodes + + - name: Create test resources in AKS + run: | + kubectl create namespace "$NAMESPACE" + kubectl apply -f k8s/crd-resourcecapsule.yaml + kubectl wait --for=condition=established --timeout=60s crd/resourcecapsules.capsules.docker.io + + cat < /tmp/capsules/test-config + basic-docker k8s-capsule create test-config 1.0 /tmp/capsules/test-config + + - name: Verify volume behavior with existing tests + run: | + go test -v -run TestAttachCapsuleToDeployment + + - name: Verify CRD behavior with existing tests + run: | + go test -v -run TestResourceCapsule + + - name: Show AKS state on failure + if: failure() + run: | + kubectl get all -n "$NAMESPACE" || true + kubectl get resourcecapsules -n "$NAMESPACE" || true + kubectl get deployment test-app -n "$NAMESPACE" -o yaml || true + + - name: Cleanup AKS test namespace + if: always() + run: | + kubectl delete namespace "$NAMESPACE" --ignore-not-found=true diff --git a/README.md b/README.md index d48dfc1..970a33b 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,32 @@ This is a **teaching/runtime prototype** designed for: - Root privileges for namespace operations - Optional: Kubernetes cluster for CRD features +## Simple Azure deployment and verification (AKS) + +This repository includes a manual GitHub Actions workflow to run the project’s Kubernetes verification flow on Azure Kubernetes Service. + +Workflow file: +- `.github/workflows/azure-aks-verify.yml` + +What it does: +- Logs into Azure and connects to an AKS cluster +- Deploys test resources (ConfigMap, `ResourceCapsule` CRD object, Deployment) +- Runs project verification focused on: + - volume behavior (`TestAttachCapsuleToDeployment`) + - new ResourceCapsule CRD concepts (`TestResourceCapsule`) + +Required GitHub secrets: +- `AZURE_CLIENT_ID` +- `AZURE_TENANT_ID` +- `AZURE_SUBSCRIPTION_ID` + +How to run: +1. Open **Actions** → **Deploy and Verify on Azure AKS** +2. Click **Run workflow** +3. Provide: + - `resource_group` + - `aks_cluster` + ## Build steps ### build go code From 8f765d767374fad060c25be6f7772b980b29b0ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:46:16 +0000 Subject: [PATCH 6/9] Fix credentialed localhost registry resolution to use HTTP Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/9b259d2d-2dd2-4094-849b-5fb1f50355c2 Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main.go | 3 +++ main_test.go | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/main.go b/main.go index ab12ec4..c3628dc 100644 --- a/main.go +++ b/main.go @@ -617,6 +617,9 @@ func resolveRegistry(imageName string) (string, string) { parts := strings.SplitN(imageName, "/", 2) if len(parts) == 2 { host := parts[0] + if at := strings.LastIndex(host, "@"); at >= 0 && at < len(host)-1 { + host = host[at+1:] + } if strings.Contains(host, ".") || strings.Contains(host, ":") || host == "localhost" { registryURL = registryURLForHost(host) repo = parts[1] diff --git a/main_test.go b/main_test.go index 5be0eb0..8293c65 100644 --- a/main_test.go +++ b/main_test.go @@ -158,6 +158,12 @@ func TestResolveRegistry(t *testing.T) { wantRegistry: "http://[::1]:5000/v2/", wantRepository: "alpine:latest", }, + { + name: "credentialed local registry over http", + imageName: "user:password@localhost:5000/alpine", + wantRegistry: "http://localhost:5000/v2/", + wantRepository: "alpine", + }, } for _, tt := range tests { From b439617f2d4d48274359ebbb27356af060344b69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:47:45 +0000 Subject: [PATCH 7/9] Adjust credential host stripping to handle all @ positions Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/9b259d2d-2dd2-4094-849b-5fb1f50355c2 Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index c3628dc..744209c 100644 --- a/main.go +++ b/main.go @@ -617,7 +617,7 @@ func resolveRegistry(imageName string) (string, string) { parts := strings.SplitN(imageName, "/", 2) if len(parts) == 2 { host := parts[0] - if at := strings.LastIndex(host, "@"); at >= 0 && at < len(host)-1 { + if at := strings.LastIndex(host, "@"); at >= 0 { host = host[at+1:] } if strings.Contains(host, ".") || strings.Contains(host, ":") || host == "localhost" { From 2d1fcf2fe3d2d8312b926de027b70749db1935ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:48:58 +0000 Subject: [PATCH 8/9] Add edge-case test for @ in registry credentials Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/9b259d2d-2dd2-4094-849b-5fb1f50355c2 Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main_test.go b/main_test.go index 8293c65..ea008a5 100644 --- a/main_test.go +++ b/main_test.go @@ -164,6 +164,12 @@ func TestResolveRegistry(t *testing.T) { wantRegistry: "http://localhost:5000/v2/", wantRepository: "alpine", }, + { + name: "credentialed local registry with @ in username over http", + imageName: "user@domain:password@localhost:5000/alpine:latest", + wantRegistry: "http://localhost:5000/v2/", + wantRepository: "alpine:latest", + }, } for _, tt := range tests { From ab520aff746d0a48c9ee0ee7aa7ac11e6c08e19c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:50:12 +0000 Subject: [PATCH 9/9] Use placeholder credentials in registry resolution tests Agent-Logs-Url: https://github.com/j143/basic-docker-engine/sessions/9b259d2d-2dd2-4094-849b-5fb1f50355c2 Co-authored-by: j143 <53068787+j143@users.noreply.github.com> --- main_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main_test.go b/main_test.go index ea008a5..912fb5d 100644 --- a/main_test.go +++ b/main_test.go @@ -160,13 +160,13 @@ func TestResolveRegistry(t *testing.T) { }, { name: "credentialed local registry over http", - imageName: "user:password@localhost:5000/alpine", + imageName: "testuser:testpass@localhost:5000/alpine", wantRegistry: "http://localhost:5000/v2/", wantRepository: "alpine", }, { name: "credentialed local registry with @ in username over http", - imageName: "user@domain:password@localhost:5000/alpine:latest", + imageName: "testuser@example.com:testpass@localhost:5000/alpine:latest", wantRegistry: "http://localhost:5000/v2/", wantRepository: "alpine:latest", },