diff --git a/discovery/discovery.go b/discovery/discovery.go index b2dc6595..17fe77ca 100644 --- a/discovery/discovery.go +++ b/discovery/discovery.go @@ -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 @@ -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 @@ -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) diff --git a/discovery/discovery_test.go b/discovery/discovery_test.go index bb2ca254..ef7db7a4 100644 --- a/discovery/discovery_test.go +++ b/discovery/discovery_test.go @@ -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( @@ -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"}, diff --git a/discovery/testdata/meta06.html b/discovery/testdata/meta06.html index 0814f58b..487cd280 100644 --- a/discovery/testdata/meta06.html +++ b/discovery/testdata/meta06.html @@ -3,9 +3,19 @@ My app + - - + + + + + + + + + + + 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..a6f01a54 100644 --- a/spec/discovery.md +++ b/spec/discovery.md @@ -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 + + + +``` + +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.