Add image parser utils
Signed-off-by: RainbowMango <renhongcai@huawei.com>
This commit is contained in:
parent
763c2a10e7
commit
0203c60ab7
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue