Skip to content

Commit 9a376f2

Browse files
authored
fix(registry): support repush image (#2229)
1 parent 10c8b2c commit 9a376f2

File tree

11 files changed

+384
-9
lines changed

11 files changed

+384
-9
lines changed

api/registry/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const (
3030
ChartGroupFinalize FinalizerName = "chartgroup"
3131
// ChartFinalize is an internal finalizer values to Chart.
3232
ChartFinalize FinalizerName = "chart"
33+
// RegistryClientUserAgent is the user agent for tke registry client
34+
RegistryClientUserAgent = "tke-registry-client"
3335
)
3436

3537
// +genclient

api/registry/v1/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const (
3030
ChartGroupFinalize FinalizerName = "chartgroup"
3131
// ChartFinalize is an internal finalizer values to Chart.
3232
ChartFinalize FinalizerName = "chart"
33+
// RegistryClientUserAgent is the user agent for tke registry client
34+
RegistryClientUserAgent = "tke-registry-client"
3335
)
3436

3537
// +genclient

pkg/registry/apiserver/apiserver.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package apiserver
2020

2121
import (
22+
"github.com/docker/libtrust"
2223
"k8s.io/apiserver/pkg/registry/generic"
2324
genericapiserver "k8s.io/apiserver/pkg/server"
2425
serverstorage "k8s.io/apiserver/pkg/server/storage"
@@ -133,6 +134,11 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
133134
}
134135
}
135136

137+
pk, err := libtrust.LoadKeyFile(c.ExtraConfig.RegistryConfig.Security.TokenPrivateKeyFile)
138+
if err != nil {
139+
return nil, err
140+
}
141+
136142
// The order here is preserved in discovery.
137143
restStorageProviders := []storage.RESTStorageProvider{
138144
&registryrest.StorageProvider{
@@ -146,6 +152,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
146152
PlatformClient: c.ExtraConfig.PlatformClient,
147153
RegistryConfig: c.ExtraConfig.RegistryConfig,
148154
Authorizer: c.GenericConfig.Authorization.Authorizer,
155+
TokenPrivateKey: pk,
149156
},
150157
}
151158
m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...)

pkg/registry/distribution/auth/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
148148
return
149149
}
150150

