Browse code

registry: getting Endpoint ironned out

Signed-off-by: Vincent Batts <vbatts@redhat.com>

Vincent Batts authored on 2014/08/27 08:21:04
Showing 8 changed files
... ...
@@ -52,7 +52,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
52 52
 		return job.Error(err)
53 53
 	}
54 54
 
55
-	endpoint, err := registry.ExpandAndVerifyRegistryUrl(hostname)
55
+	endpoint, err := registry.NewEndpoint(hostname)
56 56
 	if err != nil {
57 57
 		return job.Error(err)
58 58
 	}
... ...
@@ -62,7 +62,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
62 62
 		return job.Error(err)
63 63
 	}
64 64
 
65
-	if endpoint == registry.IndexServerAddress() {
65
+	if endpoint.String() == registry.IndexServerAddress() {
66 66
 		// If pull "index.docker.io/foo/bar", it's stored locally under "foo/bar"
67 67
 		localName = remoteName
68 68
 
... ...
@@ -214,7 +214,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
214 214
 		return job.Error(err)
215 215
 	}
216 216
 
217
-	endpoint, err := registry.ExpandAndVerifyRegistryUrl(hostname)
217
+	endpoint, err := registry.NewEndpoint(hostname)
218 218
 	if err != nil {
219 219
 		return job.Error(err)
220 220
 	}
... ...
@@ -243,7 +243,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
243 243
 
244 244
 	var token []string
245 245
 	job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", localName))
246
-	if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint, token, sf); err != nil {
246
+	if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint.String(), token, sf); err != nil {
247 247
 		return job.Error(err)
248 248
 	}
249 249
 	return engine.StatusOK
