Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image pruner: Determine protocol just once #14914

Merged
merged 2 commits into from
Aug 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions pkg/image/apis/image/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const (
ImportRegistryNotAllowed = "registry is not allowed for import"
)

var errNoRegistryURLPathAllowed = fmt.Errorf("no path after <host>[:<port>] is allowed")
var errNoRegistryURLQueryAllowed = fmt.Errorf("no query arguments are allowed after <host>[:<port>]")
var errRegistryURLHostEmpty = fmt.Errorf("no host name specified")

// DefaultRegistry returns the default Docker registry (host or host:port), or false if it is not available.
type DefaultRegistry interface {
DefaultRegistry() (string, bool)
Expand Down Expand Up @@ -1161,3 +1165,41 @@ func (tagref TagReference) HasAnnotationTag(searchTag string) bool {
}
return false
}

// ValidateRegistryURL returns error if the given input is not a valid registry URL. The url may be prefixed
// with http:// or https:// schema. It may not contain any path or query after the host:[port].
func ValidateRegistryURL(registryURL string) error {
var (
u *url.URL
err error
parts = strings.SplitN(registryURL, "://", 2)
)

switch len(parts) {
case 2:
u, err = url.Parse(registryURL)
if err != nil {
return err
}
switch u.Scheme {
case "http", "https":
default:
return fmt.Errorf("unsupported scheme: %s", u.Scheme)
}
case 1:
u, err = url.Parse("https://" + registryURL)
if err != nil {
return err
}
}
if len(u.Path) > 0 && u.Path != "/" {
return errNoRegistryURLPathAllowed
}
if len(u.RawQuery) > 0 {
return errNoRegistryURLQueryAllowed
}
if len(u.Host) == 0 {
return errRegistryURLHostEmpty
}
return nil
}
82 changes: 82 additions & 0 deletions pkg/image/apis/image/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1834,3 +1834,85 @@ func TestDockerImageReferenceForImage(t *testing.T) {
t.Errorf("expected failure for unknown image")
}
}

func TestValidateRegistryURL(t *testing.T) {
for _, tc := range []struct {
input string
expectedError bool
expectedErrorString string
}{
{input: "172.30.30.30:5000"},
{input: ":5000"},
{input: "[fd12:3456:789a:1::1]:80/"},
{input: "[fd12:3456:789a:1::1]:80"},
{input: "http://172.30.30.30:5000"},
{input: "http://[fd12:3456:789a:1::1]:5000/"},
{input: "http://[fd12:3456:789a:1::1]:5000"},
{input: "http://registry.org:5000"},
{input: "https://172.30.30.30:5000"},
{input: "https://:80/"},
{input: "https://[fd12:3456:789a:1::1]/"},
{input: "https://[fd12:3456:789a:1::1]"},
{input: "https://[fd12:3456:789a:1::1]:5000/"},
{input: "https://[fd12:3456:789a:1::1]:5000"},
{input: "https://registry.org/"},
{input: "https://registry.org"},
{input: "localhost/"},
{input: "localhost"},
{input: "localhost:80"},
{input: "registry.org/"},
{input: "registry.org"},
{input: "registry.org:5000"},

{
input: "httpss://registry.org",
expectedErrorString: "unsupported scheme: httpss",
},
{
input: "ftp://registry.org",
expectedErrorString: "unsupported scheme: ftp",
},
{
input: "http://registry.org://",
expectedErrorString: errNoRegistryURLPathAllowed.Error(),
},
{
input: "http://registry.org/path",
expectedErrorString: errNoRegistryURLPathAllowed.Error(),
},
{
input: "[fd12:3456:789a:1::1",
expectedError: true,
},
{
input: "bad url",
expectedError: true,
},
{
input: "/registry.org",
expectedErrorString: errNoRegistryURLPathAllowed.Error(),
},
{
input: "https:///",
expectedErrorString: errRegistryURLHostEmpty.Error(),
},
{
input: "http://registry.org?parm=arg",
expectedErrorString: errNoRegistryURLQueryAllowed.Error(),
},
} {

err := ValidateRegistryURL(tc.input)
if err != nil {
if len(tc.expectedErrorString) > 0 && err.Error() != tc.expectedErrorString {
t.Errorf("[%s] unexpected error string: %q != %q", tc.input, err.Error(), tc.expectedErrorString)
} else if len(tc.expectedErrorString) == 0 && !tc.expectedError {
t.Errorf("[%s] unexpected error: %q", tc.input, err.Error())
}
} else if len(tc.expectedErrorString) > 0 {
t.Errorf("[%s] got non-error while expecting %q", tc.input, tc.expectedErrorString)
} else if tc.expectedError {
t.Errorf("[%s] got unexpected non-error", tc.input)
}
}
}
2 changes: 2 additions & 0 deletions pkg/image/prune/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package prune contains logic for pruning images and interoperating with the integrated Docker registry.
package prune
208 changes: 208 additions & 0 deletions pkg/image/prune/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package prune

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Golint comments: should have a package comment, unless it's in another file for this package. More info.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in another file.


import (
"fmt"
"net/http"
"net/url"
"sort"
"strings"

"github.com/docker/distribution/registry/api/errcode"
"github.com/golang/glog"

kerrors "k8s.io/apimachinery/pkg/util/errors"

imageapi "github.com/openshift/origin/pkg/image/apis/image"
"github.com/openshift/origin/pkg/util/netutils"
)

// order younger images before older
type imgByAge []*imageapi.Image

func (ba imgByAge) Len() int { return len(ba) }
func (ba imgByAge) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] }
func (ba imgByAge) Less(i, j int) bool {
return ba[i].CreationTimestamp.After(ba[j].CreationTimestamp.Time)
}

