/* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "context" "fmt" "net/http" "os" "path/filepath" "testing" "github.com/containerd/containerd/log/logtest" "github.com/containerd/containerd/remotes/docker" ) const allCaps = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush func TestDefaultHosts(t *testing.T) { ctx := logtest.WithT(context.Background(), t) resolve := ConfigureHosts(ctx, HostOptions{}) for _, tc := range []struct { host string expected []docker.RegistryHost }{ { host: "docker.io", expected: []docker.RegistryHost{ { Scheme: "https", Host: "registry-1.docker.io", Path: "/v2", Capabilities: allCaps, }, }, }, } { hosts, err := resolve(tc.host) if err != nil { t.Errorf("[%s] resolve failed: %v", tc.host, err) continue } if len(hosts) != len(tc.expected) { t.Errorf("[%s] unexpected number of hosts %d, expected %d", tc.host, len(hosts), len(tc.expected)) continue } for j := range hosts { if !compareRegistryHost(hosts[j], tc.expected[j]) { t.Errorf("[%s] [%d] unexpected host %v, expected %v", tc.host, j, hosts[j], tc.expected[j]) break } } } } func TestParseHostFile(t *testing.T) { const testtoml = ` server = "https://test-default.registry" ca = "/etc/path/default" [header] x-custom-1 = "custom header" [host."https://mirror.registry"] capabilities = ["pull"] ca = "/etc/certs/mirror.pem" skip_verify = false [host."https://mirror.registry".header] x-custom-2 = ["value1", "value2"] [host."https://mirror-bak.registry/us"] capabilities = ["pull"] skip_verify = true [host."http://mirror.registry"] capabilities = ["pull"] [host."https://test-1.registry"] capabilities = ["pull", "resolve", "push"] ca = ["/etc/certs/test-1-ca.pem", "/etc/certs/special.pem"] client = [["/etc/certs/client.cert", "/etc/certs/client.key"],["/etc/certs/client.pem", ""]] [host."https://test-2.registry"] client = "/etc/certs/client.pem" [host."https://test-3.registry"] client = ["/etc/certs/client-1.pem", "/etc/certs/client-2.pem"] [host."https://noncompliantmirror.registry/v2/namespaceprefix"] capabilities = ["pull"] override_path = true [host."https://noprefixnoncompliant.registry"] override_path = true ` var tb, fb = true, false expected := []hostConfig{ { scheme: "https", host: "mirror.registry", path: "/v2", capabilities: docker.HostCapabilityPull, caCerts: []string{filepath.FromSlash("/etc/certs/mirror.pem")}, skipVerify: &fb, header: http.Header{"x-custom-2": {"value1", "value2"}}, }, { scheme: "https", host: "mirror-bak.registry", path: "/us/v2", capabilities: docker.HostCapabilityPull, skipVerify: &tb, }, { scheme: "http", host: "mirror.registry", path: "/v2", capabilities: docker.HostCapabilityPull, }, { scheme: "https", host: "test-1.registry", path: "/v2", capabilities: allCaps, caCerts: []string{filepath.FromSlash("/etc/certs/test-1-ca.pem"), filepath.FromSlash("/etc/certs/special.pem")}, clientPairs: [][2]string{ {filepath.FromSlash("/etc/certs/client.cert"), filepath.FromSlash("/etc/certs/client.key")}, {filepath.FromSlash("/etc/certs/client.pem"), ""}, }, }, { scheme: "https", host: "test-2.registry", path: "/v2", capabilities: allCaps, clientPairs: [][2]string{ {filepath.FromSlash("/etc/certs/client.pem")}, }, }, { scheme: "https", host: "test-3.registry", path: "/v2", capabilities: allCaps, clientPairs: [][2]string{ {filepath.FromSlash("/etc/certs/client-1.pem")}, {filepath.FromSlash("/etc/certs/client-2.pem")}, }, }, { scheme: "https", host: "noncompliantmirror.registry", path: "/v2/namespaceprefix", capabilities: docker.HostCapabilityPull, }, { scheme: "https", host: "noprefixnoncompliant.registry", capabilities: allCaps, }, { scheme: "https", host: "test-default.registry", path: "/v2", capabilities: allCaps, caCerts: []string{filepath.FromSlash("/etc/path/default")}, header: http.Header{"x-custom-1": {"custom header"}}, }, } hosts, err := parseHostsFile("", []byte(testtoml)) if err != nil { t.Fatal(err) } defer func() { if t.Failed() { t.Log("HostConfigs...\nActual:\n" + printHostConfig(hosts) + "Expected:\n" + printHostConfig(expected)) } }() if len(hosts) != len(expected) { t.Fatalf("Unexpected number of hosts %d, expected %d", len(hosts), len(expected)) } for i := range hosts { if !compareHostConfig(hosts[i], expected[i]) { t.Fatalf("Mismatch at host %d", i) } } } func TestLoadCertFiles(t *testing.T) { dir := t.TempDir() type testCase struct { input hostConfig } cases := map[string]testCase{ "crt only": { input: hostConfig{host: "testing.io", caCerts: []string{filepath.Join(dir, "testing.io", "ca.crt")}}, }, "crt and cert pair": { input: hostConfig{ host: "testing.io", caCerts: []string{filepath.Join(dir, "testing.io", "ca.crt")}, clientPairs: [][2]string{ { filepath.Join(dir, "testing.io", "client.cert"), filepath.Join(dir, "testing.io", "client.key"), }, }, }, }, "cert pair only": { input: hostConfig{ host: "testing.io", clientPairs: [][2]string{ { filepath.Join(dir, "testing.io", "client.cert"), filepath.Join(dir, "testing.io", "client.key"), }, }, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { hostDir := filepath.Join(dir, tc.input.host) if err := os.MkdirAll(hostDir, 0700); err != nil { t.Fatal(err) } defer os.RemoveAll(hostDir) for _, f := range tc.input.caCerts { if err := os.WriteFile(f, testKey, 0600); err != nil { t.Fatal(err) } } for _, pair := range tc.input.clientPairs { if err := os.WriteFile(pair[0], testKey, 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(pair[1], testKey, 0600); err != nil { t.Fatal(err) } } configs, err := loadHostDir(context.Background(), hostDir) if err != nil { t.Fatal(err) } if len(configs) != 1 { t.Fatalf("\nexpected:\n%+v\ngot:\n%+v", tc.input, configs) } cfg := configs[0] cfg.host = tc.input.host if !compareHostConfig(cfg, tc.input) { t.Errorf("\nexpected:\n%+v:\n\ngot:\n%+v", tc.input, cfg) } }) } } func compareRegistryHost(j, k docker.RegistryHost) bool { if j.Scheme != k.Scheme { return false } if j.Host != k.Host { return false } if j.Path != k.Path { return false } if j.Capabilities != k.Capabilities { return false } // Not comparing TLS configs or authorizations return true } func compareHostConfig(j, k hostConfig) bool { if j.scheme != k.scheme { return false } if j.host != k.host { return false } if j.path != k.path { return false } if j.capabilities != k.capabilities { return false } if len(j.caCerts) != len(k.caCerts) { return false } for i := range j.caCerts { if j.caCerts[i] != k.caCerts[i] { return false } } if len(j.clientPairs) != len(k.clientPairs) { return false } for i := range j.clientPairs { if j.clientPairs[i][0] != k.clientPairs[i][0] { return false } if j.clientPairs[i][1] != k.clientPairs[i][1] { return false } } if j.skipVerify != nil && k.skipVerify != nil { if *j.skipVerify != *k.skipVerify { return false } } else if j.skipVerify != nil || k.skipVerify != nil { return false } if len(j.header) != len(k.header) { return false } for key := range j.header { if len(j.header[key]) != len(k.header[key]) { return false } for i := range j.header[key] { if j.header[key][i] != k.header[key][i] { return false } } } return true } func printHostConfig(hc []hostConfig) string { b := bytes.NewBuffer(nil) for i := range hc { fmt.Fprintf(b, "\t[%d]\tscheme: %q\n", i, hc[i].scheme) fmt.Fprintf(b, "\t\thost: %q\n", hc[i].host) fmt.Fprintf(b, "\t\tpath: %q\n", hc[i].path) fmt.Fprintf(b, "\t\tcaps: %03b\n", hc[i].capabilities) fmt.Fprintf(b, "\t\tca: %#v\n", hc[i].caCerts) fmt.Fprintf(b, "\t\tclients: %#v\n", hc[i].clientPairs) if hc[i].skipVerify == nil { fmt.Fprintf(b, "\t\tskip-verify: %v\n", hc[i].skipVerify) } else { fmt.Fprintf(b, "\t\tskip-verify: %t\n", *hc[i].skipVerify) } fmt.Fprintf(b, "\t\theader: %#v\n", hc[i].header) } return b.String() } var ( testKey = []byte(`-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDa+zvPgFXwra4S 0DzEWRgZHxVTDG1sJsnN/jOaHCNpRyABGVW5kdei9WFWv3dpiELI+guQMjdUL++w M68bs6cXKW+1nW6u5uWuGwklOwkoKoeHkkn/vHef7ybk+5qdk6AYY0DKQsrBBOvj f0WAnG+1xi8VIOEBmce0/47MexOiuILVkjokgdmDCOc8ShkT6/EJTCsI1wDew/4G 9IiRzw2xSM0ZATAtEC3HEBRLJGWZQtuKlLCuzJ+erOWUcg2cjnSgR3PmaAXE//5g SoeqEbtTo1satf9AR4VvreIAI8m0eyo8ABMLTkZovEFcUUHetL63hdqItjCeRfrQ zK4LMRFbAgMBAAECggEBAJtP6UHo0gtcA8SQMSlJz4+xvhwjClDUyfjyPIMnRe5b ZdWhtG1jhT+tLhaqwfT1kfidcCobk6aAQU4FukK5jt8cooB7Yo9mcKylvDzNvFbi ozGCjj113JpwsnNiCG2O0NO7Qa6y5L810GCQWik3yvtvzuD7atsJyN0VDKD3Ahw7 1X8z76grZFlhVMCTAA3vAJ2y2p3sd+TGC/PIhnsvChwxEorGCnMj93mBaUI7zZRY EZhlk4ZvC9sUvlVUuYC+wAHjasgN9s3AzsOBSx+Xt3NaXQHzhL0mVo/vu/pjjFBs WBLR1PBoIfveTJPOp+Hrr4cuCK0NuX9sWlWPYLl5A2ECgYEA5fq3n4PhbJ2BuTS5 AVgOmjRpk1eogb6aSY+cx7Mr++ADF9EYXc5tgKoUsDeeiiyK2lv6IKavoTWT1kdd shiclyEzp2CxG5GtbC/g2XHiBLepgo1fjfev3btCmIeGVBjglOx4F3gEsRygrAID zcz94m2I+uqLT8hvWnccIqScglkCgYEA88H2ji4Nvx6TmqCLcER0vNDVoxxDfgGb iohvenD2jmmdTnezTddsgECAI8L0BPNS/0vBCduTjs5BqhKbIfQvuK5CANMUcxuQ twWH8kPvTYJVgsmWP6sSXSz3PohWC5EA9xACExGtyN6d7sLUCV0SBhjlcgMvGuDM lP6NjyyWctMCgYBKdfGr+QQsqZaNw48+6ybXMK8aIKCTWYYU2SW21sEf7PizZmTQ Qnzb0rWeFHQFYsSWTH9gwPdOZ8107GheuG9C02IpCDpvpawTwjC31pKKWnjMpz9P 9OkBDpdSUVbhtahJL4L2fkpumck/x+s5X+y3uiVGsFfovgmnrbbzVH7ECQKBgQCC MYs7DaYR+obkA/P2FtozL2esIyB5YOpu58iDIWrPTeHTU2PVo8Y0Cj9m2m3zZvNh oFiOp1T85XV1HVL2o7IJdimSvyshAAwfdTjTUS2zvHVn0bwKbZj1Y1r7b15l9yEI 1OgGv16O9zhrmmweRDOoRgvnBYRXWtJqkjuRyULiOQKBgQC/lSYigV32Eb8Eg1pv 7OcPWv4qV4880lRE0MXuQ4VFa4+pqvdziYFYQD4jDYJ4IX9l//bsobL0j7z0P0Gk wDFti9bRwRoO1ntqoA8n2pDLlLRGl0dyjB6fHzp27oqtyf1HRlHiow7Gqx5b5JOk tycYKwA3DuaSyqPe6MthLneq8w== -----END PRIVATE KEY----- `) )