From 6d1ebc1fbc8a2e7edf3a3715440c4ee801dd3008 Mon Sep 17 00:00:00 2001 From: bupd Date: Tue, 2 Jul 2024 01:47:12 +0530 Subject: [PATCH 1/8] Refactor processing of URL and File paths - This commit adds simplified functions to handle file processing of url - Also handles both processing in separate functions Signed-off-by: bupd --- config.toml | 6 ++-- main.go | 102 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 65 insertions(+), 43 deletions(-) diff --git a/config.toml b/config.toml index 93b9ebd..1fa3a4f 100644 --- a/config.toml +++ b/config.toml @@ -1,12 +1,12 @@ # Wether to us the built-in Zot registry or not -bring_own_registry = true +bring_own_registry = false # URL of own registry own_registry_adr = "127.0.0.1:5000" # URL of remote registry OR local file path -# url_or_file = "https://demo.goharbor.io/v2/myproject/album-server" - url_or_file = "http://localhost:5001/v2/library/busybox" +url_or_file = "https://demo.goharbor.io/v2/myproject/album-server" + # url_or_file = "http://localhost:5001/v2/library/busybox" # For testing purposes : # https://demo.goharbor.io/v2/myproject/album-server diff --git a/main.go b/main.go index 0201807..a497b04 100644 --- a/main.go +++ b/main.go @@ -31,8 +31,7 @@ import ( func main() { err := run() if err != nil { - fmt.Println(err) - os.Exit(1) + log.Fatalf("Error running satellite: %v", err) } } @@ -83,14 +82,6 @@ func run() error { if bringOwnRegistry { registryAdr := viper.GetString("own_registry_adr") - // Validate registryAdr format - // matched, err := regexp.MatchString(`^127\.0\.0\.1:\d{1,5}$`, registryAdr) - // if err != nil { - // return fmt.Errorf("error validating registry address: %w", err) - // } - // if matched { - // return fmt.Errorf("invalid registry address format: %s", registryAdr) - // } os.Setenv("ZOT_URL", registryAdr) fmt.Println("Registry URL set to:", registryAdr) } else { @@ -108,42 +99,22 @@ func run() error { } input := viper.GetString("url_or_file") + // Attempt to parse the input as a URL parsedURL, err := url.Parse(input) + // If parsing as URL fails or no scheme detected, treat it as a file path if err != nil || parsedURL.Scheme == "" { - if strings.ContainsAny(input, "\\:*?\"<>|") { - fmt.Println("Path contains invalid characters. Please check the configuration.") - return fmt.Errorf("invalid file path") - } - dir, err := os.Getwd() + // Treat input as a file path + err = processFilePath(input, fetcher) if err != nil { - fmt.Println("Error getting current directory:", err) - return err - } - absPath := filepath.Join(dir, input) - if _, err := os.Stat(absPath); os.IsNotExist(err) { - fmt.Println("No URL or file found. Please check the configuration.") - return fmt.Errorf("file not found") + log.Fatalf("Error in processing file path: %v", err) } - fmt.Println("Input is a valid file path.") - fetcher = store.FileImageListFetcher(input) - os.Setenv("USER_INPUT", input) } else { - fmt.Println("Input is a valid URL.") + // Process input as a URL fetcher = store.RemoteImageListFetcher(input) - os.Setenv("USER_INPUT", input) - parts := strings.SplitN(input, "://", 2) - scheme := parts[0] + "://" - os.Setenv("SCHEME", scheme) - hostAndPath := parts[1] - hostParts := strings.Split(hostAndPath, "/") - host := hostParts[0] - os.Setenv("HOST", host) - apiVersion := hostParts[1] - os.Setenv("API_VERSION", apiVersion) - registry := hostParts[2] - os.Setenv("REGISTRY", registry) - repository := hostParts[3] - os.Setenv("REPOSITORY", repository) + err = processURL(input) + if err != nil { + log.Fatalf("Error in processing URL: %v", err) + } } err = godotenv.Load() @@ -165,3 +136,54 @@ func run() error { } return nil } + +func processFilePath(input string, fetcher store.ImageFetcher) error { + // Check for invalid characters in file path + if strings.ContainsAny(input, "\\:*?\"<>|") { + fmt.Println("Path contains invalid characters. Please check the configuration.") + return fmt.Errorf("invalid file path") + } + dir, err := os.Getwd() + if err != nil { + fmt.Println("Error getting current directory:", err) + return err + } + absPath := filepath.Join(dir, input) + if _, err := os.Stat(absPath); os.IsNotExist(err) { + fmt.Println("No URL or file found. Please check the configuration.") + return fmt.Errorf("file not found") + } + fmt.Println("Input is a valid file path.") + fetcher = store.FileImageListFetcher(input) + os.Setenv("USER_INPUT", input) + + return nil +} + +func processURL(input string) error { + fmt.Println("Input is a valid URL.") + + // Set environment variables + os.Setenv("USER_INPUT", input) + + // Extract URL components + parts := strings.SplitN(input, "://", 2) + scheme := parts[0] + "://" + os.Setenv("SCHEME", scheme) + + hostAndPath := parts[1] + hostParts := strings.Split(hostAndPath, "/") + host := hostParts[0] + os.Setenv("HOST", host) + + apiVersion := hostParts[1] + os.Setenv("API_VERSION", apiVersion) + + registry := hostParts[2] + os.Setenv("REGISTRY", registry) + + repository := hostParts[3] + os.Setenv("REPOSITORY", repository) + + return nil +} From f58e1e1301201b434aaabd2516ab0705289515e7 Mon Sep 17 00:00:00 2001 From: bupd Date: Tue, 2 Jul 2024 18:51:16 +0530 Subject: [PATCH 2/8] refactor image fetch over http Signed-off-by: bupd --- internal/store/file-fetch.go | 2 +- internal/store/http-fetch.go | 111 +++++++++++++++++++----------- internal/store/in-memory-store.go | 4 +- main.go | 8 +-- 4 files changed, 79 insertions(+), 46 deletions(-) diff --git a/internal/store/file-fetch.go b/internal/store/file-fetch.go index 3ff56af..b7f9945 100644 --- a/internal/store/file-fetch.go +++ b/internal/store/file-fetch.go @@ -24,7 +24,7 @@ type ImageData struct { Repositories []Repository `json:"repositories"` } -func (f *FileImageList) Type() string { +func (f *FileImageList) SourceType() string { return "File" } diff --git a/internal/store/http-fetch.go b/internal/store/http-fetch.go index 309287a..e6c9a25 100644 --- a/internal/store/http-fetch.go +++ b/internal/store/http-fetch.go @@ -14,95 +14,128 @@ import ( "github.com/google/go-containerregistry/pkg/crane" ) -type RemoteImageList struct { +// RemoteImageSource represents a source of images from a remote URL. +type RemoteImageSource struct { BaseURL string } +// TagListResponse represents the JSON structure for the tags list response. type TagListResponse struct { Name string `json:"name"` Tags []string `json:"tags"` } -func RemoteImageListFetcher(url string) *RemoteImageList { - return &RemoteImageList{ - BaseURL: url, - } +// NewRemoteImageSource creates a new RemoteImageSource instance. +func NewRemoteImageSource(url string) *RemoteImageSource { + return &RemoteImageSource{BaseURL: url} } -func (r *RemoteImageList) Type() string { +// SourceType returns the type of the image source as a string. +func (r *RemoteImageSource) SourceType() string { return "Remote" } -func (client *RemoteImageList) List(ctx context.Context) ([]Image, error) { - // Construct the URL for fetching tags - url := client.BaseURL + "/tags/list" +// FetchImages retrieves a list of images from the remote repository. +func (r *RemoteImageSource) List(ctx context.Context) ([]Image, error) { + url := r.BaseURL + "/tags/list" + authHeader, err := createAuthHeader() + if err != nil { + return nil, fmt.Errorf("error creating auth header: %w", err) + } + + body, err := fetchResponseBody(url, authHeader) + if err != nil { + return nil, fmt.Errorf("error fetching tags list: %w", err) + } + + images, err := parseTagsResponse(body) + if err != nil { + return nil, fmt.Errorf("error parsing tags response: %w", err) + } + + fmt.Println("Fetched", len(images), "images:", images) + return images, nil +} + +// FetchDigest fetches the digest for a specific image tag. +func (r *RemoteImageSource) GetDigest(ctx context.Context, tag string) (string, error) { + imageRef := fmt.Sprintf("%s:%s", r.BaseURL, tag) + imageRef = cleanImageReference(imageRef) - // Encode credentials for Basic Authentication + digest, err := fetchImageDigest(imageRef) + if err != nil { + return "", fmt.Errorf("error fetching digest for %s: %w", imageRef, err) + } + + return digest, nil +} + +// createAuthHeader generates the authorization header for HTTP requests. +func createAuthHeader() (string, error) { username := os.Getenv("HARBOR_USERNAME") password := os.Getenv("HARBOR_PASSWORD") + if username == "" || password == "" { + return "", fmt.Errorf("environment variables HARBOR_USERNAME or HARBOR_PASSWORD not set") + } auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + return "Basic " + auth, nil +} - // Create a new HTTP request +// fetchResponseBody makes an HTTP GET request and returns the response body. +func fetchResponseBody(url, authHeader string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } + req.Header.Set("Authorization", authHeader) - // Set the Authorization header - req.Header.Set("Authorization", "Basic "+auth) - - // Send the request - httpClient := &http.Client{} - resp, err := httpClient.Do(req) + client := &http.Client{} + resp, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("failed to fetch tags: %w", err) + return nil, fmt.Errorf("failed to fetch response: %w", err) } defer resp.Body.Close() - // Read the response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - // Unmarshal the JSON response - var tagListResponse TagListResponse - if err := json.Unmarshal(body, &tagListResponse); err != nil { + return body, nil +} + +// parseTagsResponse unmarshals the tags list response and constructs image references. +func parseTagsResponse(body []byte) ([]Image, error) { + var tagList TagListResponse + if err := json.Unmarshal(body, &tagList); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err) } - // Prepare a slice to store the images var images []Image - - // Iterate over the tags and construct the image references - for _, tag := range tagListResponse.Tags { - images = append(images, Image{ - Name: fmt.Sprintf("%s:%s", tagListResponse.Name, tag), - }) + for _, tag := range tagList.Tags { + images = append(images, Image{Name: fmt.Sprintf("%s:%s", tagList.Name, tag)}) } - fmt.Println("Fetched", len(images), "images :", images) + return images, nil } -func (client *RemoteImageList) GetDigest(ctx context.Context, tag string) (string, error) { - // Construct the image reference - imageRef := fmt.Sprintf("%s:%s", client.BaseURL, tag) - // Remove extra characters from the URL +// cleanImageReference cleans up the image reference string. +func cleanImageReference(imageRef string) string { imageRef = imageRef[strings.Index(imageRef, "//")+2:] - imageRef = strings.ReplaceAll(imageRef, "/v2", "") + return strings.ReplaceAll(imageRef, "/v2", "") +} - // Encode credentials for Basic Authentication +// fetchImageDigest retrieves the digest for an image reference. +func fetchImageDigest(imageRef string) (string, error) { username := os.Getenv("HARBOR_USERNAME") password := os.Getenv("HARBOR_PASSWORD") - // Use crane.Digest to get the digest of the image digest, err := crane.Digest(imageRef, crane.WithAuth(&authn.Basic{ Username: username, Password: password, }), crane.Insecure) if err != nil { - fmt.Printf("failed to fetch digest for %s: %v\n", imageRef, err) - return "", nil + return "", fmt.Errorf("failed to fetch digest: %w", err) } return digest, nil diff --git a/internal/store/in-memory-store.go b/internal/store/in-memory-store.go index de3a702..7b04cb6 100644 --- a/internal/store/in-memory-store.go +++ b/internal/store/in-memory-store.go @@ -28,7 +28,7 @@ type Storer interface { type ImageFetcher interface { List(ctx context.Context) ([]Image, error) GetDigest(ctx context.Context, tag string) (string, error) - Type() string + SourceType() string } func NewInMemoryStore(fetcher ImageFetcher) Storer { @@ -49,7 +49,7 @@ func (s *inMemoryStore) List(ctx context.Context) ([]Image, error) { } // Handle File and Remote fetcher types differently - switch s.fetcher.Type() { + switch s.fetcher.SourceType() { case "File": for _, img := range imageList { // Check if the image already exists in the store diff --git a/main.go b/main.go index a497b04..2aa1d08 100644 --- a/main.go +++ b/main.go @@ -104,17 +104,18 @@ func run() error { // If parsing as URL fails or no scheme detected, treat it as a file path if err != nil || parsedURL.Scheme == "" { // Treat input as a file path - err = processFilePath(input, fetcher) + err = processFilePath(input) if err != nil { log.Fatalf("Error in processing file path: %v", err) } + fetcher = store.FileImageListFetcher(input) } else { // Process input as a URL - fetcher = store.RemoteImageListFetcher(input) err = processURL(input) if err != nil { log.Fatalf("Error in processing URL: %v", err) } + fetcher = store.NewRemoteImageSource(input) } err = godotenv.Load() @@ -137,7 +138,7 @@ func run() error { return nil } -func processFilePath(input string, fetcher store.ImageFetcher) error { +func processFilePath(input string) error { // Check for invalid characters in file path if strings.ContainsAny(input, "\\:*?\"<>|") { fmt.Println("Path contains invalid characters. Please check the configuration.") @@ -154,7 +155,6 @@ func processFilePath(input string, fetcher store.ImageFetcher) error { return fmt.Errorf("file not found") } fmt.Println("Input is a valid file path.") - fetcher = store.FileImageListFetcher(input) os.Setenv("USER_INPUT", input) return nil From bc42eaef07fe37a58561d19bb3f1f015ccc417a3 Mon Sep 17 00:00:00 2001 From: bupd Date: Wed, 3 Jul 2024 04:40:16 +0530 Subject: [PATCH 3/8] better error handling Signed-off-by: bupd --- internal/store/file-fetch.go | 1 + internal/store/http-fetch.go | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/store/file-fetch.go b/internal/store/file-fetch.go index b7f9945..2c39555 100644 --- a/internal/store/file-fetch.go +++ b/internal/store/file-fetch.go @@ -73,5 +73,6 @@ func (client *FileImageList) List(ctx context.Context) ([]Image, error) { } func (client *FileImageList) GetDigest(ctx context.Context, tag string) (string, error) { + // TODO: Implement GetDigest for FileImageList return "Not implemented yet", nil } diff --git a/internal/store/http-fetch.go b/internal/store/http-fetch.go index e6c9a25..3d85c7e 100644 --- a/internal/store/http-fetch.go +++ b/internal/store/http-fetch.go @@ -45,12 +45,12 @@ func (r *RemoteImageSource) List(ctx context.Context) ([]Image, error) { body, err := fetchResponseBody(url, authHeader) if err != nil { - return nil, fmt.Errorf("error fetching tags list: %w", err) + return nil, fmt.Errorf("error fetching tags list from %s: %w", url, err) } images, err := parseTagsResponse(body) if err != nil { - return nil, fmt.Errorf("error parsing tags response: %w", err) + return nil, fmt.Errorf("error parsing tags response from %s: %w", url, err) } fmt.Println("Fetched", len(images), "images:", images) @@ -85,20 +85,20 @@ func createAuthHeader() (string, error) { func fetchResponseBody(url, authHeader string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, fmt.Errorf("failed to create request for %s: %w", url, err) } req.Header.Set("Authorization", authHeader) client := &http.Client{} resp, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("failed to fetch response: %w", err) + return nil, fmt.Errorf("failed to fetch response from %s: %w", url, err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response body from %s: %w", url, err) } return body, nil @@ -135,7 +135,7 @@ func fetchImageDigest(imageRef string) (string, error) { Password: password, }), crane.Insecure) if err != nil { - return "", fmt.Errorf("failed to fetch digest: %w", err) + return "", fmt.Errorf("failed to fetch digest for %s: %w", imageRef, err) } return digest, nil From 530cd8f8a3ef76745d1eebfcec91a8db7b4b9ef9 Mon Sep 17 00:00:00 2001 From: bupd Date: Wed, 3 Jul 2024 04:44:01 +0530 Subject: [PATCH 4/8] Refactor store and modularize code Signed-off-by: bupd --- internal/store/in-memory-store.go | 234 +++++++++++++++++------------- 1 file changed, 135 insertions(+), 99 deletions(-) diff --git a/internal/store/in-memory-store.go b/internal/store/in-memory-store.go index 7b04cb6..d64d2a2 100644 --- a/internal/store/in-memory-store.go +++ b/internal/store/in-memory-store.go @@ -9,28 +9,33 @@ import ( "github.com/google/go-containerregistry/pkg/crane" ) +// Image represents a container image with its digest and name. type Image struct { Digest string Name string } +// inMemoryStore stores images in memory and uses an ImageFetcher to manage images. type inMemoryStore struct { images map[string]string fetcher ImageFetcher } +// Storer is the interface for image storage operations. type Storer interface { List(ctx context.Context) ([]Image, error) - Add(ctx context.Context, digest string, image string) error - Remove(ctx context.Context, digest string, image string) error + Add(ctx context.Context, digest, image string) error + Remove(ctx context.Context, digest, image string) error } +// ImageFetcher is the interface for fetching images. type ImageFetcher interface { List(ctx context.Context) ([]Image, error) GetDigest(ctx context.Context, tag string) (string, error) SourceType() string } +// NewInMemoryStore creates a new in-memory store with the given ImageFetcher. func NewInMemoryStore(fetcher ImageFetcher) Storer { return &inMemoryStore{ images: make(map[string]string), @@ -38,123 +43,151 @@ func NewInMemoryStore(fetcher ImageFetcher) Storer { } } +// List retrieves and synchronizes the list of images from the fetcher. func (s *inMemoryStore) List(ctx context.Context) ([]Image, error) { - var imageList []Image - var change bool - - // Fetch images from the file/remote source + // fetch List of images imageList, err := s.fetcher.List(ctx) if err != nil { return nil, err } - // Handle File and Remote fetcher types differently + var changeDetected bool + switch s.fetcher.SourceType() { case "File": - for _, img := range imageList { - // Check if the image already exists in the store - if _, exists := s.images[img.Name]; !exists { - // Add the image to the store - s.AddImage(ctx, img.Name) - change = true - } else { - fmt.Printf("Image %s already exists in the store\n", img.Name) - } + changeDetected, err = s.handleFileSource(ctx, imageList) + case "Remote": + changeDetected, err = s.handleRemoteSource(ctx, imageList) + default: + return nil, fmt.Errorf("unknown source type") + } + if err != nil { + return nil, err + } + + if changeDetected { + fmt.Println("Changes detected in the store") + return s.getImageList(), nil + } else { + fmt.Println("No changes detected in the store") + return nil, nil + } +} + +// handleFileSource handles image from a file +func (s *inMemoryStore) handleFileSource(ctx context.Context, imageList []Image) (bool, error) { + var change bool + for _, img := range imageList { + // Check if the image already exists in the store + if _, exists := s.images[img.Name]; !exists { + // Add the image to the store + s.AddImage(ctx, img.Name) + change = true + } else { + fmt.Printf("Image %s already exists in the store\n", img.Name) } + } - // Iterate over s.images and remove any image that is not found in imageList - for image := range s.images { - found := false - for _, img := range imageList { - if img.Name == image { - found = true - break - } - } - if !found { - s.RemoveImage(ctx, image) - change = true + // Iterate over s.images and remove any image that is not found in imageList + for image := range s.images { + found := false + for _, img := range imageList { + if img.Name == image { + found = true + break } } - - // Empty and refill imageList with the contents from s.images - imageList = imageList[:0] - for name, digest := range s.images { - imageList = append(imageList, Image{Name: name, Digest: digest}) + if !found { + s.RemoveImage(ctx, image) + change = true } + } - // Print out the entire store for debugging purposes - fmt.Println("Current store:") - for image := range s.images { - fmt.Printf("Image: %s\n", image) - } + // Empty and refill imageList with the contents from s.images + imageList = imageList[:0] + for name, digest := range s.images { + imageList = append(imageList, Image{Name: name, Digest: digest}) + } - case "Remote": - // Trim the imageList elements to remove the project name from the image reference - for i, img := range imageList { - parts := strings.Split(img.Name, "/") - if len(parts) > 1 { - // Take the second part as the new Reference - imageList[i].Name = parts[1] - } - } - // iterate over imageList and call GetDigest for each tag - for _, img := range imageList { - // Split the image reference to get the tag - tagParts := strings.Split(img.Name, ":") - // Check if there is a tag part, min length is 1 char - if len(tagParts) < 2 { - fmt.Println("No tag part found in the image reference") - } - // Use the last part as the tag - tag := tagParts[len(tagParts)-1] - // Get the digest for the tag - digest, err := s.fetcher.GetDigest(ctx, tag) - if err != nil { - return nil, err - } + // Print out the entire store for debugging purposes + fmt.Println("Current store:") + for image := range s.images { + fmt.Printf("Image: %s\n", image) + } - // Check if the image exists and matches the digest - if !(s.checkImageAndDigest(digest, img.Name)) { - change = true - } + return change, nil +} +// handleRemoteSource handles images fetched from a remote source. +func (s *inMemoryStore) handleRemoteSource(ctx context.Context, imageList []Image) (bool, error) { + var change bool + // Trim the imageList elements to remove the project name from the image reference + for i, img := range imageList { + parts := strings.Split(img.Name, "/") + if len(parts) > 1 { + // Take the second part as the new Reference + imageList[i].Name = parts[1] } - - // Create imageMap filled with all images from remote imageList - imageMap := make(map[string]bool) - for _, img := range imageList { - imageMap[img.Name] = true + } + // iterate over imageList and call GetDigest for each tag + for _, img := range imageList { + // Split the image reference to get the tag + tagParts := strings.Split(img.Name, ":") + // Check if there is a tag part, min length is 1 char + if len(tagParts) < 2 { + fmt.Println("No tag part found in the image reference") } - - // Iterate over in memory store and remove any image that is not found in imageMap - for digest, image := range s.images { - if _, exists := imageMap[image]; !exists { - s.Remove(ctx, digest, image) - change = true - } + // Use the last part as the tag + tag := tagParts[len(tagParts)-1] + // Get the digest for the tag + digest, err := s.fetcher.GetDigest(ctx, tag) + if err != nil { + return false, err } - // Print out the entire store for debugging purposes - fmt.Println("Current store:") - for digest, imageRef := range s.images { - fmt.Printf("Digest: %s, Image: %s\n", digest, imageRef) + + // Check if the image exists and matches the digest + if !(s.checkImageAndDigest(digest, img.Name)) { + change = true } - // Empty and refill imageList with the contents from s.images - imageList = imageList[:0] - for _, name := range s.images { - imageList = append(imageList, Image{Digest: "", Name: name}) + } + + // Create imageMap filled with all images from remote imageList + imageMap := make(map[string]bool) + for _, img := range imageList { + imageMap[img.Name] = true + } + + // Iterate over in memory store and remove any image that is not found in imageMap + for digest, image := range s.images { + if _, exists := imageMap[image]; !exists { + s.Remove(ctx, digest, image) + change = true } + } + // Print out the entire store for debugging purposes + fmt.Println("Current store:") + for digest, imageRef := range s.images { + fmt.Printf("Digest: %s, Image: %s\n", digest, imageRef) + } + // Empty and refill imageList with the contents from s.images + imageList = imageList[:0] + for _, name := range s.images { + imageList = append(imageList, Image{Digest: "", Name: name}) } - if change { - fmt.Println("Changes detected in the store") - change = false - return imageList, nil - } else { - fmt.Println("No changes detected in the store") - return nil, nil + + return change, nil +} + +// getImageList converts the in-memory store to a list of Image structs. +func (s *inMemoryStore) getImageList() []Image { + var imageList []Image + // Empty and refill imageList with the contents from s.images + for _, name := range s.images { + imageList = append(imageList, Image{Digest: "", Name: name}) } + return imageList } func (s *inMemoryStore) Add(ctx context.Context, digest string, image string) error { @@ -170,13 +203,13 @@ func (s *inMemoryStore) Add(ctx context.Context, digest string, image string) er } } -func (s *inMemoryStore) AddImage(ctx context.Context, image string) error { +func (s *inMemoryStore) AddImage(ctx context.Context, image string) { // Add the image to the store s.images[image] = "" fmt.Printf("Added image: %s\n", image) - return nil } +// Removes the image from the store func (s *inMemoryStore) Remove(ctx context.Context, digest string, image string) error { // Check if the image exists in the store if _, exists := s.images[digest]; exists { @@ -190,11 +223,10 @@ func (s *inMemoryStore) Remove(ctx context.Context, digest string, image string) } } -func (s *inMemoryStore) RemoveImage(ctx context.Context, image string) error { - // Remove the image from the store +// Remove the image from the store +func (s *inMemoryStore) RemoveImage(ctx context.Context, image string) { delete(s.images, image) fmt.Printf("Removed image: %s\n", image) - return nil } // TODO: Rework complicated logic and add support for multiple repositories @@ -222,7 +254,11 @@ func (s *inMemoryStore) checkImageAndDigest(digest string, image string) bool { } } else { // Digest exists but does not match the current image reference - s.Remove(context.Background(), storeDigest, storeImage) + if err := s.Remove(context.Background(), storeDigest, storeImage); err != nil { + fmt.Errorf("Error: %w", err) + return false + } + s.Add(context.Background(), digest, image) return false } From 20bef9cd51c8a45b4147ff4fded36f047b24970c Mon Sep 17 00:00:00 2001 From: bupd Date: Wed, 3 Jul 2024 05:59:25 +0530 Subject: [PATCH 5/8] refactor replication logic & code Signed-off-by: bupd --- internal/replicate/replicate.go | 147 ++++++++++++------------------ internal/store/in-memory-store.go | 11 ++- 2 files changed, 69 insertions(+), 89 deletions(-) diff --git a/internal/replicate/replicate.go b/internal/replicate/replicate.go index aa73454..55c7b0c 100644 --- a/internal/replicate/replicate.go +++ b/internal/replicate/replicate.go @@ -13,41 +13,47 @@ import ( "github.com/google/go-containerregistry/pkg/crane" ) +// Replicator interface for image replication and deletion. type Replicator interface { - // Replicate copies images from the source registry to the local registry. Replicate(ctx context.Context, image string) error DeleteExtraImages(ctx context.Context, imgs []store.Image) error } +// BasicReplicator implements the Replicator interface. type BasicReplicator struct{} +// ImageInfo holds the name of an image. type ImageInfo struct { Name string `json:"name"` } +// Repository holds the repository name and associated images. type Repository struct { Repository string `json:"repository"` Images []ImageInfo `json:"images"` } +// RegistryInfo holds the registry URL and repositories information. type RegistryInfo struct { RegistryUrl string `json:"registryUrl"` Repositories []Repository `json:"repositories"` } +// NewReplicator creates a new BasicReplicator. func NewReplicator() Replicator { return &BasicReplicator{} } +// Replicate copies an image from the source registry to the local registry. func (r *BasicReplicator) Replicate(ctx context.Context, image string) error { source := getPullSource(image) - - if source != "" { - CopyImage(source) + if source == "" { + return fmt.Errorf("source not found for image: %s", image) } - return nil + return CopyImage(source) } +// stripPrefix removes the prefix from the image name. func stripPrefix(imageName string) string { if idx := strings.Index(imageName, ":"); idx != -1 { return imageName[idx+1:] @@ -55,38 +61,27 @@ func stripPrefix(imageName string) string { return imageName } +// DeleteExtraImages removes images from the local registry not in the provided list. func (r *BasicReplicator) DeleteExtraImages(ctx context.Context, imgs []store.Image) error { - zotUrl := os.Getenv("ZOT_URL") - host := os.Getenv("HOST") - registry := os.Getenv("REGISTRY") - repository := os.Getenv("REPOSITORY") + localRegistry := getEnvRegistryPath() - localRegistry := fmt.Sprintf("%s/%s/%s/%s", zotUrl, host, registry, repository) fmt.Println("Syncing local registry:", localRegistry) - // Get the list of images from the local registry localImages, err := crane.ListTags(localRegistry) if err != nil { return fmt.Errorf("failed to get local registry catalog: %w", err) } - // Create a map for quick lookup of the provided image list imageMap := make(map[string]struct{}) for _, img := range imgs { - // Strip the "album-server:" prefix from the image name - strippedName := stripPrefix(img.Name) - imageMap[strippedName] = struct{}{} + imageMap[stripPrefix(img.Name)] = struct{}{} } - // Iterate over the local images and delete those not in the provided image list for _, localImage := range localImages { if _, exists := imageMap[localImage]; !exists { - // Image is not in the provided list, delete it - fmt.Print("Deleting image: ", localRegistry+":"+localImage, " ... ") - err := crane.Delete(fmt.Sprintf("%s:%s", localRegistry, localImage)) - if err != nil { + if err := crane.Delete(fmt.Sprintf("%s:%s", localRegistry, localImage)); err != nil { fmt.Printf("failed to delete image %s: %v\n", localImage, err) - return nil + return err } fmt.Printf("Deleted image: %s\n", localImage) } @@ -95,114 +90,92 @@ func (r *BasicReplicator) DeleteExtraImages(ctx context.Context, imgs []store.Im return nil } +// getPullSource constructs the source URL for pulling an image. func getPullSource(image string) string { - input := os.Getenv("USER_INPUT") scheme := os.Getenv("SCHEME") if strings.HasPrefix(scheme, "http://") || strings.HasPrefix(scheme, "https://") { - url := os.Getenv("HOST") + "/" + os.Getenv("REGISTRY") + "/" + image - return url - } else { - registryInfo, err := getFileInfo(input) - if err != nil { - return "Error loading file info: " + err.Error() - } - registryURL := registryInfo.RegistryUrl - registryURL = strings.TrimPrefix(registryURL, "https://") - registryURL = strings.TrimSuffix(registryURL, "v2/") - - // TODO: Handle multiple repositories - repositoryName := registryInfo.Repositories[0].Repository - - return registryURL + repositoryName + "/" + image + return fmt.Sprintf("%s/%s/%s", os.Getenv("HOST"), os.Getenv("REGISTRY"), image) } -} -func getFileInfo(input string) (*RegistryInfo, error) { - // Get the current working directory - workingDir, err := os.Getwd() + registryInfo, err := getFileInfo(os.Getenv("USER_INPUT")) if err != nil { - return nil, fmt.Errorf("failed to get working directory: %w", err) + fmt.Printf("Error loading file info: %v\n", err) + return "" } - // Construct the full path by joining the working directory and the input path - fullPath := filepath.Join(workingDir, input) + registryURL := strings.TrimSuffix(strings.TrimPrefix(registryInfo.RegistryUrl, "https://"), "v2/") + repositoryName := registryInfo.Repositories[0].Repository + + return fmt.Sprintf("%s%s/%s", registryURL, repositoryName, image) +} - // Read the file - jsonData, err := os.ReadFile(fullPath) +// getFileInfo reads and unmarshals the registry info from a JSON file. +func getFileInfo(input string) (*RegistryInfo, error) { + fullPath := filepath.Join(getWorkingDir(), input) + data, err := os.ReadFile(fullPath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } var registryInfo RegistryInfo - err = json.Unmarshal(jsonData, ®istryInfo) - if err != nil { + if err := json.Unmarshal(data, ®istryInfo); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) } return ®istryInfo, nil } +// CopyImage pulls an image from the source and pushes it to the destination. func CopyImage(imageName string) error { fmt.Println("Copying image:", imageName) - zotUrl := os.Getenv("ZOT_URL") - if zotUrl == "" { - return fmt.Errorf("ZOT_URL environment variable is not set") - } - - // Clean up the image name by removing any host part - cleanedImageName := removeHostName(imageName) - destRef := fmt.Sprintf("%s/%s", zotUrl, cleanedImageName) - fmt.Println("Destination reference:", destRef) - - // Get credentials from environment variables - username := os.Getenv("HARBOR_USERNAME") - password := os.Getenv("HARBOR_PASSWORD") - if username == "" || password == "" { - return fmt.Errorf("HARBOR_USERNAME or HARBOR_PASSWORD environment variable is not set") - } + destRef := fmt.Sprintf("%s/%s", os.Getenv("ZOT_URL"), removeHostName(imageName)) auth := authn.FromConfig(authn.AuthConfig{ - Username: username, - Password: password, + Username: os.Getenv("HARBOR_USERNAME"), + Password: os.Getenv("HARBOR_PASSWORD"), }) - // Pull the image with authentication srcImage, err := crane.Pull(imageName, crane.WithAuth(auth), crane.Insecure) if err != nil { - fmt.Printf("Failed to pull image: %v\n", err) return fmt.Errorf("failed to pull image: %w", err) - } else { - fmt.Println("Image pulled successfully") - fmt.Printf("Pulled image details: %+v\n", srcImage) } - // Push the image to the destination registry - err = crane.Push(srcImage, destRef, crane.Insecure) - if err != nil { - fmt.Printf("Failed to push image: %v\n", err) + if err := crane.Push(srcImage, destRef, crane.Insecure); err != nil { return fmt.Errorf("failed to push image: %w", err) - } else { - fmt.Println("Image pushed successfully") - fmt.Printf("Pushed image to: %s\n", destRef) } + fmt.Println("Image pushed successfully") + fmt.Printf("Pushed image to: %s\n", destRef) - // Delete ./local-oci-layout directory - // This is required because it is a temporary directory used by crane to pull and push images to and from - // And crane does not automatically clean it if err := os.RemoveAll("./local-oci-layout"); err != nil { - fmt.Printf("Failed to remove directory: %v\n", err) - return fmt.Errorf("failed to remove directory: %w", err) + return fmt.Errorf("failed to remove temporary directory: %w", err) } return nil } -// take only the parts after the hostname +// removeHostName removes the hostname from the image name. func removeHostName(imageName string) string { - parts := strings.Split(imageName, "/") + parts := strings.SplitN(imageName, "/", 2) if len(parts) > 1 { - return strings.Join(parts[1:], "/") + return parts[1] } - return imageName } + +// getEnvRegistryPath constructs the local registry URL from environment variables. +func getEnvRegistryPath() string { + return fmt.Sprintf("%s/%s/%s/%s", + os.Getenv("ZOT_URL"), + os.Getenv("HOST"), + os.Getenv("REGISTRY"), + os.Getenv("REPOSITORY")) +} + +// getWorkingDir returns the current working directory. +func getWorkingDir() string { + workingDir, err := os.Getwd() + if err != nil { + panic(fmt.Errorf("failed to get working directory: %w", err)) + } + return workingDir +} diff --git a/internal/store/in-memory-store.go b/internal/store/in-memory-store.go index d64d2a2..9048acb 100644 --- a/internal/store/in-memory-store.go +++ b/internal/store/in-memory-store.go @@ -161,7 +161,10 @@ func (s *inMemoryStore) handleRemoteSource(ctx context.Context, imageList []Imag // Iterate over in memory store and remove any image that is not found in imageMap for digest, image := range s.images { if _, exists := imageMap[image]; !exists { - s.Remove(ctx, digest, image) + if err := s.Remove(ctx, digest, image); err != nil { + return false, err + } + change = true } } @@ -224,9 +227,13 @@ func (s *inMemoryStore) Remove(ctx context.Context, digest string, image string) } // Remove the image from the store -func (s *inMemoryStore) RemoveImage(ctx context.Context, image string) { +func (s *inMemoryStore) RemoveImage(ctx context.Context, image string) error { + if _, exists := s.images[image]; !exists { + return fmt.Errorf("image %s not found in the store", image) + } delete(s.images, image) fmt.Printf("Removed image: %s\n", image) + return nil } // TODO: Rework complicated logic and add support for multiple repositories From b3c9149aef917d3992a9c0439ba75805486ad268 Mon Sep 17 00:00:00 2001 From: bupd Date: Wed, 3 Jul 2024 06:20:42 +0530 Subject: [PATCH 6/8] add err handling Signed-off-by: bupd --- internal/store/in-memory-store.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/store/in-memory-store.go b/internal/store/in-memory-store.go index 9048acb..56b5112 100644 --- a/internal/store/in-memory-store.go +++ b/internal/store/in-memory-store.go @@ -98,7 +98,9 @@ func (s *inMemoryStore) handleFileSource(ctx context.Context, imageList []Image) } } if !found { - s.RemoveImage(ctx, image) + if err := s.RemoveImage(ctx, image); err != nil { + return false, err + } change = true } } @@ -206,8 +208,14 @@ func (s *inMemoryStore) Add(ctx context.Context, digest string, image string) er } } +// Add the image to the store func (s *inMemoryStore) AddImage(ctx context.Context, image string) { - // Add the image to the store + if _, exists := s.images[image]; exists { + fmt.Printf( + "Warning: Image %s already exists in the store. Proceeding with the addition.\n", + image, + ) + } s.images[image] = "" fmt.Printf("Added image: %s\n", image) } From abd4b3d22601d125a3e026e50bbba47ad472d938 Mon Sep 17 00:00:00 2001 From: bupd Date: Tue, 9 Jul 2024 20:28:47 +0530 Subject: [PATCH 7/8] fix replication logic & remove redundant comments Signed-off-by: bupd --- config.toml | 3 +- internal/replicate/replicate.go | 43 ++++++--------- internal/satellite/satellite.go | 11 +++- internal/store/file-fetch.go | 2 +- internal/store/http-fetch.go | 14 ++--- internal/store/in-memory-store.go | 90 +++++++++++++++---------------- 6 files changed, 78 insertions(+), 85 deletions(-) diff --git a/config.toml b/config.toml index 1fa3a4f..fb4018f 100644 --- a/config.toml +++ b/config.toml @@ -5,8 +5,9 @@ bring_own_registry = false own_registry_adr = "127.0.0.1:5000" # URL of remote registry OR local file path +# url_or_file = "https://demo.goharbor.io/v2/library/busy" url_or_file = "https://demo.goharbor.io/v2/myproject/album-server" - # url_or_file = "http://localhost:5001/v2/library/busybox" +# url_or_file = "http://localhost:5001/v2/library/busybox" # For testing purposes : # https://demo.goharbor.io/v2/myproject/album-server diff --git a/internal/replicate/replicate.go b/internal/replicate/replicate.go index 55c7b0c..1654dc0 100644 --- a/internal/replicate/replicate.go +++ b/internal/replicate/replicate.go @@ -13,38 +13,32 @@ import ( "github.com/google/go-containerregistry/pkg/crane" ) -// Replicator interface for image replication and deletion. type Replicator interface { Replicate(ctx context.Context, image string) error DeleteExtraImages(ctx context.Context, imgs []store.Image) error } -// BasicReplicator implements the Replicator interface. type BasicReplicator struct{} -// ImageInfo holds the name of an image. type ImageInfo struct { Name string `json:"name"` } -// Repository holds the repository name and associated images. type Repository struct { Repository string `json:"repository"` Images []ImageInfo `json:"images"` } -// RegistryInfo holds the registry URL and repositories information. type RegistryInfo struct { RegistryUrl string `json:"registryUrl"` Repositories []Repository `json:"repositories"` } -// NewReplicator creates a new BasicReplicator. func NewReplicator() Replicator { return &BasicReplicator{} } -// Replicate copies an image from the source registry to the local registry. +// Replicate copies an image from source registry to local registry. func (r *BasicReplicator) Replicate(ctx context.Context, image string) error { source := getPullSource(image) if source == "" { @@ -53,7 +47,7 @@ func (r *BasicReplicator) Replicate(ctx context.Context, image string) error { return CopyImage(source) } -// stripPrefix removes the prefix from the image name. +// stripPrefix removes prefix from image name. func stripPrefix(imageName string) string { if idx := strings.Index(imageName, ":"); idx != -1 { return imageName[idx+1:] @@ -61,7 +55,6 @@ func stripPrefix(imageName string) string { return imageName } -// DeleteExtraImages removes images from the local registry not in the provided list. func (r *BasicReplicator) DeleteExtraImages(ctx context.Context, imgs []store.Image) error { localRegistry := getEnvRegistryPath() @@ -80,8 +73,7 @@ func (r *BasicReplicator) DeleteExtraImages(ctx context.Context, imgs []store.Im for _, localImage := range localImages { if _, exists := imageMap[localImage]; !exists { if err := crane.Delete(fmt.Sprintf("%s:%s", localRegistry, localImage)); err != nil { - fmt.Printf("failed to delete image %s: %v\n", localImage, err) - return err + return fmt.Errorf("failed to delete image %s: %v\n", localImage, err) } fmt.Printf("Deleted image: %s\n", localImage) } @@ -90,7 +82,7 @@ func (r *BasicReplicator) DeleteExtraImages(ctx context.Context, imgs []store.Im return nil } -// getPullSource constructs the source URL for pulling an image. +// getPullSource constructs source URL for pulling an image. func getPullSource(image string) string { scheme := os.Getenv("SCHEME") if strings.HasPrefix(scheme, "http://") || strings.HasPrefix(scheme, "https://") { @@ -103,13 +95,16 @@ func getPullSource(image string) string { return "" } - registryURL := strings.TrimSuffix(strings.TrimPrefix(registryInfo.RegistryUrl, "https://"), "v2/") + registryURL := strings.TrimSuffix( + strings.TrimPrefix(registryInfo.RegistryUrl, "https://"), + "v2/", + ) repositoryName := registryInfo.Repositories[0].Repository - return fmt.Sprintf("%s%s/%s", registryURL, repositoryName, image) + return fmt.Sprintf("%s/%s/%s", registryURL, repositoryName, image) } -// getFileInfo reads and unmarshals the registry info from a JSON file. +// getFileInfo reads and unmarshals registry info from a JSON file. func getFileInfo(input string) (*RegistryInfo, error) { fullPath := filepath.Join(getWorkingDir(), input) data, err := os.ReadFile(fullPath) @@ -125,7 +120,7 @@ func getFileInfo(input string) (*RegistryInfo, error) { return ®istryInfo, nil } -// CopyImage pulls an image from the source and pushes it to the destination. +// CopyImage pulls an image from source and pushes it to destination. func CopyImage(imageName string) error { fmt.Println("Copying image:", imageName) destRef := fmt.Sprintf("%s/%s", os.Getenv("ZOT_URL"), removeHostName(imageName)) @@ -135,14 +130,11 @@ func CopyImage(imageName string) error { Password: os.Getenv("HARBOR_PASSWORD"), }) - srcImage, err := crane.Pull(imageName, crane.WithAuth(auth), crane.Insecure) + err := crane.Copy(imageName, destRef, crane.Insecure, crane.WithAuth(auth)) if err != nil { - return fmt.Errorf("failed to pull image: %w", err) + return fmt.Errorf("Error in copying image from %s to %s: %v", imageName, destRef, err) } - if err := crane.Push(srcImage, destRef, crane.Insecure); err != nil { - return fmt.Errorf("failed to push image: %w", err) - } fmt.Println("Image pushed successfully") fmt.Printf("Pushed image to: %s\n", destRef) @@ -153,7 +145,7 @@ func CopyImage(imageName string) error { return nil } -// removeHostName removes the hostname from the image name. +// removeHostName removes hostname from image name. func removeHostName(imageName string) string { parts := strings.SplitN(imageName, "/", 2) if len(parts) > 1 { @@ -162,16 +154,15 @@ func removeHostName(imageName string) string { return imageName } -// getEnvRegistryPath constructs the local registry URL from environment variables. +// getEnvRegistryPath constructs local registry URL from environment variables. func getEnvRegistryPath() string { - return fmt.Sprintf("%s/%s/%s/%s", + return fmt.Sprintf("%s/%s/%s", os.Getenv("ZOT_URL"), - os.Getenv("HOST"), os.Getenv("REGISTRY"), os.Getenv("REPOSITORY")) } -// getWorkingDir returns the current working directory. +// getWorkingDir returns current working directory. func getWorkingDir() string { workingDir, err := os.Getwd() if err != nil { diff --git a/internal/satellite/satellite.go b/internal/satellite/satellite.go index e7bdf79..ebc3816 100644 --- a/internal/satellite/satellite.go +++ b/internal/satellite/satellite.go @@ -25,6 +25,7 @@ func (s *Satellite) Run(ctx context.Context) error { // Execute the initial operation immediately without waiting for the ticker imgs, err := s.storer.List(ctx) if err != nil { + return err } if len(imgs) == 0 { @@ -36,7 +37,10 @@ func (s *Satellite) Run(ctx context.Context) error { return err } } - s.replicator.DeleteExtraImages(ctx, imgs) + err = s.replicator.DeleteExtraImages(ctx, imgs) + if err != nil { + return err + } } fmt.Print("--------------------------------\n") @@ -62,7 +66,10 @@ func (s *Satellite) Run(ctx context.Context) error { return err } } - s.replicator.DeleteExtraImages(ctx, imgs) + err = s.replicator.DeleteExtraImages(ctx, imgs) + if err != nil { + return err + } } } fmt.Print("--------------------------------\n") diff --git a/internal/store/file-fetch.go b/internal/store/file-fetch.go index 2c39555..4c895f5 100644 --- a/internal/store/file-fetch.go +++ b/internal/store/file-fetch.go @@ -73,6 +73,6 @@ func (client *FileImageList) List(ctx context.Context) ([]Image, error) { } func (client *FileImageList) GetDigest(ctx context.Context, tag string) (string, error) { - // TODO: Implement GetDigest for FileImageList + // TODO: Implement GetDigest for FileImageList return "Not implemented yet", nil } diff --git a/internal/store/http-fetch.go b/internal/store/http-fetch.go index 3d85c7e..dbe975f 100644 --- a/internal/store/http-fetch.go +++ b/internal/store/http-fetch.go @@ -14,28 +14,24 @@ import ( "github.com/google/go-containerregistry/pkg/crane" ) -// RemoteImageSource represents a source of images from a remote URL. type RemoteImageSource struct { BaseURL string } -// TagListResponse represents the JSON structure for the tags list response. type TagListResponse struct { Name string `json:"name"` Tags []string `json:"tags"` } -// NewRemoteImageSource creates a new RemoteImageSource instance. func NewRemoteImageSource(url string) *RemoteImageSource { return &RemoteImageSource{BaseURL: url} } -// SourceType returns the type of the image source as a string. func (r *RemoteImageSource) SourceType() string { return "Remote" } -// FetchImages retrieves a list of images from the remote repository. +// retrieves a list of images from remote repository. func (r *RemoteImageSource) List(ctx context.Context) ([]Image, error) { url := r.BaseURL + "/tags/list" authHeader, err := createAuthHeader() @@ -57,7 +53,7 @@ func (r *RemoteImageSource) List(ctx context.Context) ([]Image, error) { return images, nil } -// FetchDigest fetches the digest for a specific image tag. +// fetches digest for a specific image. func (r *RemoteImageSource) GetDigest(ctx context.Context, tag string) (string, error) { imageRef := fmt.Sprintf("%s:%s", r.BaseURL, tag) imageRef = cleanImageReference(imageRef) @@ -70,7 +66,7 @@ func (r *RemoteImageSource) GetDigest(ctx context.Context, tag string) (string, return digest, nil } -// createAuthHeader generates the authorization header for HTTP requests. +// createAuthHeader generates authorization header for HTTP requests. func createAuthHeader() (string, error) { username := os.Getenv("HARBOR_USERNAME") password := os.Getenv("HARBOR_PASSWORD") @@ -81,7 +77,7 @@ func createAuthHeader() (string, error) { return "Basic " + auth, nil } -// fetchResponseBody makes an HTTP GET request and returns the response body. +// fetchResponseBody makes an HTTP GET request and returns response body. func fetchResponseBody(url, authHeader string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -125,7 +121,7 @@ func cleanImageReference(imageRef string) string { return strings.ReplaceAll(imageRef, "/v2", "") } -// fetchImageDigest retrieves the digest for an image reference. +// fetchImageDigest retrieves digest for an image reference. func fetchImageDigest(imageRef string) (string, error) { username := os.Getenv("HARBOR_USERNAME") password := os.Getenv("HARBOR_PASSWORD") diff --git a/internal/store/in-memory-store.go b/internal/store/in-memory-store.go index 56b5112..40cf50e 100644 --- a/internal/store/in-memory-store.go +++ b/internal/store/in-memory-store.go @@ -3,39 +3,35 @@ package store import ( "context" "fmt" + "log" "os" "strings" "github.com/google/go-containerregistry/pkg/crane" ) -// Image represents a container image with its digest and name. type Image struct { Digest string Name string } -// inMemoryStore stores images in memory and uses an ImageFetcher to manage images. type inMemoryStore struct { images map[string]string fetcher ImageFetcher } -// Storer is the interface for image storage operations. type Storer interface { List(ctx context.Context) ([]Image, error) Add(ctx context.Context, digest, image string) error Remove(ctx context.Context, digest, image string) error } -// ImageFetcher is the interface for fetching images. type ImageFetcher interface { List(ctx context.Context) ([]Image, error) GetDigest(ctx context.Context, tag string) (string, error) SourceType() string } -// NewInMemoryStore creates a new in-memory store with the given ImageFetcher. func NewInMemoryStore(fetcher ImageFetcher) Storer { return &inMemoryStore{ images: make(map[string]string), @@ -43,7 +39,7 @@ func NewInMemoryStore(fetcher ImageFetcher) Storer { } } -// List retrieves and synchronizes the list of images from the fetcher. +// List retrieves and synchronizes the list of images func (s *inMemoryStore) List(ctx context.Context) ([]Image, error) { // fetch List of images imageList, err := s.fetcher.List(ctx) @@ -66,7 +62,7 @@ func (s *inMemoryStore) List(ctx context.Context) ([]Image, error) { } if changeDetected { - fmt.Println("Changes detected in the store") + fmt.Println("Changes detected in store") return s.getImageList(), nil } else { fmt.Println("No changes detected in the store") @@ -78,13 +74,13 @@ func (s *inMemoryStore) List(ctx context.Context) ([]Image, error) { func (s *inMemoryStore) handleFileSource(ctx context.Context, imageList []Image) (bool, error) { var change bool for _, img := range imageList { - // Check if the image already exists in the store + // Check if image already exists in store if _, exists := s.images[img.Name]; !exists { - // Add the image to the store + // Add image to store s.AddImage(ctx, img.Name) change = true } else { - fmt.Printf("Image %s already exists in the store\n", img.Name) + fmt.Printf("Image %s already exists in store\n", img.Name) } } @@ -105,13 +101,13 @@ func (s *inMemoryStore) handleFileSource(ctx context.Context, imageList []Image) } } - // Empty and refill imageList with the contents from s.images + // Empty and refill imageList with contents from s.images imageList = imageList[:0] for name, digest := range s.images { imageList = append(imageList, Image{Name: name, Digest: digest}) } - // Print out the entire store for debugging purposes + // Print out entire store for debugging purposes fmt.Println("Current store:") for image := range s.images { fmt.Printf("Image: %s\n", image) @@ -123,31 +119,31 @@ func (s *inMemoryStore) handleFileSource(ctx context.Context, imageList []Image) // handleRemoteSource handles images fetched from a remote source. func (s *inMemoryStore) handleRemoteSource(ctx context.Context, imageList []Image) (bool, error) { var change bool - // Trim the imageList elements to remove the project name from the image reference + // Trim the imageList elements to remove project name from image reference for i, img := range imageList { parts := strings.Split(img.Name, "/") if len(parts) > 1 { - // Take the second part as the new Reference + // Take second part as new Reference imageList[i].Name = parts[1] } } // iterate over imageList and call GetDigest for each tag for _, img := range imageList { - // Split the image reference to get the tag + // Split image reference to get tag tagParts := strings.Split(img.Name, ":") // Check if there is a tag part, min length is 1 char if len(tagParts) < 2 { - fmt.Println("No tag part found in the image reference") + fmt.Println("No tag part found in image reference") } - // Use the last part as the tag + // Use last part as tag tag := tagParts[len(tagParts)-1] - // Get the digest for the tag + // Get digest for tag digest, err := s.fetcher.GetDigest(ctx, tag) if err != nil { return false, err } - // Check if the image exists and matches the digest + // Check if image exists and matches the digest if !(s.checkImageAndDigest(digest, img.Name)) { change = true } @@ -176,7 +172,7 @@ func (s *inMemoryStore) handleRemoteSource(ctx context.Context, imageList []Imag fmt.Printf("Digest: %s, Image: %s\n", digest, imageRef) } - // Empty and refill imageList with the contents from s.images + // Empty and refill imageList with contents from s.images imageList = imageList[:0] for _, name := range s.images { imageList = append(imageList, Image{Digest: "", Name: name}) @@ -188,31 +184,31 @@ func (s *inMemoryStore) handleRemoteSource(ctx context.Context, imageList []Imag // getImageList converts the in-memory store to a list of Image structs. func (s *inMemoryStore) getImageList() []Image { var imageList []Image - // Empty and refill imageList with the contents from s.images + // Empty and refill imageList with contents from s.images for _, name := range s.images { imageList = append(imageList, Image{Digest: "", Name: name}) } return imageList } +// Add image and its digest to store func (s *inMemoryStore) Add(ctx context.Context, digest string, image string) error { - // Check if the image already exists in the store + // Check if image already exists in store if _, exists := s.images[digest]; exists { - fmt.Printf("Image: %s, digest: %s already exists in the store.\n", image, digest) - return fmt.Errorf("image %s already exists in the store", image) + fmt.Printf("Image: %s, digest: %s already exists in store.\n", image, digest) + return fmt.Errorf("image %s already exists in store", image) } else { - // Add the image and its digest to the store s.images[digest] = image fmt.Printf("Successfully added image: %s, digest: %s\n", image, digest) return nil } } -// Add the image to the store +// Add image to store func (s *inMemoryStore) AddImage(ctx context.Context, image string) { if _, exists := s.images[image]; exists { fmt.Printf( - "Warning: Image %s already exists in the store. Proceeding with the addition.\n", + "Warning: Image %s already exists in store. Proceeding with the addition.\n", image, ) } @@ -220,11 +216,11 @@ func (s *inMemoryStore) AddImage(ctx context.Context, image string) { fmt.Printf("Added image: %s\n", image) } -// Removes the image from the store +// Removes image from store func (s *inMemoryStore) Remove(ctx context.Context, digest string, image string) error { - // Check if the image exists in the store + // Check if image exists in store if _, exists := s.images[digest]; exists { - // Remove the image and its digest from the store + // Remove image and its digest from store delete(s.images, digest) fmt.Printf("Successfully removed image: %s, digest: %s\n", image, digest) return nil @@ -234,7 +230,7 @@ func (s *inMemoryStore) Remove(ctx context.Context, digest string, image string) } } -// Remove the image from the store +// Remove image from store func (s *inMemoryStore) RemoveImage(ctx context.Context, image string) error { if _, exists := s.images[image]; !exists { return fmt.Errorf("image %s not found in the store", image) @@ -245,22 +241,22 @@ func (s *inMemoryStore) RemoveImage(ctx context.Context, image string) error { } // TODO: Rework complicated logic and add support for multiple repositories -// checkImageAndDigest checks if the image exists in the store and if the digest matches the image reference +// checkImageAndDigest checks if image exists in store and if the digest matches the image reference func (s *inMemoryStore) checkImageAndDigest(digest string, image string) bool { - // Check if the received image exists in the store + // Check if received image exists in store for storeDigest, storeImage := range s.images { if storeImage == image { - // Image exists, now check if the digest matches + // Image exists, now check if digest matches if storeDigest == digest { - // Digest exists and matches the current image's - // Remove what comes before the ":" in image and set it to tag variable + // Digest exists and matches current image's + // Remove what comes before ":" in image and set it to tag variable tag := strings.Split(image, ":")[1] localRegistryDigest, err := GetLocalDigest(context.Background(), tag) if err != nil { fmt.Println("Error getting digest from local registry:", err) return false } else { - // Check if the digest from the local registry matches the digest from the store + // Check if digest from local registry matches digest from store if digest == localRegistryDigest { return true } else { @@ -268,20 +264,22 @@ func (s *inMemoryStore) checkImageAndDigest(digest string, image string) bool { } } } else { - // Digest exists but does not match the current image reference + // Digest exists but does not match current image reference if err := s.Remove(context.Background(), storeDigest, storeImage); err != nil { - fmt.Errorf("Error: %w", err) + log.Println("Error: %w", err) return false } - s.Add(context.Background(), digest, image) + if err := s.Add(context.Background(), digest, image); err != nil { + log.Fatalf("Error in adding image to store: %v", err) + } return false } } } - // Try to add the image to the store - // Add will check if it already exists in the store before adding + // Try to add image to store + // Add will check if it already exists in store before adding // If adding was successful, return true, else return false err := s.Add(context.Background(), digest, image) return err != nil @@ -290,15 +288,15 @@ func (s *inMemoryStore) checkImageAndDigest(digest string, image string) bool { func GetLocalDigest(ctx context.Context, tag string) (string, error) { zotUrl := os.Getenv("ZOT_URL") userURL := os.Getenv("USER_INPUT") - // Remove extra characters from the URLs + // Remove extra characters from URLs userURL = userURL[strings.Index(userURL, "//")+2:] userURL = strings.ReplaceAll(userURL, "/v2", "") regUrl := removeHostName(userURL) - // Construct the URL for fetching the digest + // Construct URL for fetching digest url := zotUrl + "/" + regUrl + ":" + tag - // Use crane.Digest to get the digest of the image + // Use crane.Digest to get digest of image digest, err := crane.Digest(url) if err != nil { return "", fmt.Errorf("failed to get digest using crane: %w", err) @@ -307,7 +305,7 @@ func GetLocalDigest(ctx context.Context, tag string) (string, error) { return digest, nil } -// Split the imageName by "/" and take only the parts after the hostname +// Split imageName by "/" and take only parts after hostname func removeHostName(imageName string) string { parts := strings.Split(imageName, "/") if len(parts) > 1 { From bda73b5d1344a1ba2809525375f96b170c72cc33 Mon Sep 17 00:00:00 2001 From: bupd Date: Wed, 10 Jul 2024 04:23:40 +0530 Subject: [PATCH 8/8] update tests Signed-off-by: bupd --- test/e2e/satellite_test.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/e2e/satellite_test.go b/test/e2e/satellite_test.go index 3175d31..b923aae 100644 --- a/test/e2e/satellite_test.go +++ b/test/e2e/satellite_test.go @@ -106,12 +106,8 @@ func pushImageToSourceRegistry( // add crane & push images container = container.WithExec([]string{"apk", "add", "crane"}). - WithExec([]string{"docker", "pull", "busybox:1.36"}). - WithExec([]string{"docker", "pull", "busybox:stable"}). WithExec([]string{"crane", "copy", "busybox:1.36", "source:5000/library/busybox:1.36", "--insecure"}). - WithExec([]string{"crane", "copy", "busybox:stable", "source:5000/library/busybox:stable", "--insecure"}). - WithExec([]string{"crane", "digest", "source:5000/library/busybox:1.36", "--insecure"}). - WithExec([]string{"crane", "digest", "source:5000/library/busybox:stable", "--insecure"}) + WithExec([]string{"crane", "digest", "source:5000/library/busybox:1.36", "--insecure"}) // check pushed images exist container = container.WithExec([]string{"crane", "catalog", "source:5000", "--insecure"}) @@ -155,16 +151,14 @@ func buildSatellite( WithExec([]string{"cat", "config.toml"}). WithExec([]string{"apk", "add", "crane"}). WithExec([]string{"crane", "-v", "catalog", "source:5000", "--insecure"}). - WithExec([]string{"crane", "digest", "source:5000/library/busybox:stable", "--insecure"}). + WithExec([]string{"crane", "digest", "source:5000/library/busybox:1.36", "--insecure"}). WithExec([]string{"go", "build", "-o", appBinary, sourceFile}). WithExposedPort(9090). WithExec([]string{"go", "run", "./test/e2e/test.go"}) assert.NoError(t, err, "Test failed in buildSatellite") - stdOut, err := container.Stdout(ctx) - assert.NoError(t, err, "Failed to get stdOut in Satellite") - + stdOut, _ := container.Stdout(ctx) fmt.Println(stdOut) }