250 250
new file mode 100644
... ...
@@ -0,0 +1,129 @@
0
+package registry
1
+
2
+import (
3
+	"encoding/json"
4
+	"errors"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"net/url"
9
+	"strings"
10
+
11
+	"github.com/docker/docker/pkg/log"
12
+)
13
+
14
+// scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version.
15
+func scanForApiVersion(hostname string) (string, APIVersion) {
16
+	var (
17
+		chunks        []string
18
+		apiVersionStr string
19
+	)
20
+	if strings.HasSuffix(hostname, "/") {
21
+		chunks = strings.Split(hostname[:len(hostname)-1], "/")
22
+		apiVersionStr = chunks[len(chunks)-1]
23
+	} else {
24
+		chunks = strings.Split(hostname, "/")
25
+		apiVersionStr = chunks[len(chunks)-1]
26
+	}
27
+	for k, v := range apiVersions {
28
+		if apiVersionStr == v {
29
+			hostname = strings.Join(chunks[:len(chunks)-1], "/")
30
+			return hostname, k
31
+		}
32
+	}
33
+	return hostname, DefaultAPIVersion
34
+}
35
+
36
+func NewEndpoint(hostname string) (*Endpoint, error) {
37
+	var (
38
+		endpoint        Endpoint
39
+		trimmedHostname string
40
+		err             error
41
+	)
42
+	if !strings.HasPrefix(hostname, "http") {
43
+		hostname = "https://" + hostname
44
+	}
45
+	trimmedHostname, endpoint.Version = scanForApiVersion(hostname)
46
+	endpoint.URL, err = url.Parse(trimmedHostname)
47
+	if err != nil {
48
+		return nil, err
49
+	}
50
+
51
+	endpoint.URL.Scheme = "https"
52
+	if _, err := endpoint.Ping(); err != nil {
53
+		log.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
54
+		// TODO: Check if http fallback is enabled
55
+		endpoint.URL.Scheme = "http"
56
+		if _, err = endpoint.Ping(); err != nil {
57
+			return nil, errors.New("Invalid Registry endpoint: " + err.Error())
58
+		}
59
+	}
60
+
61
+	return &endpoint, nil
62
+}
63
+
64
+type Endpoint struct {
65
+	URL     *url.URL
66
+	Version APIVersion
67
+}
68
+
69
+// Get the formated URL for the root of this registry Endpoint
70
+func (e Endpoint) String() string {
71
+	return fmt.Sprintf("%s/v%d/", e.URL.String(), e.Version)
72
+}
73
+
74
+func (e Endpoint) VersionString(version APIVersion) string {
75
+	return fmt.Sprintf("%s/v%d/", e.URL.String(), version)
76
+}
77
+
78
+func (e Endpoint) Ping() (RegistryInfo, error) {
79
+	if e.String() == IndexServerAddress() {
80
+		// Skip the check, we now this one is valid
81
+		// (and we never want to fallback to http in case of error)
82
+		return RegistryInfo{Standalone: false}, nil
83
+	}
84
+
85
+	req, err := http.NewRequest("GET", e.String()+"_ping", nil)
86
+	if err != nil {
87
+		return RegistryInfo{Standalone: false}, err
88
+	}
89
+
90
+	resp, _, err := doRequest(req, nil, ConnectTimeout)
91
+	if err != nil {
92
+		return RegistryInfo{Standalone: false}, err
93
+	}
94
+
95
+	defer resp.Body.Close()
96
+
97
+	jsonString, err := ioutil.ReadAll(resp.Body)
98
+	if err != nil {
99
+		return RegistryInfo{Standalone: false}, fmt.Errorf("Error while reading the http response: %s", err)
100
+	}
101
+
102
+	// If the header is absent, we assume true for compatibility with earlier
103
+	// versions of the registry. default to true
104
+	info := RegistryInfo{
105
+		Standalone: true,
106
+	}
107
+	if err := json.Unmarshal(jsonString, &info); err != nil {
108
+		log.Debugf("Error unmarshalling the _ping RegistryInfo: %s", err)
109
+		// don't stop here. Just assume sane defaults
110
+	}
111
+	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
112
+		log.Debugf("Registry version header: '%s'", hdr)
113
+		info.Version = hdr
114
+	}
115
+	log.Debugf("RegistryInfo.Version: %q", info.Version)
116
+
117
+	standalone := resp.Header.Get("X-Docker-Registry-Standalone")
118
+	log.Debugf("Registry standalone header: '%s'", standalone)
119
+	// Accepted values are "true" (case-insensitive) and "1".
120
+	if strings.EqualFold(standalone, "true") || standalone == "1" {
121
+		info.Standalone = true
122
+	} else if len(standalone) > 0 {
123
+		// there is a header set, and it is not "true" or "1", so assume fails
124
+		info.Standalone = false
125
+	}
126
+	log.Debugf("RegistryInfo.Standalone: %q", info.Standalone)
127
+	return info, nil
128
+}
... ...
@@ -3,7 +3,6 @@ package registry
3 3
 import (
4 4
 	"crypto/tls"
5 5
 	"crypto/x509"
6
-	"encoding/json"
7 6
 	"errors"
8 7
 	"fmt"
9 8
 	"io/ioutil"
... ...
@@ -15,7 +14,6 @@ import (
15 15
 	"strings"
16 16
 	"time"
17 17
 
18
-	"github.com/docker/docker/pkg/log"
19 18
 	"github.com/docker/docker/utils"
20 19
 )
21 20
 
... ...
@@ -152,55 +150,6 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*htt
152 152
 	return nil, nil, nil
153 153
 }
154 154
 
155
-func pingRegistryEndpoint(endpoint string) (RegistryInfo, error) {
156
-	if endpoint == IndexServerAddress() {
157
-		// Skip the check, we now this one is valid
158
-		// (and we never want to fallback to http in case of error)
159
-		return RegistryInfo{Standalone: false}, nil
160
-	}
161
-
162
-	req, err := http.NewRequest("GET", endpoint+"_ping", nil)
163
-	if err != nil {
164
-		return RegistryInfo{Standalone: false}, err
165
-	}
166
-
167
-	resp, _, err := doRequest(req, nil, ConnectTimeout)
168
-	if err != nil {
169
-		return RegistryInfo{Standalone: false}, err
170
-	}
171
-
172
-	defer resp.Body.Close()
173
-
174
-	jsonString, err := ioutil.ReadAll(resp.Body)
175
-	if err != nil {
176
-		return RegistryInfo{Standalone: false}, fmt.Errorf("Error while reading the http response: %s", err)
177
-	}
178
-
179
-	// If the header is absent, we assume true for compatibility with earlier
180
-	// versions of the registry. default to true
181
-	info := RegistryInfo{
182
-		Standalone: true,
183
-	}
184
-	if err := json.Unmarshal(jsonString, &info); err != nil {
185
-		log.Debugf("Error unmarshalling the _ping RegistryInfo: %s", err)
186
-		// don't stop here. Just assume sane defaults
187
-	}
188
-	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
189
-		log.Debugf("Registry version header: '%s'", hdr)
190
-		info.Version = hdr
191
-	}
192
-	log.Debugf("RegistryInfo.Version: %q", info.Version)
193
-
194
-	standalone := resp.Header.Get("X-Docker-Registry-Standalone")
195
-	log.Debugf("Registry standalone header: '%s'", standalone)
196
-	if !strings.EqualFold(standalone, "true") && standalone != "1" && len(standalone) > 0 {
197
-		// there is a header set, and it is not "true" or "1", so assume fails
198
-		info.Standalone = false
199
-	}
200
-	log.Debugf("RegistryInfo.Standalone: %q", info.Standalone)
201
-	return info, nil
202
-}
203
-
204 155
 func validateRepositoryName(repositoryName string) error {
205 156
 	var (
206 157
 		namespace string
... ...
@@ -252,33 +201,6 @@ func ResolveRepositoryName(reposName string) (string, string, error) {
252 252
 	return hostname, reposName, nil
253 253
 }
254 254
 
255
-// this method expands the registry name as used in the prefix of a repo
256
-// to a full url. if it already is a url, there will be no change.
257
-// The registry is pinged to test if it http or https
258
-func ExpandAndVerifyRegistryUrl(hostname string) (string, error) {
259
-	if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") {
260
-		// if there is no slash after https:// (8 characters) then we have no path in the url
261
-		if strings.LastIndex(hostname, "/") < 9 {
262
-			// there is no path given. Expand with default path
263
-			hostname = hostname + "/v1/"
264
-		}
265
-		if _, err := pingRegistryEndpoint(hostname); err != nil {
266
-			return "", errors.New("Invalid Registry endpoint: " + err.Error())
267
-		}
268
-		return hostname, nil
269
-	}
270
-	endpoint := fmt.Sprintf("https://%s/v1/", hostname)
271
-	if _, err := pingRegistryEndpoint(endpoint); err != nil {
272
-		log.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
273
-		endpoint = fmt.Sprintf("http://%s/v1/", hostname)
274
-		if _, err = pingRegistryEndpoint(endpoint); err != nil {
275
-			//TODO: triggering highland build can be done there without "failing"
276
-			return "", errors.New("Invalid Registry endpoint: " + err.Error())
277
-		}
278
-	}
279
-	return endpoint, nil
280
-}
281
-
282 255
 func trustedLocation(req *http.Request) bool {
283 256
 	var (
284 257
 		trusteds = []string{"docker.com", "docker.io"}
... ...
@@ -18,7 +18,11 @@ var (
18 18
 
19 19
 func spawnTestRegistrySession(t *testing.T) *Session {
20 20
 	authConfig := &AuthConfig{}
21
-	r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), makeURL("/v1/"), true)
21
+	endpoint, err := NewEndpoint(makeURL("/v1/"))
22
+	if err != nil {
23
+		t.Fatal(err)
24
+	}
25
+	r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true)
22 26
 	if err != nil {
23 27
 		t.Fatal(err)
24 28
 	}
... ...
@@ -26,7 +30,11 @@ func spawnTestRegistrySession(t *testing.T) *Session {
26 26
 }
27 27
 
28 28
 func TestPingRegistryEndpoint(t *testing.T) {
29
-	regInfo, err := pingRegistryEndpoint(makeURL("/v1/"))
29
+	ep, err := NewEndpoint(makeURL("/v1/"))
30
+	if err != nil {
31
+		t.Fatal(err)
32
+	}
33
+	regInfo, err := ep.Ping()
30 34
 	if err != nil {
31 35
 		t.Fatal(err)
32 36
 	}
... ...
@@ -197,7 +205,7 @@ func TestPushImageJSONIndex(t *testing.T) {
197 197
 	if repoData == nil {
198 198
 		t.Fatal("Expected RepositoryData object")
199 199
 	}
200
-	repoData, err = r.PushImageJSONIndex("foo42/bar", imgData, true, []string{r.indexEndpoint})
200
+	repoData, err = r.PushImageJSONIndex("foo42/bar", imgData, true, []string{r.indexEndpoint.String()})
201 201
 	if err != nil {
202 202
 		t.Fatal(err)
203 203
 	}
... ...
@@ -40,11 +40,14 @@ func (s *Service) Auth(job *engine.Job) engine.Status {
40 40
 	job.GetenvJson("authConfig", authConfig)
41 41
 	// TODO: this is only done here because auth and registry need to be merged into one pkg
42 42
 	if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
43
-		addr, err = ExpandAndVerifyRegistryUrl(addr)
43
+		endpoint, err := NewEndpoint(addr)
44 44
 		if err != nil {
45 45
 			return job.Error(err)
46 46
 		}
47
-		authConfig.ServerAddress = addr
47
+		if _, err := endpoint.Ping(); err != nil {
48
+			return job.Error(err)
49
+		}
50
+		authConfig.ServerAddress = endpoint.String()
48 51
 	}
49 52
 	status, err := Login(authConfig, HTTPRequestFactory(nil))
50 53
 	if err != nil {
... ...
@@ -86,11 +89,11 @@ func (s *Service) Search(job *engine.Job) engine.Status {
86 86
 	if err != nil {
87 87
 		return job.Error(err)
88 88
 	}
89
-	hostname, err = ExpandAndVerifyRegistryUrl(hostname)
89
+	endpoint, err := NewEndpoint(hostname)
90 90
 	if err != nil {
91 91
 		return job.Error(err)
92 92
 	}
93
-	r, err := NewSession(authConfig, HTTPRequestFactory(metaHeaders), hostname, true)
93
+	r, err := NewSession(authConfig, HTTPRequestFactory(metaHeaders), endpoint, true)
94 94
 	if err != nil {
95 95
 		return job.Error(err)
96 96
 	}
... ...
@@ -25,15 +25,15 @@ import (
25 25
 type Session struct {
26 26
 	authConfig    *AuthConfig
27 27
 	reqFactory    *utils.HTTPRequestFactory
28
-	indexEndpoint string
28
+	indexEndpoint *Endpoint
29 29
 	jar           *cookiejar.Jar
30 30
 	timeout       TimeoutType
31 31
 }
32 32
 
33
-func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, indexEndpoint string, timeout bool) (r *Session, err error) {
33
+func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpoint *Endpoint, timeout bool) (r *Session, err error) {
34 34
 	r = &Session{
35 35
 		authConfig:    authConfig,
36
-		indexEndpoint: indexEndpoint,
36
+		indexEndpoint: endpoint,
37 37
 	}
38 38
 
39 39
 	if timeout {
... ...
@@ -47,13 +47,13 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, index
47 47
 
48 48
 	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
49 49
 	// alongside our requests.
50
-	if indexEndpoint != IndexServerAddress() && strings.HasPrefix(indexEndpoint, "https://") {
51
-		info, err := pingRegistryEndpoint(indexEndpoint)
50
+	if r.indexEndpoint.String() != IndexServerAddress() && r.indexEndpoint.URL.Scheme == "https" {
51
+		info, err := r.indexEndpoint.Ping()
52 52
 		if err != nil {
53 53
 			return nil, err
54 54
 		}
55 55
 		if info.Standalone {
56
-			log.Debugf("Endpoint %s is eligible for private registry registry. Enabling decorator.", indexEndpoint)
56
+			log.Debugf("Endpoint %s is eligible for private registry registry. Enabling decorator.", r.indexEndpoint.String())
57 57
 			dec := utils.NewHTTPAuthDecorator(authConfig.Username, authConfig.Password)
58 58
 			factory.AddDecorator(dec)
59 59
 		}
... ...
@@ -261,8 +261,7 @@ func buildEndpointsList(headers []string, indexEp string) ([]string, error) {
261 261
 }
262 262
 
263 263
 func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) {
264
-	indexEp := r.indexEndpoint
265
-	repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote)
264
+	repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), remote)
266 265
 
267 266
 	log.Debugf("[registry] Calling GET %s", repositoryTarget)
268 267
 
... ...
@@ -296,17 +295,13 @@ func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) {
296 296
 
297 297
 	var endpoints []string
298 298
 	if res.Header.Get("X-Docker-Endpoints") != "" {
299
-		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], indexEp)
299
+		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
300 300
 		if err != nil {
301 301
 			return nil, err
302 302
 		}
303 303
 	} else {
304 304
 		// Assume the endpoint is on the same host
305
-		u, err := url.Parse(indexEp)
306
-		if err != nil {
307
-			return nil, err
308
-		}
309
-		endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", u.Scheme, req.URL.Host))
305
+		endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host))
310 306
 	}
311 307
 
312 308
 	checksumsJSON, err := ioutil.ReadAll(res.Body)
... ...
@@ -474,7 +469,6 @@ func (r *Session) PushRegistryTag(remote, revision, tag, registry string, token
474 474
 
475 475
 func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) {
476 476
 	cleanImgList := []*ImgData{}
477
-	indexEp := r.indexEndpoint
478 477
 
479 478
 	if validate {
480 479
 		for _, elem := range imgList {
... ...
@@ -494,7 +488,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate
494 494
 	if validate {
495 495
 		suffix = "images"
496 496
 	}
497
-	u := fmt.Sprintf("%srepositories/%s/%s", indexEp, remote, suffix)
497
+	u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote, suffix)
498 498
 	log.Debugf("[registry] PUT %s", u)
499 499
 	log.Debugf("Image list pushed to index:\n%s", imgListJSON)
500 500
 	req, err := r.reqFactory.NewRequest("PUT", u, bytes.NewReader(imgListJSON))
... ...
@@ -552,7 +546,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate
552 552
 		}
553 553
 
554 554
 		if res.Header.Get("X-Docker-Endpoints") != "" {
555
-			endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], indexEp)
555
+			endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
556 556
 			if err != nil {
557 557
 				return nil, err
558 558
 			}
... ...
@@ -578,7 +572,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate
578 578
 
579 579
 func (r *Session) SearchRepositories(term string) (*SearchResults, error) {
580 580
 	log.Debugf("Index server: %s", r.indexEndpoint)
581
-	u := r.indexEndpoint + "search?q=" + url.QueryEscape(term)
581
+	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term)
582 582
 	req, err := r.reqFactory.NewRequest("GET", u, nil)
583 583
 	if err != nil {
584 584
 		return nil, err
... ...
@@ -31,3 +31,21 @@ type RegistryInfo struct {
31 31
 	Version    string `json:"version"`
32 32
 	Standalone bool   `json:"standalone"`
33 33
 }
34
+
35
+type APIVersion int
36
+
37
+func (av APIVersion) String() string {
38
+	return apiVersions[av]
39
+}
40
+
41
+var DefaultAPIVersion APIVersion = APIVersion1
42
+var apiVersions = map[APIVersion]string{
43
+	1: "v1",
44
+	2: "v2",
45
+}
46
+
47
+const (
48
+	_           = iota
49
+	APIVersion1 = iota
50
+	APIVersion2
51
+)