From 4b4e3899d6ae266aaee0c0fdf65e2d7ebb304673 Mon Sep 17 00:00:00 2001 From: Janardhan Pulivarthi Date: Sun, 13 Apr 2025 12:04:36 +0000 Subject: [PATCH 1/4] add registry support --- .basic-docker-config | 3 ++ main.go | 67 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .basic-docker-config diff --git a/.basic-docker-config b/.basic-docker-config new file mode 100644 index 0000000..baa519d --- /dev/null +++ b/.basic-docker-config @@ -0,0 +1,3 @@ +# Configuration file for basic-docker +# Default registry URL +registryURL=https://registry.example.com/images/ \ No newline at end of file diff --git a/main.go b/main.go index eeff750..0c8b2a7 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "encoding/json" "fmt" "os" @@ -34,6 +35,42 @@ type ImageLayer struct { AppLayerPath string } +// Load configuration from a .basic-docker-config file +func loadConfig() map[string]string { + config := map[string]string{ + "registryURL": "https://registry.example.com/images/", // Default registry URL + } + + configFile := filepath.Join(os.Getenv("HOME"), ".basic-docker-config") + if _, err := os.Stat(configFile); err == nil { + file, err := os.Open(configFile) + if err != nil { + fmt.Printf("Warning: Failed to open config file: %v\n", err) + return config + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue // Skip empty lines and comments + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + config[key] = value + } + } + if err := scanner.Err(); err != nil { + fmt.Printf("Warning: Failed to read config file: %v\n", err) + } + } + + return config +} + // To initialize the directories func initDirectories() error { dirs := []string{ @@ -813,11 +850,29 @@ func atoi(s string) int { return result } +// Update fetchImage to fetch the image from a registry func fetchImage(imageName string) error { - // Placeholder function for fetching an image - fmt.Printf("Fetching image '%s'...\n", imageName) - // Simulate fetching the image - time.Sleep(2 * time.Second) - fmt.Printf("Image '%s' fetched successfully.\n", imageName) - return nil + fmt.Printf("Fetching image '%s' from registry...\n", imageName) + + // Simulate fetching the image from a registry + registryURL := "https://registry.example.com/images/" + imageName + fmt.Printf("Connecting to registry at %s\n", registryURL) + + // Simulate download process + time.Sleep(2 * time.Second) + + // Create a directory for the image + imagePath := filepath.Join(imagesDir, imageName) + if err := os.MkdirAll(imagePath, 0755); err != nil { + return fmt.Errorf("failed to create image directory: %v", err) + } + + // Simulate saving the image to the directory + imageFile := filepath.Join(imagePath, "image.tar") + if err := os.WriteFile(imageFile, []byte("dummy image content"), 0644); err != nil { + return fmt.Errorf("failed to save image: %v", err) + } + + fmt.Printf("Image '%s' fetched and saved successfully.\n", imageName) + return nil } From 74c32460fa4ee9655b03cf9f828c09cea23b8035 Mon Sep 17 00:00:00 2001 From: Janardhan Pulivarthi Date: Sun, 13 Apr 2025 12:48:57 +0000 Subject: [PATCH 2/4] add logic to download and extract the image file --- .basic-docker-config | 2 +- main.go | 206 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 163 insertions(+), 45 deletions(-) diff --git a/.basic-docker-config b/.basic-docker-config index baa519d..e50ea99 100644 --- a/.basic-docker-config +++ b/.basic-docker-config @@ -1,3 +1,3 @@ # Configuration file for basic-docker # Default registry URL -registryURL=https://registry.example.com/images/ \ No newline at end of file +registryURL=https://registry.hub.docker.com/v2/repositories/ \ No newline at end of file diff --git a/main.go b/main.go index 0c8b2a7..026b05f 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,12 @@ package main import ( + "archive/tar" "bufio" "encoding/json" "fmt" + "io" + "net/http" "os" "os/exec" "path/filepath" @@ -37,37 +40,45 @@ type ImageLayer struct { // Load configuration from a .basic-docker-config file func loadConfig() map[string]string { - config := map[string]string{ - "registryURL": "https://registry.example.com/images/", // Default registry URL - } + config := map[string]string{} // Initialize an empty configuration map - configFile := filepath.Join(os.Getenv("HOME"), ".basic-docker-config") - if _, err := os.Stat(configFile); err == nil { - file, err := os.Open(configFile) - if err != nil { - fmt.Printf("Warning: Failed to open config file: %v\n", err) - return config + configFile := ".basic-docker-config" // Use the config file in the current directory + fmt.Printf("Debug: Looking for config file at %s\n", configFile) + + if _, err := os.Stat(configFile); err != nil { + if os.IsNotExist(err) { + fmt.Printf("Error: Config file not found at %s\n", configFile) + } else { + fmt.Printf("Error: Failed to access config file: %v\n", err) } - defer file.Close() + return config + } - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue // Skip empty lines and comments - } - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - config[key] = value - } + file, err := os.Open(configFile) + if err != nil { + fmt.Printf("Error: Failed to open config file: %v\n", err) + return config + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue // Skip empty lines and comments } - if err := scanner.Err(); err != nil { - fmt.Printf("Warning: Failed to read config file: %v\n", err) + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + config[key] = value } } + if err := scanner.Err(); err != nil { + fmt.Printf("Error: Failed to read config file: %v\n", err) + } + fmt.Printf("Debug: Loaded config: %v\n", config) return config } @@ -155,6 +166,16 @@ func main() { printSystemInfo() case "exec": execCommand() + case "delete": + if len(os.Args) < 3 { + fmt.Println("Error: Image name required for delete") + os.Exit(1) + } + imageName := os.Args[2] + if err := deleteImage(imageName); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } default: fmt.Printf("Unknown command: %s\n", os.Args[1]) printUsage() @@ -169,6 +190,7 @@ func printUsage() { fmt.Println(" basic-docker images - List available images") fmt.Println(" basic-docker info - Show system information") fmt.Println(" basic-docker exec [args...] - Execute a command in a running container") + fmt.Println(" basic-docker delete - Delete a Docker image") } func printSystemInfo() { @@ -274,23 +296,22 @@ func run() { os.Exit(1) } - // Fix deadlock by adding a timeout mechanism + // Check if a command is provided if len(os.Args) < 4 { - fmt.Println("No command provided. Keeping the container process alive with a timeout.") - timeout := time.After(10 * time.Minute) // Set a timeout of 10 minutes - select { - case <-timeout: - fmt.Println("Timeout reached. Exiting container process.") - os.Exit(0) - } + fmt.Println("Error: No command provided to run inside the container.") + os.Exit(1) } + // Extract the command and its arguments + command := os.Args[3] + args := os.Args[4:] + // Update the fallback logic to avoid using unshare entirely in limited isolation if hasNamespacePrivileges && !inContainer { // Full isolation approach for pure Linux environments - runWithNamespaces(containerID, rootfs, os.Args[2], os.Args[3:]) + runWithNamespaces(containerID, rootfs, command, args) } else { - runWithoutNamespaces(containerID, rootfs, os.Args[2], os.Args[3:]) + runWithoutNamespaces(containerID, rootfs, command, args) } fmt.Printf("Container %s exited\n", containerID) @@ -852,27 +873,124 @@ func atoi(s string) int { // Update fetchImage to fetch the image from a registry func fetchImage(imageName string) error { - fmt.Printf("Fetching image '%s' from registry...\n", imageName) + // Load configuration from a .basic-docker-config file + config := loadConfig() + + // Fetch the registry URL from the configuration + registryURL, exists := config["registryURL"] + if !exists { + fmt.Println("Error: registryURL not found in configuration.") + os.Exit(1) + } - // Simulate fetching the image from a registry - registryURL := "https://registry.example.com/images/" + imageName - fmt.Printf("Connecting to registry at %s\n", registryURL) + // Fetch the image from the configured Docker registry + fmt.Printf("Fetching image '%s' from Docker registry...\n", imageName) + fmt.Printf("Connecting to registry at %s%s\n", registryURL, imageName) - // Simulate download process - time.Sleep(2 * time.Second) + // Perform an HTTP GET request to fetch the image + resp, err := http.Get(fmt.Sprintf("%s%s", registryURL, imageName)) + if err != nil { + return fmt.Errorf("Failed to fetch image '%s': %v", imageName, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Failed to fetch image '%s': received status code %d", imageName, resp.StatusCode) + } // Create a directory for the image imagePath := filepath.Join(imagesDir, imageName) if err := os.MkdirAll(imagePath, 0755); err != nil { - return fmt.Errorf("failed to create image directory: %v", err) + return fmt.Errorf("Failed to create image directory: %v", err) } - // Simulate saving the image to the directory + // Save the image data to a file imageFile := filepath.Join(imagePath, "image.tar") - if err := os.WriteFile(imageFile, []byte("dummy image content"), 0644); err != nil { - return fmt.Errorf("failed to save image: %v", err) + outFile, err := os.Create(imageFile) + if err != nil { + return fmt.Errorf("Failed to create image file: %v", err) + } + defer outFile.Close() + + if _, err := io.Copy(outFile, resp.Body); err != nil { + return fmt.Errorf("Failed to save image data: %v", err) } fmt.Printf("Image '%s' fetched and saved successfully.\n", imageName) + + // Load the image into the container runtime + fmt.Printf("Loading image '%s' into the container runtime...\n", imageName) + if err := extractTar(imageFile, imagePath); err != nil { + return fmt.Errorf("Failed to load image '%s': %v", imageName, err) + } + fmt.Printf("Image '%s' loaded successfully.\n", imageName) + + return nil +} + +func deleteImage(imageName string) error { + // Construct the image path + imagePath := filepath.Join(imagesDir, imageName) + + // Check if the image exists + if _, err := os.Stat(imagePath); os.IsNotExist(err) { + return fmt.Errorf("Image '%s' does not exist", imageName) + } + + // Remove the image directory + if err := os.RemoveAll(imagePath); err != nil { + return fmt.Errorf("Failed to delete image '%s': %v", imageName) + } + + fmt.Printf("Image '%s' deleted successfully.\n", imageName) + return nil +} + +func extractTar(tarFile, destDir string) error { + // Open the tar file + file, err := os.Open(tarFile) + if err != nil { + return fmt.Errorf("failed to open tar file: %v", err) + } + defer file.Close() + + // Create a tar reader + tarReader := tar.NewReader(file) + + // Extract files from the tar archive + for { + header, err := tarReader.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + return fmt.Errorf("failed to read tar header: %v", err) + } + + // Determine the target path + targetPath := filepath.Join(destDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + // Create directories + if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + case tar.TypeReg: + // Create files + outFile, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer outFile.Close() + + if _, err := io.Copy(outFile, tarReader); err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + default: + return fmt.Errorf("unsupported tar entry type: %v", header.Typeflag) + } + } + return nil } From 88f0686a55dd9dc544dd48a0b9d6bd67dfed4ac4 Mon Sep 17 00:00:00 2001 From: Janardhan Pulivarthi Date: Sun, 13 Apr 2025 14:03:09 +0000 Subject: [PATCH 3/4] Fix Docker authentication and manifest fetching issues --- .basic-docker-config | 2 +- main.go | 165 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 136 insertions(+), 31 deletions(-) diff --git a/.basic-docker-config b/.basic-docker-config index e50ea99..bfdcded 100644 --- a/.basic-docker-config +++ b/.basic-docker-config @@ -1,3 +1,3 @@ # Configuration file for basic-docker # Default registry URL -registryURL=https://registry.hub.docker.com/v2/repositories/ \ No newline at end of file +registryURL=https://registry.hub.docker.com/v2/repositories/ diff --git a/main.go b/main.go index 026b05f..0e3c19e 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bufio" "encoding/json" + "encoding/base64" "fmt" "io" "net/http" @@ -871,60 +872,164 @@ func atoi(s string) int { return result } -// Update fetchImage to fetch the image from a registry +// Update fetchImage to use Docker Registry HTTP API v2 func fetchImage(imageName string) error { // Load configuration from a .basic-docker-config file config := loadConfig() - // Fetch the registry URL from the configuration - registryURL, exists := config["registryURL"] - if !exists { - fmt.Println("Error: registryURL not found in configuration.") - os.Exit(1) + // Split the image name into repository and tag + parts := strings.Split(imageName, ":") + repository := parts[0] + tag := "latest" // Default to 'latest' if no tag is provided + if len(parts) > 1 { + tag = parts[1] + } + + // Adjust repository name for Docker Hub + if strings.HasPrefix(repository, "library/") { + repository = strings.TrimPrefix(repository, "library/") + } + + // Authenticate with the Docker registry to obtain a token + username, usernameExists := config["username"] + password, passwordExists := config["password"] + + if !usernameExists || !passwordExists { + return fmt.Errorf("Error: Docker registry credentials (username and password) are missing in the configuration.") + } + + authURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", repository) + + // Log the authentication URL and repository name for debugging + fmt.Printf("Debug: Authentication URL: %s\n", authURL) + fmt.Printf("Debug: Repository: %s\n", repository) + + req, err := http.NewRequest("GET", authURL, nil) + if err != nil { + return fmt.Errorf("Failed to create authentication request: %v", err) } - // Fetch the image from the configured Docker registry - fmt.Printf("Fetching image '%s' from Docker registry...\n", imageName) - fmt.Printf("Connecting to registry at %s%s\n", registryURL, imageName) + // Use the PAT as the password for authentication + req.SetBasicAuth(username, password) // Here, 'password' is the PAT - // Perform an HTTP GET request to fetch the image - resp, err := http.Get(fmt.Sprintf("%s%s", registryURL, imageName)) + authResp, err := http.DefaultClient.Do(req) if err != nil { - return fmt.Errorf("Failed to fetch image '%s': %v", imageName, err) + return fmt.Errorf("Failed to authenticate with Docker registry: %v", err) + } + defer authResp.Body.Close() + + if authResp.StatusCode != http.StatusOK { + return fmt.Errorf("Failed to authenticate with Docker registry: received status code %d", authResp.StatusCode) + } + + // Log the full response body for debugging + authRespBody, err := io.ReadAll(authResp.Body) + if err != nil { + return fmt.Errorf("Failed to read authentication response body: %v", err) + } + fmt.Printf("Debug: Authentication response body: %s\n", string(authRespBody)) + + // Decode the authentication response + var authData map[string]interface{} + if err := json.Unmarshal(authRespBody, &authData); err != nil { + return fmt.Errorf("Failed to decode authentication response: %v", err) + } + + token, ok := authData["token"].(string) + if !ok { + return fmt.Errorf("Failed to retrieve token from authentication response") + } + + // Decode and log the token payload for debugging (if it's a JWT) + parts = strings.Split(token, ".") + if len(parts) == 3 { + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err == nil { + fmt.Printf("Debug: Token payload: %s\n", string(payload)) + } else { + fmt.Printf("Warning: Failed to decode token payload: %v\n", err) + } + } + + // Log the full manifest request for debugging + manifestURL := fmt.Sprintf("https://registry-1.docker.io/v2/%s/manifests/%s", repository, tag) + fmt.Printf("Debug: Manifest request URL: %s\n", manifestURL) + fmt.Printf("Debug: Authorization header: Bearer %s\n", token) + + req, err = http.NewRequest("GET", manifestURL, nil) + if err != nil { + return fmt.Errorf("Failed to create manifest request: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("Failed to fetch manifest: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Failed to fetch image '%s': received status code %d", imageName, resp.StatusCode) + respBody, _ := io.ReadAll(resp.Body) + fmt.Printf("Debug: Manifest response body: %s\n", string(respBody)) + return fmt.Errorf("Failed to fetch manifest: received status code %d", resp.StatusCode) + } + + var manifest map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { + return fmt.Errorf("Failed to decode manifest: %v", err) + } + + fmt.Printf("Manifest fetched successfully: %v\n", manifest) + + // Extract and download layers + layers, ok := manifest["layers"].([]interface{}) + if !ok { + return fmt.Errorf("Invalid manifest format: missing layers") } - // Create a directory for the image imagePath := filepath.Join(imagesDir, imageName) if err := os.MkdirAll(imagePath, 0755); err != nil { return fmt.Errorf("Failed to create image directory: %v", err) } - // Save the image data to a file - imageFile := filepath.Join(imagePath, "image.tar") - outFile, err := os.Create(imageFile) - if err != nil { - return fmt.Errorf("Failed to create image file: %v", err) - } - defer outFile.Close() + for _, layer := range layers { + layerMap, ok := layer.(map[string]interface{}) + if !ok { + return fmt.Errorf("Invalid layer format") + } - if _, err := io.Copy(outFile, resp.Body); err != nil { - return fmt.Errorf("Failed to save image data: %v", err) - } + layerURL, ok := layerMap["url"].(string) + if !ok { + return fmt.Errorf("Invalid layer format: missing URL") + } - fmt.Printf("Image '%s' fetched and saved successfully.\n", imageName) + fmt.Printf("Downloading layer from %s\n", layerURL) + + layerResp, err := http.Get(layerURL) + if err != nil { + return fmt.Errorf("Failed to download layer: %v", err) + } + defer layerResp.Body.Close() + + if layerResp.StatusCode != http.StatusOK { + return fmt.Errorf("Failed to download layer: received status code %d", layerResp.StatusCode) + } - // Load the image into the container runtime - fmt.Printf("Loading image '%s' into the container runtime...\n", imageName) - if err := extractTar(imageFile, imagePath); err != nil { - return fmt.Errorf("Failed to load image '%s': %v", imageName, err) + layerFile := filepath.Join(imagePath, filepath.Base(layerURL)) + outFile, err := os.Create(layerFile) + if err != nil { + return fmt.Errorf("Failed to create layer file: %v", err) + } + defer outFile.Close() + + if _, err := io.Copy(outFile, layerResp.Body); err != nil { + return fmt.Errorf("Failed to save layer: %v", err) + } + + fmt.Printf("Layer saved to %s\n", layerFile) } - fmt.Printf("Image '%s' loaded successfully.\n", imageName) + fmt.Printf("Image '%s' fetched and saved successfully.\n", imageName) return nil } From 1228f8059ec08d15c9649ac435a48bc8eede2bce Mon Sep 17 00:00:00 2001 From: Janardhan Pulivarthi Date: Sun, 13 Apr 2025 14:07:08 +0000 Subject: [PATCH 4/4] update the image management diagram --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2da6ca0..79c0149 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,8 @@ graph TD Pull["Pull Function"] ListImages["ListImages Function"] ExtractLayer["extractLayer Function"] + Pull -->|Downloads| Layers + Layers -->|Processes| ExtractLayer end subgraph Main Application