Skip to content
This repository has been archived by the owner on Aug 14, 2020. It is now read-only.

Commit

Permalink
discovery: return only best matching templates.
Browse files Browse the repository at this point in the history
Actually the spec doesn't clarify when an endpoint template should be accepted or
not. Now, the appc/spec implementation returns only endpoints that can
be fully rendered. This means that it will also accept a template if it
doesn't contain some of the required discovery labels.

This looks good, but, trying to implement some meta discovery ideas it
will bring to unwanted endpoints.

Example 1:
Suppose I want to implement the "latest" pattern behavior:

```html
<!-- ACIs with specific version -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
<!-- Latest ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-latest-{os}-{arch}.{ext}">
```

If, on discovery, I ask for the _name_, _os_ and _arch_
labels only the second template will be rendered (since the first cannot
be fully rendered due to missing _version_ label). So I achieved latest
pattern.

But if I'm asking for a specific _version_ both templates will be
rendered.

Example 2:
As an extension of the first example, suppose I want to create a global
meta discovery for all my images on example.com. So on the root of my
server I'll put some meta tags using example.com as prefix:

```html
<!-- ACIs with specific version -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
<!-- noarch ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}-noarch.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-{version}-noarch.{ext}">
<!-- Latest ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-latest-{os}-{arch}.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-latest-{os}-{arch}.{ext}">
<!-- Latest noarch ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-latest-noarch.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-latest-noarch.{ext}">
```

With this tags I want to implement global "latest" and "noarch" patterns
and also return multiple mirrors.

If, on discovery, I ask only for the _name_ and _version_ labels the
template 3 and 4 will be rendered. So I achieved a versioned noarch pattern.

But, unfortunately, also the last two templates will be rendered.

And, as the first example, if I'm asking for a specific _name_,
_version_, _os_ and _arch_ ALL the templates will be rendered.

Since the spec says:
```
Note that multiple `ac-discovery` tags MAY be returned for a given
prefix-match [snip] In this case, the client implementation MAY choose
which to use at its own discretion.
```

If an implementation chooses the second it can download the wrong image version.

This patch makes template matching stricter choosing only best matching templates.
Best matching templates are the one where all the templates labels can
be substituted and with the highest number of substituted labels.

With this we can obtain various patterns like latest and noarch and also
keeping the ability to return multiple mirror urls. See also the tests
added in this commit.

It also documents this behavior.

It should not BREAK many of the current meta tags implementation (it
depends on how the client uses the returned endpoints, for example rkt,
currently, only uses the first one)
  • Loading branch information
sgotti committed Apr 12, 2016
1 parent 80ab01d commit 8d0262c
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 14 deletions.
29 changes: 21 additions & 8 deletions discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,26 @@ func extractACMeta(r io.Reader) []acMeta {
}
}

func renderTemplate(tpl string, kvs ...string) (string, bool) {
func renderTemplate(tpl string, kvs ...string) (string, int, bool) {
count := 0
for i := 0; i < len(kvs); i += 2 {
k := kvs[i]
v := kvs[i+1]
if k != "{name}" && strings.Contains(tpl, k) {
count++
}
tpl = strings.Replace(tpl, k, v, -1)
}
return tpl, !templateExpression.MatchString(tpl)
return tpl, count, !templateExpression.MatchString(tpl)
}

func createTemplateVars(app App) []string {
tplVars := []string{"{name}", app.Name.String()}
// If a label is called "name", it will be ignored as it appears after
// in the slice
// Ignore labels called "name"
for n, v := range app.Labels {
if n == "name" {
continue
}
tplVars = append(tplVars, fmt.Sprintf("{%s}", n), v)
}
return tplVars
Expand All @@ -144,6 +150,7 @@ func doDiscover(pre string, hostHeaders map[string]http.Header, app App, insecur

dd := &discoveryData{}

bestCount := 0
for _, m := range meta {
if !strings.HasPrefix(app.Name.String(), m.prefix) {
continue
Expand All @@ -152,16 +159,22 @@ func doDiscover(pre string, hostHeaders map[string]http.Header, app App, insecur
switch m.name {
case "ac-discovery":
// Ignore not handled variables as {ext} isn't already rendered.
uri, _ := renderTemplate(m.uri, tplVars...)
asc, ok := renderTemplate(uri, "{ext}", "aci.asc")
uri, count, _ := renderTemplate(m.uri, tplVars...)
asc, _, ok := renderTemplate(uri, "{ext}", "aci.asc")
if !ok {
continue
}
aci, ok := renderTemplate(uri, "{ext}", "aci")
aci, _, ok := renderTemplate(uri, "{ext}", "aci")
if !ok {
continue
}
dd.ACIEndpoints = append(dd.ACIEndpoints, ACIEndpoint{ACI: aci, ASC: asc})

if count > bestCount {
dd.ACIEndpoints = ACIEndpoints{ACIEndpoint{ACI: aci, ASC: asc}}
bestCount = count
} else if count == bestCount {
dd.ACIEndpoints = append(dd.ACIEndpoints, ACIEndpoint{ACI: aci, ASC: asc})
}

case "ac-discovery-pubkeys":
dd.PublicKeys = append(dd.PublicKeys, m.uri)
Expand Down
141 changes: 137 additions & 4 deletions discovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ func TestDiscoverEndpoints(t *testing.T) {
nil,
},
// Test multiple ACIEndpoints.
// Should render the first two endpoint, since the others match less labels
{
&mockHTTPDoer{
doer: fakeHTTPGet(
Expand Down Expand Up @@ -488,12 +489,144 @@ func TestDiscoverEndpoints(t *testing.T) {
ASC: "https://storage.example.com/example.com/myapp-1.0.0-linux-amd64.aci.asc",
},
ACIEndpoint{
ACI: "https://storage.example.com/example.com/myapp-1.0.0.aci",
ASC: "https://storage.example.com/example.com/myapp-1.0.0.aci.asc",
ACI: "https://mirror.storage.example.com/example.com/myapp-1.0.0-linux-amd64.aci",
ASC: "https://mirror.storage.example.com/example.com/myapp-1.0.0-linux-amd64.aci.asc",
},
},
[]string{"https://example.com/pubkeys.gpg"},
nil,
},
// Test multiple ACIEndpoints.
// Should render endpoint 3 and 4, since the 1 and 2 doesn't fully render and the others match less labels
// Example noarch versioned matching
{
&mockHTTPDoer{
doer: fakeHTTPGet(
[]meta{
{"/myapp",
"meta06.html",
},
},
nil,
),
},
true,
true,
App{
Name: "example.com/myapp",
Labels: map[types.ACIdentifier]string{
"version": "1.0.0",
},
},
[]ACIEndpoint{
ACIEndpoint{
ACI: "https://storage.example.com/example.com/myapp-1.0.0-noarch.aci",
ASC: "https://storage.example.com/example.com/myapp-1.0.0-noarch.aci.asc",
},
ACIEndpoint{
ACI: "https://mirror.storage.example.com/example.com/myapp-1.0.0-noarch.aci",
ASC: "https://mirror.storage.example.com/example.com/myapp-1.0.0-noarch.aci.asc",
},
},
[]string{"https://example.com/pubkeys.gpg"},
nil,
},
// Test multiple ACIEndpoints.
// Should render endpoint 5 and 6, since the 1, 2, 3, 4 doesn't fully render and the others match less labels
// Example latest matching
{
&mockHTTPDoer{
doer: fakeHTTPGet(
[]meta{
{"/myapp",
"meta06.html",
},
},
nil,
),
},
true,
true,
App{
Name: "example.com/myapp",
Labels: map[types.ACIdentifier]string{
"os": "linux",
"arch": "amd64",
},
},
[]ACIEndpoint{
ACIEndpoint{
ACI: "https://storage.example.com/example.com/myapp-latest-linux-amd64.aci",
ASC: "https://storage.example.com/example.com/myapp-latest-linux-amd64.aci.asc",
},
ACIEndpoint{
ACI: "https://mirror.storage.example.com/example.com/myapp-latest-linux-amd64.aci",
ASC: "https://mirror.storage.example.com/example.com/myapp-latest-linux-amd64.aci.asc",
},
},
[]string{"https://example.com/pubkeys.gpg"},
nil,
},
// Test multiple ACIEndpoints.
// Should render endpoint 7 and 8, since the others don't fully render.
// Example noarch latest matching
{
&mockHTTPDoer{
doer: fakeHTTPGet(
[]meta{
{"/myapp",
"meta06.html",
},
},
nil,
),
},
true,
true,
App{
Name: "example.com/myapp",
Labels: map[types.ACIdentifier]string{},
},
[]ACIEndpoint{
ACIEndpoint{
ACI: "https://storage.example.com/example.com/myapp-latest-noarch.aci",
ASC: "https://storage.example.com/example.com/myapp-latest-noarch.aci.asc",
},
ACIEndpoint{
ACI: "hdfs://storage.example.com/example.com/myapp-1.0.0-linux-amd64.aci",
ASC: "hdfs://storage.example.com/example.com/myapp-1.0.0-linux-amd64.aci.asc",
ACI: "https://mirror.storage.example.com/example.com/myapp-latest-noarch.aci",
ASC: "https://mirror.storage.example.com/example.com/myapp-latest-noarch.aci.asc",
},
},
[]string{"https://example.com/pubkeys.gpg"},
nil,
},

// Test a discovery string that has an hardcoded app name instead of using the provided {name}
{
&mockHTTPDoer{
doer: fakeHTTPGet(
[]meta{
{"/myapp",
"meta07.html",
},
},
nil,
),
},
true,
true,
App{
Name: "example.com/myapp",
Labels: map[types.ACIdentifier]string{
"version": "1.0.0",
"os": "linux",
"arch": "amd64",
},
},
[]ACIEndpoint{
ACIEndpoint{
ACI: "https://storage.example.com/myapp-1.0.0-linux-amd64.aci",
ASC: "https://storage.example.com/myapp-1.0.0-linux-amd64.aci.asc",
},
},
[]string{"https://example.com/pubkeys.gpg"},
Expand Down
14 changes: 12 additions & 2 deletions discovery/testdata/meta06.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@
<html>
<head>
<title>My app</title>
<!-- ACIs with specific version -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}.{ext}">
<meta name="ac-discovery" content="example.com hdfs://storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
<!-- noarch ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}-noarch.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-{version}-noarch.{ext}">
<!-- Latest ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-latest-{os}-{arch}.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-latest-{os}-{arch}.{ext}">
<!-- Latest noarch ACIs -->
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-latest-noarch.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{name}-latest-noarch.{ext}">

<meta name="ac-discovery-pubkeys" content="example.com https://example.com/pubkeys.gpg">
</head>

Expand Down
13 changes: 13 additions & 0 deletions discovery/testdata/meta07.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>

<html>
<head>
<title>My app</title>
<meta name="ac-discovery" content="example.com/myapp https://storage.example.com/myapp-{version}-{os}-{arch}.{ext}">
<meta name="ac-discovery-pubkeys" content="example.com https://example.com/pubkeys.gpg">
</head>

<body>
<h1>My App</h1>
</body>
</html>
12 changes: 12 additions & 0 deletions spec/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ curl $(echo "$urltmpl" | sed -e "s/{name}/$name/" -e "s/{version}/$version/" -e

where _name_, _version_, _os_, and _arch_ are set to their respective values for the image, and _ext_ is either `aci` or `aci.asc` for retrieving an App Container Image or signature respectively.

The client MUST accept only best matching templates. Best matching templates are the one where all the templates labels can be substituted and with the highest number of substituted labels.

For example given these meta tags:
```html
<meta name="ac-discovery" content="example.com https://storage.example.com/{os}/{arch}/{name}-{version}.{ext}">
<meta name="ac-discovery" content="example.com https://mirror.storage.example.com/{os}/{arch}/{name}-{version}.{ext}">
<meta name="ac-discovery" content="example.com https://storage.example.com/{os}/{arch}/{name}-latest.{ext}">
```

If the client requires the image name _name_ and labels _version_, _os_, _arch_ only the first two templates will be rendered since they match 3 labels (while the the last matches only 2 labels since it doesn't match the _version_ label).
If the client requires the image name _name_ and labels _os_, _arch_ only the last template will be rendered since in the first template '{version}' cannot be substituted.

Note that multiple `ac-discovery` tags MAY be returned for a given prefix-match (for example, with different scheme names representing different transport mechanisms).
In this case, the client implementation MAY choose which to use at its own discretion.
Public discovery implementations SHOULD always provide at least one HTTPS URL template.
Expand Down

0 comments on commit 8d0262c

Please sign in to comment.