From d2793c7a8ced9de6bdfc3f26de9dac70b23a151d Mon Sep 17 00:00:00 2001 From: Simone Gotti Date: Tue, 22 Mar 2016 17:37:51 +0100 Subject: [PATCH] discovery: match only templates that satisfy all required labels 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 ``` 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 ``` With this tags I want to implement a "latest" and also "noarch" pattern. If, on discovery, I ask only for the _name_ and _version_ labels the second template will be rendered. So I achieved noarch pattern. But, unfortunately, also the last template 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 template that fully satisfy the required labels. It can probably BREAK some of the current meta tags implementations. It also documents this behavior. --- discovery/discovery.go | 18 ++++++++++------ discovery/discovery_test.go | 39 +++++++++++++++++++++++++++++----- discovery/testdata/meta07.html | 13 ++++++++++++ spec/discovery.md | 11 ++++++++++ 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 discovery/testdata/meta07.html diff --git a/discovery/discovery.go b/discovery/discovery.go index b2dc6595..844d2749 100644 --- a/discovery/discovery.go +++ b/discovery/discovery.go @@ -111,6 +111,9 @@ func renderTemplate(tpl string, kvs ...string) (string, bool) { for i := 0; i < len(kvs); i += 2 { k := kvs[i] v := kvs[i+1] + if k != "{name}" && !strings.Contains(tpl, k) { + return tpl, false + } tpl = strings.Replace(tpl, k, v, -1) } return tpl, !templateExpression.MatchString(tpl) @@ -118,9 +121,11 @@ func renderTemplate(tpl string, kvs ...string) (string, bool) { 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 @@ -151,13 +156,14 @@ 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") + // Ignore not handled variables as only {ext} has been rendered + aci, _ := renderTemplate(m.uri, "{ext}", "aci") + asc, _ := renderTemplate(m.uri, "{ext}", "aci.asc") + aci, ok := renderTemplate(aci, tplVars...) if !ok { continue } - aci, ok := renderTemplate(uri, "{ext}", "aci") + asc, ok = renderTemplate(asc, tplVars...) if !ok { continue } diff --git a/discovery/discovery_test.go b/discovery/discovery_test.go index bb2ca254..4111fce0 100644 --- a/discovery/discovery_test.go +++ b/discovery/discovery_test.go @@ -460,7 +460,9 @@ func TestDiscoverEndpoints(t *testing.T) { []string{"https://example.com/pubkeys.gpg"}, nil, }, - // Test multiple ACIEndpoints. + // Test multiple endpoints. + // Should render two endpoint, since '' + // doesn't contain all the required labels { &mockHTTPDoer{ doer: fakeHTTPGet( @@ -487,10 +489,6 @@ func TestDiscoverEndpoints(t *testing.T) { ACI: "https://storage.example.com/example.com/myapp-1.0.0-linux-amd64.aci", 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", - }, 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", @@ -499,6 +497,37 @@ func TestDiscoverEndpoints(t *testing.T) { []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"}, + nil, + }, // Test with an auth header { diff --git a/discovery/testdata/meta07.html b/discovery/testdata/meta07.html new file mode 100644 index 00000000..a9b0b800 --- /dev/null +++ b/discovery/testdata/meta07.html @@ -0,0 +1,13 @@ + + + + + My app + + + + + +

My App

+ + diff --git a/spec/discovery.md b/spec/discovery.md index b5033ef3..1a671c58 100644 --- a/spec/discovery.md +++ b/spec/discovery.md @@ -59,6 +59,17 @@ 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 templates that can be fully substituted and that satisfy all the required labels. + +For example given these meta tags: +```html + + +``` + +If the client requires the labels _name_, _version_, _os_, _arch_ only the first template will be rendered since the second doesn't satisfy the _version_ label. +If the client requires the labels _name_, _os_, _arch_ only the second 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.