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