Add image parser utils

Signed-off-by: RainbowMango <renhongcai@huawei.com>
This commit is contained in:
RainbowMango 2021-05-07 15:56:03 +08:00 committed by Hongcai Ren
parent 763c2a10e7
commit 0203c60ab7
3 changed files with 406 additions and 0 deletions

View File

@ -0,0 +1,43 @@
package imageparser
import "regexp"
/*
This code is directly lifted from the distribution codebase as they haven't been exported.
For reference: https://github.com/distribution/distribution/blob/9329f6a62b67d5e06d50dc93997c7705a075fcd9/reference/regexp.go
*/
var (
// match compiles the string to a regular expression.
match = regexp.MustCompile
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = match(`[\w][\w.-]{0,127}`)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = anchored(TagRegexp)
// DigestRegexp matches valid digests.
DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = anchored(DigestRegexp)
)
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
return match(`^` + expression(res...).String() + `$`)
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...*regexp.Regexp) *regexp.Regexp {
var s string
for _, re := range res {
s += re.String()
}
return match(s)
}

View File

@ -0,0 +1,172 @@
package imageparser
import (
_ "crypto/sha256" // initialize crypto/sha256 to enable sha256 algorithm
_ "crypto/sha512" // initialize crypto/sha512 to enable sha512 algorithm
"strings"
"github.com/distribution/distribution/v3/reference"
)
// Components make up a whole image.
// Basically we presume an image can be made of `[domain][:port][path]<name>[:tag][@sha256:digest]`, ie:
// fictional.registry.example:10443/karmada/karmada-controller-manager:v1.0.0 or
// fictional.registry.example:10443/karmada/karmada-controller-manager@sha256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c
type Components struct {
// hostname is the prefix of referencing image, like "k8s.gcr.io".
// It may optionally be followed by a port number, like "fictional.registry.example:10443".
hostname string
// repository is the short name of referencing image, like "karmada-controller-manager".
// It may be made up of slash-separated components, like "karmada/karmada-controller-manager".
repository string
// tag is the tag of referencing image. ie:
// - latest
// - v1.19.1
tag string
// digest is the digest of referencing image. ie:
// - sha256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c
digest string
}
// Hostname returns the hostname, as well known as image registry.
func (c *Components) Hostname() string {
return c.hostname
}
// SetHostname sets the hostname.
func (c *Components) SetHostname(hostname string) {
c.hostname = hostname
}
// RemoveHostname removes the hostname.
func (c *Components) RemoveHostname() {
c.hostname = ""
}
// Repository returns the repository without hostname.
func (c *Components) Repository() string {
return c.repository
}
// SetRepository sets the repository.
func (c *Components) SetRepository(repository string) {
c.repository = repository
}
// RemoveRepository removes the repository.
func (c *Components) RemoveRepository() {
c.repository = ""
}
// FullRepository returns the whole repository, including hostname if not empty.
func (c *Components) FullRepository() string {
if c.hostname == "" {
return c.repository
}
return c.hostname + "/" + c.repository
}
// String returns the full name of the image, including repository and tag(or digest).
func (c *Components) String() string {
if c.tag != "" {
return c.FullRepository() + ":" + c.tag
} else if c.digest != "" {
return c.FullRepository() + "@" + c.digest
}
return c.FullRepository()
}
// Tag returns the tag.
func (c *Components) Tag() string {
return c.tag
}
// SetTag sets the tag.
func (c *Components) SetTag(tag string) {
c.tag = tag
}
// RemoveTag removes the tag.
func (c *Components) RemoveTag() {
c.tag = ""
}
// Digest returns the digest.
func (c *Components) Digest() string {
return c.digest
}
// SetDigest sets the digest.
func (c *Components) SetDigest(digest string) {
c.digest = digest
}
// RemoveDigest removes the digest.
func (c *Components) RemoveDigest() {
c.digest = ""
}
// TagOrDigest returns image tag if not empty otherwise returns image digest.
func (c *Components) TagOrDigest() string {
if c.tag != "" {
return c.tag
}
return c.digest
}
// SetTagOrDigest sets image tag or digest by matching the input.
func (c *Components) SetTagOrDigest(input string) {
if anchoredTagRegexp.MatchString(input) {
c.SetTag(input)
return
}
if anchoredDigestRegexp.MatchString(input) {
c.SetDigest(input)
}
}
// RemoveTagOrDigest removes tag or digest.
// Since tag and digest don't co-exist, so remove tag if tag not empty, otherwise remove digest.
func (c *Components) RemoveTagOrDigest() {
if c.tag != "" {
c.tag = ""
} else if c.digest != "" {
c.digest = ""
}
}
// Parse returns a Components of the given image.
func Parse(image string) (*Components, error) {
ref, err := reference.Parse(image)
if err != nil {
return nil, err
}
comp := &Components{}
if named, ok := ref.(reference.Named); ok {
comp.hostname, comp.repository = SplitHostname(named.Name())
}
if tagged, ok := ref.(reference.Tagged); ok {
comp.tag = tagged.Tag()
} else if digested, ok := ref.(reference.Digested); ok {
comp.digest = digested.Digest().String()
}
return comp, nil
}
// SplitHostname splits a repository name(ie: k8s.gcr.io/kube-apiserver) to hostname(k8s.gcr.io) and remotename(kube-apiserver) string.
func SplitHostname(name string) (hostname, remoteName string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
hostname, remoteName = "", name
} else {
hostname, remoteName = name[:i], name[i+1:]
}
return
}

