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 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..744209c 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,16 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" "syscall" "time" - "runtime" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -146,7 +148,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 +187,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 +232,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 +511,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 +528,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 +551,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 +610,48 @@ 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 at := strings.LastIndex(host, "@"); at >= 0 { + host = host[at+1:] + } + if strings.Contains(host, ".") || strings.Contains(host, ":") || host == "localhost" { + registryURL = registryURLForHost(host) + repo = parts[1] + } + } + + return registryURL, repo +} + +func registryURLForHost(host string) string { + 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"} @@ -754,7 +791,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 +799,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 +815,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 +827,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 +846,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 +1276,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 +1293,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 +1314,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 +1322,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 +1330,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 +1341,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 +1371,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 +1383,7 @@ func runDockerCapsuleBenchmark() { return } defer os.Remove(testFile) - + // Benchmark capsule access iterations := 10000 start := time.Now() @@ -1358,7 +1395,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 +1403,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 +1435,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 +1513,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 +1523,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 +1622,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 +1645,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 +1669,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 +1692,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 +1706,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 +1716,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 +1744,7 @@ func handleMonitoringCommand() { return } containerID := os.Args[3] - + showMonitoringCorrelation(containerID) default: @@ -1721,7 +1758,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 +1766,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 +1774,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 +1812,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 +1860,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 +1876,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..912fb5d 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,82 @@ 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: "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", + 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", + }, + { + name: "ipv6 loopback local registry over http", + imageName: "[::1]:5000/alpine:latest", + wantRegistry: "http://[::1]:5000/v2/", + wantRepository: "alpine:latest", + }, + { + name: "credentialed local registry over http", + imageName: "testuser:testpass@localhost:5000/alpine", + wantRegistry: "http://localhost:5000/v2/", + wantRepository: "alpine", + }, + { + name: "credentialed local registry with @ in username over http", + imageName: "testuser@example.com:testpass@localhost:5000/alpine:latest", + wantRegistry: "http://localhost: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 +424,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 +}