From b9366d31df97bb9cdb39086760cad455a0674e74 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 18 Sep 2024 14:29:44 +0200 Subject: [PATCH 1/5] test: Add compose-based dev&test environment Add a compose-based environment to build, run, and test the composectl utility. The environment includes the foundries specific docker registry and daemon (vanilla version + required patches). Signed-off-by: Mike Sul --- .gitignore | 1 + dev-shell.sh | 9 ++++ test/compose/.env.test | 9 ++++ test/compose/Dockerfile | 13 ++++++ test/compose/docker-compose.yml | 42 +++++++++++++++++++ test/compose/registry/certs/gen-certs.sh | 3 ++ test/compose/registry/certs/openssl.cnf | 25 ++++++++++++ test/compose/registry/certs/registry.crt | 31 ++++++++++++++ test/compose/registry/certs/registry.key | 52 ++++++++++++++++++++++++ test/compose/registry/config.yml | 14 +++++++ test/compose/registry/daemon.json | 3 ++ 11 files changed, 202 insertions(+) create mode 100755 dev-shell.sh create mode 100644 test/compose/.env.test create mode 100644 test/compose/Dockerfile create mode 100644 test/compose/docker-compose.yml create mode 100755 test/compose/registry/certs/gen-certs.sh create mode 100644 test/compose/registry/certs/openssl.cnf create mode 100644 test/compose/registry/certs/registry.crt create mode 100644 test/compose/registry/certs/registry.key create mode 100644 test/compose/registry/config.yml create mode 100644 test/compose/registry/daemon.json diff --git a/.gitignore b/.gitignore index 16af516..1607c77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea bin/ +.cache diff --git a/dev-shell.sh b/dev-shell.sh new file mode 100755 index 0000000..a339b91 --- /dev/null +++ b/dev-shell.sh @@ -0,0 +1,9 @@ +down() { + docker compose --env-file=test/compose/.env.test -f test/compose/docker-compose.yml down --remove-orphans + # remove the docker runtime and compose app store volumes + docker volume rm compose_docker-runtime compose_reset-apps +} + +trap down EXIT + +docker compose --env-file=test/compose/.env.test -f test/compose/docker-compose.yml run composectl $@ diff --git a/test/compose/.env.test b/test/compose/.env.test new file mode 100644 index 0000000..06b1c22 --- /dev/null +++ b/test/compose/.env.test @@ -0,0 +1,9 @@ +SRC_DIR=$PWD +BIN_DIR=$SRC_DIR/bin +TEST_DIR=$SRC_DIR/test +CPS_DIR=$TEST_DIR/compose + +REG_DIR=$CPS_DIR/registry +REG_CERT_DIR=$REG_DIR/certs +# This must be relative to SRC_DIR path +REG_CERT=test/compose/registry/certs/registry.crt diff --git a/test/compose/Dockerfile b/test/compose/Dockerfile new file mode 100644 index 0000000..de30ebf --- /dev/null +++ b/test/compose/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.22-alpine + +ARG REG_CERT=test/compose/registry/certs/registry.crt +ARG SRC_DIR=$PWD + +RUN apk add make curl docker docker-compose git + +COPY $REG_CERT /etc/ssl/certs/registry.crt +RUN cat /etc/ssl/certs/registry.crt >> /etc/ssl/certs/ca-certificates.crt +WORKDIR /build +COPY go.mod go.sum ./ +RUN go mod download +RUN git config --global --add safe.directory $SRC_DIR diff --git a/test/compose/docker-compose.yml b/test/compose/docker-compose.yml new file mode 100644 index 0000000..c84a2ac --- /dev/null +++ b/test/compose/docker-compose.yml @@ -0,0 +1,42 @@ +services: + registry: + image: ghcr.io/foundriesio/docker-distribution:2.8.3-fio + volumes: + - ${REG_CERT_DIR}:/certs + - ${REG_DIR}/config.yml:/etc/docker/registry/config.yml + + dockerd: + image: ghcr.io/foundriesio/moby:25.0.3_fio + command: ["dockerd", "-D", "-H", "unix:///var/run/docker/docker.sock"] + privileged: true + volumes: + - docker-runtime:/var/run/docker + - ${REG_DIR}/daemon.json:/etc/docker/daemon.json:ro + - reset-apps:/var/sota/reset-apps + + composectl: + build: + context: ${SRC_DIR} + dockerfile: ${CPS_DIR}/Dockerfile + args: + REG_CERT: ${REG_CERT} + SRC_DIR: ${SRC_DIR} + volumes: + - ${SRC_DIR}:${SRC_DIR} + - ${BIN_DIR}:${SRC_DIR}/bin + - docker-runtime:/var/run/docker + - reset-apps:/var/sota/reset-apps + working_dir: ${SRC_DIR} + depends_on: + - registry + - dockerd + environment: + - GOCACHE=${SRC_DIR}/.cache + - DOCKER_HOST=unix:///var/run/docker/docker.sock + - COMPOSECTL_EXE=${BIN_DIR}/composectl + - SRC_DIR=${SRC_DIR} + - STOREROOT=/var/sota/reset-apps + +volumes: + docker-runtime: + reset-apps: diff --git a/test/compose/registry/certs/gen-certs.sh b/test/compose/registry/certs/gen-certs.sh new file mode 100755 index 0000000..12dcecf --- /dev/null +++ b/test/compose/registry/certs/gen-certs.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +openssl req -new -x509 -nodes -days 36500 -config openssl.cnf -keyout registry.key -out registry.crt diff --git a/test/compose/registry/certs/openssl.cnf b/test/compose/registry/certs/openssl.cnf new file mode 100644 index 0000000..5b3e2a7 --- /dev/null +++ b/test/compose/registry/certs/openssl.cnf @@ -0,0 +1,25 @@ +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +req_extensions = req_ext +x509_extensions = v3_req +prompt = no + +[ req_distinguished_name ] +C = US +ST = State +L = City +O = Organization +CN = registry + +[ req_ext ] +subjectAltName = @alt_names + +[ v3_req ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = registry +DNS.2 = localhost +IP.1 = 127.0.0.1 + diff --git a/test/compose/registry/certs/registry.crt b/test/compose/registry/certs/registry.crt new file mode 100644 index 0000000..b101840 --- /dev/null +++ b/test/compose/registry/certs/registry.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIUD6KzUJQJR7CPdPYnPZxOiAyNdckwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xETAPBgNVBAMMCHJlZ2lzdHJ5MCAXDTI0 +MDkxOTA5NTYyM1oYDzIxMjQwODI2MDk1NjIzWjBWMQswCQYDVQQGEwJVUzEOMAwG +A1UECAwFU3RhdGUxDTALBgNVBAcMBENpdHkxFTATBgNVBAoMDE9yZ2FuaXphdGlv +bjERMA8GA1UEAwwIcmVnaXN0cnkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQC7U1ouHJ/2wJt2q19gzt/rfCStH74Elwti0hQ0+Bpg97P2ooD2GixYzWHx +Y5oeVoAAswVA8A0RWTOLw9qIijdWTsG32CUBSaAjdnD6TeawxsayUwUq9xZYweQJ +rGUYzsGs6fBMRjriPGLVcOw9TArEPmXMucd2W7qRrWBMrLvCXzcaETFy57uM5AQG +mEBnvXcGdOlEjN4VNv/gretSkVVX93f/Efw8MWrN+ABXXthEoKAv+iriPrfeU0JW +kx6tFr3SS7/Xg6XbRAHNWhxSfkSY8SGTv+1qeDpSv3YUBkzPcxbWWej1FCaonwmR +xIk8x1ysFJLVg8MPnvZS9TZisKajBziwoVZufKm0DrD2BVAlDlGmsqRlwwNEGg31 +fRdEayqYZm8LVoOuHSPudsXRHZaLZcbiPUmucxS48EofUVrdwiGtW17vDdweEyLh +nnILtlvsSHJWN13vkxqyBlx83kGaeGFdncoQHGZLd+GaZdP4pjywLra5a/SX0qQv +qkzlOIGUhdMXE0XNpBHo8k25DLfYiTtgrZdJuWpJzqFsW0FPtgGdaJ6+8VwGSmo4 +r1JM+/T5FDQWt9j/d2QQYYfpSvkxOFOb2x6Z7NuTI6eHqtWPGfDAxFv79wF7XJ8K +ZvXq29iVgXCkC0IdMLKq9HfDJ49IzTH3ozt9g3MDCUfj29oRpQIDAQABoygwJjAk +BgNVHREEHTAbgghyZWdpc3RyeYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEB +CwUAA4ICAQCJMp0ia0ko27uWSELbla0rz7GBj5FBj96SlEmeAFlJum/oJiWxAdyR +ih+NzwoZoA1YszEihETVByAcevCqA0KexanS6bbJhDVh+EGJW0PQo5H/eXsrOwPF +2v932vg7occmaHyOxJQyulLv9i+NNc2Upilv49nEUCQaqrF0Ts5JS1Qe+w0ENtfR +NoB6Qmgqs5rHDDJNPsjCLvmnb3trbRzlS5VbrC30iMvcCfslcXsbl/gcCZbcJh3h +y0Ju4xP26o4xK+mW1mbCBXZJ6rByzs3B8Pn1l5K9dbKxvb6c4yw/GuTDuzzHkgG+ +wBTfoqkxk/nnU4l+jgl2qT4DjCZBnonk4Abj9UP4ZPB0qLCrR7h/lfex1rOZ5Het +7Wl2aJtA19zuIAE4qMuQfgtcB8NLTrj6dWUCCmfJdhKjad+dA8uKgXYmT/NYYuvp +wr/g/Kjf7DaPs8SN+zP+MXEX6rXPCxYi5NQfpJ8bEC8o3TWLTrfWNAdkJNYS4vCf +P/QXl40YeIhjtMkyyY3LH1+yLRMc6uWd+YG9H6t7mP6+pYnIZ8uCgR2yXQQcDYDt +q4seLCcyvkOrt0eDqth4VDS1SweDyIodYPDMA9mwsBjY3AT5m0nbvhZf4vGwxDhK +elQdZQYmUjzxgz9kKjyzDPYscTZXGQramIBsskcmAsYuByYND7Nvow== +-----END CERTIFICATE----- diff --git a/test/compose/registry/certs/registry.key b/test/compose/registry/certs/registry.key new file mode 100644 index 0000000..79ef69b --- /dev/null +++ b/test/compose/registry/certs/registry.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +cz11Y7m1L6FbApCu066rZJn9BmA2Hw== +-----END PRIVATE KEY----- diff --git a/test/compose/registry/config.yml b/test/compose/registry/config.yml new file mode 100644 index 0000000..53335d6 --- /dev/null +++ b/test/compose/registry/config.yml @@ -0,0 +1,14 @@ +version: 0.1 +log: + level: debug + fields: + service: registry + environment: development +storage: + filesystem: + rootdirectory: /var/lib/registry +http: + addr: :5000 + tls: + certificate: /certs/registry.crt + key: /certs/registry.key diff --git a/test/compose/registry/daemon.json b/test/compose/registry/daemon.json new file mode 100644 index 0000000..492e203 --- /dev/null +++ b/test/compose/registry/daemon.json @@ -0,0 +1,3 @@ +{ + "insecure-registries": ["registry:5000"] +} From 0252aa7e09d5153af15e4e003d7fdc0b7e5efd85 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 18 Sep 2024 14:59:34 +0200 Subject: [PATCH 2/5] test: Add test fixtures and base e2e test Signed-off-by: Mike Sul --- Makefile | 4 + cmd/composectl/cmd/check.go | 25 ++-- cmd/composectl/cmd/list.go | 6 +- test/fixtures/composectl_cmds.go | 215 +++++++++++++++++++++++++++++++ test/integration/smoke_test.go | 29 +++++ 5 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 test/fixtures/composectl_cmds.go create mode 100644 test/integration/smoke_test.go diff --git a/Makefile b/Makefile index ec7c147..a6cbda3 100644 --- a/Makefile +++ b/Makefile @@ -68,3 +68,7 @@ check: format tidy-mod: go mod tidy -go=$(MODVER) + +# target should be run only in the dev container +test-e2e: $(exe) + go test -v ./... diff --git a/cmd/composectl/cmd/check.go b/cmd/composectl/cmd/check.go index 4d647af..dcc2080 100644 --- a/cmd/composectl/cmd/check.go +++ b/cmd/composectl/cmd/check.go @@ -35,19 +35,24 @@ type ( CheckInstall bool } - checkAppResult struct { + CheckAppResult struct { MissingBlobs map[digest.Digest]compose.BlobInfo `json:"missing_blobs"` TotalPullSize int64 `json:"total_pull_size"` TotalStoreSize int64 `json:"total_store_size"` TotalRuntimeSize int64 `json:"total_runtime_size"` } + CheckAndInstallResult struct { + FetchCheck *CheckAppResult `json:"fetch_check"` + InstallCheck *InstallCheckResult `json:"install_check"` + } + appInstallCheckResult struct { AppName string `json:"app_name"` MissingImages []string `json:"missing_images"` } - installCheckResult map[string]*appInstallCheckResult + InstallCheckResult map[string]*appInstallCheckResult ) const ( @@ -90,7 +95,7 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) { quietCheck = true } cr, ui, _ := checkApps(cmd.Context(), args, *opts.UsageWatermark, *opts.SrcStorePath, quietCheck) - var ir installCheckResult + var ir InstallCheckResult var err error if opts.CheckInstall { ir, err = checkIfInstalled(cmd.Context(), args, *opts.SrcStorePath, config.DockerHost) @@ -99,8 +104,8 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) { if opts.Format == "json" { aggregatedCheckRes := struct { - FetchCheck *checkAppResult `json:"fetch_check"` - InstallCheck *installCheckResult `json:"install_check"` + FetchCheck *CheckAppResult `json:"fetch_check"` + InstallCheck *InstallCheckResult `json:"install_check"` }{ FetchCheck: cr, InstallCheck: &ir, @@ -128,7 +133,7 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) { } } -func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcStorePath string, quiet bool) (*checkAppResult, *compose.UsageInfo, []compose.App) { +func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcStorePath string, quiet bool) (*CheckAppResult, *compose.UsageInfo, []compose.App) { if usageWatermark < MinUsageWatermark { DieNotNil(fmt.Errorf("the specified usage watermark is lower than the minimum allowed; %d < %d", usageWatermark, MinUsageWatermark)) } @@ -151,7 +156,7 @@ func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcSt var apps []compose.App blobsToPull := map[digest.Digest]compose.BlobInfo{} - checkRes := checkAppResult{MissingBlobs: blobsToPull} + checkRes := CheckAppResult{MissingBlobs: blobsToPull} for _, appRef := range appRefs { if !quiet { @@ -229,12 +234,12 @@ func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcSt return &checkRes, ui, apps } -func (cr *checkAppResult) print() { +func (cr *CheckAppResult) print() { fmt.Printf("%d blobs to pull; total download size: %s, total store size: %s, total runtime size of missing blobs: %s, total required: %s\n", len(cr.MissingBlobs), units.BytesSize(float64(cr.TotalPullSize)), units.BytesSize(float64(cr.TotalStoreSize)), units.BytesSize(float64(cr.TotalRuntimeSize)), units.BytesSize(float64(cr.TotalStoreSize+cr.TotalRuntimeSize))) } -func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string, dockerHost string) (installCheckResult, error) { +func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string, dockerHost string) (InstallCheckResult, error) { cli, err := compose.GetDockerClient(dockerHost) if err != nil { return nil, err @@ -254,7 +259,7 @@ func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string } } - checkResult := installCheckResult{} + checkResult := InstallCheckResult{} blobProvider := compose.NewStoreBlobProvider(path.Join(srcStorePath, "blobs", "sha256")) for _, appRef := range appRefs { app, _, err := v1.NewAppLoader().LoadAppTree(ctx, blobProvider, platforms.OnlyStrict(config.Platform), appRef) diff --git a/cmd/composectl/cmd/list.go b/cmd/composectl/cmd/list.go index 7b61a30..460ad9e 100644 --- a/cmd/composectl/cmd/list.go +++ b/cmd/composectl/cmd/list.go @@ -18,7 +18,7 @@ type ( listOptions struct { Format string } - appJsonOutput struct { + AppJsonOutput struct { Name string `json:"name"` URI string `json:"uri"` } @@ -42,9 +42,9 @@ func listApps(cmd *cobra.Command, args []string, opts *listOptions) { apps, err := cs.ListApps(cmd.Context()) DieNotNil(err) if opts.Format == "json" { - var lsOutput []appJsonOutput + var lsOutput []AppJsonOutput for _, app := range apps { - lsOutput = append(lsOutput, appJsonOutput{ + lsOutput = append(lsOutput, AppJsonOutput{ Name: app.Name, URI: app.String(), }) diff --git a/test/fixtures/composectl_cmds.go b/test/fixtures/composectl_cmds.go new file mode 100644 index 0000000..0222f6b --- /dev/null +++ b/test/fixtures/composectl_cmds.go @@ -0,0 +1,215 @@ +package fixtures + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + composectl "github.com/foundriesio/composeapp/cmd/composectl/cmd" + rand2 "math/rand" + "os" + "os/exec" + "path" + "testing" + "time" +) + +var ( + composeExec = os.Getenv("COMPOSECTL_EXE") +) + +type ( + App struct { + Name string + BaseUri string + PublishedUri string + Dir string + } +) + +func NewApp(t *testing.T, composeDef string, appName ...string) *App { + var name string + if len(appName) > 0 { + name = appName[0] + } else { + name = randomString(5) + } + appDir := path.Join(t.TempDir(), name) + err := os.MkdirAll(appDir, 0o755) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(path.Join(appDir, "docker-compose.yml"), []byte(composeDef), 0o640) + if err != nil { + t.Fatal(err) + } + return &App{ + Name: name, + BaseUri: "registry:5000/factory/" + name, + Dir: appDir, + } +} + +func (a *App) Publish(t *testing.T) { + t.Run("publish app", func(t *testing.T) { + digestFile := path.Join(a.Dir, "digest.sha256") + tag, err := randomStringCrypto(7) + if err != nil { + t.Fatalf("failed to generate a random image tag value: %s\n", err) + } + runCmd(t, a.Dir, "publish", "-d", digestFile, a.BaseUri+":"+tag, "amd64") + if b, err := os.ReadFile(digestFile); err == nil { + a.PublishedUri = a.BaseUri + "@" + string(b) + } else { + t.Fatalf("failed to read the published app digest: %s\n", err) + } + fmt.Printf("published app uri: %s\n", a.PublishedUri) + }) +} + +func (a *App) Pull(t *testing.T) { + a.runCmd(t, "pull app", "pull", a.PublishedUri, "-u", "90") +} + +func (a *App) Remove(t *testing.T) { + a.runCmd(t, "remove app", "rm", a.PublishedUri) +} + +func (a *App) Install(t *testing.T) { + a.runCmd(t, "install app", "install", a.PublishedUri) +} + +func (a *App) Uninstall(t *testing.T) { + a.runCmd(t, "uninstall app", "uninstall", a.Name) +} + +func (a *App) Run(t *testing.T) { + a.runCmd(t, "run app", "run", a.Name) +} + +func (a *App) Up(t *testing.T) { + t.Run("compose up", func(t *testing.T) { + homeDir, homeDirErr := os.UserHomeDir() + if homeDirErr != nil { + t.Errorf("failed to get home directory path: %s\n", homeDirErr) + } + composeRoot := path.Join(homeDir, ".composeapps/projects", a.Name) + + c := exec.Command("docker", "compose", "up", "--remove-orphans", "-d") + c.Dir = composeRoot + output, err := c.CombinedOutput() + if err != nil { + t.Errorf("failed to run `docker compose up -d` command: %s\n", output) + } + }) +} + +func (a *App) Stop(t *testing.T) { + a.runCmd(t, "stop app", "stop", a.Name) +} + +func (a *App) CheckFetched(t *testing.T) { + t.Run("list app", func(t *testing.T) { + output := runCmd(t, a.Dir, "ls", "--format", "json") + var lsOutput []composectl.AppJsonOutput + if err := json.Unmarshal(output, &lsOutput); err != nil { + t.Errorf("failed to unmarshal app list output: %s\n", err) + } + if a.PublishedUri != lsOutput[0].URI { + t.Errorf("app uri in the list output does not equal to the published app;"+ + " published app uri: %s, app list uri: %s\n", a.PublishedUri, lsOutput[0].URI) + } + }) + t.Run("check app", func(t *testing.T) { + output := runCmd(t, a.Dir, "check", "--local", a.PublishedUri, "--format", "json") + checkResult := composectl.CheckAndInstallResult{} + if err := json.Unmarshal(output, &checkResult); err != nil { + t.Errorf("failed to unmarshal check app result: %s\n", err) + } + if len(checkResult.FetchCheck.MissingBlobs) > 0 { + t.Errorf("There are missing app blobs: %+v\n", checkResult.FetchCheck.MissingBlobs) + } + }) +} + +func (a *App) CheckInstalled(t *testing.T) { + t.Run("check if installed", func(t *testing.T) { + output := runCmd(t, a.Dir, "check", "--local", "--install", a.PublishedUri, "--format", "json") + checkResult := composectl.CheckAndInstallResult{} + if err := json.Unmarshal(output, &checkResult); err != nil { + t.Errorf("failed to unmarshal check app result: %s\n", err) + } + + if len(checkResult.FetchCheck.MissingBlobs) > 0 { + t.Errorf("there are missing app blobs: %+v\n", checkResult.FetchCheck.MissingBlobs) + } + if checkResult.InstallCheck == nil || len(*checkResult.InstallCheck) != 1 { + t.Errorf("invalid install check result: %+v\n", checkResult.InstallCheck) + } + installCheckRes, ok := (*checkResult.InstallCheck)[a.PublishedUri] + if !ok { + t.Errorf("no app in the install check result: %+v\n", *checkResult.InstallCheck) + } + if len(installCheckRes.MissingImages) > 0 { + t.Errorf("there are missing app images in docker store: %+v\n", installCheckRes.MissingImages) + } + }) +} + +func (a *App) CheckRunning(t *testing.T) { + t.Run("check if running", func(t *testing.T) { + output := runCmd(t, "", "ps", a.PublishedUri, "--format", "json") + var psOutput map[string]composectl.App + if err := json.Unmarshal(output, &psOutput); err != nil { + t.Errorf("failed to unmarshal app ps output: %s\n", err) + } + if len(psOutput) != 1 { + t.Errorf("expected one element in ps output, got: %d\n", len(psOutput)) + } + appStatus, ok := psOutput[a.PublishedUri] + if !ok { + t.Errorf("no app URI in the ps output: %+v\n", psOutput) + } + if appStatus.State != "running" { + t.Errorf("app is not running, its state: %+s\n", appStatus.State) + } + }) +} + +func (a *App) runCmd(t *testing.T, desc string, args ...string) { + t.Run(desc, func(t *testing.T) { + runCmd(t, a.Dir, args...) + }) +} + +func runCmd(t *testing.T, appDir string, args ...string) []byte { + c := exec.Command(composeExec, args...) + if len(appDir) > 0 { + c.Dir = appDir + } + output, err := c.CombinedOutput() + if err != nil { + t.Fatalf("failed to run `%s` command: %s\n", args[0], output) + } + return output +} + +func randomStringCrypto(length int) (string, error) { + bytes := make([]byte, length) + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(bytes)[:length], nil +} + +func randomString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz" + seededRand := rand2.New(rand2.NewSource(time.Now().UnixNano())) + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} diff --git a/test/integration/smoke_test.go b/test/integration/smoke_test.go new file mode 100644 index 0000000..806a988 --- /dev/null +++ b/test/integration/smoke_test.go @@ -0,0 +1,29 @@ +package e2e_tests + +import ( + "github.com/foundriesio/composeapp/test/fixtures" + "testing" +) + +func TestSmoke(t *testing.T) { + appComposeDef := ` +services: + busybox: + image: ghcr.io/foundriesio/busybox:1.36 + command: sh -c "while true; do sleep 60; done" +` + app := fixtures.NewApp(t, appComposeDef) + app.Publish(t) + + app.Pull(t) + defer app.Remove(t) + app.CheckFetched(t) + + app.Install(t) + defer app.Uninstall(t) + app.CheckInstalled(t) + + app.Run(t) + defer app.Stop(t) + app.CheckRunning(t) +} From 12a8f4d0ebfe323771f1b8195038f4b5264412f5 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 18 Sep 2024 15:19:05 +0200 Subject: [PATCH 3/5] test: Add tests for two edge cases Add tests for the following cases: 1. The same image is used by more than one compose service. 2. There are more than one version of the same app in the app store. Signed-off-by: Mike Sul --- Makefile | 5 ++- test/fixtures/preload-images.sh | 29 ++++++++++++ test/integration/edge_cases_test.go | 69 +++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100755 test/fixtures/preload-images.sh create mode 100644 test/integration/edge_cases_test.go diff --git a/Makefile b/Makefile index a6cbda3..c4c8b5e 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,9 @@ check: format tidy-mod: go mod tidy -go=$(MODVER) +preload-images: + test/fixtures/preload-images.sh + # target should be run only in the dev container -test-e2e: $(exe) +test-e2e: $(exe) preload-images go test -v ./... diff --git a/test/fixtures/preload-images.sh b/test/fixtures/preload-images.sh new file mode 100755 index 0000000..2c28d3f --- /dev/null +++ b/test/fixtures/preload-images.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +DIR="$(dirname "$(realpath "$0")")" +REGISTRY_URL="registry:5000" +IMAGE_NAME="factory/runner-image" +IMAGE_TAG="v0.1" +IMAGE_URI="${REGISTRY_URL}/${IMAGE_NAME}:${IMAGE_TAG}" + +SRC_IMAGE="ghcr.io/foundriesio/busybox:1.36" + +# Check if the image exists in the registry +check_image() { + local response=$(curl -s -o /dev/null -w "%{http_code}" \ + "https://${REGISTRY_URL}/v2/${IMAGE_NAME}/manifests/${IMAGE_TAG}") + + if [[ "$response" == "200" || "$response" == "302" ]]; then + return 0 + else + return 1 + fi +} + +if ! check_image; then + docker pull ${SRC_IMAGE} + docker tag ${SRC_IMAGE} ${IMAGE_URI} + docker push ${IMAGE_URI} +else + echo "Image ${IMAGE_URI} exists in the registry." +fi diff --git a/test/integration/edge_cases_test.go b/test/integration/edge_cases_test.go new file mode 100644 index 0000000..f1fc355 --- /dev/null +++ b/test/integration/edge_cases_test.go @@ -0,0 +1,69 @@ +package e2e_tests + +import ( + f "github.com/foundriesio/composeapp/test/fixtures" + "testing" +) + +func TestAppImageMultiUse(t *testing.T) { + appComposeDef := ` +services: + srvs-01: + image: registry:5000/factory/runner-image:v0.1 + command: sh -c "while true; do sleep 60; done" + ports: + - 8080:80 + srvs-02: + image: registry:5000/factory/runner-image:v0.1 + command: sh -c "while true; do sleep 60; done" +` + app := f.NewApp(t, appComposeDef) + app.Publish(t) + + app.Pull(t) + defer app.Remove(t) + + app.Install(t) + defer app.Uninstall(t) + + app.Run(t) + defer app.Stop(t) + app.CheckRunning(t) +} + +func TestAppMultipleVersionsInStore(t *testing.T) { + appComposeDef := ` +services: + srvs-01: + image: registry:5000/factory/runner-image:v0.1 + command: sh -c "while true; do sleep 60; done" + environment: + - VERSION = 0.1 +` + appComposeDef02 := ` +services: + srvs-01: + image: registry:5000/factory/runner-image:v0.1 + command: sh -c "while true; do sleep 60; done" + environment: + - VERSION = 0.2 +` + app := f.NewApp(t, appComposeDef) + app.Publish(t) + + app02 := f.NewApp(t, appComposeDef02, app.Name) + app02.Publish(t) + + app.Pull(t) + defer app.Remove(t) + + app02.Pull(t) + defer app02.Remove(t) + + app.Install(t) + defer app.Uninstall(t) + + app.Up(t) + defer app.Stop(t) + app.CheckRunning(t) +} From 8bba07d09d58484ea6010ba6e52919ada1af8f98 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 18 Sep 2024 15:23:33 +0200 Subject: [PATCH 4/5] ci: Add github workflow to run all tests Signed-off-by: Mike Sul --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6c406c9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: Tests +on: + push: + pull_request: + branches: + - main + +jobs: + test: + name: Build and run all tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: build + run: docker compose --env-file=test/compose/.env.test -f test/compose/docker-compose.yml run composectl make + - name: test + run: docker compose --env-file=test/compose/.env.test -f test/compose/docker-compose.yml run composectl make test-e2e + - name: teardown test env + run: docker compose --env-file=test/compose/.env.test -f test/compose/docker-compose.yml down From 6eca2756071f7d600ad1f2f4ff0227147a9b78c2 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Fri, 20 Sep 2024 12:27:19 +0200 Subject: [PATCH 5/5] readme: Update README with info about dev&test env Signed-off-by: Mike Sul --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ecaf8e0..14bdb2d 100644 --- a/README.md +++ b/README.md @@ -88,4 +88,22 @@ composectl rm [] [--prune] ``` ```commandline composectl prune -``` \ No newline at end of file +``` + +## Development And Testing + +The dev&test environment based on docker compose contains all required elements to build, manually test, as well as run automated tests. +To launch the environment and enter into its shell just run: +```commandline +./dev-shell.sh +``` +It will take some time to start the environment for the first time because: +1. The docker daemon (dind) and the registry container (distribution) images have to be pulled. +2. The dev&test container should be built. + +Once you are logged into the container shell you can build and run the `composectl` utility, test it manually +and run automated tests: +```commandline +./dev-shell.sh +make test-e2e +```