Skip to content

Commit b32e73a

Browse files
committed
*: implement additional pull registries
Signed-off-by: Antonio Murdaca <[email protected]>
1 parent 0eb5cd5 commit b32e73a

File tree

9 files changed

+258
-40
lines changed

9 files changed

+258
-40
lines changed

cmd/crio/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ image_volumes = "{{ .ImageVolumes }}"
131131
insecure_registries = [
132132
{{ range $opt := .InsecureRegistries }}{{ printf "\t%q,\n" $opt }}{{ end }}]
133133
134+
# additional_registries TODO(runcom)
135+
additional_registries = [
136+
{{ range $opt := .AdditionalRegistries }}{{ printf "\t%q,\n" $opt }}{{ end }}]
137+
134138
# The "crio.network" table contains settings pertaining to the
135139
# management of CNI plugins.
136140
[crio.network]

cmd/crio/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func mergeConfig(config *server.Config, ctx *cli.Context) error {
7777
if ctx.GlobalIsSet("insecure-registry") {
7878
config.InsecureRegistries = ctx.GlobalStringSlice("insecure-registry")
7979
}
80+
if ctx.GlobalIsSet("additional-registry") {
81+
config.AdditionalRegistries = ctx.GlobalStringSlice("additional-registry")
82+
}
8083
if ctx.GlobalIsSet("default-transport") {
8184
config.DefaultTransport = ctx.GlobalString("default-transport")
8285
}
@@ -219,6 +222,10 @@ func main() {
219222
Name: "insecure-registry",
220223
Usage: "whether to disable TLS verification for the given registry",
221224
},
225+
cli.StringSliceFlag{
226+
Name: "additional-registry",
227+
Usage: "TODO(runcom)",
228+
},
222229
cli.StringFlag{
223230
Name: "default-transport",
224231
Usage: "default transport",

pkg/storage/image.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package storage
22

33
import (
44
"net"
5+
"path/filepath"
6+
"strings"
57

68
"github.com/containers/image/copy"
79
"github.com/containers/image/docker/reference"
@@ -31,6 +33,7 @@ type imageService struct {
3133
defaultTransport string
3234
insecureRegistryCIDRs []*net.IPNet
3335
indexConfigs map[string]*indexInfo
36+
registries []string
3437
}
3538

3639
// ImageServer wraps up various CRI-related activities into a reusable
@@ -50,6 +53,8 @@ type ImageServer interface {
5053
GetStore() storage.Store
5154
// CanPull preliminary checks whether we're allowed to pull an image
5255
CanPull(imageName string, options *copy.Options) (bool, error)
56+
// TODO(runcom)
57+
ResolveNames(imageName string) []string
5358
}
5459

5560
func (svc *imageService) ListImages(filter string) ([]ImageResult, error) {
@@ -272,11 +277,49 @@ func (svc *imageService) isSecureIndex(indexName string) bool {
272277
return true
273278
}
274279

280+
func isValidHostname(hostname string) bool {
281+
return hostname != "" && !strings.Contains(hostname, "/") &&
282+
(strings.Contains(hostname, ".") ||
283+
strings.Contains(hostname, ":") || hostname == "localhost")
284+
}
285+
286+
func (svc *imageService) ResolveNames(imageName string) []string {
287+
288+
// TODO(runcom): if it's docker.io, use ParseNormalizedNamed to get a
289+
// correct docker.io FQ image name!!!!
290+
291+
domain, rest := splitDomain(imageName)
292+
if len(domain) != 0 && isValidHostname(domain) {
293+
// this means the image is already fully qualified
294+
return []string{imageName}
295+
}
296+
// this means we got an image in the form of "busybox"
297+
// we need to use additional registries...
298+
// normalize the unqualified image to be domain/repo/image...
299+
images := []string{}
300+
for _, r := range svc.registries {
301+
// TODO(runcom): use a CONST
302+
if r == "docker.io" {
303+
// no additional registries configured, docker is meant
304+
// containers/image will take care of this
305+
images = append(images, imageName)
306+
continue
307+
}
308+
path := rest
309+
if !isValidHostname(domain) {
310+
// This is the case where we have an image like "runcom/busybox"
311+
path = imageName
312+
}
313+
images = append(images, filepath.Join(r, path))
314+
}
315+
return images
316+
}
317+
275318
// GetImageService returns an ImageServer that uses the passed-in store, and
276319
// which will prepend the passed-in defaultTransport value to an image name if
277320
// a name that's passed to its PullImage() method can't be resolved to an image
278321
// in the store and can't be resolved to a source on its own.
279-
func GetImageService(store storage.Store, defaultTransport string, insecureRegistries []string) (ImageServer, error) {
322+
func GetImageService(store storage.Store, defaultTransport string, insecureRegistries []string, additionalRegistries []string) (ImageServer, error) {
280323
if store == nil {
281324
var err error
282325
store, err = storage.GetStore(storage.DefaultStoreOptions)
@@ -285,11 +328,19 @@ func GetImageService(store storage.Store, defaultTransport string, insecureRegis
285328
}
286329
}
287330

331+
if len(additionalRegistries) == 0 {
332+
// TODO(runcom): make docker.io a CONST
333+
additionalRegistries = []string{"docker.io"}
334+
} else {
335+
additionalRegistries = append(additionalRegistries, "docker.io")
336+
}
337+
288338
is := &imageService{
289339
store: store,
290340
defaultTransport: defaultTransport,
291341
indexConfigs: make(map[string]*indexInfo, 0),
292342
insecureRegistryCIDRs: make([]*net.IPNet, 0),
343+
registries: additionalRegistries,
293344
}
294345

295346
insecureRegistries = append(insecureRegistries, "127.0.0.0/8")

pkg/storage/image_regexp.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package storage
2+
3+
// TODO(runcom): explain why we need this here!!!
4+
5+
import "regexp"
6+
7+
var (
8+
// alphaNumericRegexp defines the alpha numeric atom, typically a
9+
// component of names. This only allows lower case characters and digits.
10+
alphaNumericRegexp = match(`[a-z0-9]+`)
11+
12+
// separatorRegexp defines the separators allowed to be embedded in name
13+
// components. This allow one period, one or two underscore and multiple
14+
// dashes.
15+
separatorRegexp = match(`(?:[._]|__|[-]*)`)
16+
17+
// nameComponentRegexp restricts registry path component names to start
18+
// with at least one letter or number, with following parts able to be
19+
// separated by one period, one or two underscore and multiple dashes.
20+
nameComponentRegexp = expression(
21+
alphaNumericRegexp,
22+
optional(repeated(separatorRegexp, alphaNumericRegexp)))
23+
24+
// domainComponentRegexp restricts the registry domain component of a
25+
// repository name to start with a component as defined by domainRegexp
26+
// and followed by an optional port.
27+
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
28+
29+
// domainRegexp defines the structure of potential domain components
30+
// that may be part of image names. This is purposely a subset of what is
31+
// allowed by DNS to ensure backwards compatibility with Docker image
32+
// names.
33+
domainRegexp = expression(
34+
domainComponentRegexp,
35+
optional(repeated(literal(`.`), domainComponentRegexp)),
36+
optional(literal(`:`), match(`[0-9]+`)))
37+
38+
// NameRegexp is the format for the name component of references. The
39+
// regexp has capturing groups for the domain and name part omitting
40+
// the separating forward slash from either.
41+
NameRegexp = expression(
42+
optional(domainRegexp, literal(`/`)),
43+
nameComponentRegexp,
44+
optional(repeated(literal(`/`), nameComponentRegexp)))
45+
46+
// anchoredNameRegexp is used to parse a name value, capturing the
47+
// domain and trailing components.
48+
anchoredNameRegexp = anchored(
49+
optional(capture(domainRegexp), literal(`/`)),
50+
capture(nameComponentRegexp,
51+
optional(repeated(literal(`/`), nameComponentRegexp))))
52+
53+
// IdentifierRegexp is the format for string identifier used as a
54+
// content addressable identifier using sha256. These identifiers
55+
// are like digests without the algorithm, since sha256 is used.
56+
IdentifierRegexp = match(`([a-f0-9]{64})`)
57+
58+
// ShortIdentifierRegexp is the format used to represent a prefix
59+
// of an identifier. A prefix may be used to match a sha256 identifier
60+
// within a list of trusted identifiers.
61+
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
62+
)
63+
64+
// match compiles the string to a regular expression.
65+
var match = regexp.MustCompile
66+
67+
// literal compiles s into a literal regular expression, escaping any regexp
68+
// reserved characters.
69+
func literal(s string) *regexp.Regexp {
70+
re := match(regexp.QuoteMeta(s))
71+
72+
if _, complete := re.LiteralPrefix(); !complete {
73+
panic("must be a literal")
74+
}
75+
76+
return re
77+
}
78+
79+
func splitDomain(name string) (string, string) {
80+
match := anchoredNameRegexp.FindStringSubmatch(name)
81+
if len(match) != 3 {
82+
return "", name
83+
}
84+
return match[1], match[2]
85+
}
86+
87+
// expression defines a full expression, where each regular expression must
88+
// follow the previous.
89+
func expression(res ...*regexp.Regexp) *regexp.Regexp {
90+
var s string
91+
for _, re := range res {
92+
s += re.String()
93+
}
94+
95+
return match(s)
96+
}
97+
98+
// optional wraps the expression in a non-capturing group and makes the
99+
// production optional.
100+
func optional(res ...*regexp.Regexp) *regexp.Regexp {
101+
return match(group(expression(res...)).String() + `?`)
102+
}
103+
104+
// repeated wraps the regexp in a non-capturing group to get one or more
105+
// matches.
106+
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
107+
return match(group(expression(res...)).String() + `+`)
108+
}
109+
110+
// group wraps the regexp in a non-capturing group.
111+
func group(res ...*regexp.Regexp) *regexp.Regexp {
112+
return match(`(?:` + expression(res...).String() + `)`)
113+
}
114+
115+
// capture wraps the expression in a capturing group.
116+
func capture(res ...*regexp.Regexp) *regexp.Regexp {
117+
return match(`(` + expression(res...).String() + `)`)
118+
}
119+
120+
// anchored anchors the regular expression by adding start and end delimiters.
121+
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
122+
return match(`^` + expression(res...).String() + `$`)
123+
}

server/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ type ImageConfig struct {
169169
InsecureRegistries []string `toml:"insecure_registries"`
170170
// ImageVolumes controls how volumes specified in image config are handled
171171
ImageVolumes ImageVolumesType `toml:"image_volumes"`
172+
// TODO(runcom)
173+
AdditionalRegistries []string `toml:"additional_registries"`
172174
}
173175

174176
// NetworkConfig represents the "crio.network" TOML config table

server/image_pull.go

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
// PullImage pulls a image with authentication config.
1515
func (s *Server) PullImage(ctx context.Context, req *pb.PullImageRequest) (*pb.PullImageResponse, error) {
1616
logrus.Debugf("PullImageRequest: %+v", req)
17-
// TODO(runcom?): deal with AuthConfig in req.GetAuth()
1817
// TODO: what else do we need here? (Signatures when the story isn't just pulling from docker://)
1918
image := ""
2019
img := req.GetImage()
@@ -23,51 +22,68 @@ func (s *Server) PullImage(ctx context.Context, req *pb.PullImageRequest) (*pb.P
2322
}
2423

2524
var (
26-
username string
27-
password string
25+
images = s.StorageImageServer().ResolveNames(image)
26+
pulled string
27+
err error
2828
)
29-
if req.GetAuth() != nil {
30-
username = req.GetAuth().Username
31-
password = req.GetAuth().Password
32-
if req.GetAuth().Auth != "" {
33-
var err error
34-
username, password, err = decodeDockerAuth(req.GetAuth().Auth)
35-
if err != nil {
36-
return nil, err
29+
for _, img := range images {
30+
var (
31+
username string
32+
password string
33+
)
34+
if req.GetAuth() != nil {
35+
username = req.GetAuth().Username
36+
password = req.GetAuth().Password
37+
if req.GetAuth().Auth != "" {
38+
username, password, err = decodeDockerAuth(req.GetAuth().Auth)
39+
if err != nil {
40+
logrus.Debugf("error decoding authentication for image %s: %v", img, err)
41+
continue
42+
}
3743
}
3844
}
39-
}
40-
options := &copy.Options{
41-
SourceCtx: &types.SystemContext{},
42-
}
43-
// a not empty username should be sufficient to decide whether to send auth
44-
// or not I guess
45-
if username != "" {
46-
options.SourceCtx = &types.SystemContext{
47-
DockerAuthConfig: &types.DockerAuthConfig{
48-
Username: username,
49-
Password: password,
50-
},
45+
options := &copy.Options{
46+
SourceCtx: &types.SystemContext{},
47+
}
48+
// a not empty username should be sufficient to decide whether to send auth
49+
// or not I guess
50+
if username != "" {
51+
options.SourceCtx = &types.SystemContext{
52+
DockerAuthConfig: &types.DockerAuthConfig{
53+
Username: username,
54+
Password: password,
55+
},
56+
}
5157
}
52-
}
5358

54-
canPull, err := s.StorageImageServer().CanPull(image, options)
55-
if err != nil && !canPull {
56-
return nil, err
57-
}
59+
var canPull bool
60+
canPull, err = s.StorageImageServer().CanPull(img, options)
61+
if err != nil && !canPull {
62+
logrus.Debugf("error checking image %s: %v", img, err)
63+
continue
64+
}
5865

59-
// let's be smart, docker doesn't repull if image already exists.
60-
if _, err := s.StorageImageServer().ImageStatus(s.ImageContext(), image); err == nil {
61-
return &pb.PullImageResponse{
62-
ImageRef: image,
63-
}, nil
64-
}
66+
// let's be smart, docker doesn't repull if image already exists.
67+
_, err = s.StorageImageServer().ImageStatus(s.ImageContext(), img)
68+
if err == nil {
69+
logrus.Debugf("image %s already in store, skipping pull", img)
70+
pulled = img
71+
break
72+
}
6573

66-
if _, err := s.StorageImageServer().PullImage(s.ImageContext(), image, options); err != nil {
74+
_, err = s.StorageImageServer().PullImage(s.ImageContext(), img, options)
75+
if err != nil {
76+
logrus.Debugf("error pulling image %s: %v", img, err)
77+
continue
78+
}
79+
pulled = img
80+
break
81+
}
82+
if pulled == "" && err != nil {
6783
return nil, err
6884
}
6985
resp := &pb.PullImageResponse{
70-
ImageRef: image,
86+
ImageRef: pulled,
7187
}
7288
logrus.Debugf("PullImageResponse: %+v", resp)
7389
return resp, nil

server/image_remove.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,21 @@ func (s *Server) RemoveImage(ctx context.Context, req *pb.RemoveImageRequest) (*
1919
if image == "" {
2020
return nil, fmt.Errorf("no image specified")
2121
}
22-
err := s.StorageImageServer().RemoveImage(s.ImageContext(), image)
23-
if err != nil {
22+
var (
23+
images = s.StorageImageServer().ResolveNames(image)
24+
err error
25+
deleted bool
26+
)
27+
for _, img := range images {
28+
err = s.StorageImageServer().RemoveImage(s.ImageContext(), img)
29+
if err != nil {
30+
logrus.Debugf("error deleting image %s: %v", img, err)
31+
continue
32+
}
33+
deleted = true
34+
break
35+
}
36+
if !deleted && err != nil {
2437
return nil, err
2538
}
2639
resp := &pb.RemoveImageResponse{}

0 commit comments

Comments
 (0)