Skip to content
Merged
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.23.1-dev
v0.23.2
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Controller Utility Functions](libs/controller.md)
- [Custom Resource Definitions](libs/crds.md)
- [Error Handling](libs/errors.md)
- [Image Parsing](libs/image.md)
- [JSON Patch](libs/jsonpatch.md)
- [Logging](libs/logging.md)
- [Key-Value Pairs](libs/pairs.md)
Expand Down
42 changes: 42 additions & 0 deletions docs/libs/image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Image Parsing

The `pkg/image` package provides utilities for parsing container image references into their constituent components.

## ParseImage Function

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.

### Function Signature

```go
func ParseImage(image string) (imageName string, tag string, digest string, err error)
```

### Behavior

- **Default Tag**: If no tag is specified, it defaults to `"latest"`
- **Digest Support**: Handles images with SHA256 digests (indicated by `@sha256:...`)
- **Registry URLs**: Properly handles registry URLs with ports (e.g., `registry.io:5000/image:tag`)
- **Validation**: Returns an error for empty image strings

### Examples

```go
// Basic image with tag
imageName, tag, digest, err := ParseImage("nginx:1.19.0")
// Returns: "nginx", "1.19.0", "", nil

// Image without tag (defaults to latest)
imageName, tag, digest, err := ParseImage("nginx")
// Returns: "nginx", "latest", "", nil

// Image with digest only
imageName, tag, digest, err := ParseImage("nginx@sha256:abcdef...")
// Returns: "nginx", "", "sha256:abcdef...", nil

// Image with both tag and digest
imageName, tag, digest, err := ParseImage("nginx:1.19.0@sha256:abcdef...")
// Returns: "nginx", "1.19.0", "sha256:abcdef...", nil
```

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.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.27.1
github.com/onsi/gomega v1.38.2
github.com/openmcp-project/controller-utils/api v0.23.1
github.com/openmcp-project/controller-utils/api v0.23.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
Expand Down
55 changes: 55 additions & 0 deletions pkg/image/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package image

import (
"fmt"
"strings"
)

// ParseImage parses a container image string and returns the image name, tag, and digest.
// If no tag is specified, it defaults to "latest". If a digest is present, it is returned as well.
// Examples of valid image strings:
// - "nginx:1.19.0" -> imageName: "nginx", tag: "1.19.0", digest: ""
// - "nginx" -> imageName: "nginx", tag: "latest", digest: ""
// - "nginx@sha256:abcdef..." -> imageName: "nginx", tag: "", digest: "sha256:abcdef..."
// - "nginx:1.19.0@sha256:abcdef..." -> imageName: "nginx", tag: "1.19.0", digest: "sha256:abcdef..."
func ParseImage(image string) (imageName string, tag string, digest string, err error) {
if image == "" {
return "", "", "", fmt.Errorf("image string cannot be empty")
}

// Check if the image contains a digest (indicated by @)
digestIndex := strings.LastIndex(image, "@")

var tagPart string
if digestIndex != -1 {
// Extract digest
digest = image[digestIndex+1:]
tagPart = image[:digestIndex]
} else {
tagPart = image
}

// Find the last colon to separate the tag from the image name
// We use LastIndex to handle registry URLs with ports (e.g., registry.io:5000/image:tag)
colonIndex := strings.LastIndex(tagPart, ":")

// If there's a digest but no colon in the tag part, it's a digest-only image
if digestIndex != -1 && colonIndex == -1 {
imageName = tagPart
return imageName, "", digest, nil
}

// If there's no colon, it means no explicit tag was provided
// In this case, default to "latest" tag
if colonIndex == -1 {
imageName = tagPart
return imageName, "latest", digest, nil
}

// Extract image name (everything before the last colon)
imageName = tagPart[:colonIndex]
// Extract tag (everything after the last colon)
tag = tagPart[colonIndex+1:]

return imageName, tag, digest, nil
}
143 changes: 143 additions & 0 deletions pkg/image/image_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package image

import (
"testing"
)

func TestParseImage(t *testing.T) {
tests := []struct {
name string
image string
expectedImageName string
expectedTag string
expectedDigest string
expectError bool
}{
{
name: "image with tag only",
image: "nginx:1.21.0",
expectedImageName: "nginx",
expectedTag: "1.21.0",
expectedDigest: "",
expectError: false,
},
{
name: "image with tag and digest",
image: "nginx:1.21.0@sha256:abc123def456",
expectedImageName: "nginx",
expectedTag: "1.21.0",
expectedDigest: "sha256:abc123def456",
expectError: false,
},
{
name: "image with latest tag",
image: "ubuntu:latest",
expectedImageName: "ubuntu",
expectedTag: "latest",
expectedDigest: "",
expectError: false,
},
{
name: "image with version tag and digest",
image: "registry.io/myapp:v2.1.3@sha256:fedcba987654",
expectedImageName: "registry.io/myapp",
expectedTag: "v2.1.3",
expectedDigest: "sha256:fedcba987654",
expectError: false,
},
{
name: "image without explicit tag (defaults to latest)",
image: "nginx",
expectedImageName: "nginx",
expectedTag: "latest",
expectedDigest: "",
expectError: false,
},
{
name: "empty image string",
image: "",
expectedImageName: "",
expectedTag: "",
expectedDigest: "",
expectError: true,
},
{
name: "image with multiple colons in name",
image: "registry.io:5000/namespace/image:v1.0.0",
expectedImageName: "registry.io:5000/namespace/image",
expectedTag: "v1.0.0",
expectedDigest: "",
expectError: false,
},
{
name: "image with multiple colons and digest",
image: "registry.io:5000/namespace/image:v1.0.0@sha256:123456789abc",
expectedImageName: "registry.io:5000/namespace/image",
expectedTag: "v1.0.0",
expectedDigest: "sha256:123456789abc",
expectError: false,
},
{
name: "image with digest only (no tag)",
image: "nginx@sha256:abc123def456",
expectedImageName: "nginx",
expectedTag: "",
expectedDigest: "sha256:abc123def456",
expectError: false,
},
{
name: "complex registry with namespace and tag",
image: "ghcr.io/openmcp-project/components/github.com/openmcp-project/openmcp:v0.0.11",
expectedImageName: "ghcr.io/openmcp-project/components/github.com/openmcp-project/openmcp",
expectedTag: "v0.0.11",
expectedDigest: "",
expectError: false,
},
{
name: "image with port and path",
image: "localhost:5000/my-namespace/my-image:1.2.3",
expectedImageName: "localhost:5000/my-namespace/my-image",
expectedTag: "1.2.3",
expectedDigest: "",
expectError: false,
},
{
name: "image with port, path and digest",
image: "localhost:5000/my-namespace/my-image:1.2.3@sha256:abcdef123456",
expectedImageName: "localhost:5000/my-namespace/my-image",
expectedTag: "1.2.3",
expectedDigest: "sha256:abcdef123456",
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
imageName, tag, digest, err := ParseImage(tt.image)

if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}

if err != nil {
t.Errorf("unexpected error: %v", err)
return
}

if imageName != tt.expectedImageName {
t.Errorf("expected image name %q, got %q", tt.expectedImageName, imageName)
}

if tag != tt.expectedTag {
t.Errorf("expected tag %q, got %q", tt.expectedTag, tag)
}

if digest != tt.expectedDigest {
t.Errorf("expected digest %q, got %q", tt.expectedDigest, digest)
}
})
}
}
Loading