mirror of https://github.com/helm/helm
commit
6d6e88a86e
@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
entries:
|
||||||
|
foo:
|
||||||
|
- name: foo
|
||||||
|
description: Foo Chart
|
||||||
|
engine: gotpl
|
||||||
|
home: https://k8s.io/helm
|
||||||
|
keywords: []
|
||||||
|
maintainers: []
|
||||||
|
sources:
|
||||||
|
- https://github.com/kubernetes/charts
|
||||||
|
urls:
|
||||||
|
- http://username:password@example.com/foo-1.2.3.tgz
|
||||||
|
version: 1.2.3
|
||||||
|
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
entries:
|
||||||
|
foo:
|
||||||
|
- name: foo
|
||||||
|
description: Foo Chart
|
||||||
|
engine: gotpl
|
||||||
|
home: https://k8s.io/helm
|
||||||
|
keywords: []
|
||||||
|
maintainers: []
|
||||||
|
sources:
|
||||||
|
- https://github.com/kubernetes/charts
|
||||||
|
urls:
|
||||||
|
- https://example.com/foo-1.2.3.tgz
|
||||||
|
version: 1.2.3
|
||||||
|
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package repo // import "k8s.io/helm/pkg/repo"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
|
||||||
|
"k8s.io/helm/pkg/chartutil"
|
||||||
|
"k8s.io/helm/pkg/provenance"
|
||||||
|
"k8s.io/helm/pkg/tlsutil"
|
||||||
|
"k8s.io/helm/pkg/urlutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entry represents a collection of parameters for chart repository
|
||||||
|
type Entry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Cache string `json:"cache"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
CertFile string `json:"certFile"`
|
||||||
|
KeyFile string `json:"keyFile"`
|
||||||
|
CAFile string `json:"caFile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChartRepository represents a chart repository
|
||||||
|
type ChartRepository struct {
|
||||||
|
Config *Entry
|
||||||
|
ChartPaths []string
|
||||||
|
IndexFile *IndexFile
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter is an interface to support GET to the specified URL.
|
||||||
|
type Getter interface {
|
||||||
|
Get(url string) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChartRepository constructs ChartRepository
|
||||||
|
func NewChartRepository(cfg *Entry) (*ChartRepository, error) {
|
||||||
|
var client *http.Client
|
||||||
|
if cfg.CertFile != "" && cfg.KeyFile != "" && cfg.CAFile != "" {
|
||||||
|
tlsConf, err := tlsutil.NewClientTLS(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create TLS config for client: %s", err.Error())
|
||||||
|
}
|
||||||
|
tlsConf.BuildNameToCertificate()
|
||||||
|
|
||||||
|
sni, err := urlutil.ExtractHostname(cfg.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConf.ServerName = sni
|
||||||
|
|
||||||
|
client = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: tlsConf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ChartRepository{
|
||||||
|
Config: cfg,
|
||||||
|
IndexFile: NewIndexFile(),
|
||||||
|
Client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get issues a GET using configured client to the specified URL.
|
||||||
|
func (r *ChartRepository) Get(url string) (*http.Response, error) {
|
||||||
|
resp, err := r.Client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads a directory of charts as if it were a repository.
|
||||||
|
//
|
||||||
|
// It requires the presence of an index.yaml file in the directory.
|
||||||
|
func (r *ChartRepository) Load() error {
|
||||||
|
dirInfo, err := os.Stat(r.Config.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !dirInfo.IsDir() {
|
||||||
|
return fmt.Errorf("%q is not a directory", r.Config.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Why are we recursively walking directories?
|
||||||
|
// FIXME: Why are we not reading the repositories.yaml to figure out
|
||||||
|
// what repos to use?
|
||||||
|
filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error {
|
||||||
|
if !f.IsDir() {
|
||||||
|
if strings.Contains(f.Name(), "-index.yaml") {
|
||||||
|
i, err := LoadIndexFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.IndexFile = i
|
||||||
|
} else if strings.HasSuffix(f.Name(), ".tgz") {
|
||||||
|
r.ChartPaths = append(r.ChartPaths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadIndexFile fetches the index from a repository.
|
||||||
|
func (r *ChartRepository) DownloadIndexFile() error {
|
||||||
|
var indexURL string
|
||||||
|
|
||||||
|
indexURL = strings.TrimSuffix(r.Config.URL, "/") + "/index.yaml"
|
||||||
|
resp, err := r.Get(indexURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
index, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := loadIndex(index); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(r.Config.Cache, index, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index generates an index for the chart repository and writes an index.yaml file.
|
||||||
|
func (r *ChartRepository) Index() error {
|
||||||
|
err := r.generateIndex()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.saveIndexFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChartRepository) saveIndexFile() error {
|
||||||
|
index, err := yaml.Marshal(r.IndexFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChartRepository) generateIndex() error {
|
||||||
|
for _, path := range r.ChartPaths {
|
||||||
|
ch, err := chartutil.Load(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest, err := provenance.DigestFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
|
||||||
|
r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
|
||||||
|
}
|
||||||
|
// TODO: If a chart exists, but has a different Digest, should we error?
|
||||||
|
}
|
||||||
|
r.IndexFile.SortEntries()
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testRepository = "testdata/repository"
|
||||||
|
testURL = "http://example-charts.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadChartRepository(t *testing.T) {
|
||||||
|
r, err := NewChartRepository(&Entry{
|
||||||
|
Name: testRepository,
|
||||||
|
URL: testURL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Load(); err != nil {
|
||||||
|
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := []string{
|
||||||
|
filepath.Join(testRepository, "frobnitz-1.2.3.tgz"),
|
||||||
|
filepath.Join(testRepository, "sprocket-1.1.0.tgz"),
|
||||||
|
filepath.Join(testRepository, "sprocket-1.2.0.tgz"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Config.Name != testRepository {
|
||||||
|
t.Errorf("Expected %s as Name but got %s", testRepository, r.Config.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(r.ChartPaths, paths) {
|
||||||
|
t.Errorf("Expected %#v but got %#v\n", paths, r.ChartPaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Config.URL != testURL {
|
||||||
|
t.Errorf("Expected url for chart repository to be %s but got %s", testURL, r.Config.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndex(t *testing.T) {
|
||||||
|
r, err := NewChartRepository(&Entry{
|
||||||
|
Name: testRepository,
|
||||||
|
URL: testURL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Load(); err != nil {
|
||||||
|
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Index()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error performing index: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempIndexPath := filepath.Join(testRepository, indexPath)
|
||||||
|
actual, err := LoadIndexFile(tempIndexPath)
|
||||||
|
defer os.Remove(tempIndexPath) // clean up
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error loading index file %v", err)
|
||||||
|
}
|
||||||
|
verifyIndex(t, actual)
|
||||||
|
|
||||||
|
// Re-index and test again.
|
||||||
|
err = r.Index()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error performing re-index: %s\n", err)
|
||||||
|
}
|
||||||
|
second, err := LoadIndexFile(tempIndexPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error re-loading index file %v", err)
|
||||||
|
}
|
||||||
|
verifyIndex(t, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIndex(t *testing.T, actual *IndexFile) {
|
||||||
|
var empty time.Time
|
||||||
|
if actual.Generated == empty {
|
||||||
|
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual.APIVersion != APIVersionV1 {
|
||||||
|
t.Error("Expected v1 API")
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := actual.Entries
|
||||||
|
if numEntries := len(entries); numEntries != 2 {
|
||||||
|
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
expects := map[string]ChartVersions{
|
||||||
|
"frobnitz": {
|
||||||
|
{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "frobnitz",
|
||||||
|
Version: "1.2.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sprocket": {
|
||||||
|
{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "sprocket",
|
||||||
|
Version: "1.2.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "sprocket",
|
||||||
|
Version: "1.1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, versions := range expects {
|
||||||
|
got, ok := entries[name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Could not find %q entry", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(versions) != len(got) {
|
||||||
|
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, e := range versions {
|
||||||
|
g := got[i]
|
||||||
|
if e.Name != g.Name {
|
||||||
|
t.Errorf("Expected %q, got %q", e.Name, g.Name)
|
||||||
|
}
|
||||||
|
if e.Version != g.Version {
|
||||||
|
t.Errorf("Expected %q, got %q", e.Version, g.Version)
|
||||||
|
}
|
||||||
|
if len(g.Keywords) != 3 {
|
||||||
|
t.Error("Expected 3 keyrwords.")
|
||||||
|
}
|
||||||
|
if len(g.Maintainers) != 2 {
|
||||||
|
t.Error("Expected 2 maintainers.")
|
||||||
|
}
|
||||||
|
if g.Created == empty {
|
||||||
|
t.Error("Expected created to be non-empty")
|
||||||
|
}
|
||||||
|
if g.Description == "" {
|
||||||
|
t.Error("Expected description to be non-empty")
|
||||||
|
}
|
||||||
|
if g.Home == "" {
|
||||||
|
t.Error("Expected home to be non-empty")
|
||||||
|
}
|
||||||
|
if g.Digest == "" {
|
||||||
|
t.Error("Expected digest to be non-empty")
|
||||||
|
}
|
||||||
|
if len(g.URLs) != 1 {
|
||||||
|
t.Error("Expected exactly 1 URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tlsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewClientTLS returns tls.Config appropriate for client auth.
|
||||||
|
func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
|
||||||
|
cert, err := CertFromFilePair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cp, err := CertPoolFromFile(caFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*cert},
|
||||||
|
RootCAs: cp,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertPoolFromFile returns an x509.CertPool containing the certificates
|
||||||
|
// in the given PEM-encoded file.
|
||||||
|
// Returns an error if the file could not be read, a certificate could not
|
||||||
|
// be parsed, or if the file does not contain any certificates
|
||||||
|
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
|
||||||
|
b, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read CA file: %v", filename)
|
||||||
|
}
|
||||||
|
cp := x509.NewCertPool()
|
||||||
|
if !cp.AppendCertsFromPEM(b) {
|
||||||
|
return nil, fmt.Errorf("failed to append certificates from file: %s", filename)
|
||||||
|
}
|
||||||
|
return cp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertFromFilePair returns an tls.Certificate containing the
|
||||||
|
// certificates public/private key pair from a pair of given PEM-encoded files.
|
||||||
|
// Returns an error if the file could not be read, a certificate could not
|
||||||
|
// be parsed, or if the file does not contain any certificates
|
||||||
|
func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) {
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't load key pair from cert %s and key %s", certFile, keyFile)
|
||||||
|
}
|
||||||
|
return &cert, err
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package urlutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// URLJoin joins a base URL to one or more path components.
|
||||||
|
//
|
||||||
|
// It's like filepath.Join for URLs. If the baseURL is pathish, this will still
|
||||||
|
// perform a join.
|
||||||
|
//
|
||||||
|
// If the URL is unparsable, this returns an error.
|
||||||
|
func URLJoin(baseURL string, paths ...string) (string, error) {
|
||||||
|
u, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// We want path instead of filepath because path always uses /.
|
||||||
|
all := []string{u.Path}
|
||||||
|
all = append(all, paths...)
|
||||||
|
u.Path = path.Join(all...)
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal normalizes two URLs and then compares for equality.
|
||||||
|
func Equal(a, b string) bool {
|
||||||
|
au, err := url.Parse(a)
|
||||||
|
if err != nil {
|
||||||
|
a = filepath.Clean(a)
|
||||||
|
b = filepath.Clean(b)
|
||||||
|
// If urls are paths, return true only if they are an exact match
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
bu, err := url.Parse(b)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range []*url.URL{au, bu} {
|
||||||
|
if u.Path == "" {
|
||||||
|
u.Path = "/"
|
||||||
|
}
|
||||||
|
u.Path = filepath.Clean(u.Path)
|
||||||
|
}
|
||||||
|
return au.String() == bu.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractHostname returns hostname from URL
|
||||||
|
func ExtractHostname(addr string) (string, error) {
|
||||||
|
u, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(u.Host)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return host, nil
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package urlutil
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestUrlJoin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name, url, expect string
|
||||||
|
paths []string
|
||||||
|
}{
|
||||||
|
{name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"},
|
||||||
|
{name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"},
|
||||||
|
{name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"},
|
||||||
|
{name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"},
|
||||||
|
{name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got, err := URLJoin(tt.url, tt.paths...); err != nil {
|
||||||
|
t.Errorf("%s: error %q", tt.name, err)
|
||||||
|
} else if got != tt.expect {
|
||||||
|
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEqual(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
a, b string
|
||||||
|
match bool
|
||||||
|
}{
|
||||||
|
{"http://example.com", "http://example.com", true},
|
||||||
|
{"http://example.com", "http://another.example.com", false},
|
||||||
|
{"https://example.com", "https://example.com", true},
|
||||||
|
{"http://example.com/", "http://example.com", true},
|
||||||
|
{"https://example.com", "http://example.com", false},
|
||||||
|
{"http://example.com/foo", "http://example.com/foo/", true},
|
||||||
|
{"http://example.com/foo//", "http://example.com/foo/", true},
|
||||||
|
{"http://example.com/./foo/", "http://example.com/foo/", true},
|
||||||
|
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
|
||||||
|
{"/foo", "/foo", true},
|
||||||
|
{"/foo", "/foo/", true},
|
||||||
|
{"/foo/.", "/foo/", true},
|
||||||
|
} {
|
||||||
|
if tt.match != Equal(tt.a, tt.b) {
|
||||||
|
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue