mirror of https://github.com/helm/helm
The url package adds short, long, and local URL parsing. It also supports switching between short and long names.pull/195/head
parent
ffe15f1585
commit
204f9872ee
@ -0,0 +1,189 @@
|
||||
/* 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
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue