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