Skip to content

Commit 6930a6d

Browse files
committed
feat(RELEASE-1989): skopeo helper client
This commit implements skopeo client supporting copy and inspect operations. Also it provides fake skopeo client which can be injected instead of real skopeo client during tests Assiste-By: claude Signed-off-by: Jindrich Luza <jluza@redhat.com>
1 parent f9b1b50 commit 6930a6d

9 files changed

Lines changed: 2519 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Fake Skopeo Client Implementation Summary
2+
3+
## What Was Implemented
4+
5+
A complete fake/mock implementation of `SkopeoClient` for testing purposes, based on the design discussion.
6+
7+
## Files Created
8+
9+
1. **`scripts/python/helpers/fake/__init__.py`**
10+
- Exports `FakeSkopeoClient` and `patch_skopeo_client()`
11+
- The `patch_skopeo_client()` function performs monkey-patching
12+
13+
2. **`scripts/python/helpers/fake/skopeo.py`**
14+
- `FakeSkopeoClient` class - main implementation
15+
- Loads YAML config from `RELEASE_SERVICE_UTILS_FAKE_SKOPEO_SETUP` env var
16+
- Implements `inspect()` and `copy()` with full method signatures
17+
- Supports regex matching, first-match-wins rule evaluation
18+
- Validates config at load time
19+
- Raises `SkopeoClientError` when no match found
20+
21+
3. **`scripts/python/helpers/fake/README.md`**
22+
- Comprehensive documentation
23+
- Usage examples
24+
- YAML format reference
25+
- Troubleshooting guide
26+
27+
4. **`scripts/python/helpers/fake/example_config.yaml`**
28+
- Demonstrates all features: exact matching, regex, success/failure cases
29+
- Ready-to-use examples for common test scenarios
30+
31+
5. **`scripts/python/helpers/fake/test_fake_skopeo.py`**
32+
- 13 unit tests covering all functionality
33+
- All tests passing
34+
- Validates load-time checks, runtime matching, error handling
35+
36+
6. **`scripts/python/helpers/fake/example_test.sh`**
37+
- Working end-to-end example using bash wrapper
38+
- Demonstrates integration with `publish_index_image`
39+
- Shows how to use in Tekton/CI tests
40+
41+
## Key Design Decisions
42+
43+
### Activation Mechanism
44+
- **Environment variable**: `RELEASE_SERVICE_UTILS_FAKE_SKOPEO_SETUP` points to YAML config
45+
- **Monkey patching**: `patch_skopeo_client()` replaces real client before imports
46+
- **Drop-in replacement**: Same constructor and method signatures as `SkopeoClient`
47+
48+
### YAML Structure
49+
- Top-level operations: `inspect`, `copy`
50+
- Each operation has list of rules with `match` and optional `return`
51+
- First matching rule wins (top-to-bottom evaluation)
52+
53+
### Matching Logic
54+
- Only fields specified in `match` must match
55+
- Extra parameters in actual calls are ignored
56+
- Secrets are always ignored in matching
57+
- `None` values don't match specified patterns
58+
- Regex support via `{regex: "pattern"}` with fullmatch semantics
59+
60+
### Validation
61+
- **Load-time validation**: YAML syntax, rule structure, return types
62+
- **Runtime validation**: Regex patterns applied during matching
63+
- **Error messages**: Detailed, showing attempted params and config file path
64+
65+
### Return Value Handling
66+
67+
**For `inspect()`:**
68+
- `format` specified in match → return must be string
69+
- No `format` in match → return must be dict
70+
- Auto-detected based on match rule
71+
72+
**For `copy()`:**
73+
- Omit `return` section → success (returns `None`)
74+
- `return: {success: true}` → explicit success
75+
- `return: {success: false, ...}` → raises `SkopeoClientError`
76+
- Default values: `returncode=1`, `stdout=""`, `stderr=""`
77+
78+
## Usage Pattern for Tests
79+
80+
```bash
81+
# 1. Create mock config
82+
cat > mock-config.yaml <<EOF
83+
inspect:
84+
- match:
85+
image: "docker://quay.io/target:tag"
86+
format: "{{.Digest}}"
87+
return: "sha256:different"
88+
89+
copy:
90+
- match:
91+
source: "docker://quay.io/source@sha256:abc"
92+
destination: "docker://quay.io/target:tag"
93+
EOF
94+
95+
# 2. Set environment variable
96+
export RELEASE_SERVICE_UTILS_FAKE_SKOPEO_SETUP=/path/to/mock-config.yaml
97+
98+
# 3. Create bash wrapper
99+
publish_index_image() {
100+
python3 - "$@" <<'PYTHON'
101+
import sys
102+
sys.path.insert(0, '/path/to/helpers')
103+
sys.path.insert(0, '/path/to/tasks/internal')
104+
105+
from fake import patch_skopeo_client
106+
patch_skopeo_client()
107+
108+
from publish_index_image import main
109+
sys.exit(main())
110+
PYTHON
111+
}
112+
113+
# 4. Run your test
114+
publish_index_image --source-index "..." --target-index "..."
115+
```
116+
117+
## Testing the Implementation
118+
119+
```bash
120+
# Run unit tests
121+
pytest scripts/python/helpers/fake/test_fake_skopeo.py -v
122+
123+
# Run integration example
124+
./scripts/python/helpers/fake/example_test.sh
125+
```
126+
127+
## What's NOT Included
128+
129+
Based on our design discussion, the following were explicitly excluded:
130+
131+
- Multiple config file support (only single file)
132+
- Special debug/strict/recording modes
133+
- Validation of dict field names (type-only validation)
134+
- Matching on constructor parameters
135+
- Matching on credential values
136+
- Template parsing for `format` parameter (returns pre-formatted strings)
137+
138+
These can be added later if needed without breaking existing configs.
139+
140+
## Next Steps
141+
142+
1. **In your other project**: Create test YAML configs specific to your test scenarios
143+
2. **Update CI/Tekton tasks**: Use the bash wrapper pattern to inject fake client
144+
3. **Write tests**: Use `example_test.sh` as a template for your specific test cases
145+
4. **Iterate**: Add more rules to your YAML configs as you encounter new test scenarios
146+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Fake implementations for testing."""
2+
3+
from fake.skopeo import FakeSkopeoClient
4+
5+
6+
def patch_skopeo_client() -> None:
7+
"""Monkey-patch skopeo.SkopeoClient with FakeSkopeoClient.
8+
9+
This function replaces the real SkopeoClient with the fake implementation.
10+
Must be called before importing any modules that use SkopeoClient.
11+
"""
12+
import skopeo
13+
14+
skopeo.SkopeoClient = FakeSkopeoClient
15+
16+
17+
__all__ = ["FakeSkopeoClient", "patch_skopeo_client"]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
# Example mock configuration for FakeSkopeoClient
3+
#
4+
# This file demonstrates the YAML structure for defining mock responses
5+
# for skopeo operations (inspect and copy).
6+
#
7+
# Usage:
8+
# export RELEASE_SERVICE_UTILS_FAKE_SKOPEO_SETUP=/path/to/this/file.yaml
9+
# python -c "from fake import patch_skopeo_client; patch_skopeo_client(); ..."
10+
11+
# Inspect operation rules
12+
inspect:
13+
# Example 1: Return formatted string when format parameter is specified
14+
- match:
15+
image: "docker://quay.io/source/image@sha256:abc123def456"
16+
format: "{{.Digest}}"
17+
return: "sha256:abc123def456"
18+
19+
# Example 2: Return full manifest dict when no format specified
20+
- match:
21+
image: "docker://quay.io/target/image:v1.0"
22+
return:
23+
Digest: "sha256:def456abc123"
24+
Name: "quay.io/target/image"
25+
RepoTags:
26+
- "v1.0"
27+
- "latest"
28+
Labels:
29+
version: "1.0.0"
30+
maintainer: "team@example.com"
31+
32+
# Example 3: Regex matching for dynamic image names
33+
- match:
34+
image:
35+
regex: "docker://quay.io/.*@sha256:[a-f0-9]{64}"
36+
format: "{{.Digest}}"
37+
return: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
38+
39+
# Example 4: Regex matching without format (returns dict)
40+
- match:
41+
image:
42+
regex: "docker://registry-proxy\\.engineering\\.redhat\\.com/.*"
43+
return:
44+
Digest: "sha256:fedcba987654"
45+
Name: "registry-proxy.engineering.redhat.com/rh-osbs/openshift-golang-builder"
46+
47+
# Copy operation rules
48+
copy:
49+
# Example 1: Successful copy (no return section = success)
50+
- match:
51+
source: "docker://quay.io/source/image@sha256:abc123"
52+
destination: "docker://quay.io/dest/image:tag"
53+
54+
# Example 2: Successful copy with explicit return
55+
- match:
56+
source: "docker://quay.io/another/image:v1"
57+
destination: "docker://quay.io/target/image:v1"
58+
return:
59+
success: true
60+
61+
# Example 3: Failed copy with custom error
62+
- match:
63+
source: "docker://quay.io/nonexistent/image:tag"
64+
destination: "docker://quay.io/dest/image:tag"
65+
return:
66+
success: false
67+
stderr: "Error: manifest unknown: manifest unknown"
68+
returncode: 1
69+
70+
# Example 4: Authentication failure
71+
- match:
72+
source: "docker://quay.io/private/image:tag"
73+
destination: "docker://quay.io/dest/image:tag"
74+
return:
75+
success: false
76+
stderr: "Error: authentication required"
77+
returncode: 1
78+
79+
# Example 5: Regex matching for source with digest
80+
- match:
81+
source:
82+
regex: "docker://quay.io/source/.*@sha256:[a-f0-9]{64}"
83+
destination:
84+
regex: "docker://quay.io/dest/.*:.*"
85+
# Omit return = success

0 commit comments

Comments
 (0)