// order younger image stream before older
type isByAge []imageapi.ImageStream

func (ba isByAge) Len() int { return len(ba) }
func (ba isByAge) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] }
func (ba isByAge) Less(i, j int) bool {
return ba[i].CreationTimestamp.After(ba[j].CreationTimestamp.Time)
}

// DetermineRegistryHost returns registry host embedded in a pull-spec of the latest unmanaged image or the
// latest imagestream from the provided lists. If no such pull-spec is found, error is returned.
func DetermineRegistryHost(images *imageapi.ImageList, imageStreams *imageapi.ImageStreamList) (string, error) {
var pullSpec string
var managedImages []*imageapi.Image

// 1st try to determine registry url from a pull spec of the youngest managed image
for i := range images.Items {
image := &images.Items[i]
if image.Annotations[imageapi.ManagedByOpenShiftAnnotation] != "true" {
continue
}
managedImages = append(managedImages, image)
}
// be sure to pick up the newest managed image which should have an up to date information
sort.Sort(imgByAge(managedImages))

if len(managedImages) > 0 {
pullSpec = managedImages[0].DockerImageReference
} else {
// 2nd try to get the pull spec from any image stream
// Sorting by creation timestamp may not get us up to date info. Modification time would be much
// better if there were such an attribute.
sort.Sort(isByAge(imageStreams.Items))
for _, is := range imageStreams.Items {
if len(is.Status.DockerImageRepository) == 0 {
continue
}
pullSpec = is.Status.DockerImageRepository
}
}

if len(pullSpec) == 0 {
return "", fmt.Errorf("no managed image found")
}

ref, err := imageapi.ParseDockerImageReference(pullSpec)
if err != nil {
return "", fmt.Errorf("unable to parse %q: %v", pullSpec, err)
}

if len(ref.Registry) == 0 {
return "", fmt.Errorf("%s does not include a registry", pullSpec)
}

return ref.Registry, nil
}

// RegistryPinger performs a health check against a registry.
type RegistryPinger interface {
// Ping performs a health check against registry. It returns registry url qualified with schema unless an
// error occurs.
Ping(registry string) (*url.URL, error)
}

// DefaultRegistryPinger implements RegistryPinger.
type DefaultRegistryPinger struct {
Client *http.Client
Insecure bool
}

// Ping verifies that the integrated registry is ready, determines its transport protocol and returns its url
// or error.
func (drp *DefaultRegistryPinger) Ping(registry string) (*url.URL, error) {
var (
registryURL *url.URL
err error
)

pathLoop:
// first try the new default / path, then fall-back to the obsolete /healthz endpoint
for _, path := range []string{"/", "/healthz"} {
registryURL, err = TryProtocolsWithRegistryURL(registry, drp.Insecure, func(u url.URL) error {
u.Path = path
healthResponse, err := drp.Client.Get(u.String())
if err != nil {
return err
}
defer healthResponse.Body.Close()

if healthResponse.StatusCode != http.StatusOK {
return &retryPath{err: fmt.Errorf("unexpected status: %s", healthResponse.Status)}
}

return nil
})

// determine whether to retry with another endpoint
switch t := err.(type) {
case *retryPath:
// return the nested error if this is the last ping attempt
err = t.err
continue pathLoop
case kerrors.Aggregate:
// if any aggregated error indicates a possible retry, do it
for _, err := range t.Errors() {
if _, ok := err.(*retryPath); ok {
continue pathLoop
}
}
}

break
}

return registryURL, err
}

// DryRunRegistryPinger implements RegistryPinger.
type DryRunRegistryPinger struct {
}

// Ping implements Ping method.
func (*DryRunRegistryPinger) Ping(registry string) (*url.URL, error) {
return url.Parse("https://" + registry)
}

// TryProtocolsWithRegistryURL runs given action with different protocols until no error is returned. The
// https protocol is the first attempt. If it fails and allowInsecure is true, http will be the next. Obtained
// errors will be concatenated and returned.
func TryProtocolsWithRegistryURL(registry string, allowInsecure bool, action func(registryURL url.URL) error) (*url.URL, error) {
var errs []error

if !strings.Contains(registry, "://") {
registry = "unset://" + registry
}
url, err := url.Parse(registry)
if err != nil {
return nil, err
}
var protos []string
switch {
case len(url.Scheme) > 0 && url.Scheme != "unset":
protos = []string{url.Scheme}
case allowInsecure || netutils.IsPrivateAddress(registry):
protos = []string{"https", "http"}
default:
protos = []string{"https"}
}
registry = url.Host

for _, proto := range protos {
glog.V(4).Infof("Trying protocol %s for the registry URL %s", proto, registry)
url.Scheme = proto
err := action(*url)
if err == nil {
return url, nil
}

if err != nil {
glog.V(4).Infof("Error with %s for %s: %v", proto, registry, err)
}

if _, ok := err.(*errcode.Errors); ok {
// we got a response back from the registry, so return it
return url, err
}
errs = append(errs, err)
if proto == "https" && strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") && !allowInsecure {
errs = append(errs, fmt.Errorf("\n* Append --force-insecure if you really want to prune the registry using insecure connection."))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Golint errors: error strings should not be capitalized or end with punctuation or a newline. More info.

} else if proto == "http" && strings.Contains(err.Error(), "malformed HTTP response") {
errs = append(errs, fmt.Errorf("\n* Are you trying to connect to a TLS-enabled registry without TLS?"))
}
}

return nil, kerrors.NewAggregate(errs)
}

// retryPath is an error indicating that another connection attempt may be retried with a different path
type retryPath struct{ err error }

func (rp *retryPath) Error() string { return rp.err.Error() }
Loading