Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and Simplify Codebase for Harbor Satellite #30

Merged
merged 8 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions config.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/store/file-fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type ImageData struct {
Repositories []Repository `json:"repositories"`
}

func (f *FileImageList) Type() string {
func (f *FileImageList) SourceType() string {
return "File"
}

Expand Down
111 changes: 72 additions & 39 deletions internal/store/http-fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
bupd marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
}
bupd marked this conversation as resolved.
Show resolved Hide resolved

// 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
}
bupd marked this conversation as resolved.
Show resolved Hide resolved

// 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
bupd marked this conversation as resolved.
Show resolved Hide resolved
}

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)
}
bupd marked this conversation as resolved.
Show resolved Hide resolved

return digest, nil
Expand Down
4 changes: 2 additions & 2 deletions internal/store/in-memory-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
bupd marked this conversation as resolved.
Show resolved Hide resolved
case "File":
for _, img := range imageList {
// Check if the image already exists in the store
Expand Down
102 changes: 62 additions & 40 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -108,42 +99,23 @@ 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
bupd marked this conversation as resolved.
Show resolved Hide resolved
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)
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.")
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)
// Process input as a URL
err = processURL(input)
bupd marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Fatalf("Error in processing URL: %v", err)
}
fetcher = store.NewRemoteImageSource(input)
}

err = godotenv.Load()
Expand All @@ -165,3 +137,53 @@ func run() error {
}
return nil
}

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.")
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.")
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
}