View File

@ -0,0 +1,191 @@
package imageparser
import (
"fmt"
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
name string
image string
expectHostname string
expectRepository string
expectTag string
expectDigest string
}{
{
name: "simple",
image: "pause",
expectHostname: "",
expectRepository: "pause",
expectTag: "",
},
{
name: "repository",
image: "subpath/imagename:v1.0.0",
expectHostname: "",
expectRepository: "subpath/imagename",
expectTag: "v1.0.0",
},
{
name: "normal",
image: "fictional.registry.example/imagename:v1.0.0",
expectHostname: "fictional.registry.example",
expectRepository: "imagename",
expectTag: "v1.0.0",
},
{
name: "hostname with port",
image: "fictional.registry.example:10443/subpath/imagename:v1.0.0",
expectHostname: "fictional.registry.example:10443",
expectRepository: "subpath/imagename",
expectTag: "v1.0.0",
},
{
name: "digest",
image: "fictional.registry.example:10443/subpath/imagename@sha256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
expectHostname: "fictional.registry.example:10443",
expectRepository: "subpath/imagename",
expectDigest: "sha256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
},
}
for _, test := range tests {
tc := test
t.Run(tc.name, func(t *testing.T) {
comp, err := Parse(tc.image)
if err != nil {
t.Fatalf("unexpected error but got: %v", err)
}
if comp.String() != tc.image {
t.Fatalf("full name changed from %s to %s", tc.image, comp.String())
}
if comp.Hostname() != tc.expectHostname {
t.Fatalf("expected registry: %s, but got: %s", tc.expectHostname, comp.Hostname())
}
if comp.Repository() != tc.expectRepository {
t.Fatalf("expected name: %s, but got: %s", tc.expectRepository, comp.Repository())
}
if comp.Tag() != tc.expectTag {
t.Fatalf("expected tag: %s, but got: %s", tc.expectTag, comp.Tag())
}
if comp.Digest() != tc.expectDigest {
t.Fatalf("expected digest: %s, but got: %s", tc.expectDigest, comp.Digest())
}
})
}
}
func TestSplitHostname(t *testing.T) {
tests := []struct {
name string
input string
expectedHostname string
expectedRepository string
}{
{
name: "empty image got nothing",
input: "",
expectedHostname: "",
expectedRepository: "",
},
{
name: "simple repository",
input: "imagename",
expectedHostname: "",
expectedRepository: "imagename",
},
{
name: "repository with sub-path",
input: "subpath/imagename",
expectedHostname: "",
expectedRepository: "subpath/imagename",
},
{
name: "canonical image",
input: "fictional.registry.example/subpath/imagename",
expectedHostname: "fictional.registry.example",
expectedRepository: "subpath/imagename",
},
{
name: "hostname with port",
input: "fictional.registry.example:10443/subpath/imagename",
expectedHostname: "fictional.registry.example:10443",
expectedRepository: "subpath/imagename",
},
{
name: "hostname with IP",
input: "10.10.10.10:10443/subpath/imagename",
expectedHostname: "10.10.10.10:10443",
expectedRepository: "subpath/imagename",
},
}
for _, test := range tests {
tc := test
t.Run(tc.name, func(t *testing.T) {
hostname, repository := SplitHostname(tc.input)
if hostname != tc.expectedHostname {
t.Fatalf("expected hostname: %s, but got: %s", tc.expectedHostname, hostname)
}
if repository != tc.expectedRepository {
t.Fatalf("expected repository: %s, but got: %s", tc.expectedRepository, repository)
}
})
}
}
func ExampleComponents_SetHostname() {
image := "imagename:v1.0.0"
comp, err := Parse(image)
if err != nil {
panic(err)
}
comp.SetHostname("fictional.registry.example") // add hostname
fmt.Println(comp.String())
comp.SetHostname("gcr.io") // update hostname
fmt.Println(comp.String())
comp.RemoveHostname() // remove hostname
fmt.Println(comp.String())
// Output:
// fictional.registry.example/imagename:v1.0.0
// gcr.io/imagename:v1.0.0
// imagename:v1.0.0
}
func ExampleComponents_SetRepository() {
image := "gcr.io/kube-apiserver:v1.19.0"
comp, err := Parse(image)
if err != nil {
panic(err)
}
comp.SetRepository("kube-controller-manager") // update
fmt.Println(comp.String())
// Output:
// gcr.io/kube-controller-manager:v1.19.0
}
func ExampleComponents_SetTagOrDigest() {
image := "gcr.io/kube-apiserver"
comp, err := Parse(image)
if err != nil {
panic(err)
}
comp.SetTagOrDigest("v1.19.0") // set
fmt.Println(comp.String())
comp.RemoveTagOrDigest() // remove tag
fmt.Println(comp.String())
comp.SetTagOrDigest("sha256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c") // update
fmt.Println(comp.String())
comp.RemoveTagOrDigest() // remove digest
fmt.Println(comp.String())
// Output:
// gcr.io/kube-apiserver:v1.19.0
// gcr.io/kube-apiserver
// gcr.io/kube-apiserver@sha256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c
// gcr.io/kube-apiserver
}