Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ dist/
build/

# Entrypoint for the application
!/cmd/d8
!/cmd/d8

# E2E test logs
testing/e2e/.logs/
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ linters:
- examples$
- _test\.go$
- deepcopy\.go$
- testing/
formatters:
enable:
- gci
Expand Down
35 changes: 35 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,41 @@ tasks:
cmds:
- task: _test:go

test:e2e:mirror:
desc: Run full cycle E2E mirror test (platform + modules + security)
summary: |
E2E test for d8 mirror pull/push commands.
Required: E2E_LICENSE_TOKEN env variable

Example: E2E_LICENSE_TOKEN=xxx task test:e2e:mirror
cmds:
- task build
- go test -v -count=1 -timeout 180m -tags=e2e -run 'TestFullCycleE2E' ./testing/e2e/mirror {{ .CLI_ARGS }}

test:e2e:mirror:platform:
desc: Run platform E2E test
cmds:
- task build
- go test -v -count=1 -timeout 120m -tags=e2e -run 'TestPlatform' ./testing/e2e/mirror {{ .CLI_ARGS }}

test:e2e:mirror:modules:
desc: Run all modules E2E test
cmds:
- task build
- go test -v -count=1 -timeout 120m -tags=e2e -run 'TestModulesE2E$' ./testing/e2e/mirror {{ .CLI_ARGS }}


test:e2e:mirror:security:
desc: Run security E2E test
cmds:
- task build
- go test -v -count=1 -timeout 30m -tags=e2e -run 'TestSecurityE2E' ./testing/e2e/mirror {{ .CLI_ARGS }}

