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

feat: Add E2E Test for Harbor Satellite #27

Merged
merged 5 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Binary file removed .DS_Store
Binary file not shown.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.env
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
Expand Down
9 changes: 5 additions & 4 deletions config.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Wether to us the built-in Zot registry or not
bring_own_registry = false
bring_own_registry = true

# URL of own registry
own_registry_adr = "127.0.0.1:8585"
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 = "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
# /image-list/images.json
# /image-list/images.json
25 changes: 18 additions & 7 deletions internal/replicate/replicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ func NewReplicator() Replicator {
}

func (r *BasicReplicator) Replicate(ctx context.Context, image string) error {

source := getPullSource(image)

if source != "" {
Expand Down Expand Up @@ -98,7 +97,8 @@ func (r *BasicReplicator) DeleteExtraImages(ctx context.Context, imgs []store.Im

func getPullSource(image string) string {
input := os.Getenv("USER_INPUT")
if os.Getenv("SCHEME") == "https://" {
scheme := os.Getenv("SCHEME")
if strings.HasPrefix(scheme, "http://") || strings.HasPrefix(scheme, "https://") {
bupd marked this conversation as resolved.
Show resolved Hide resolved
url := os.Getenv("HOST") + "/" + os.Getenv("REGISTRY") + "/" + image
return url
} else {
Expand All @@ -115,7 +115,6 @@ func getPullSource(image string) string {

return registryURL + repositoryName + "/" + image
}

}

func getFileInfo(input string) (*RegistryInfo, error) {
Expand Down Expand Up @@ -150,8 +149,10 @@ func CopyImage(imageName string) error {
return fmt.Errorf("ZOT_URL environment variable is not set")
}

srcRef := imageName
destRef := zotUrl + "/" + imageName
// 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")
Expand All @@ -166,7 +167,7 @@ func CopyImage(imageName string) error {
})

// Pull the image with authentication
srcImage, err := crane.Pull(srcRef, crane.WithAuth(auth))
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)
Expand All @@ -176,7 +177,7 @@ func CopyImage(imageName string) error {
}

// Push the image to the destination registry
err = crane.Push(srcImage, destRef)
err = crane.Push(srcImage, destRef, crane.Insecure)
if err != nil {
fmt.Printf("Failed to push image: %v\n", err)
return fmt.Errorf("failed to push image: %w", err)
Expand All @@ -195,3 +196,13 @@ func CopyImage(imageName string) error {

return nil
}

// take only the parts after the hostname
func removeHostName(imageName string) string {
parts := strings.Split(imageName, "/")
if len(parts) > 1 {
return strings.Join(parts[1:], "/")
}

return imageName
}
2 changes: 1 addition & 1 deletion internal/store/http-fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (client *RemoteImageList) GetDigest(ctx context.Context, tag string) (strin
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
Expand Down
17 changes: 12 additions & 5 deletions internal/store/in-memory-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ func (s *inMemoryStore) List(ctx context.Context) ([]Image, error) {
fmt.Println("No changes detected in the store")
return nil, nil
}

}

func (s *inMemoryStore) Add(ctx context.Context, digest string, image string) error {
Expand Down Expand Up @@ -201,7 +200,6 @@ 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
func (s *inMemoryStore) checkImageAndDigest(digest string, image string) bool {

// Check if the received image exists in the store
for storeDigest, storeImage := range s.images {
if storeImage == image {
Expand Down Expand Up @@ -236,19 +234,18 @@ func (s *inMemoryStore) checkImageAndDigest(digest string, image string) bool {
// If adding was successful, return true, else return false
err := s.Add(context.Background(), digest, image)
return err != nil

}

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
userURL = userURL[strings.Index(userURL, "//")+2:]
userURL = strings.ReplaceAll(userURL, "/v2", "")

regUrl := removeHostName(userURL)
// Construct the URL for fetching the digest
url := zotUrl + "/" + userURL + ":" + tag
url := zotUrl + "/" + regUrl + ":" + tag

// Use crane.Digest to get the digest of the image
digest, err := crane.Digest(url)
Expand All @@ -258,3 +255,13 @@ func GetLocalDigest(ctx context.Context, tag string) (string, error) {

return digest, nil
}

// Split the imageName by "/" and take only the parts after the hostname
func removeHostName(imageName string) string {
parts := strings.Split(imageName, "/")
if len(parts) > 1 {
return strings.Join(parts[1:], "/")
}

return imageName
}
16 changes: 7 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -85,13 +84,13 @@ func run() error {
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)
}
// 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 @@ -105,7 +104,6 @@ func run() error {
cancel()
return err
}

})
}

Expand Down
Binary file removed registry/.DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion registry/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
"log": {
"level": ""
}
}
}
178 changes: 178 additions & 0 deletions test/e2e/satellite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package main

import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"

"dagger.io/dagger"
"github.com/stretchr/testify/assert"
)

const (
appDir = "/app"
appBinary = "app"
sourceFile = "main.go"
)

func TestSatellite(t *testing.T) {
ctx := context.Background()

// Initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
assert.NoError(t, err, "Failed to connect to Dagger")
defer client.Close()

// Set up Source Registry
source, err := setupSourceRegistry(t, client, ctx)
assert.NoError(t, err, "Failed to set up source registry")

// Set up Destination registry
dest, err := setupDestinationRegistry(t, client, ctx)
assert.NoError(t, err, "Failed to set up destination registry")

// Push images to Source registry
pushImageToSourceRegistry(t, ctx, client, source)
assert.NoError(t, err, "Failed to upload image to source registry")

// Build & Run Satellite
buildSatellite(t, client, ctx, source, dest)
assert.NoError(t, err, "Failed to build and run Satellite")
}

// Setup Source Registry as a Dagger Service
func setupSourceRegistry(
t *testing.T,
client *dagger.Client,
ctx context.Context,
) (*dagger.Service, error) {
// socket to connect to host Docker
socket := client.Host().UnixSocket("/var/run/docker.sock")

container, err := client.Container().
From("registry:2").
WithExposedPort(5000).
WithUnixSocket("/var/run/docker.sock", socket).
WithEnvVariable("DOCKER_HOST", "unix:///var/run/docker.sock").
WithEnvVariable("CACHEBUSTER", time.Now().String()).
AsService().Start(ctx)

assert.NoError(t, err, "Failed setting up source registry.")

return container, nil
}

// Setup Destination Registry as a Dagger Service
func setupDestinationRegistry(
t *testing.T,
client *dagger.Client,
ctx context.Context,
) (*dagger.Service, error) {
// socket to connect to host Docker
socket := client.Host().UnixSocket("/var/run/docker.sock")

container, err := client.Container().
From("registry:2").
WithExposedPort(5000).
WithUnixSocket("/var/run/docker.sock", socket).
WithEnvVariable("DOCKER_HOST", "unix:///var/run/docker.sock").
WithEnvVariable("CACHEBUSTER", time.Now().String()).
AsService().Start(ctx)

assert.NoError(t, err, "Failed setting up destination registry")

return container, nil
}

// Push image to the Source registry
func pushImageToSourceRegistry(
t *testing.T,
ctx context.Context,
client *dagger.Client,
source *dagger.Service,
) {
// socket to connect to host Docker
socket := client.Host().UnixSocket("/var/run/docker.sock")

container := client.Container().
From("docker:dind").
WithUnixSocket("/var/run/docker.sock", socket).
WithEnvVariable("DOCKER_HOST", "unix:///var/run/docker.sock").
WithEnvVariable("CACHEBUSTER", time.Now().String()).
WithServiceBinding("source", source)

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

// check pushed images exist
container = container.WithExec([]string{"crane", "catalog", "source:5000", "--insecure"})

stdOut, err := container.Stdout(ctx)
assert.NoError(t, err, "Failed to print stdOut in pushing Image to Source")

fmt.Println(stdOut)
}

// buildSatellite and test test the connection
func buildSatellite(
t *testing.T,
client *dagger.Client,
ctx context.Context,
source *dagger.Service,
dest *dagger.Service,
) {
socket := client.Host().UnixSocket("/var/run/docker.sock")

// Get the directory
parentDir, err := getProjectDir()
assert.NoError(t, err, "Failed to get Project Directory")

// Use the directory path in Dagger
dir := client.Host().Directory(parentDir)

// Get configuration file on the host
configFile := client.Host().File("./testdata/config.toml")

// Configure and build the Satellite
container := client.Container().From("golang:alpine").WithDirectory(appDir, dir).
WithWorkdir(appDir).
WithServiceBinding("source", source).
WithServiceBinding("dest", dest).
WithUnixSocket("/var/run/docker.sock", socket).
WithEnvVariable("DOCKER_HOST", "unix:///var/run/docker.sock").
WithEnvVariable("CACHEBUSTER", time.Now().String()).
WithExec([]string{"cat", "config.toml"}).
WithFile("./config.toml", configFile).
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{"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")

fmt.Println(stdOut)
}

// Gets the directory of the project
func getProjectDir() (string, error) {
currentDir, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Abs(filepath.Join(currentDir, "../.."))
}
Loading