Skip to content

Commit c9b360e

Browse files
Merge pull request #3684 from Azure/feat/allow-exact-tag-match
feat: search exact tag match avoiding pagination
2 parents 4db1389 + 77b7338 commit c9b360e

File tree

12 files changed

+738
-31
lines changed

12 files changed

+738
-31
lines changed

tooling/image-updater/README.md

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ When using `--env stg` or `--env prod`, the tool operates in **promotion mode**:
9191

9292
## Output Format
9393

94-
When the tool updates image digests in YAML files, it automatically adds inline comments with version tag and timestamp information:
94+
When the tool updates image digests in YAML files, it automatically adds inline comments with version information and timestamp:
9595

9696
```yaml
9797
defaults:
@@ -102,11 +102,28 @@ defaults:
102102
103103
This helps track:
104104
105-
- **Tag name**: The version or tag name (e.g., `v1.18.4`)
105+
- **Version**: The version information from either:
106+
- Container label (if `versionLabel` is configured) - e.g., a commit hash from `org.opencontainers.image.revision`
107+
- Tag name (if no version label is configured) - e.g., `v1.18.4`
106108
- **Timestamp**: When the image was created/published (format: `YYYY-MM-DD HH:MM`)
107109

108110
The comments are automatically generated and updated each time the tool runs.
109111

112+
### Version Labels
113+
114+
By default, when using the `tag` field (e.g., `tag: "latest"`), the tool automatically extracts version information from the `org.opencontainers.image.revision` container label if present. This provides meaningful version information even when using generic tags like "latest" or "stable".
115+
116+
You can customize the label to extract using the `versionLabel` field:
117+
118+
```yaml
119+
source:
120+
image: quay.io/example/image
121+
tag: "latest"
122+
versionLabel: "org.opencontainers.image.revision" # Default when using 'tag'
123+
```
124+
125+
When using `tagPattern`, no version label is extracted by default (uses the tag name), but you can explicitly configure one if needed.
126+
110127
## Configuration
111128

112129
Define images to monitor and target files to update. Each image can optionally specify Azure Key Vault credentials for authentication.
@@ -158,6 +175,27 @@ images:
158175
filePath: ../../config/config.yaml
159176
env: dev
160177
178+
# Quay.io image pinned to specific version (e.g., during rollback)
179+
pko-manager:
180+
source:
181+
image: quay.io/package-operator/package-operator-manager
182+
tag: "v1.18.3" # Pin to specific version instead of using pattern
183+
targets:
184+
- jsonPath: defaults.pko.imageManager.digest
185+
filePath: ../../config/config.yaml
186+
env: dev
187+
188+
# Image using generic tag with version label extraction
189+
my-app:
190+
source:
191+
image: quay.io/example/my-app
192+
tag: "latest" # Generic tag
193+
versionLabel: "org.opencontainers.image.revision" # Extracts commit hash from label (default)
194+
targets:
195+
- jsonPath: defaults.myApp.image.digest
196+
filePath: ../../config/config.yaml
197+
env: dev
198+
161199
# Private ACR image requiring authentication
162200
arohcpfrontend:
163201
source:
@@ -344,18 +382,57 @@ images:
344382
- Read access to the specified Key Vault
345383
- Pull secret must be stored in Key Vault in Docker config.json format (supports both base64-encoded and raw JSON)
346384

347-
## Tag Patterns
385+
## Tag Selection
386+
387+
You can specify which image tag to use in two ways:
388+
389+
### Option 1: Specific Tag (Recommended for pinning versions)
390+
391+
Use the `tag` field to specify an exact tag name:
392+
393+
```yaml
394+
source:
395+
image: quay.io/package-operator/package-operator-package
396+
tag: "v1.18.3" # Pin to specific version
397+
```
398+
399+
**Use cases:**
400+
- Pinning to a specific version temporarily (e.g., during a rollback)
401+
- Testing a specific release
402+
- Production stability requirements
348403

349-
Common regex patterns for filtering tags:
404+
**Performance benefits:**
405+
- **No pagination required** - fetches only the specified tag directly from the registry
406+
- Faster execution compared to pattern matching which requires listing all tags
350407

408+
### Option 2: Tag Pattern (Recommended for automatic updates)
409+
410+
Use the `tagPattern` field with a regex pattern to automatically select the latest matching tag:
411+
412+
```yaml
413+
source:
414+
image: quay.io/package-operator/package-operator-package
415+
tagPattern: "^v\\d+\\.\\d+\\.\\d+$" # Match any semantic version
416+
```
417+
418+
**Common regex patterns:**
351419
- `^[a-f0-9]{7}$` - 7-character commit hashes (short)
352420
- `^[a-f0-9]{40}$` - 40-character commit hashes (full)
353421
- `^sha256-[a-f0-9]{64}$` - SHA256-prefixed single-arch images
354422
- `^latest$` - Only 'latest' tag
355423
- `^v\\d+\\.\\d+\\.\\d+$` - Semantic versions (v1.2.3)
356424
- `^main-.*` - Tags starting with 'main-'
357425

358-
If no pattern is specified, uses the most recently pushed tag.
426+
**Use cases:**
427+
- Continuous updates to the latest version matching a pattern
428+
- Development and staging environments
429+
- Following a release branch
430+
431+
### Important Notes
432+
433+
- `tag` and `tagPattern` are **mutually exclusive** - you can only specify one
434+
- If neither is specified, the tool uses the most recently pushed tag
435+
- When using `tag`, the tool will find and use that exact tag (case-sensitive)
359436

360437
## Architecture Filtering
361438

@@ -456,7 +533,9 @@ Use `--verbosity 2` or higher when debugging authentication issues, tag filterin
456533
| Field | Type | Required | Default | Description |
457534
|-------|------|----------|---------|-------------|
458535
| `image` | string | Yes | - | Full image reference (registry/repository) |
459-
| `tagPattern` | string | No | - | Regex pattern to filter tags (uses most recent if omitted) |
536+
| `tag` | string | No | - | Exact tag to use (mutually exclusive with `tagPattern`) |
537+
| `tagPattern` | string | No | - | Regex pattern to filter tags (mutually exclusive with `tag`) |
538+
| `versionLabel` | string | No | `org.opencontainers.image.revision` (when using `tag`), empty (when using `tagPattern`) | Container label to extract for human-friendly version in comments and output table. Defaults to `org.opencontainers.image.revision` when using `tag` field. |
460539
| `architecture` | string | No | `amd64` | Target architecture for single-arch images (`amd64`, `arm64`, etc.) |
461540
| `multiArch` | bool | No | `false` | If `true`, fetches multi-arch manifest list digest |
462541
| `useAuth` | bool | No | `false` | If `true`, uses authentication (required for private registries) |
@@ -466,6 +545,8 @@ Use `--verbosity 2` or higher when debugging authentication issues, tag filterin
466545

467546
**Notes**:
468547

548+
- `tag` and `tagPattern` are mutually exclusive - only one can be specified
549+
- If neither `tag` nor `tagPattern` is specified, uses the most recently pushed tag
469550
- `multiArch` and `architecture` are mutually exclusive
470551
- `useAuth` defaults to `false` for all registries
471552
- For private registries, explicitly set `useAuth: true`

tooling/image-updater/config.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ images:
4040
hypershift:
4141
source:
4242
image: quay.io/redhat-services-prod/crt-redhat-acm-tenant/hypershift/hypershift-operator
43-
tagPattern: "^[a-f0-9]{7}$"
43+
tag: "latest"
4444
multiArch: true # Use multi-arch manifest
4545
useAuth: true
4646
keyVault:
@@ -64,7 +64,7 @@ images:
6464
source:
6565
image: quay.io/package-operator/package-operator-package
6666
# tagPattern: "^v\\d+\\.\\d+\\.\\d+$"
67-
tagPattern: "v1.18.3" # Current image is failing, we needed to rollback
67+
tag: "v1.18.3" # Current image is failing, we needed to rollback
6868
targets:
6969
- jsonPath: defaults.pko.imagePackage.digest
7070
filePath: ../../config/config.yaml
@@ -82,7 +82,7 @@ images:
8282
source:
8383
image: quay.io/package-operator/package-operator-manager
8484
# tagPattern: "^v\\d+\\.\\d+\\.\\d+$"
85-
tagPattern: "v1.18.3" # Current image is failing, we needed to rollback
85+
tag: "v1.18.3" # Current image is failing, we needed to rollback
8686
targets:
8787
- jsonPath: defaults.pko.imageManager.digest
8888
filePath: ../../config/config.yaml
@@ -100,7 +100,7 @@ images:
100100
source:
101101
image: quay.io/package-operator/remote-phase-manager
102102
# tagPattern: "^v\\d+\\.\\d+\\.\\d+$"
103-
tagPattern: "v1.18.3" # Current image is failing, we needed to rollback
103+
tag: "v1.18.3" # Current image is failing, we needed to rollback
104104
targets:
105105
- jsonPath: defaults.pko.remotePhaseManager.digest
106106
filePath: ../../config/config.yaml
@@ -118,7 +118,8 @@ images:
118118
clusters-service:
119119
source:
120120
image: quay.io/app-sre/aro-hcp-clusters-service
121-
tagPattern: "^[a-f0-9]{7}$"
121+
tag: "latest"
122+
versionLabel: "vcs-ref"
122123
useAuth: true
123124
keyVault:
124125
url: "https://arohcpdev-global.vault.azure.net/"

tooling/image-updater/internal/clients/acr.go

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
type ACRClient struct {
3030
client *azcontainerregistry.Client
3131
registryURL string
32+
useAuth bool
3233
}
3334

3435
// NewACRClient creates a new Azure Container Registry client
@@ -37,6 +38,7 @@ type ACRClient struct {
3738
func NewACRClient(registryURL string, useAuth bool) (*ACRClient, error) {
3839
acr := &ACRClient{
3940
registryURL: registryURL,
41+
useAuth: useAuth,
4042
}
4143

4244
var client *azcontainerregistry.Client
@@ -132,7 +134,7 @@ func (c *ACRClient) getClient() *azcontainerregistry.Client {
132134
return c.client
133135
}
134136

135-
func (c *ACRClient) GetArchSpecificDigest(ctx context.Context, repository string, tagPattern string, arch string, multiArch bool) (*Tag, error) {
137+
func (c *ACRClient) GetArchSpecificDigest(ctx context.Context, repository string, tagPattern string, arch string, multiArch bool, versionLabel string) (*Tag, error) {
136138
logger, err := logr.FromContext(ctx)
137139
if err != nil {
138140
return nil, fmt.Errorf("logger not found in context: %w", err)
@@ -183,6 +185,7 @@ func (c *ACRClient) GetArchSpecificDigest(ctx context.Context, repository string
183185
// If multiArch is requested and this is a multi-arch manifest, return it
184186
if multiArch && len(manifest.RelatedArtifacts) > 0 {
185187
logger.V(2).Info("found multi-arch manifest", "tag", tag.Name, "relatedArtifacts", len(manifest.RelatedArtifacts), "digest", tag.Digest)
188+
tag.Version = extractVersionLabel(ctx, c.registryURL, repository, tag.Name, versionLabel, c.useAuth)
186189
return &tag, nil
187190
}
188191

@@ -199,6 +202,7 @@ func (c *ACRClient) GetArchSpecificDigest(ctx context.Context, repository string
199202
normalizedArch := NormalizeArchitecture(string(*manifest.Architecture))
200203

201204
if normalizedArch == arch && string(*manifest.OperatingSystem) == "linux" {
205+
tag.Version = extractVersionLabel(ctx, c.registryURL, repository, tag.Name, versionLabel, c.useAuth)
202206
return &tag, nil
203207
}
204208

@@ -210,3 +214,82 @@ func (c *ACRClient) GetArchSpecificDigest(ctx context.Context, repository string
210214
}
211215
return nil, fmt.Errorf("no single-arch %s/linux image found for repository %s (all tags are either multi-arch or different architecture)", arch, repository)
212216
}
217+
218+
// GetDigestForTag fetches the digest for a specific tag without pagination
219+
func (c *ACRClient) GetDigestForTag(ctx context.Context, repository string, tagName string, arch string, multiArch bool, versionLabel string) (*Tag, error) {
220+
logger, err := logr.FromContext(ctx)
221+
if err != nil {
222+
return nil, fmt.Errorf("logger not found in context: %w", err)
223+
}
224+
225+
logger.V(2).Info("fetching digest for specific tag", "registry", c.registryURL, "repository", repository, "tag", tagName)
226+
227+
// Check if context is cancelled before processing
228+
select {
229+
case <-ctx.Done():
230+
return nil, fmt.Errorf("operation cancelled: %w", ctx.Err())
231+
default:
232+
}
233+
234+
client := c.getClient()
235+
236+
// Get tag properties to get the digest
237+
tagProps, err := client.GetTagProperties(ctx, repository, tagName, nil)
238+
if err != nil {
239+
return nil, fmt.Errorf("failed to get tag properties for tag %s: %w", tagName, err)
240+
}
241+
242+
if tagProps.Tag == nil || tagProps.Tag.Digest == nil {
243+
return nil, fmt.Errorf("tag %s has no digest information", tagName)
244+
}
245+
246+
tag := Tag{
247+
Name: tagName,
248+
Digest: *tagProps.Tag.Digest,
249+
}
250+
251+
if tagProps.Tag.CreatedOn != nil {
252+
tag.LastModified = *tagProps.Tag.CreatedOn
253+
}
254+
255+
// Get manifest properties to check architecture
256+
manifestProps, err := client.GetManifestProperties(ctx, repository, tag.Digest, nil)
257+
if err != nil {
258+
return nil, fmt.Errorf("failed to fetch manifest properties for tag %s (digest: %s): %w", tagName, tag.Digest, err)
259+
}
260+
261+
if manifestProps.Manifest == nil {
262+
return nil, fmt.Errorf("tag %s has no manifest information", tagName)
263+
}
264+
265+
manifest := manifestProps.Manifest
266+
267+
// If multiArch is requested, verify this is a multi-arch manifest
268+
if multiArch {
269+
if len(manifest.RelatedArtifacts) == 0 {
270+
return nil, fmt.Errorf("tag %s is not a multi-arch manifest", tagName)
271+
}
272+
logger.V(2).Info("found multi-arch manifest", "tag", tagName, "relatedArtifacts", len(manifest.RelatedArtifacts), "digest", tag.Digest)
273+
return &tag, nil
274+
}
275+
276+
// For single-arch, verify it's not a multi-arch manifest
277+
if len(manifest.RelatedArtifacts) > 0 {
278+
return nil, fmt.Errorf("tag %s is a multi-arch manifest, but single-arch was requested (use multiArch: true)", tagName)
279+
}
280+
281+
if manifest.Architecture == nil || manifest.OperatingSystem == nil {
282+
return nil, fmt.Errorf("tag %s is missing architecture or OS information", tagName)
283+
}
284+
285+
normalizedArch := NormalizeArchitecture(string(*manifest.Architecture))
286+
287+
if normalizedArch != arch || string(*manifest.OperatingSystem) != "linux" {
288+
return nil, fmt.Errorf("tag %s has architecture %s/%s, but %s/linux was requested", tagName, string(*manifest.Architecture), string(*manifest.OperatingSystem), arch)
289+
}
290+
291+
tag.Version = extractVersionLabel(ctx, c.registryURL, repository, tagName, versionLabel, c.useAuth)
292+
logger.V(2).Info("found matching image", "tag", tagName, "arch", normalizedArch, "digest", tag.Digest)
293+
294+
return &tag, nil
295+
}

0 commit comments

Comments
 (0)