func TestFetchImageCache(t *testing.T) { dir, err := ioutil.TempDir("", "fetch-image-cache") if err != nil { t.Fatalf("error creating tempdir: %v", err) } defer os.RemoveAll(dir) s, err := imagestore.NewStore(dir) if err != nil { t.Fatalf("unexpected error %v", err) } defer s.Dump(false) ks, ksPath, err := keystore.NewTestKeystore() if err != nil { t.Errorf("unexpected error %v", err) } defer os.RemoveAll(ksPath) key := keystoretest.KeyMap["example.com/app"] if _, err := ks.StoreTrustedKeyPrefix("example.com/app", bytes.NewBufferString(key.ArmoredPublicKey)); err != nil { t.Fatalf("unexpected error %v", err) } a, err := aci.NewBasicACI(dir, "example.com/app") defer a.Close() if err != nil { t.Fatalf("unexpected error %v", err) } // Rewind the ACI if _, err := a.Seek(0, 0); err != nil { t.Fatalf("unexpected error %v", err) } asc, err := aci.NewDetachedSignature(key.ArmoredPrivateKey, a) if err != nil { t.Fatalf("unexpected error %v", err) } // Rewind the ACI if _, err := a.Seek(0, 0); err != nil { t.Fatalf("unexpected error %v", err) } aciBody, err := ioutil.ReadAll(a) if err != nil { t.Fatalf("unexpected error: %v", err) } ascBody, err := ioutil.ReadAll(asc) if err != nil { t.Fatalf("unexpected error: %v", err) } nocacheServer := &cachingServerHandler{ aciBody: aciBody, ascBody: ascBody, etag: "", maxAge: 0, t: t, } etagServer := &cachingServerHandler{ aciBody: aciBody, ascBody: ascBody, etag: "123456789", maxAge: 0, t: t, } maxAgeServer := &cachingServerHandler{ aciBody: aciBody, ascBody: ascBody, etag: "", maxAge: 10, t: t, } etagMaxAgeServer := &cachingServerHandler{ aciBody: aciBody, ascBody: ascBody, etag: "123456789", maxAge: 10, t: t, } nocacheTS := httptest.NewServer(nocacheServer) defer nocacheTS.Close() etagTS := httptest.NewServer(etagServer) defer etagTS.Close() maxAgeTS := httptest.NewServer(maxAgeServer) defer maxAgeTS.Close() etagMaxAgeTS := httptest.NewServer(etagMaxAgeServer) defer etagMaxAgeTS.Close() type testData struct { URL string etag string cacheMaxAge int shouldUseCached bool } tests := []testData{ {nocacheTS.URL, "", 0, false}, {etagTS.URL, "123456789", 0, true}, {maxAgeTS.URL, "", 10, true}, {etagMaxAgeTS.URL, "123456789", 10, true}, } testFn := func(tt testData, useRedirect bool) { aciURL := fmt.Sprintf("%s/app.aci", tt.URL) if useRedirect { redirectingTS := httptest.NewServer(&redirectingServerHandler{destServer: tt.URL}) defer redirectingTS.Close() aciURL = fmt.Sprintf("%s/app.aci", redirectingTS.URL) } ft := &image.Fetcher{ S: s, Ks: ks, InsecureFlags: secureFlags, // Skip local store NoStore: true, } u, err := url.Parse(aciURL) if err != nil { t.Fatalf("unexpected error %v", err) } d, err := dist.NewACIArchiveFromTransportURL(u) if err != nil { t.Fatalf("unexpected error %v", err) } _, err = ft.FetchImage(d, u.String(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } rem, err := s.GetRemote(aciURL) if err != nil { t.Fatalf("Error getting remote info: %v\n", err) } if rem.ETag != tt.etag { t.Errorf("expected remote to have a ETag header argument") } if rem.CacheMaxAge != tt.cacheMaxAge { t.Errorf("expected max-age header argument to be %q", tt.cacheMaxAge) } downloadTime := rem.DownloadTime _, err = ft.FetchImage(d, u.String(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } rem, err = s.GetRemote(aciURL) if err != nil { t.Fatalf("Error getting remote info: %v\n", err) } if rem.ETag != tt.etag { t.Errorf("expected remote to have a ETag header argument") } if rem.CacheMaxAge != tt.cacheMaxAge { t.Errorf("expected max-age header argument to be %q", tt.cacheMaxAge) } if tt.shouldUseCached { if downloadTime != rem.DownloadTime { t.Errorf("expected current download time to be the same as the previous one (no download) but they differ") } } else { if downloadTime == rem.DownloadTime { t.Errorf("expected current download time to be different from the previous one (new image download) but they are the same") } } if err := s.RemoveACI(rem.BlobKey); err != nil { t.Fatalf("unexpected error: %v", err) } } // repeat the tests with and without a redirecting server for i := 0; i <= 1; i++ { useRedirect := false if i == 1 { useRedirect = true } for _, tt := range tests { testFn(tt, useRedirect) } } }
func TestDownloading(t *testing.T) { dir, err := ioutil.TempDir("", "download-image") if err != nil { t.Fatalf("error creating tempdir: %v", err) } defer os.RemoveAll(dir) imj := `{ "acKind": "ImageManifest", "acVersion": "0.7.4", "name": "example.com/test01" }` entries := []*aci.ACIEntry{ // An empty file { Contents: "hello", Header: &tar.Header{ Name: "rootfs/file01.txt", Size: 5, }, }, } aci, err := aci.NewACI(dir, imj, entries) if err != nil { t.Fatalf("error creating test tar: %v", err) } // Rewind the ACI if _, err := aci.Seek(0, 0); err != nil { t.Fatalf("unexpected error: %v", err) } body, err := ioutil.ReadAll(aci) if err != nil { t.Fatalf("unexpected error: %v", err) } noauthServer := &serverHandler{ body: body, t: t, auth: "none", } basicServer := &serverHandler{ body: body, t: t, auth: "basic", } oauthServer := &serverHandler{ body: body, t: t, auth: "bearer", } denyServer := &serverHandler{ body: body, t: t, auth: "deny", } noAuthTS := httptest.NewTLSServer(noauthServer) defer noAuthTS.Close() basicTS := httptest.NewTLSServer(basicServer) defer basicTS.Close() oauthTS := httptest.NewTLSServer(oauthServer) defer oauthTS.Close() denyAuthTS := httptest.NewServer(denyServer) noAuth := http.Header{} // YmFyOmJheg== is base64(bar:baz) basicAuth := http.Header{"Authorization": {"Basic YmFyOmJheg=="}} bearerAuth := http.Header{"Authorization": {"Bearer sometoken"}} urlToName := map[string]string{ noAuthTS.URL: "no auth", basicTS.URL: "basic", oauthTS.URL: "oauth", denyAuthTS.URL: "deny auth", } tests := []struct { aciURL string remoteExists bool options http.Header authFail bool }{ {noAuthTS.URL, false, noAuth, false}, {noAuthTS.URL, true, noAuth, false}, {noAuthTS.URL, true, bearerAuth, false}, {noAuthTS.URL, true, basicAuth, false}, {basicTS.URL, false, noAuth, true}, {basicTS.URL, false, bearerAuth, true}, {basicTS.URL, false, basicAuth, false}, {oauthTS.URL, false, noAuth, true}, {oauthTS.URL, false, basicAuth, true}, {oauthTS.URL, false, bearerAuth, false}, {denyAuthTS.URL, false, basicAuth, false}, {denyAuthTS.URL, true, bearerAuth, false}, {denyAuthTS.URL, true, noAuth, false}, } s, err := imagestore.NewStore(dir) if err != nil { t.Fatalf("unexpected error %v", err) } for _, tt := range tests { _, err := s.GetRemote(tt.aciURL) if err != nil { if err != imagestore.ErrRemoteNotFound { t.Fatalf("unexpected err: %v", err) } if tt.remoteExists { t.Fatalf("should've found the remote, got %v", err) } } else if !tt.remoteExists { t.Fatalf("should've gotten a remote not found error") } parsed, err := url.Parse(tt.aciURL) if err != nil { panic(fmt.Sprintf("Invalid url from test server: %s", tt.aciURL)) } headers := map[string]config.Headerer{ parsed.Host: &testHeaderer{tt.options}, } ft := &image.Fetcher{ S: s, Headers: headers, InsecureFlags: insecureFlags, } u, err := url.Parse(tt.aciURL) if err != nil { t.Fatalf("unexpected error %v", err) } d, err := dist.NewACIArchiveFromTransportURL(u) if err != nil { t.Fatalf("unexpected error %v", err) } _, err = ft.FetchImage(d, tt.aciURL, "") if err != nil && !tt.authFail { t.Fatalf("expected download to succeed, it failed: %v (server: %q, headers: `%v`)", err, urlToName[tt.aciURL], tt.options) } if err == nil && tt.authFail { t.Fatalf("expected download to fail, it succeeded (server: %q, headers: `%v`)", urlToName[tt.aciURL], tt.options) } } s.Dump(false) }
func TestFetchImage(t *testing.T) { dir, err := ioutil.TempDir("", "fetch-image") if err != nil { t.Fatalf("error creating tempdir: %v", err) } defer os.RemoveAll(dir) s, err := imagestore.NewStore(dir) if err != nil { t.Fatalf("unexpected error %v", err) } defer s.Dump(false) ks, ksPath, err := keystore.NewTestKeystore() if err != nil { t.Errorf("unexpected error %v", err) } defer os.RemoveAll(ksPath) key := keystoretest.KeyMap["example.com/app"] if _, err := ks.StoreTrustedKeyPrefix("example.com/app", bytes.NewBufferString(key.ArmoredPublicKey)); err != nil { t.Fatalf("unexpected error %v", err) } a, err := aci.NewBasicACI(dir, "example.com/app") defer a.Close() if err != nil { t.Fatalf("unexpected error %v", err) } // Rewind the ACI if _, err := a.Seek(0, 0); err != nil { t.Fatalf("unexpected error %v", err) } asc, err := aci.NewDetachedSignature(key.ArmoredPrivateKey, a) if err != nil { t.Fatalf("unexpected error %v", err) } // Rewind the ACI. if _, err := a.Seek(0, 0); err != nil { t.Fatalf("unexpected error %v", err) } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch filepath.Ext(r.URL.Path) { case ".aci": io.Copy(w, a) return case ".asc": io.Copy(w, asc) return default: t.Fatalf("unknown extension %v", r.URL.Path) } })) defer ts.Close() ft := &image.Fetcher{ S: s, Ks: ks, InsecureFlags: secureFlags, } u, err := url.Parse(fmt.Sprintf("%s/app.aci", ts.URL)) if err != nil { t.Fatalf("unexpected error %v", err) } d, err := dist.NewACIArchiveFromTransportURL(u) if err != nil { t.Fatalf("unexpected error %v", err) } _, err = ft.FetchImage(d, u.String(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } }
// DistFromImageString return the distribution for the given input image string func DistFromImageString(is string) (dist.Distribution, error) { u, err := url.Parse(is) if err != nil { return nil, errwrap.Wrap(fmt.Errorf("failed to parse image url %q", is), err) } // Convert user friendly image string names to internal distribution URIs // file:///full/path/to/aci/file.aci -> archive:aci:file%3A%2F%2F%2Ffull%2Fpath%2Fto%2Faci%2Ffile.aci switch u.Scheme { case "": // no scheme given, hence it is an appc image name or path appImageType := guessAppcOrPath(is, []string{schema.ACIExtension}) switch appImageType { case imageStringName: app, err := discovery.NewAppFromString(is) if err != nil { return nil, fmt.Errorf("invalid appc image string %q: %v", is, err) } return dist.NewAppcFromApp(app), nil case imageStringPath: absPath, err := filepath.Abs(is) if err != nil { return nil, errwrap.Wrap(fmt.Errorf("failed to get an absolute path for %q", is), err) } is = "file://" + absPath // given a file:// image string, call this function again to return an ACI distribution return DistFromImageString(is) default: return nil, fmt.Errorf("invalid image string type %q", appImageType) } case "file", "http", "https": // An ACI archive with any transport type (file, http, s3 etc...) and final aci extension if filepath.Ext(u.Path) == schema.ACIExtension { dist, err := dist.NewACIArchiveFromTransportURL(u) if err != nil { return nil, fmt.Errorf("archive distribution creation error: %v", err) } return dist, nil } case "docker": // Accept both docker: and docker:// uri dockerStr := is if strings.HasPrefix(dockerStr, "docker://") { dockerStr = strings.TrimPrefix(dockerStr, "docker://") } else if strings.HasPrefix(dockerStr, "docker:") { dockerStr = strings.TrimPrefix(dockerStr, "docker:") } dist, err := dist.NewDockerFromString(dockerStr) if err != nil { return nil, fmt.Errorf("docker distribution creation error: %v", err) } return dist, nil case dist.Scheme: // cimd return dist.Parse(is) default: // any other scheme is a an appc image name, i.e. "my-app:v1.0" app, err := discovery.NewAppFromString(is) if err != nil { return nil, fmt.Errorf("invalid appc image string %q: %v", is, err) } return dist.NewAppcFromApp(app), nil } return nil, fmt.Errorf("invalid image string %q", is) }