Skip to content

Commit

Permalink
Migrate to distribution/reference (#224)
Browse files Browse the repository at this point in the history
Use github.com/docker/distribution/reference for parsing references.

Fixes #209

Signed-off-by: Steve Larkin <[email protected]>
  • Loading branch information
sel authored Dec 20, 2023
1 parent 0aa60de commit 05e74c7
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 104 deletions.
115 changes: 11 additions & 104 deletions pkg/oci/reference.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
package oci

import (
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
)

var (
validPortRegEx = regexp.MustCompile(`^([1-9]\d{0,3}|0|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`) // adapted from https://stackoverflow.com/a/12968117
// TODO: Currently we don't support digests, so we are only splitting on the
// colon. However, when we add support for digests, we'll need to use the
// regexp anyway to split on both colons and @, so leaving it like this for
// now
referenceDelimiter = regexp.MustCompile(`[:]`)
errEmptyRepo = errors.New("parsed repo was empty")
errTooManyColons = errors.New("ref may only contain a single colon character (:) unless specifying a port number")
"github.com/docker/distribution/reference"
)

type (
Expand All @@ -30,34 +16,22 @@ type (

// ParseReference converts a string to a Reference
func ParseReference(s string) (*Reference, error) {
if s == "" {
return nil, errEmptyRepo
r, err := reference.Parse(s)
if err != nil {
return nil, err
}
// Split the components of the string on the colon or @, if it is more than 3,
// immediately return an error. Other validation will be performed later in
// the function
splitComponents := fixSplitComponents(referenceDelimiter.Split(s, -1))

var ref *Reference
switch len(splitComponents) {
case 1:
ref = &Reference{Repo: splitComponents[0]}
case 2:
ref = &Reference{Repo: splitComponents[0], Tag: splitComponents[1]}
case 3:
ref = &Reference{Repo: strings.Join(splitComponents[:2], ":"), Tag: splitComponents[2]}
default:
return nil, errTooManyColons
var ref Reference

if named, ok := r.(reference.Named); ok {
ref.Repo = named.Name()
}

ref.mutate()
// ensure the reference is valid
err := ref.validate()
if err != nil {
return nil, err
if tagged, ok := r.(reference.Tagged); ok {
ref.Tag = tagged.Tag()
}

return ref, nil
return &ref, nil
}

// FullName the full name of a reference (repo:tag)
Expand All @@ -67,70 +41,3 @@ func (ref *Reference) FullName() string {
}
return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag)
}

// mutate assigns default tag when it is empty
func (ref *Reference) mutate() {
if ref.Tag == "" {
ref.Tag = "latest"
}
}

// validate makes sure the ref meets our criteria
func (ref *Reference) validate() error {
err := ref.validateRepo()
if err != nil {
return err
}
return ref.validateNumColons()
}

// validateRepo checks that the Repo field is non-empty
func (ref *Reference) validateRepo() error {
if ref.Repo == "" {
return errEmptyRepo
}
// Makes sure the repo results in a parsable URL (similar to what is done
// with containerd reference parsing)
_, err := url.Parse("//" + ref.Repo)
return err
}

// validateNumColon ensures the ref only contains a single colon character (:)
// (or potentially two, there might be a port number specified i.e. :5000)
func (ref *Reference) validateNumColons() error {
if strings.Contains(ref.Tag, ":") {
return errTooManyColons
}
parts := strings.Split(ref.Repo, ":")
lastIndex := len(parts) - 1
if 1 < lastIndex {
return errTooManyColons
}
if 0 < lastIndex {
port := strings.Split(parts[lastIndex], "/")[0]
if !isValidPort(port) {
return errTooManyColons
}
}
return nil
}

// isValidPort returns whether or not a string looks like a valid port
func isValidPort(s string) bool {
return validPortRegEx.MatchString(s)
}

// fixSplitComponents this will modify reference parts based on presence of port
// Example: {localhost, 5000/x/y/z, 0.1.0} => {localhost:5000/x/y/z, 0.1.0}
func fixSplitComponents(c []string) []string {
if len(c) <= 1 {
return c
}
possiblePortParts := strings.Split(c[1], "/")
if _, err := strconv.Atoi(possiblePortParts[0]); err == nil {
components := []string{strings.Join(c[:2], ":")}
components = append(components, c[2:]...)
return components
}
return c
}
43 changes: 43 additions & 0 deletions pkg/oci/reference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package oci

import (
"reflect"
"testing"
)

func TestParseReference(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want *Reference
wantErr bool
}{
{
name: "Test single digit tag (from issue #209).",
args: args{s: "foo:1"},
want: &Reference{Tag: "1", Repo: "foo"},
wantErr: false,
},
{
name: "Test mulitple digit-only tag.",
args: args{s: "registry.example.com/project/repo:1612367"},
want: &Reference{Tag: "1612367", Repo: "registry.example.com/project/repo"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseReference(tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("ParseReference() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseReference() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 05e74c7

Please sign in to comment.