test:e2e:mirror:logs:clean:
desc: Clean E2E test logs
cmds:
- rm -rf ./testing/e2e/.logs/*

lint:
desc: Run golangci-lint with auto-fix
cmds:
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/chanced/caps v1.0.2 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cilium/ebpf v0.12.3 // indirect
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,16 @@ github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiw
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/chanced/caps v1.0.2 h1:RELvNN4lZajqSXJGzPaU7z8B4LK2+o2Oc/upeWdgMOA=
github.com/chanced/caps v1.0.2/go.mod h1:SJhRzeYLKJ3OmzyQXhdZ7Etj7lqqWoPtQ1zcSJRtQjs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
Expand Down
2 changes: 0 additions & 2 deletions pkg/libmirror/images/digests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ func TestExtractImageDigestsFromDeckhouseInstaller(t *testing.T) {

installersLayout := createOCILayoutWithInstallerImage(t, "nonexistent.registry.com/deckhouse", installerTag, expectedImages)
client := mock.NewRegistryClientMock(t)
client.GetRegistryMock.Return("nonexistent.registry.com")
client.WithSegmentMock.Return(client)
client.CheckImageExistsMock.Return(nil)
images, err := ExtractImageDigestsFromDeckhouseInstaller(
&params.PullParams{BaseParams: params.BaseParams{
Expand Down
24 changes: 15 additions & 9 deletions pkg/libmirror/layouts/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ func TestPushLayoutToRepoWithParallelism(t *testing.T) {

const totalImages, layersPerImage = 10, 3
imagesLayout := createEmptyOCILayout(t)
host, repoPath, _ := mirrorTestUtils.SetupEmptyRegistryRepo(false)
reg := mirrorTestUtils.SetupTestRegistry(false)
defer reg.Close()
repoPath := reg.Host + "/deckhouse/ee"

client := mock.NewRegistryClientMock(t)
client.PushImageMock.Return(nil)
Expand All @@ -50,7 +52,7 @@ func TestPushLayoutToRepoWithParallelism(t *testing.T) {
digest, err := img.Digest()
s.NoError(err)
err = imagesLayout.AppendImage(img, platformOpt, layout.WithAnnotations(map[string]string{
"org.opencontainers.image.ref.name": host + repoPath + "@" + digest.String(),
"org.opencontainers.image.ref.name": repoPath + "@" + digest.String(),
"io.deckhouse.image.short_tag": digest.Hex,
}))
s.NoError(err)
Expand All @@ -59,7 +61,7 @@ func TestPushLayoutToRepoWithParallelism(t *testing.T) {
err := PushLayoutToRepo(
client,
imagesLayout,
host+repoPath, // Images repo
repoPath,
authn.Anonymous,
log.NewSLogger(slog.LevelDebug),
params.ParallelismConfig{
Expand All @@ -81,7 +83,9 @@ func TestPushLayoutToRepoWithoutParallelism(t *testing.T) {

const totalImages, layersPerImage = 10, 3
imagesLayout := createEmptyOCILayout(t)
host, repoPath, _ := mirrorTestUtils.SetupEmptyRegistryRepo(false)
reg := mirrorTestUtils.SetupTestRegistry(false)
defer reg.Close()
repoPath := reg.Host + "/deckhouse/ee"

client := mock.NewRegistryClientMock(t)
client.PushImageMock.Return(nil)
Expand All @@ -93,7 +97,7 @@ func TestPushLayoutToRepoWithoutParallelism(t *testing.T) {
digest, err := img.Digest()
s.NoError(err)
err = imagesLayout.AppendImage(img, platformOpt, layout.WithAnnotations(map[string]string{
"org.opencontainers.image.ref.name": host + repoPath + "@" + digest.String(),
"org.opencontainers.image.ref.name": repoPath + "@" + digest.String(),
"io.deckhouse.image.short_tag": digest.Hex,
}))
s.NoError(err)
Expand All @@ -102,7 +106,7 @@ func TestPushLayoutToRepoWithoutParallelism(t *testing.T) {
err := PushLayoutToRepo(
client,
imagesLayout,
host+repoPath, // Images repo
repoPath,
authn.Anonymous,
log.NewSLogger(slog.LevelDebug),
params.ParallelismConfig{
Expand All @@ -121,21 +125,23 @@ func TestPushLayoutToRepoWithoutParallelism(t *testing.T) {

func TestPushEmptyLayoutToRepo(t *testing.T) {
s := require.New(t)
host, repoPath, blobHandler := mirrorTestUtils.SetupEmptyRegistryRepo(false)
reg := mirrorTestUtils.SetupTestRegistry(false)
defer reg.Close()
repoPath := reg.Host + "/deckhouse/ee"

client := mock.NewRegistryClientMock(t)

emptyLayout := createEmptyOCILayout(t)
err := PushLayoutToRepo(
client,
emptyLayout,
host+repoPath,
repoPath,
authn.Anonymous,
log.NewSLogger(slog.LevelDebug),
params.DefaultParallelism,
true, // Use plain insecure HTTP
false, // TLS verification irrelevant to HTTP requests
)
s.NoError(err, "Push should not fail")
s.Len(blobHandler.ListBlobs(), 0, "No blobs should be pushed to registry")
s.Len(reg.ListBlobs(), 0, "No blobs should be pushed to registry")
}
182 changes: 182 additions & 0 deletions testing/e2e/mirror/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# E2E Tests for d8 mirror

End-to-end tests for the `d8 mirror pull` and `d8 mirror push` commands.

## Overview

These tests perform **complete mirror cycles with verification** to ensure:
1. All expected images are downloaded from source
2. All images are correctly pushed to target registry
3. All images match between source and target (deep comparison)

## Test Types

| Test | Description | Timeout | Command |
|------|-------------|---------|---------|
| **Full Cycle** | Platform + Modules + Security | 3h | `task test:e2e:mirror` |
| **Platform** | Deckhouse core only | 2h | `task test:e2e:mirror:platform` |
| **Platform Stable** | Only stable channel | 2h | `task test:e2e:mirror:platform` + `E2E_DECKHOUSE_TAG=stable` |
| **Modules** | All modules | 2h | `task test:e2e:mirror:modules` |
| **Single Module** | One module (fast) | 2h | `task test:e2e:mirror:modules` + `E2E_INCLUDE_MODULES=module-name` |
| **Security** | Security DBs only | 30m | `task test:e2e:mirror:security` |

## Verification Approach

### Step 1: Read Expected Images from Source
Before pulling, we independently read what SHOULD be downloaded:
- Release channel versions from source registry
- `images_digests.json` from each installer image
- Module list and versions

### Step 2: Pull & Push
Execute `d8 mirror pull` and `d8 mirror push`

### Step 3: Verify
Compare expected images with what's actually in target:
- All expected digests must exist in target
- All images in target must match source (manifest, config, layers)

This catches:
- **Pull bugs** - if pull forgets to download an image
- **Push bugs** - if push fails to upload an image
- **Data corruption** - if any digest doesn't match

## Running Tests

### Quick Start

```bash
# Full cycle with license token
E2E_LICENSE_TOKEN=xxx task test:e2e:mirror

# Platform only (faster)
E2E_LICENSE_TOKEN=xxx E2E_DECKHOUSE_TAG=stable task test:e2e:mirror:platform

# Single module (fastest)
E2E_LICENSE_TOKEN=xxx E2E_INCLUDE_MODULES=module-name task test:e2e:mirror:modules
```

### Using Environment Variables

```bash
# Official registry with license
E2E_LICENSE_TOKEN=your_license_token \
task test:e2e:mirror

# Local registry
E2E_SOURCE_REGISTRY=localhost:443/deckhouse \
E2E_SOURCE_USER=admin \
E2E_SOURCE_PASSWORD=secret \
E2E_TLS_SKIP_VERIFY=true \
task test:e2e:mirror:platform

# Specific release channel
E2E_LICENSE_TOKEN=xxx \
E2E_DECKHOUSE_TAG=stable \
task test:e2e:mirror:platform

# Specific modules only
E2E_LICENSE_TOKEN=xxx \
E2E_INCLUDE_MODULES="pod-reloader,neuvector" \
task test:e2e:mirror:modules
```

### Configuration Options

| Flag | Environment Variable | Default | Description |
|------|---------------------|---------|-------------|
| `-source-registry` | `E2E_SOURCE_REGISTRY` | `registry.deckhouse.ru/deckhouse/fe` | Source registry |
| `-source-user` | `E2E_SOURCE_USER` | | Source registry username |
| `-source-password` | `E2E_SOURCE_PASSWORD` | | Source registry password |
| `-license-token` | `E2E_LICENSE_TOKEN` | | License token |
| `-target-registry` | `E2E_TARGET_REGISTRY` | `""` (in-memory) | Target registry |
| `-target-user` | `E2E_TARGET_USER` | | Target registry username |
| `-target-password` | `E2E_TARGET_PASSWORD` | | Target registry password |
| `-tls-skip-verify` | `E2E_TLS_SKIP_VERIFY` | `false` | Skip TLS verification |
| `-deckhouse-tag` | `E2E_DECKHOUSE_TAG` | | Specific tag/channel (e.g., `stable`, `v1.65.8`) |
| `-no-modules` | `E2E_NO_MODULES` | `false` | Skip modules |
| `-no-platform` | `E2E_NO_PLATFORM` | `false` | Skip platform |
| `-no-security` | `E2E_NO_SECURITY` | `false` | Skip security DBs |
| `-include-modules` | `E2E_INCLUDE_MODULES` | | Comma-separated module list |
| `-keep-bundle` | `E2E_KEEP_BUNDLE` | `false` | Keep bundle after test |
| `-existing-bundle` | `E2E_EXISTING_BUNDLE` | | Path to existing bundle (skip pull) |
| `-d8-binary` | `E2E_D8_BINARY` | `bin/d8` | Path to d8 binary |
| `-new-pull` | `E2E_NEW_PULL` | `false` | Use experimental pull |

## Test Artifacts

Tests produce artifacts in `testing/e2e/.logs/<test>-<timestamp>/`:

```
testing/e2e/.logs/TestFullCycleE2E-20251226-114128/
├── test.log # Full command output (pull/push)
├── report.txt # Test summary report
└── comparison.txt # Detailed registry comparison
```

### Cleaning Up Logs

```bash
task test:e2e:mirror:logs:clean
```

## Requirements

- Built `d8` binary (run `task build`)
- Valid credentials for source registry
- Network access
- Disk space for bundle (20-50 GB depending on scope)

## What Gets Verified

### Platform Test
1. Release channels exist (alpha, beta, stable, rock-solid)
2. Install images for each version
3. All digests from `images_digests.json` exist in target

### Modules Test
1. Module list matches expected
2. Each module has release tags
3. Module images match source

### Security Test
1. All security databases exist (trivy-db, trivy-bdu, etc.)
2. Tags match source

## Troubleshooting

### "Source authentication not provided"
```bash
E2E_LICENSE_TOKEN=your_token task test:e2e:mirror
```

### "Pull failed"
1. Check `d8` binary: `task build`
2. Check network access
3. For self-signed certs: `E2E_TLS_SKIP_VERIFY=true`

### "Verification failed"
Check `comparison.txt`:
- **Missing in target**: Pull or push didn't transfer the image
- **Digest mismatch**: Data corruption or version skew

### Running Against Local Registry
```bash
E2E_SOURCE_REGISTRY=localhost:5000/deckhouse \
E2E_SOURCE_USER=admin \
E2E_SOURCE_PASSWORD=admin \
E2E_TLS_SKIP_VERIFY=true \
task test:e2e:mirror:platform
```

### Keep Bundle for Debugging
```bash
E2E_KEEP_BUNDLE=true E2E_LICENSE_TOKEN=xxx task test:e2e:mirror:platform
# Bundle location shown in test output
```

### Use Existing Bundle (Skip Pull)
```bash
E2E_EXISTING_BUNDLE=/path/to/bundle E2E_LICENSE_TOKEN=xxx task test:e2e:mirror:platform
# Test will skip pull step and use existing bundle
```
Loading
Loading