151-
jwtToken, err := makeToken(username, access, h.expiredHours, h.privateKey)
151+
jwtToken, err := MakeToken(username, access, h.expiredHours, h.privateKey)
152152
if err != nil {
153153
log.Error("Failed create token for docker registry authentication",
154154
log.String("username", username),

pkg/registry/distribution/auth/token.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ import (
2222
"crypto"
2323
"encoding/base64"
2424
"fmt"
25-
"github.com/docker/distribution/registry/auth/token"
26-
"github.com/docker/libtrust"
2725
"math/rand"
2826
"strings"
2927
"time"
28+
29+
"github.com/docker/distribution/registry/auth/token"
30+
"github.com/docker/libtrust"
3031
)
3132

3233
const (
@@ -43,7 +44,7 @@ type Token struct {
4344
}
4445

4546
// makeToken makes a valid jwt token based on params.
46-
func makeToken(username string, access []*token.ResourceActions, expiredHours int64, privateKey libtrust.PrivateKey) (*Token, error) {
47+
func MakeToken(username string, access []*token.ResourceActions, expiredHours int64, privateKey libtrust.PrivateKey) (*Token, error) {
4748
tk, expiresIn, issuedAt, err := makeTokenCore(Issuer, username, Service, expiredHours, access, privateKey)
4849
if err != nil {
4950
return nil, err
@@ -123,3 +124,12 @@ func randString(length int) (string, error) {
123124
func base64UrlEncode(b []byte) string {
124125
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
125126
}
127+
128+
// GetToken returns the content of the token
129+
func (t *Token) GetToken() string {
130+
token := t.Token
131+
if len(token) == 0 {
132+
token = t.AccessToken
133+
}
134+
return token
135+
}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package client
2+
3+
import (
4+
"crypto/tls"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"net/url"
10+
"sort"
11+
"strings"
12+
13+
"github.com/docker/distribution/manifest/manifestlist"
14+
"github.com/docker/distribution/manifest/schema1"
15+
"github.com/docker/distribution/manifest/schema2"
16+
"github.com/docker/distribution/registry/auth/token"
17+
"github.com/docker/libtrust"
18+
"tkestack.io/tke/api/registry"
19+
"tkestack.io/tke/pkg/registry/distribution/auth"
20+
"tkestack.io/tke/pkg/util/log"
21+
)
22+
23+
var ManifestAccepts = []string{
24+
manifestlist.MediaTypeManifestList,
25+
schema2.MediaTypeManifest,
26+
schema1.MediaTypeSignedManifest,
27+
schema1.MediaTypeManifest,
28+
}
29+
30+
// Repository holds information of a repository entity
31+
type Repository struct {
32+
Endpoint *url.URL
33+
client *http.Client
34+
privateKey libtrust.PrivateKey
35+
}
36+
37+
// NewRepository returns an instance of Repository
38+
func NewRepository(endpoint string, privateKey libtrust.PrivateKey) (*Repository, error) {
39+
u, err := ParseEndpoint(endpoint)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
tr := &http.Transport{
45+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
46+
}
47+
client := &http.Client{Transport: tr}
48+
49+
repository := &Repository{
50+
Endpoint: u,
51+
client: client,
52+
privateKey: privateKey,
53+
}
54+
55+
return repository, nil
56+
}
57+
58+
// ParseEndpoint parses endpoint to a URL
59+
func ParseEndpoint(endpoint string) (*url.URL, error) {
60+
endpoint = strings.Trim(endpoint, " ")
61+
endpoint = strings.TrimRight(endpoint, "/")
62+
if len(endpoint) == 0 {
63+
return nil, fmt.Errorf("empty URL")
64+
}
65+
i := strings.Index(endpoint, "://")
66+
if i >= 0 {
67+
scheme := endpoint[:i]
68+
if scheme != "http" && scheme != "https" {
69+
return nil, fmt.Errorf("invalid scheme: %s", scheme)
70+
}
71+
} else {
72+
endpoint = "http://" + endpoint
73+
}
74+
75+
return url.ParseRequestURI(endpoint)
76+
}
77+
78+
// DeleteTag ...
79+
func (r *Repository) DeleteTag(repoName, tag, user, tenantID string) error {
80+
digest, exist, err := r.ManifestExist(tag, repoName, tag, user, tenantID)
81+
if err != nil {
82+
return err
83+
}
84+
85+
if !exist {
86+
log.Warnf("repo: %s:%s manifests not found.", repoName, tag)
87+
return nil
88+
}
89+
90+
if err := r.DeleteManifest(digest, repoName, tag, user, tenantID); err != nil {
91+
return err
92+
}
93+
return nil
94+
}
95+
96+
// ListTag ...
97+
func (r *Repository) ListTag(repoName, user, tenantID string) ([]string, error) {
98+
tags := []string{}
99+
req, err := http.NewRequest("GET", buildTagListURL(r.Endpoint.String(), repoName), nil)
100+
if err != nil {
101+
return tags, err
102+
}
103+
err = r.withAuthInfo(req, repoName, user, tenantID)
104+
if err != nil {
105+
return tags, err
106+
}
107+
108+
resp, err := r.client.Do(req)
109+
if err != nil {
110+
return tags, parseError(err)
111+
}
112+
113+
defer resp.Body.Close()
114+
115+
b, err := ioutil.ReadAll(resp.Body)
116+
if err != nil {
117+
return tags, err
118+
}
119+
120+
if resp.StatusCode == http.StatusOK {
121+
tagsResp := struct {
122+
Tags []string `json:"tags"`
123+
}{}
124+
125+
if err := json.Unmarshal(b, &tagsResp); err != nil {
126+
return tags, err
127+
}
128+
sort.Strings(tags)
129+
tags = tagsResp.Tags
130+
131+
return tags, nil
132+
} else if resp.StatusCode == http.StatusNotFound {
133+
return tags, nil
134+
}
135+
136+
return tags, &Error{
137+
Code: resp.StatusCode,
138+
Message: string(b),
139+
}
140+
141+
}
142+
143+
// ManifestExist ...
144+
func (r *Repository) ManifestExist(reference, repoName, tag, user, tenantID string) (digest string, exist bool, err error) {
145+
req, err := http.NewRequest("HEAD", buildManifestURL(r.Endpoint.String(), repoName, reference), nil)
146+
if err != nil {
147+
return
148+
}
149+
err = r.withAuthInfo(req, repoName, user, tenantID)
150+
if err != nil {
151+
return
152+
}
153+
154+
for _, mediaType := range ManifestAccepts {
155+
req.Header.Add("Accept", mediaType)
156+
}
157+
158+
resp, err := r.client.Do(req)
159+
if err != nil {
160+
err = parseError(err)
161+
return
162+
}
163+
164+
defer resp.Body.Close()
165+
166+
if resp.StatusCode == http.StatusOK {
167+
exist = true
168+
digest = resp.Header.Get("Docker-Content-Digest")
169+
return
170+
}
171+
172+
if resp.StatusCode == http.StatusNotFound {
173+
return
174+
}
175+
176+
b, err := ioutil.ReadAll(resp.Body)
177+
if err != nil {
178+
return
179+
}
180+
181+
err = &Error{
182+
Code: resp.StatusCode,
183+
Message: string(b),
184+
}
185+
return
186+
}
187+
188+
// DeleteManifest ...
189+
func (r *Repository) DeleteManifest(digest, repoName, tag, user, tenantID string) error {
190+
req, err := http.NewRequest("DELETE", buildManifestURL(r.Endpoint.String(), repoName, digest), nil)
191+
if err != nil {
192+
return err
193+
}
194+
err = r.withAuthInfo(req, repoName, user, tenantID)
195+
if err != nil {
196+
return err
197+
}
198+
199+
resp, err := r.client.Do(req)
200+
if err != nil {
201+
return parseError(err)
202+
}
203+
204+
defer resp.Body.Close()
205+
206+
if resp.StatusCode == http.StatusAccepted {
207+
return nil
208+
}
209+
210+
b, err := ioutil.ReadAll(resp.Body)
211+
if err != nil {
212+
return err
213+
}
214+
215+
return &Error{
216+
Code: resp.StatusCode,
217+
Message: string(b),
218+
}
219+
}
220+
221+
func (r *Repository) withAuthInfo(req *http.Request, repoName, user, tenantID string) error {
222+
access := []*token.ResourceActions{
223+
{
224+
Type: "repository",
225+
Actions: []string{"*", "pull"},
226+
// to make token be available, should rename repo name with tenantID
227+
Name: fmt.Sprintf("%s-%s", tenantID, repoName),
228+
},
229+
}
230+
token, err := auth.MakeToken(user, access, 24, r.privateKey)
231+
if err != nil {
232+
return err
233+
}
234+
log.Infof("token: %s", token.GetToken())
235+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.GetToken()))
236+
// set registry client UA to avoid error cased by reporting event of the deleted repo
237+
req.Header.Set("User-Agent", registry.RegistryClientUserAgent)
238+
return nil
239+
}
240+
241+
func buildManifestURL(endpoint, repoName, reference string) string {
242+
return fmt.Sprintf("%s/v2/%s/manifests/%s", endpoint, repoName, reference)
243+
}
244+
245+
func buildTagListURL(endpoint, repoName string) string {
246+
return fmt.Sprintf("%s/v2/%s/tags/list", endpoint, repoName)
247+
}
248+
249+
func parseError(err error) error {
250+
if urlErr, ok := err.(*url.Error); ok {
251+
if regErr, ok := urlErr.Err.(*Error); ok {
252+
return regErr
253+
}
254+
}
255+
return err
256+
}
257+
258+
// Error wrap HTTP status code and message as an error
259+
type Error struct {
260+
Code int `json:"code"`
261+
Message string `json:"message"`
262+
}
263+
264+
// Error ...
265+
func (e *Error) Error() string {
266+
return fmt.Sprintf("http error: code %d, message %s", e.Code, e.Message)
267+
}
268+
269+
// String wraps the error msg to the well formatted error message
270+
func (e *Error) String() string {
271+
data, err := json.Marshal(&e)
272+
if err != nil {
273+
return e.Message
274+
}
275+
return string(data)
276+
}

0 commit comments

Comments
 (0)