Skip to content

Commit f2a2968

Browse files
Prince Rachit Sinharoboquat
Prince Rachit Sinha
authored andcommitted
[image-builder] Intro non-docker url rewrite
Process docker api urls starting with /v2/ differently from non-docker api url whose path does not start with /v2/.
1 parent 563b648 commit f2a2968

File tree

2 files changed

+232
-3
lines changed

2 files changed

+232
-3
lines changed

components/image-builder-bob/pkg/proxy/proxy.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Repo struct {
5252
Auth func() docker.Authorizer
5353
}
5454

55-
func rewriteURL(u *url.URL, fromRepo, toRepo, host, tag string) {
55+
func rewriteDockerAPIURL(u *url.URL, fromRepo, toRepo, host, tag string) {
5656
var (
5757
from = "/v2/" + strings.Trim(fromRepo, "/") + "/"
5858
to = "/v2/" + strings.Trim(toRepo, "/") + "/"
@@ -79,6 +79,27 @@ func rewriteURL(u *url.URL, fromRepo, toRepo, host, tag string) {
7979
u.Host = host
8080
}
8181

82+
// rewriteNonDockerAPIURL is used when a url has to be rewritten but the url
83+
// contains a non docker api path
84+
func rewriteNonDockerAPIURL(u *url.URL, fromPrefix, toPrefix, host string) {
85+
var (
86+
from = "/" + strings.Trim(fromPrefix, "/") + "/"
87+
to = "/" + strings.Trim(toPrefix, "/") + "/"
88+
)
89+
if fromPrefix == "" {
90+
from = "/"
91+
}
92+
if toPrefix == "" {
93+
to = "/"
94+
}
95+
u.Path = to + strings.TrimPrefix(u.Path, from)
96+
97+
// we reset the escaped encoding hint, because EscapedPath will produce a valid encoding.
98+
u.RawPath = ""
99+
100+
u.Host = host
101+
}
102+
82103
// ServeHTTP serves the proxy
83104
func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
84105
ctx := r.Context()
@@ -88,9 +109,20 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
88109
alias string
89110
)
90111
for k, v := range proxy.Aliases {
112+
// Docker api request
91113
if strings.HasPrefix(r.URL.Path, "/v2/"+k+"/") {
92114
repo = &v
93115
alias = k
116+
rewriteDockerAPIURL(r.URL, alias, repo.Repo, repo.Host, repo.Tag)
117+
break
118+
}
119+
// Non-Docker api request
120+
if strings.HasPrefix(r.URL.Path, "/"+k+"/") {
121+
// We will use the same repo/alias and its credentials but we will set target
122+
// repo as empty
123+
repo = &v
124+
alias = k
125+
rewriteNonDockerAPIURL(r.URL, alias, "", repo.Host)
94126
break
95127
}
96128
}
@@ -99,7 +131,6 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
99131
return
100132
}
101133

102-
rewriteURL(r.URL, alias, repo.Repo, repo.Host, repo.Tag)
103134
r.Host = r.URL.Host
104135

105136
auth := repo.Auth()
@@ -186,13 +217,23 @@ func (proxy *Proxy) reverse(alias string) *httputil.ReverseProxy {
186217
rp.ModifyResponse = func(r *http.Response) error {
187218
// Some registries return a Location header which we must rewrite to still push
188219
// through this proxy.
220+
// We support only relative URLs and not absolute URLs.
189221
if loc := r.Header.Get("Location"); loc != "" {
190222
lurl, err := url.Parse(loc)
191223
if err != nil {
192224
return err
193225
}
194226

195-
rewriteURL(lurl, repo.Repo, alias, proxy.Host.Host, "")
227+
if strings.HasPrefix(loc, "/v2/") {
228+
rewriteDockerAPIURL(lurl, repo.Repo, alias, proxy.Host.Host, "")
229+
} else {
230+
// since this is a non docker api location we
231+
// do not need to process the path.
232+
// All docker api URLs always start with /v2/. See spec
233+
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#endpoints
234+
rewriteNonDockerAPIURL(lurl, "", alias, repo.Host)
235+
}
236+
196237
lurl.Host = proxy.Host.Host
197238
// force scheme to http assuming this proxy never runs as https
198239
lurl.Scheme = proxy.Host.Scheme
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package proxy
6+
7+
import (
8+
"net/url"
9+
"testing"
10+
)
11+
12+
func TestRewriteNonDockerAPIURL(t *testing.T) {
13+
type input struct {
14+
u url.URL
15+
fromPrefix string
16+
toPrefix string
17+
host string
18+
}
19+
tests := []struct {
20+
Name string
21+
in input
22+
u url.URL
23+
}{
24+
{
25+
Name: "toPrefix is empty",
26+
in: input{
27+
fromPrefix: "base",
28+
toPrefix: "",
29+
host: "europe-docker.pkg.dev",
30+
u: url.URL{
31+
Host: "localhost.com",
32+
Path: "/base/artifacts-uploads/namespaces/prince-tf-experiments/repositories/dazzle/uploads/somedata",
33+
},
34+
},
35+
u: url.URL{
36+
Host: "europe-docker.pkg.dev",
37+
Path: "/artifacts-uploads/namespaces/prince-tf-experiments/repositories/dazzle/uploads/somedata",
38+
},
39+
},
40+
{
41+
Name: "fromPrefix is empty",
42+
in: input{
43+
fromPrefix: "",
44+
toPrefix: "base",
45+
host: "localhost.com",
46+
u: url.URL{
47+
Host: "europe-docker.pkg.dev",
48+
Path: "/artifacts-uploads/namespaces/prince-tf-experiments/repositories/dazzle/uploads/somedata",
49+
},
50+
},
51+
u: url.URL{
52+
Host: "localhost.com",
53+
Path: "/base/artifacts-uploads/namespaces/prince-tf-experiments/repositories/dazzle/uploads/somedata",
54+
},
55+
},
56+
{
57+
Name: "fromPrefix and toPrefix are not empty",
58+
in: input{
59+
fromPrefix: "from",
60+
toPrefix: "to",
61+
host: "localhost.com",
62+
u: url.URL{
63+
Host: "example.com",
64+
Path: "/from/some/random/path",
65+
},
66+
},
67+
u: url.URL{
68+
Host: "localhost.com",
69+
Path: "/to/some/random/path",
70+
},
71+
},
72+
{
73+
Name: "fromPrefix and toPrefix are empty",
74+
in: input{
75+
fromPrefix: "",
76+
toPrefix: "",
77+
host: "localhost.com",
78+
u: url.URL{
79+
Host: "example.com",
80+
Path: "/some/random/path",
81+
},
82+
},
83+
u: url.URL{
84+
Host: "localhost.com",
85+
Path: "/some/random/path",
86+
},
87+
},
88+
}
89+
90+
for _, test := range tests {
91+
t.Run(test.Name, func(t *testing.T) {
92+
rewriteNonDockerAPIURL(&test.in.u, test.in.fromPrefix, test.in.toPrefix, test.in.host)
93+
if test.in.u.Path != test.u.Path {
94+
t.Errorf("expected path: %s but got %s", test.u.Path, test.in.u.Path)
95+
}
96+
if test.in.u.Host != test.u.Host {
97+
t.Errorf("expected Host: %s but got %s", test.u.Host, test.in.u.Host)
98+
}
99+
if test.in.u.RawPath != test.u.RawPath {
100+
t.Errorf("expected RawPath: %s but got %s", test.u.RawPath, test.in.u.RawPath)
101+
}
102+
})
103+
}
104+
105+
}
106+
107+
func TestRewriteDockerAPIURL(t *testing.T) {
108+
type input struct {
109+
u url.URL
110+
fromRepo string
111+
toRepo string
112+
host string
113+
tag string
114+
}
115+
tests := []struct {
116+
Name string
117+
in input
118+
u url.URL
119+
}{
120+
{
121+
Name: "remote to localhost",
122+
in: input{
123+
fromRepo: "base-images",
124+
toRepo: "base",
125+
host: "localhost.com",
126+
tag: "",
127+
u: url.URL{
128+
Host: "prince.azurecr.io",
129+
Path: "/v2/base-images/some/random/path",
130+
},
131+
},
132+
u: url.URL{
133+
Host: "localhost.com",
134+
Path: "/v2/base/some/random/path",
135+
},
136+
},
137+
{
138+
Name: "localhost to remote",
139+
in: input{
140+
fromRepo: "base",
141+
toRepo: "base-images",
142+
host: "prince.azurecr.io",
143+
tag: "",
144+
u: url.URL{
145+
Host: "localhost.com",
146+
Path: "/v2/base/some/random/path",
147+
},
148+
},
149+
u: url.URL{
150+
Host: "prince.azurecr.io",
151+
Path: "/v2/base-images/some/random/path",
152+
},
153+
},
154+
{
155+
Name: "manifest reference update with tag",
156+
in: input{
157+
fromRepo: "base",
158+
toRepo: "base-images",
159+
host: "prince.azurecr.io",
160+
tag: "tag12345",
161+
u: url.URL{
162+
Host: "localhost.com",
163+
Path: "/v2/base/uploads/manifests/manifest12345",
164+
},
165+
},
166+
u: url.URL{
167+
Host: "prince.azurecr.io",
168+
Path: "/v2/base-images/uploads/manifests/tag12345",
169+
},
170+
},
171+
}
172+
173+
for _, test := range tests {
174+
t.Run(test.Name, func(t *testing.T) {
175+
rewriteDockerAPIURL(&test.in.u, test.in.fromRepo, test.in.toRepo, test.in.host, test.in.tag)
176+
if test.in.u.Path != test.u.Path {
177+
t.Errorf("expected path: %s but got %s", test.u.Path, test.in.u.Path)
178+
}
179+
if test.in.u.Host != test.u.Host {
180+
t.Errorf("expected Host: %s but got %s", test.u.Host, test.in.u.Host)
181+
}
182+
if test.in.u.RawPath != test.u.RawPath {
183+
t.Errorf("expected RawPath: %s but got %s", test.u.RawPath, test.in.u.RawPath)
184+
}
185+
})
186+
}
187+
188+
}

0 commit comments

Comments
 (0)