Skip to content

Commit 5ddb484

Browse files
feat: add func to parse image references (#172)
* feat: add function to parse image reference * feat: release v0.23.2 * docs(lib): add short documentation of the usage of the image package * docs: add image lib to documentation index
1 parent 3ac2978 commit 5ddb484

File tree

6 files changed

+243
-2
lines changed

6 files changed

+243
-2
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.23.1-dev
1+
v0.23.2

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [Controller Utility Functions](libs/controller.md)
1010
- [Custom Resource Definitions](libs/crds.md)
1111
- [Error Handling](libs/errors.md)
12+
- [Image Parsing](libs/image.md)
1213
- [JSON Patch](libs/jsonpatch.md)
1314
- [Logging](libs/logging.md)
1415
- [Key-Value Pairs](libs/pairs.md)

docs/libs/image.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Image Parsing
2+
3+
The `pkg/image` package provides utilities for parsing container image references into their constituent components.
4+
5+
## ParseImage Function
6+
7+
The `ParseImage` function parses a container image string and extracts the image name, tag, and digest components. This is useful for validating, manipulating, or analyzing container image references in Kubernetes controllers.
8+
9+
### Function Signature
10+
11+
```go
12+
func ParseImage(image string) (imageName string, tag string, digest string, err error)
13+
```
14+
15+
### Behavior
16+
17+
- **Default Tag**: If no tag is specified, it defaults to `"latest"`
18+
- **Digest Support**: Handles images with SHA256 digests (indicated by `@sha256:...`)
19+
- **Registry URLs**: Properly handles registry URLs with ports (e.g., `registry.io:5000/image:tag`)
20+
- **Validation**: Returns an error for empty image strings
21+
22+
### Examples
23+
24+
```go
25+
// Basic image with tag
26+
imageName, tag, digest, err := ParseImage("nginx:1.19.0")
27+
// Returns: "nginx", "1.19.0", "", nil
28+
29+
// Image without tag (defaults to latest)
30+
imageName, tag, digest, err := ParseImage("nginx")
31+
// Returns: "nginx", "latest", "", nil
32+
33+
// Image with digest only
34+
imageName, tag, digest, err := ParseImage("nginx@sha256:abcdef...")
35+
// Returns: "nginx", "", "sha256:abcdef...", nil
36+
37+
// Image with both tag and digest
38+
imageName, tag, digest, err := ParseImage("nginx:1.19.0@sha256:abcdef...")
39+
// Returns: "nginx", "1.19.0", "sha256:abcdef...", nil
40+
```
41+
42+
This function is particularly useful when working with container images in Kubernetes controllers, allowing you to extract and validate image components for further processing or validation.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/google/uuid v1.6.0
1212
github.com/onsi/ginkgo/v2 v2.27.1
1313
github.com/onsi/gomega v1.38.2
14-
github.com/openmcp-project/controller-utils/api v0.23.1
14+
github.com/openmcp-project/controller-utils/api v0.23.2
1515
github.com/spf13/pflag v1.0.10
1616
github.com/stretchr/testify v1.11.1
1717
go.uber.org/zap v1.27.0

pkg/image/image.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package image
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// ParseImage parses a container image string and returns the image name, tag, and digest.
9+
// If no tag is specified, it defaults to "latest". If a digest is present, it is returned as well.
10+
// Examples of valid image strings:
11+
// - "nginx:1.19.0" -> imageName: "nginx", tag: "1.19.0", digest: ""
12+
// - "nginx" -> imageName: "nginx", tag: "latest", digest: ""
13+
// - "nginx@sha256:abcdef..." -> imageName: "nginx", tag: "", digest: "sha256:abcdef..."
14+
// - "nginx:1.19.0@sha256:abcdef..." -> imageName: "nginx", tag: "1.19.0", digest: "sha256:abcdef..."
15+
func ParseImage(image string) (imageName string, tag string, digest string, err error) {
16+
if image == "" {
17+
return "", "", "", fmt.Errorf("image string cannot be empty")
18+
}
19+
20+
// Check if the image contains a digest (indicated by @)
21+
digestIndex := strings.LastIndex(image, "@")
22+
23+
var tagPart string
24+
if digestIndex != -1 {
25+
// Extract digest
26+
digest = image[digestIndex+1:]
27+
tagPart = image[:digestIndex]
28+
} else {
29+
tagPart = image
30+
}
31+
32+
// Find the last colon to separate the tag from the image name
33+
// We use LastIndex to handle registry URLs with ports (e.g., registry.io:5000/image:tag)
34+
colonIndex := strings.LastIndex(tagPart, ":")
35+
36+
// If there's a digest but no colon in the tag part, it's a digest-only image
37+
if digestIndex != -1 && colonIndex == -1 {
38+
imageName = tagPart
39+
return imageName, "", digest, nil
40+
}
41+
42+
// If there's no colon, it means no explicit tag was provided
43+
// In this case, default to "latest" tag
44+
if colonIndex == -1 {
45+
imageName = tagPart
46+
return imageName, "latest", digest, nil
47+
}
48+
49+
// Extract image name (everything before the last colon)
50+
imageName = tagPart[:colonIndex]
51+
// Extract tag (everything after the last colon)
52+
tag = tagPart[colonIndex+1:]
53+
54+
return imageName, tag, digest, nil
55+
}

pkg/image/image_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package image
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestParseImage(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
image string
11+
expectedImageName string
12+
expectedTag string
13+
expectedDigest string
14+
expectError bool
15+
}{
16+
{
17+
name: "image with tag only",
18+
image: "nginx:1.21.0",
19+
expectedImageName: "nginx",
20+
expectedTag: "1.21.0",
21+
expectedDigest: "",
22+
expectError: false,
23+
},
24+
{
25+
name: "image with tag and digest",
26+
image: "nginx:1.21.0@sha256:abc123def456",
27+
expectedImageName: "nginx",
28+
expectedTag: "1.21.0",
29+
expectedDigest: "sha256:abc123def456",
30+
expectError: false,
31+
},
32+
{
33+
name: "image with latest tag",
34+
image: "ubuntu:latest",
35+
expectedImageName: "ubuntu",
36+
expectedTag: "latest",
37+
expectedDigest: "",
38+
expectError: false,
39+
},
40+
{
41+
name: "image with version tag and digest",
42+
image: "registry.io/myapp:v2.1.3@sha256:fedcba987654",
43+
expectedImageName: "registry.io/myapp",
44+
expectedTag: "v2.1.3",
45+
expectedDigest: "sha256:fedcba987654",
46+
expectError: false,
47+
},
48+
{
49+
name: "image without explicit tag (defaults to latest)",
50+
image: "nginx",
51+
expectedImageName: "nginx",
52+
expectedTag: "latest",
53+
expectedDigest: "",
54+
expectError: false,
55+
},
56+
{
57+
name: "empty image string",
58+
image: "",
59+
expectedImageName: "",
60+
expectedTag: "",
61+
expectedDigest: "",
62+
expectError: true,
63+
},
64+
{
65+
name: "image with multiple colons in name",
66+
image: "registry.io:5000/namespace/image:v1.0.0",
67+
expectedImageName: "registry.io:5000/namespace/image",
68+
expectedTag: "v1.0.0",
69+
expectedDigest: "",
70+
expectError: false,
71+
},
72+
{
73+
name: "image with multiple colons and digest",
74+
image: "registry.io:5000/namespace/image:v1.0.0@sha256:123456789abc",
75+
expectedImageName: "registry.io:5000/namespace/image",
76+
expectedTag: "v1.0.0",
77+
expectedDigest: "sha256:123456789abc",
78+
expectError: false,
79+
},
80+
{
81+
name: "image with digest only (no tag)",
82+
image: "nginx@sha256:abc123def456",
83+
expectedImageName: "nginx",
84+
expectedTag: "",
85+
expectedDigest: "sha256:abc123def456",
86+
expectError: false,
87+
},
88+
{
89+
name: "complex registry with namespace and tag",
90+
image: "ghcr.io/openmcp-project/components/github.com/openmcp-project/openmcp:v0.0.11",
91+
expectedImageName: "ghcr.io/openmcp-project/components/github.com/openmcp-project/openmcp",
92+
expectedTag: "v0.0.11",
93+
expectedDigest: "",
94+
expectError: false,
95+
},
96+
{
97+
name: "image with port and path",
98+
image: "localhost:5000/my-namespace/my-image:1.2.3",
99+
expectedImageName: "localhost:5000/my-namespace/my-image",
100+
expectedTag: "1.2.3",
101+
expectedDigest: "",
102+
expectError: false,
103+
},
104+
{
105+
name: "image with port, path and digest",
106+
image: "localhost:5000/my-namespace/my-image:1.2.3@sha256:abcdef123456",
107+
expectedImageName: "localhost:5000/my-namespace/my-image",
108+
expectedTag: "1.2.3",
109+
expectedDigest: "sha256:abcdef123456",
110+
expectError: false,
111+
},
112+
}
113+
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
imageName, tag, digest, err := ParseImage(tt.image)
117+
118+
if tt.expectError {
119+
if err == nil {
120+
t.Errorf("expected error but got none")
121+
}
122+
return
123+
}
124+
125+
if err != nil {
126+
t.Errorf("unexpected error: %v", err)
127+
return
128+
}
129+
130+
if imageName != tt.expectedImageName {
131+
t.Errorf("expected image name %q, got %q", tt.expectedImageName, imageName)
132+
}
133+
134+
if tag != tt.expectedTag {
135+
t.Errorf("expected tag %q, got %q", tt.expectedTag, tag)
136+
}
137+
138+
if digest != tt.expectedDigest {
139+
t.Errorf("expected digest %q, got %q", tt.expectedDigest, digest)
140+
}
141+
})
142+
}
143+
}

0 commit comments

Comments
 (0)