Commit 204f9872 authored by Matt Butcher's avatar Matt Butcher

Add short/long/local names according to new spec.

The url package adds short, long, and local URL parsing.

It also supports switching between short and long names.
parent ffe15f15
/* package URL handles Helm-DM URLs
Helm uses three kinds of URLs:
- Fully qualified (long) names: https://example.com/foo/bar-1.2.3.tgz
- Short names: helm:example.com/foo/bar#1.2.3
- Local names: file:///foo/bar
This package provides utilities for working with this type of URL.
*/
package url
import (
"errors"
"fmt"
"net/url"
"path/filepath"
"regexp"
"strings"
)
const (
SchemeHTTP = "http"
SchemeHTTPS = "https"
SchemeHelm = "helm"
SchemeFile = "file"
)
// TarNameRegex parses the name component of a URI and breaks it into a name and version.
//
// This borrows liberally from github.com/Masterminds/semver.
const TarNameRegex = `([0-9A-Za-z\-_/]+)-(v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)(.tgz)?`
var tnregexp *regexp.Regexp
func init() {
tnregexp = regexp.MustCompile("^" + TarNameRegex + "$")
}
type URL struct {
// The scheme of the URL. Typically one of http, https, helm, or file.
Scheme string
// The host information, if applicable.
Host string
// The bucket name
Bucket string
// The chart name
Name string
// The version or version range.
Version string
// If this is a local chart, the path to the chart.
LocalRef string
isLocal bool
original string
}
func Parse(path string) (*URL, error) {
// Check for absolute or relative path.
if path[0] == '.' || path[0] == '/' {
return &URL{
LocalRef: path,
isLocal: true,
original: path,
}, nil
}
// TODO: Do we want to support file:///foo/bar.tgz?
if strings.HasPrefix(path, SchemeFile+":") {
path := strings.TrimPrefix(path, SchemeFile+":")
return &URL{
LocalRef: filepath.Clean(path),
isLocal: true,
original: path,
}, nil
}
u, err := url.Parse(path)
if err != nil {
return nil, err
}
// Short name
if u.Scheme == SchemeHelm {
parts := strings.SplitN(u.Opaque, "/", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("both bucket and chart name are required in %s: %s", path, u.Path)
}
// Need to parse opaque data into bucket and chart.
return &URL{
Scheme: u.Scheme,
Host: parts[0],
Bucket: parts[1],
Name: parts[2],
Version: u.Fragment,
original: path,
}, nil
}
// Long name
parts := strings.SplitN(u.Path, "/", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("both bucket and chart name are required in %s", path)
}
name, version, err := parseTarName(parts[2])
if err != nil {
return nil, err
}
return &URL{
Scheme: u.Scheme,
Host: u.Host,
Bucket: parts[1],
Name: name,
Version: version,
original: path,
}, nil
}
// IsLocal returns true if this is a local path.
func (u *URL) IsLocal() bool {
return u.isLocal
}
// Local returns a local version of the path.
//
// This will return an error if the URL does not reference a local chart.
func (u *URL) Local() (string, error) {
return u.LocalRef, nil
}
var ErrLocal = errors.New("cannot use local URL as remote")
var ErrRemote = errors.New("cannot use remote URL as local")
// Short returns a short form URL.
//
// This will return an error if the URL references a local chart.
func (u *URL) Short() (string, error) {
if u.IsLocal() {
return "", ErrLocal
}
fname := fmt.Sprintf("%s/%s/%s", u.Host, u.Bucket, u.Name)
return (&url.URL{
Scheme: SchemeHelm,
Opaque: fname,
Fragment: u.Version,
}).String(), nil
}
// Long returns a long-form URL.
//
// If secure is true, this will return an HTTPS URL, otherwise HTTP.
//
// This will return an error if the URL references a local chart.
func (u *URL) Long(secure bool) (string, error) {
if u.IsLocal() {
return "", ErrLocal
}
scheme := SchemeHTTPS
if !secure {
scheme = SchemeHTTP
}
fname := fmt.Sprintf("%s/%s-%s.tgz", u.Bucket, u.Name, u.Version)
return (&url.URL{
Scheme: scheme,
Host: u.Host,
Path: fname,
}).String(), nil
}
// parseTarName parses a long-form tarfile name.
func parseTarName(name string) (string, string, error) {
if strings.HasSuffix(name, ".tgz") {
name = strings.TrimSuffix(name, ".tgz")
}
v := tnregexp.FindStringSubmatch(name)
if v == nil {
return name, "", fmt.Errorf("invalid name %s", name)
}
return v[1], v[2], nil
}
package url
import (
"testing"
)
func TestParse(t *testing.T) {
tests := map[string]URL{
"helm:host/bucket/name#1.2.3": URL{Scheme: "helm", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"},
"https://host/bucket/name-1.2.3.tgz": URL{Scheme: "https", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"},
"http://host/bucket/name-1.2.3.tgz": URL{Scheme: "http", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"},
}
for start, expect := range tests {
u, err := Parse(start)
if err != nil {
t.Errorf("Failed parsing %s: %s", start, err)
}
if expect.Scheme != u.Scheme {
t.Errorf("Unexpected scheme: %s", u.Scheme)
}
if expect.Host != u.Host {
t.Errorf("Unexpected host: %q", u.Host)
}
if expect.Bucket != u.Bucket {
t.Errorf("Unexpected bucket: %q", u.Bucket)
}
if expect.Name != u.Name {
t.Errorf("Unexpected name: %q", u.Name)
}
if expect.Version != u.Version {
t.Errorf("Unexpected version: %q", u.Version)
}
if expect.LocalRef != u.LocalRef {
t.Errorf("Unexpected local dir: %q", u.LocalRef)
}
}
}
func TestShort(t *testing.T) {
tests := map[string]string{
"https://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3",
"http://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3",
"helm:example.com/foo/bar#1.2.3": "helm:example.com/foo/bar#1.2.3",
"helm:example.com/foo/bar#>1.2.3": "helm:example.com/foo/bar#%3E1.2.3",
}
for start, expect := range tests {
u, err := Parse(start)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
short, err := u.Short()
if err != nil {
t.Errorf("Failed to generate short: %s", err)
continue
}
if short != expect {
t.Errorf("Expected %q, got %q", expect, short)
}
}
fails := []string{"./this/is/local", "file:///this/is/local"}
for _, f := range fails {
u, err := Parse(f)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
if _, err := u.Short(); err == nil {
t.Errorf("%q should have caused an error for Short()", f)
}
}
}
func TestLong(t *testing.T) {
tests := map[string]string{
"https://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz",
"http://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz",
"helm:example.com/foo/bar#1.2.3": "https://example.com/foo/bar-1.2.3.tgz",
"helm:example.com/foo/bar#>1.2.3": "https://example.com/foo/bar-%3E1.2.3.tgz",
}
for start, expect := range tests {
t.Logf("Parsing %s", start)
u, err := Parse(start)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
long, err := u.Long(true)
if err != nil {
t.Errorf("Failed to generate long: %s", err)
continue
}
if long != expect {
t.Errorf("Expected %q, got %q", expect, long)
}
}
fails := []string{"./this/is/local", "file:///this/is/local"}
for _, f := range fails {
u, err := Parse(f)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
if _, err := u.Long(false); err == nil {
t.Errorf("%q should have caused an error for Long()", f)
}
}
}
func TestLocal(t *testing.T) {
tests := map[string]string{
"file:///foo/bar-1.2.3.tgz": "/foo/bar-1.2.3.tgz",
"file:///foo/bar": "/foo/bar",
"./foo/bar": "./foo/bar",
"/foo/bar": "/foo/bar",
}
for start, expect := range tests {
u, err := Parse(start)
if err != nil {
t.Errorf("Failed parse: %s", err)
continue
}
fin, err := u.Local()
if err != nil {
t.Errorf("Failed Local(): %s", err)
continue
}
if fin != expect {
t.Errorf("Expected %q, got %q", expect, fin)
}
}
}
func TestParseTarName(t *testing.T) {
tests := []struct{ start, name, version string }{
{"butcher-1.2.3", "butcher", "1.2.3"},
{"butcher-1.2.3.tgz", "butcher", "1.2.3"},
{"butcher-1.2.3-beta1+1234", "butcher", "1.2.3-beta1+1234"},
{"butcher-1.2.3-beta1+1234.tgz", "butcher", "1.2.3-beta1+1234"},
{"foo/butcher-1.2.3.tgz", "foo/butcher", "1.2.3"},
}
for _, tt := range tests {
n, v, e := parseTarName(tt.start)
if e != nil {
t.Errorf("Error parsing %s: %s", tt.start, e)
continue
}
if n != tt.name {
t.Errorf("Expected name %q, got %q", tt.name, n)
}
if v != tt.version {
t.Errorf("Expected version %q, got %q", tt.version, v)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment