diff --git a/client/api.go b/client/api.go index 8df2cbd..b4d64d0 100644 --- a/client/api.go +++ b/client/api.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -138,10 +138,10 @@ func (c *Client) setTags(ctx context.Context, containerID, imageID string, tags } for _, tag := range tags { - c.Logger.Logf("Setting tag %s", tag) + c.logger.Logf("Setting tag %s", tag) if _, ok := existingTags[tag]; ok { - c.Logger.Logf("%s replaces an existing tag", tag) + c.logger.Logf("%s replaces an existing tag", tag) } imgTag := ImageTag{ @@ -159,12 +159,12 @@ func (c *Client) setTags(ctx context.Context, containerID, imageID string, tags // getTags returns a tag map for the specified containerID func (c *Client) getTags(ctx context.Context, containerID string) (TagMap, error) { url := fmt.Sprintf("v1/tags/%s", containerID) - c.Logger.Logf("getTags calling %s", url) + c.logger.Logf("getTags calling %s", url) req, err := c.newRequest(ctx, http.MethodGet, url, "", nil) if err != nil { return nil, fmt.Errorf("error creating request to server:\n\t%v", err) } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("error making request to server:\n\t%v", err) } @@ -187,7 +187,7 @@ func (c *Client) getTags(ctx context.Context, containerID string) (TagMap, error // setTag sets tag on specified containerID func (c *Client) setTag(ctx context.Context, containerID string, t ImageTag) error { url := "v1/tags/" + containerID - c.Logger.Logf("setTag calling %s", url) + c.logger.Logf("setTag calling %s", url) s, err := json.Marshal(t) if err != nil { return fmt.Errorf("error encoding object to JSON:\n\t%v", err) @@ -196,7 +196,7 @@ func (c *Client) setTag(ctx context.Context, containerID string, t ImageTag) err if err != nil { return fmt.Errorf("error creating POST request:\n\t%v", err) } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("error making request to server:\n\t%v", err) } @@ -220,10 +220,10 @@ func (c *Client) setTagsV2(ctx context.Context, containerID, arch string, imageI } for _, tag := range tags { - c.Logger.Logf("Setting tag %s", tag) + c.logger.Logf("Setting tag %s", tag) if _, ok := existingTags[arch][tag]; ok { - c.Logger.Logf("%s replaces an existing tag for arch %s", tag, arch) + c.logger.Logf("%s replaces an existing tag for arch %s", tag, arch) } imgTag := ArchImageTag{ @@ -242,12 +242,12 @@ func (c *Client) setTagsV2(ctx context.Context, containerID, arch string, imageI // getTagsV2 returns a arch->tag map for the specified containerID func (c *Client) getTagsV2(ctx context.Context, containerID string) (ArchTagMap, error) { url := fmt.Sprintf("v2/tags/%s", containerID) - c.Logger.Logf("getTagsV2 calling %s", url) + c.logger.Logf("getTagsV2 calling %s", url) req, err := c.newRequest(ctx, http.MethodGet, url, "", nil) if err != nil { return nil, fmt.Errorf("error creating request to server:\n\t%v", err) } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("error making request to server:\n\t%v", err) } @@ -270,7 +270,7 @@ func (c *Client) getTagsV2(ctx context.Context, containerID string) (ArchTagMap, // setTag sets an arch->tag on specified containerID func (c *Client) setTagV2(ctx context.Context, containerID string, t ArchImageTag) error { url := "v2/tags/" + containerID - c.Logger.Logf("setTag calling %s", url) + c.logger.Logf("setTag calling %s", url) s, err := json.Marshal(t) if err != nil { return fmt.Errorf("error encoding object to JSON:\n\t%v", err) @@ -279,7 +279,7 @@ func (c *Client) setTagV2(ctx context.Context, containerID string, t ArchImageTa if err != nil { return fmt.Errorf("error creating POST request:\n\t%v", err) } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("error making request to server:\n\t%v", err) } diff --git a/client/client.go b/client/client.go index 801b5ba..7afd302 100644 --- a/client/client.go +++ b/client/client.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2019-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file // distributed with the sources of this project regarding your rights to use or distribute this // software. @@ -35,16 +35,11 @@ var DefaultConfig = &Config{} // Client describes the client details. type Client struct { - // Base URL of the service. - BaseURL *url.URL - // Auth token to include in the Authorization header of each request (if supplied). - AuthToken string - // User agent to include in each request (if supplied). - UserAgent string - // HTTPClient to use to make HTTP requests. - HTTPClient *http.Client - // Logger to be used when output is generated - Logger log.Logger + baseURL *url.URL + authToken string + userAgent string + httpClient *http.Client + logger log.Logger } const defaultBaseURL = "" @@ -81,52 +76,48 @@ func NewClient(cfg *Config) (*Client, error) { } c := &Client{ - BaseURL: baseURL, - AuthToken: cfg.AuthToken, - UserAgent: cfg.UserAgent, + baseURL: baseURL, + authToken: cfg.AuthToken, + userAgent: cfg.UserAgent, } // Set HTTP client if cfg.HTTPClient != nil { - c.HTTPClient = cfg.HTTPClient + c.httpClient = cfg.HTTPClient } else { - c.HTTPClient = http.DefaultClient + c.httpClient = http.DefaultClient } if cfg.Logger != nil { - c.Logger = cfg.Logger + c.logger = cfg.Logger } else { - c.Logger = log.DefaultLogger + c.logger = log.DefaultLogger } return c, nil } -// newRequestWithURL returns a new Request given a method, url, and (optional) body. -func (c *Client) newRequestWithURL(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) { - r, err := http.NewRequestWithContext(ctx, method, url, body) +// newRequest returns a new Request given a method, relative path, rawQuery, and (optional) body. +func (c *Client) newRequest(ctx context.Context, method, path, rawQuery string, body io.Reader) (*http.Request, error) { + u := c.baseURL.ResolveReference(&url.URL{ + Path: path, + RawQuery: rawQuery, + }) + + r, err := http.NewRequestWithContext(ctx, method, u.String(), body) if err != nil { return nil, err } - if v := c.AuthToken; v != "" { + if v := c.authToken; v != "" { if err := (bearerTokenCredentials{authToken: v}).ModifyRequest(r); err != nil { return nil, err } } - if v := c.UserAgent; v != "" { + if v := c.userAgent; v != "" { r.Header.Set("User-Agent", v) } return r, nil } - -// newRequest returns a new Request given a method, relative path, rawQuery, and (optional) body. -func (c *Client) newRequest(ctx context.Context, method, path, rawQuery string, body io.Reader) (*http.Request, error) { - u := c.BaseURL.ResolveReference(&url.URL{ - Path: path, - RawQuery: rawQuery, - }) - return c.newRequestWithURL(ctx, method, u.String(), body) -} diff --git a/client/client_test.go b/client/client_test.go index 2da11e8..b7b3e47 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2019-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file // distributed with the sources of this project regarding your rights to use or distribute this // software. @@ -7,8 +7,13 @@ package client import ( "context" + crypto_rand "crypto/rand" + "encoding/binary" "io" + "log" + math_rand "math/rand" "net/http" + "os" "strings" "testing" ) @@ -68,19 +73,19 @@ func TestNewClient(t *testing.T) { } if err == nil { - if got, want := c.BaseURL.String(), tt.wantURL; got != want { + if got, want := c.baseURL.String(), tt.wantURL; got != want { t.Errorf("got host %v, want %v", got, want) } - if got, want := c.AuthToken, tt.wantAuthToken; got != want { + if got, want := c.authToken, tt.wantAuthToken; got != want { t.Errorf("got auth token %v, want %v", got, want) } - if got, want := c.UserAgent, tt.wantUserAgent; got != want { + if got, want := c.userAgent, tt.wantUserAgent; got != want { t.Errorf("got user agent %v, want %v", got, want) } - if got, want := c.HTTPClient, tt.wantHTTPClient; got != want { + if got, want := c.httpClient, tt.wantHTTPClient; got != want { t.Errorf("got HTTP client %v, want %v", got, want) } } @@ -192,3 +197,18 @@ func TestNewRequest(t *testing.T) { }) } } + +func seedRandomNumberGenerator() { + var b [8]byte + if _, err := crypto_rand.Read(b[:]); err != nil { + log.Fatalf("error seeding random number generator: %v", err) + } + math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:]))) +} + +func TestMain(m *testing.M) { + // Total overkill seeding the random number generator + seedRandomNumberGenerator() + + os.Exit(m.Run()) +} diff --git a/client/downloader.go b/client/downloader.go index 64af725..db850d3 100644 --- a/client/downloader.go +++ b/client/downloader.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2021-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -58,7 +58,7 @@ func (c *Client) multipartDownload(ctx context.Context, u string, creds credenti // Calculate # of parts parts := uint(1 + (size-1)/spec.PartSize) - c.Logger.Logf("size: %d, parts: %d, streams: %d, partsize: %d", size, parts, spec.Concurrency, spec.PartSize) + c.logger.Logf("size: %d, parts: %d, streams: %d, partsize: %d", size, parts, spec.Concurrency, spec.PartSize) g, ctx := errgroup.WithContext(ctx) @@ -67,7 +67,7 @@ func (c *Client) multipartDownload(ctx context.Context, u string, creds credenti // Create download part workers for n := uint(0); n < spec.Concurrency; n++ { - g.Go(c.ociDownloadWorker(ctx, u, creds, ch, pb)) + g.Go(c.downloadWorker(ctx, u, creds, ch, pb)) } // Add part download requests @@ -84,11 +84,11 @@ func (c *Client) multipartDownload(ctx context.Context, u string, creds credenti return g.Wait() } -func (c *Client) ociDownloadWorker(ctx context.Context, u string, creds credentials, ch chan filePartDescriptor, pb ProgressBar) func() error { +func (c *Client) downloadWorker(ctx context.Context, u string, creds credentials, ch chan filePartDescriptor, pb ProgressBar) func() error { return func() error { // Iterate on channel 'ch' to handle download part requests for ps := range ch { - written, err := c.ociDownloadBlobPart(ctx, creds, u, &ps) + written, err := c.downloadBlobPart(ctx, creds, u, &ps) if err != nil { // Cleanly abort progress bar on error pb.Abort(true) @@ -103,7 +103,7 @@ func (c *Client) ociDownloadWorker(ctx context.Context, u string, creds credenti } } -func (c *Client) ociDownloadBlobPart(ctx context.Context, creds credentials, u string, ps *filePartDescriptor) (int64, error) { +func (c *Client) downloadBlobPart(ctx context.Context, creds credentials, u string, ps *filePartDescriptor) (int64, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return 0, err @@ -117,7 +117,7 @@ func (c *Client) ociDownloadBlobPart(ctx context.Context, creds credentials, u s req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", ps.start, ps.end)) - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return 0, err } diff --git a/client/downloader_test.go b/client/downloader_test.go index c86b772..1314213 100644 --- a/client/downloader_test.go +++ b/client/downloader_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -19,6 +19,16 @@ import ( "testing" ) +const ( + basicAuthUsername = "user" + basicAuthPassword = "password" +) + +var ( + testLogger = &stdLogger{} + creds = &basicCredentials{username: basicAuthUsername, password: basicAuthPassword} +) + type inMemoryBuffer struct { m sync.Mutex buf []byte @@ -49,6 +59,19 @@ func (l *stdLogger) Logf(f string, v ...interface{}) { log.Printf(f, v...) } +func TestParseContentRange(t *testing.T) { + const hdr = "bytes 0-1000/1000" + + size, err := parseContentRange(hdr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got, want := size, int64(1000); got != want { + t.Fatalf("unexpected content length: got %v, want %v", got, want) + } +} + func parseRangeHeader(_ *testing.T, val string) (int64, int64) { if val == "" { return 0, 0 @@ -66,16 +89,6 @@ func parseRangeHeader(_ *testing.T, val string) (int64, int64) { return start, end } -const ( - basicAuthUsername = "user" - basicAuthPassword = "password" -) - -var ( - testLogger = &stdLogger{} - creds = &basicCredentials{username: basicAuthUsername, password: basicAuthPassword} -) - func TestMultistreamDownloader(t *testing.T) { const src = "123456789012345678901234567890" size := int64(len(src)) diff --git a/client/oci.go b/client/oci.go index 32cfba0..0849df6 100644 --- a/client/oci.go +++ b/client/oci.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -48,11 +48,11 @@ func (c *Client) ociRegistryAuth(ctx context.Context, name string, accessTypes [ return nil, nil, err } - if c.UserAgent != "" { - req.Header.Set("User-Agent", c.UserAgent) + if c.userAgent != "" { + req.Header.Set("User-Agent", c.userAgent) } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return nil, nil, fmt.Errorf("error determining direct OCI registry access: %w", err) } @@ -629,9 +629,9 @@ func (c *Client) newOCIRegistry(ctx context.Context, name string, accessTypes [] } // Download directly from OCI registry - c.Logger.Logf("Using OCI registry endpoint %v", registryURI) + c.logger.Logf("Using OCI registry endpoint %v", registryURI) - return &ociRegistry{baseURL: registryURI, httpClient: c.HTTPClient, logger: c.Logger}, creds, nil + return &ociRegistry{baseURL: registryURI, httpClient: c.httpClient, logger: c.logger}, creds, nil } func (c *Client) ociDownloadImage(ctx context.Context, arch, name, tag string, w io.WriterAt, spec *Downloader, pb ProgressBar) error { @@ -714,7 +714,7 @@ func (c *Client) ociUploadImage(ctx context.Context, r io.Reader, size int64, na } } else { - c.Logger.Logf("Skipping image blob upload (matching hash exists)") + c.logger.Logf("Skipping image blob upload (matching hash exists)") id = imageDigest @@ -754,7 +754,7 @@ func (c *Client) ociUploadImage(ctx context.Context, r io.Reader, size int64, na // Add tags for _, ref := range tags { - c.Logger.Logf("Tag: %v", ref) + c.logger.Logf("Tag: %v", ref) if _, err := reg.uploadManifest(ctx, creds, name, ref, idx, v1.MediaTypeImageIndex); err != nil { return fmt.Errorf("error uploading index") diff --git a/client/pull.go b/client/pull.go index 1e0509b..2d589f9 100644 --- a/client/pull.go +++ b/client/pull.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -15,73 +15,8 @@ import ( "os" "strconv" "strings" - - jsonresp "github.com/sylabs/json-resp" ) -// DownloadImage will retrieve an image from the Container Library, saving it -// into the specified io.Writer. The timeout value for this operation is set -// within the context. It is recommended to use a large value (ie. 1800 seconds) -// to prevent timeout when downloading large images. -func (c *Client) DownloadImage(ctx context.Context, w io.Writer, arch, path, tag string, callback func(int64, io.Reader, io.Writer) error) error { - if arch != "" && !c.apiAtLeast(ctx, APIVersionV2ArchTags) { - c.Logger.Log("This library does not support architecture specific tags") - c.Logger.Log("The image returned may not be the requested architecture") - } - - if strings.Contains(path, ":") { - return fmt.Errorf("malformed image path: %s", path) - } - - if tag == "" { - tag = "latest" - } - - apiPath := fmt.Sprintf("v1/imagefile/%s:%s", strings.TrimPrefix(path, "/"), tag) - q := url.Values{} - q.Add("arch", arch) - - c.Logger.Logf("Pulling from URL: %s", apiPath) - - req, err := c.newRequest(ctx, http.MethodGet, apiPath, q.Encode(), nil) - if err != nil { - return err - } - - res, err := c.HTTPClient.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - if res.StatusCode == http.StatusNotFound { - return fmt.Errorf("requested image was not found in the library") - } - - if res.StatusCode != http.StatusOK { - err := jsonresp.ReadError(res.Body) - if err != nil { - return fmt.Errorf("download did not succeed: %v", err) - } - return fmt.Errorf("unexpected http status code: %d", res.StatusCode) - } - - c.Logger.Logf("OK response received, beginning body download") - - if callback != nil { - err = callback(res.ContentLength, res.Body, w) - } else { - _, err = io.Copy(w, res.Body) - } - if err != nil { - return err - } - - c.Logger.Logf("Download complete") - - return nil -} - // Downloader defines concurrency (# of requests) and part size for download operation. type Downloader struct { // Concurrency defines concurrency for multi-part downloads. @@ -135,7 +70,7 @@ type ProgressBar interface { Wait() } -// ConcurrentDownloadImage implements a multi-part (concurrent) downloader for +// DownloadImage implements a multi-part (concurrent) downloader for // Cloud Library images. spec is used to define transfer parameters. pb is an // optional progress bar interface. If pb is nil, NoopProgressBar is used. // @@ -143,7 +78,7 @@ type ProgressBar interface { // only files larger than Downloader.PartSize. It will automatically adjust the // concurrency for source files that do not meet minimum size for multi-part // downloads. -func (c *Client) ConcurrentDownloadImage(ctx context.Context, dst *os.File, arch, path, tag string, spec *Downloader, pb ProgressBar) error { +func (c *Client) DownloadImage(ctx context.Context, dst *os.File, arch, path, tag string, spec *Downloader, pb ProgressBar) error { if pb == nil { pb = &NoopProgressBar{} } @@ -165,25 +100,25 @@ func (c *Client) ConcurrentDownloadImage(ctx context.Context, dst *os.File, arch return err } - c.Logger.Log("Fallback to (legacy) library download") + c.logger.Log("Fallback to (legacy) library download") return c.legacyDownloadImage(ctx, arch, name, tag, dst, spec, pb) } func (c *Client) legacyDownloadImage(ctx context.Context, arch, name, tag string, dst io.WriterAt, spec *Downloader, pb ProgressBar) error { if arch != "" && !c.apiAtLeast(ctx, APIVersionV2ArchTags) { - c.Logger.Log("This library does not support architecture specific tags") - c.Logger.Log("The image returned may not be the requested architecture") + c.logger.Log("This library does not support architecture specific tags") + c.logger.Log("The image returned may not be the requested architecture") } apiPath := fmt.Sprintf("v1/imagefile/%v:%v", name, tag) q := url.Values{} q.Add("arch", arch) - c.Logger.Logf("Pulling from URL: %s", apiPath) + c.logger.Logf("Pulling from URL: %s", apiPath) customHTTPClient := &http.Client{ - Transport: c.HTTPClient.Transport, + Transport: c.httpClient.Transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { if req.Response.StatusCode == http.StatusSeeOther { return http.ErrUseLastResponse @@ -194,8 +129,8 @@ func (c *Client) legacyDownloadImage(ctx context.Context, arch, name, tag string } return nil }, - Jar: c.HTTPClient.Jar, - Timeout: c.HTTPClient.Timeout, + Jar: c.httpClient.Jar, + Timeout: c.httpClient.Timeout, } req, err := c.newRequest(ctx, http.MethodGet, apiPath, q.Encode(), nil) @@ -216,7 +151,7 @@ func (c *Client) legacyDownloadImage(ctx context.Context, arch, name, tag string if res.StatusCode == http.StatusOK { // Library endpoint does not provide HTTP redirection response, treat as single stream download - c.Logger.Log("Library endpoint does not support concurrent downloads; reverting to single stream") + c.logger.Log("Library endpoint does not support concurrent downloads; reverting to single stream") size, err := parseContentLengthHeader(res.Header.Get("Content-Length")) if err != nil { @@ -242,9 +177,9 @@ func (c *Client) legacyDownloadImage(ctx context.Context, arch, name, tag string } var creds credentials - if c.AuthToken != "" && samehost(c.BaseURL, redirectURL) { + if c.authToken != "" && samehost(c.baseURL, redirectURL) { // Only include credentials if redirected to same host as base URL - creds = bearerTokenCredentials{authToken: c.AuthToken} + creds = bearerTokenCredentials{authToken: c.authToken} } // Use redirect URL to download artifact @@ -286,7 +221,7 @@ func (c *Client) download(_ context.Context, w io.WriterAt, r io.Reader, size in return err } - c.Logger.Logf("Downloaded %v byte(s)", written) + c.logger.Logf("Downloaded %v byte(s)", written) return nil } diff --git a/client/pull_test.go b/client/pull_test.go index 186ceb6..e2041ad 100644 --- a/client/pull_test.go +++ b/client/pull_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -6,17 +6,12 @@ package client import ( - "bufio" - "bytes" "context" "crypto/sha256" - "encoding/binary" "fmt" - "io" "net/http" "net/http/httptest" "net/url" - "os" "reflect" "strings" "testing" @@ -26,136 +21,6 @@ import ( math_rand "math/rand" ) -type mockRawService struct { - t *testing.T - code int - testFile string - reqCallback func(*http.Request, *testing.T) - httpAddr string - httpPath string - httpServer *httptest.Server - baseURI string -} - -func (m *mockRawService) Run() { - mux := http.NewServeMux() - mux.HandleFunc(m.httpPath, m.ServeHTTP) - m.httpServer = httptest.NewServer(mux) - m.httpAddr = m.httpServer.Listener.Addr().String() - m.baseURI = "http://" + m.httpAddr -} - -func (m *mockRawService) Stop() { - m.httpServer.Close() -} - -func (m *mockRawService) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if m.reqCallback != nil { - m.reqCallback(r, m.t) - } - w.WriteHeader(m.code) - inFile, err := os.Open(m.testFile) - if err != nil { - m.t.Errorf("error opening file %v:", err) - } - defer inFile.Close() - - _, err = io.Copy(w, bufio.NewReader(inFile)) - if err != nil { - m.t.Errorf("Test HTTP server unable to output file: %v", err) - } -} - -func Test_DownloadImage(t *testing.T) { - f, err := os.CreateTemp("", "test") - if err != nil { - t.Fatalf("Error creating a temporary file for testing") - } - tempFile := f.Name() - f.Close() - os.Remove(tempFile) - - tests := []struct { - name string - arch string - path string - tag string - outFile string - code int - testFile string - tokenFile string - checkContent bool - expectError bool - }{ - {"Bad library ref", "amd64", "entity/collection/im,age", "tag", tempFile, http.StatusBadRequest, "test_data/test_sha256", "test_data/test_token", false, true}, - {"Server error", "amd64", "entity/collection/image", "tag", tempFile, http.StatusInternalServerError, "test_data/test_sha256", "test_data/test_token", false, true}, - {"Tags in path", "amd64", "entity/collection/image:tag", "anothertag", tempFile, http.StatusOK, "test_data/test_sha256", "test_data/test_token", false, true}, - {"Good Download", "amd64", "entity/collection/image", "tag", tempFile, http.StatusOK, "test_data/test_sha256", "test_data/test_token", true, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := mockRawService{ - t: t, - code: tt.code, - testFile: tt.testFile, - httpPath: fmt.Sprintf("/v1/imagefile/%s:%s", tt.path, tt.tag), - } - - m.Run() - defer m.Stop() - - c, err := NewClient(&Config{AuthToken: tt.tokenFile, BaseURL: m.baseURI}) - if err != nil { - t.Errorf("Error initializing client: %v", err) - } - - out, err := os.OpenFile(tt.outFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o777) - if err != nil { - t.Errorf("Error opening file %s for writing: %v", tt.outFile, err) - } - - err = c.DownloadImage(context.Background(), out, tt.arch, tt.path, tt.tag, nil) - - out.Close() - - if err != nil && !tt.expectError { - t.Errorf("Unexpected error: %v", err) - } - if err == nil && tt.expectError { - t.Errorf("Unexpected success. Expected error.") - } - - if tt.checkContent { - fileContent, err := os.ReadFile(tt.outFile) - if err != nil { - t.Errorf("Error reading test output file: %v", err) - } - testContent, err := os.ReadFile(tt.testFile) - if err != nil { - t.Errorf("Error reading test file: %v", err) - } - if !bytes.Equal(fileContent, testContent) { - t.Errorf("File contains '%v' - expected '%v'", fileContent, testContent) - } - } - }) - } -} - -func TestParseContentRange(t *testing.T) { - const hdr = "bytes 0-1000/1000" - - size, err := parseContentRange(hdr) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if got, want := size, int64(1000); got != want { - t.Fatalf("unexpected content length: got %v, want %v", got, want) - } -} - func TestParseContentLengthHeader(t *testing.T) { t.Parallel() @@ -205,42 +70,33 @@ func generateSampleData(t *testing.T) []byte { return sampleBytes } -func seedRandomNumberGenerator(t *testing.T) { - t.Helper() - - var b [8]byte - if _, err := crypto_rand.Read(b[:]); err != nil { - t.Fatalf("error seeding random number generator: %v", err) - } - math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:]))) -} - // mockLibraryServer returns *httptest.Server that mocks Cloud Library server; in particular, // it has handlers for /version, /v1/images, /v1/imagefile, and /v1/imagepart -func mockLibraryServer(t *testing.T, sampleBytes []byte, size int64, multistream bool) *httptest.Server { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/version") { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) +func mockLibraryServer(t *testing.T, sampleBytes []byte, multistream bool) *httptest.Server { + size := int64(len(sampleBytes)) - if _, err := w.Write([]byte("{\"data\": {\"apiVersion\": \"1.0.0\"}}")); err != nil { - t.Fatalf("error writing /version response: %v", err) - } - return + mux := http.NewServeMux() + + mux.HandleFunc("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + + if _, err := w.Write([]byte("{\"data\": {\"apiVersion\": \"1.0.0\"}}")); err != nil { + t.Fatalf("error writing /version response: %v", err) } + })) - if multistream && strings.HasPrefix(r.URL.Path, "/v1/images/") { + if multistream { + mux.HandleFunc("/v1/images/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte(fmt.Sprintf("{\"data\": {\"size\": %v}}", size))); err != nil { t.Fatalf("error writing /v1/images response: %v", err) } + })) - return - } - - if multistream && strings.HasPrefix(r.URL.Path, "/v1/imagefile/") { + mux.HandleFunc("/v1/imagefile/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { redirectURL := &url.URL{ Scheme: "http", Host: r.Host, @@ -248,30 +104,50 @@ func mockLibraryServer(t *testing.T, sampleBytes []byte, size int64, multistream } w.Header().Set("Location", redirectURL.String()) w.WriteHeader(http.StatusSeeOther) - return - } + })) - // Handle /v1/imagefile (single stream) or /v1/imagepart (multistream) + mux.HandleFunc("/v1/imagepart/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Handle Range request for multipart downloads + var start, end int64 + if val := r.Header.Get("Range"); val != "" { + start, end = parseRangeHeader(t, val) - // Handle Range request for multipart downloads - var start, end int64 - if val := r.Header.Get("Range"); val != "" { - start, end = parseRangeHeader(t, val) - } else { - start, end = 0, int64(size)-1 - } + if end < 0 || start < 0 || end-start+1 > size { + w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + return + } + } else { + t.Fatal("Missing HTTP Range header") + } - // Set up response headers - w.Header().Set("Content-Length", fmt.Sprintf("%v", end-start+1)) - w.Header().Set("Content-Type", "application/octet-stream") - w.WriteHeader(http.StatusOK) + writeBlob(t, sampleBytes, start, end, http.StatusPartialContent, w) + })) + } else { + // single stream + mux.HandleFunc("/v1/imagefile/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writeBlob(t, sampleBytes, 0, size-1, http.StatusOK, w) + })) + } - // Write image data - if _, err := w.Write(sampleBytes[start : end+1]); err != nil { - t.Fatalf("error writing response: %v", err) - } + mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("Unhandled HTTP request: method=[%v], path=[%v]", r.Method, r.URL.Path) })) - return srv + + return httptest.NewServer(mux) +} + +func writeBlob(t *testing.T, sampleBytes []byte, start, end int64, code int, w http.ResponseWriter) { + t.Helper() + + // Set up response headers + w.Header().Set("Content-Length", fmt.Sprintf("%v", end-start+1)) + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(code) + + // Write image data + if _, err := w.Write(sampleBytes[start : end+1]); err != nil { + t.Fatalf("error writing response: %v", err) + } } // TestLegacyDownloadImage downloads random image data from mock library and compares hash to @@ -285,9 +161,6 @@ func TestLegacyDownloadImage(t *testing.T) { {"MultiStream", true}, } - // Total overkill seeding the random number generator - seedRandomNumberGenerator(t) - for _, tt := range tests { tt := tt @@ -301,7 +174,7 @@ func TestLegacyDownloadImage(t *testing.T) { hash := sha256.Sum256(sampleBytes) // Create mock library server that responds to '/version' and '/v1/imagefile' only - srv := mockLibraryServer(t, sampleBytes, size, tt.multistreamDownload) + srv := mockLibraryServer(t, sampleBytes, tt.multistreamDownload) defer srv.Close() // Initialize scs-library-client diff --git a/client/push.go b/client/push.go index c6c8ab0..f823c38 100644 --- a/client/push.go +++ b/client/push.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -127,7 +127,7 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return nil, fmt.Errorf("error seeking to start stream: %v", err) } - c.Logger.Logf("Image hash computed as %s", imageHash) + c.logger.Logf("Image hash computed as %s", imageHash) if err := c.ociUploadImage(ctx, r, fileSize, strings.TrimPrefix(path, "library://"), arch, tags, description, "sha256."+imageHash, callback); err == nil { return nil, nil @@ -136,7 +136,7 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return nil, err } - c.Logger.Log("Fallback to (legacy) library upload") + c.logger.Log("Fallback to (legacy) library upload") // Find or create entity entity, err := c.getEntity(ctx, entityName) @@ -144,7 +144,7 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st if err != ErrNotFound { return nil, err } - c.Logger.Logf("Entity %s does not exist in library - creating it.", entityName) + c.logger.Logf("Entity %s does not exist in library - creating it.", entityName) entity, err = c.createEntity(ctx, entityName) if err != nil { return nil, err @@ -159,7 +159,7 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return nil, err } // create collection - c.Logger.Logf("Collection %s does not exist in library - creating it.", collectionName) + c.logger.Logf("Collection %s does not exist in library - creating it.", collectionName) collection, err = c.createCollection(ctx, collectionName, entity.ID) if err != nil { return nil, err @@ -174,7 +174,7 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return nil, err } // Create container - c.Logger.Logf("Container %s does not exist in library - creating it.", containerName) + c.logger.Logf("Container %s does not exist in library - creating it.", containerName) container, err = c.createContainer(ctx, containerName, collection.ID) if err != nil { return nil, err @@ -188,7 +188,7 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return nil, err } // Create image - c.Logger.Logf("Image %s does not exist in library - creating it.", imageHash) + c.logger.Logf("Image %s does not exist in library - creating it.", imageHash) image, err = c.createImage(ctx, "sha256."+imageHash, container.ID, description) if err != nil { return nil, err @@ -215,11 +215,11 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return nil, err } } else { - c.Logger.Logf("Image is already present in the library - not uploading.") + c.logger.Logf("Image is already present in the library - not uploading.") } // set tags on image - c.Logger.Logf("Setting tags against uploaded image") + c.logger.Logf("Setting tags against uploaded image") if c.apiAtLeast(ctx, APIVersionV2ArchTags) { if err := c.setTagsV2(ctx, container.ID, arch, image.ID, append(tags, parsedTags...)); err != nil { @@ -228,9 +228,9 @@ func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch st return res, nil } - c.Logger.Logf("This library does not support multiple architectures per tag.") + c.logger.Logf("This library does not support multiple architectures per tag.") - c.Logger.Logf("This tag will replace any already uploaded with the same name.") + c.logger.Logf("This tag will replace any already uploaded with the same name.") if err := c.setTags(ctx, container.ID, image.ID, append(tags, parsedTags...)); err != nil { return nil, err @@ -246,7 +246,7 @@ func (c *Client) postFileWrapper(ctx context.Context, r io.ReadSeeker, fileSize var res *UploadImageComplete - c.Logger.Log("Now uploading to the library") + c.logger.Log("Now uploading to the library") if c.apiAtLeast(ctx, APIVersionV2Upload) { // use v2 post file api. Send both md5 and sha256 checksums. If the @@ -262,11 +262,11 @@ func (c *Client) postFileWrapper(ctx context.Context, r io.ReadSeeker, fileSize if err != nil { callback.Terminate() - c.Logger.Log("Upload terminated due to error") + c.logger.Log("Upload terminated due to error") } else { callback.Finish() - c.Logger.Log("Upload completed OK") + c.logger.Log("Upload completed OK") } return res, err @@ -275,13 +275,13 @@ func (c *Client) postFileWrapper(ctx context.Context, r io.ReadSeeker, fileSize func (c *Client) postFile(ctx context.Context, fileSize int64, imageID string, callback UploadCallback) (*UploadImageComplete, error) { postURL := "v1/imagefile/" + imageID - c.Logger.Logf("postFile calling %s", postURL) + c.logger.Logf("postFile calling %s", postURL) // Make an upload request req, _ := c.newRequest(ctx, http.MethodPost, postURL, "", callback.GetReader()) // Content length is required by the API req.ContentLength = fileSize - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("error uploading file to server: %s", err.Error()) } @@ -302,7 +302,7 @@ func (c *Client) postFile(ctx context.Context, fileSize int64, imageID string, c func (c *Client) postFileV2(ctx context.Context, r io.ReadSeeker, fileSize int64, imageID string, callback UploadCallback, metadata map[string]string) (*UploadImageComplete, error) { if fileSize > minimumPartSize { // only attempt multipart upload if size greater than S3 minimum - c.Logger.Log("Attempting to use multipart uploader") + c.logger.Log("Attempting to use multipart uploader") var err error var res *UploadImageComplete @@ -322,7 +322,7 @@ func (c *Client) postFileV2(ctx context.Context, r io.ReadSeeker, fileSize int64 } // fallback to legacy uploader - c.Logger.Log("Using legacy (single part) uploader") + c.logger.Log("Using legacy (single part) uploader") return c.legacyPostFileV2(ctx, fileSize, imageID, callback, metadata) } @@ -340,18 +340,18 @@ func (c *Client) postFileV2Multipart(ctx context.Context, r io.ReadSeeker, fileS // parts and part size response, err := c.startMultipartUpload(ctx, fileSize, imageID) if err != nil { - c.Logger.Logf("Error starting multipart upload: %v", err) + c.logger.Logf("Error starting multipart upload: %v", err) return nil, err } - c.Logger.Logf("Multi-part upload: ID=[%s] totalParts=[%d] partSize=[%d]", response.UploadID, response.TotalParts, fileSize) + c.logger.Logf("Multi-part upload: ID=[%s] totalParts=[%d] partSize=[%d]", response.UploadID, response.TotalParts, fileSize) // Enable S3 compliance mode by default val := response.Options[OptionS3Compliant] s3Compliant := val == "" || val == "true" - c.Logger.Logf("S3 compliant option: %v", s3Compliant) + c.logger.Logf("S3 compliant option: %v", s3Compliant) // maintain list of completed parts which will be passed to the completion function completedParts := []CompletedPart{} @@ -361,7 +361,7 @@ func (c *Client) postFileV2Multipart(ctx context.Context, r io.ReadSeeker, fileS for nPart := 1; nPart <= response.TotalParts; nPart++ { partSize := getPartSize(bytesRemaining, response.PartSize) - c.Logger.Logf("Uploading part %d (%d bytes)", nPart, partSize) + c.logger.Logf("Uploading part %d (%d bytes)", nPart, partSize) mgr := &uploadManager{ Source: r, @@ -374,10 +374,10 @@ func (c *Client) postFileV2Multipart(ctx context.Context, r io.ReadSeeker, fileS etag, err := c.multipartUploadPart(ctx, nPart, mgr, callback, s3Compliant) if err != nil { // error uploading part - c.Logger.Logf("Error uploading part %d: %v", nPart, err) + c.logger.Logf("Error uploading part %d: %v", nPart, err) if err := c.abortMultipartUpload(ctx, mgr); err != nil { - c.Logger.Logf("Error aborting multipart upload: %v", err) + c.logger.Logf("Error aborting multipart upload: %v", err) } return nil, err } @@ -389,7 +389,7 @@ func (c *Client) postFileV2Multipart(ctx context.Context, r io.ReadSeeker, fileS bytesRemaining -= partSize } - c.Logger.Logf("Uploaded %d parts", response.TotalParts) + c.logger.Logf("Uploaded %d parts", response.TotalParts) return c.completeMultipartUpload(ctx, &completedParts, &uploadManager{ ImageID: imageID, @@ -410,7 +410,7 @@ func (c *Client) startMultipartUpload(ctx context.Context, fileSize int64, image // attempt to initiate multipart upload postURL := fmt.Sprintf("v2/imagefile/%s/_multipart", imageID) - c.Logger.Logf("startMultipartUpload calling %s", postURL) + c.logger.Logf("startMultipartUpload calling %s", postURL) body := MultipartUploadStartRequest{ Size: fileSize, @@ -450,7 +450,7 @@ func remoteSHA256ChecksumSupport(u *url.URL) bool { func (c *Client) legacyPostFileV2(ctx context.Context, fileSize int64, imageID string, callback UploadCallback, metadata map[string]string) (*UploadImageComplete, error) { postURL := fmt.Sprintf("v2/imagefile/%s", imageID) - c.Logger.Logf("legacyPostFileV2 calling %s", postURL) + c.logger.Logf("legacyPostFileV2 calling %s", postURL) // issue upload request (POST) to obtain presigned S3 URL body := UploadImageRequest{ @@ -495,7 +495,7 @@ func (c *Client) legacyPostFileV2(ctx context.Context, fileSize int64, imageID s req.Header.Set("x-amz-content-sha256", metadata["sha256sum"]) } - resp, err := c.HTTPClient.Do(req) + resp, err := c.httpClient.Do(req) callback.Finish() if err != nil { return nil, fmt.Errorf("error uploading image: %v", err) @@ -539,13 +539,13 @@ func (c *Client) multipartUploadPart(ctx context.Context, partNumber int, m *upl // calculate sha256sum of part being uploaded chunkHash, err = getPartSHA256Sum(m.Source, int64(m.Size)) if err != nil { - c.Logger.Logf("Error calculating SHA256 checksum: %v", err) + c.logger.Logf("Error calculating SHA256 checksum: %v", err) return "", err } // rollback file pointer to beginning of part if _, err := m.Source.Seek(-(int64(m.Size)), io.SeekCurrent); err != nil { - c.Logger.Logf("Error repositioning file pointer: %v", err) + c.logger.Logf("Error repositioning file pointer: %v", err) return "", err } } @@ -553,7 +553,7 @@ func (c *Client) multipartUploadPart(ctx context.Context, partNumber int, m *upl // send request to cloud-library for presigned PUT url uri := fmt.Sprintf("v2/imagefile/%s/_multipart", m.ImageID) - c.Logger.Logf("multipartUploadPart calling %s", uri) + c.logger.Logf("multipartUploadPart calling %s", uri) objJSON, err := c.apiUpdate(ctx, uri, UploadImagePartRequest{ PartSize: m.Size, @@ -582,32 +582,32 @@ func (c *Client) multipartUploadPart(ctx context.Context, partNumber int, m *upl req.Header.Add("x-amz-content-sha256", chunkHash) } - resp, err := c.HTTPClient.Do(req) + resp, err := c.httpClient.Do(req) if err != nil { - c.Logger.Logf("Failure uploading to presigned URL: %v", err) + c.logger.Logf("Failure uploading to presigned URL: %v", err) return "", err } defer resp.Body.Close() // process response from S3 if resp.StatusCode != http.StatusOK { - c.Logger.Logf("Object store returned an error: %d", resp.StatusCode) + c.logger.Logf("Object store returned an error: %d", resp.StatusCode) return "", fmt.Errorf("object store returned an error: %d", resp.StatusCode) } etag := resp.Header.Get("ETag") - c.Logger.Logf("Part %d accepted (ETag: %s)", partNumber, etag) + c.logger.Logf("Part %d accepted (ETag: %s)", partNumber, etag) return etag, nil } func (c *Client) completeMultipartUpload(ctx context.Context, completedParts *[]CompletedPart, m *uploadManager) (*UploadImageComplete, error) { - c.Logger.Logf("Completing multipart upload: %s", m.UploadID) + c.logger.Logf("Completing multipart upload: %s", m.UploadID) uri := fmt.Sprintf("v2/imagefile/%s/_multipart_complete", m.ImageID) - c.Logger.Logf("completeMultipartUpload calling %s", uri) + c.logger.Logf("completeMultipartUpload calling %s", uri) body := CompleteMultipartUploadRequest{ UploadID: m.UploadID, @@ -616,13 +616,13 @@ func (c *Client) completeMultipartUpload(ctx context.Context, completedParts *[] objJSON, err := c.apiUpdate(ctx, uri, body) if err != nil { - c.Logger.Logf("Error completing multipart upload: %v", err) + c.logger.Logf("Error completing multipart upload: %v", err) return nil, err } var res CompleteMultipartUploadResponse if err := json.Unmarshal(objJSON, &res); err != nil { - c.Logger.Logf("Error decoding complete multipart upload request: %v", err) + c.logger.Logf("Error decoding complete multipart upload request: %v", err) return nil, err } @@ -635,18 +635,18 @@ func (c *Client) completeMultipartUpload(ctx context.Context, completedParts *[] } func (c *Client) abortMultipartUpload(ctx context.Context, m *uploadManager) error { - c.Logger.Logf("Aborting multipart upload ID: %s", m.UploadID) + c.logger.Logf("Aborting multipart upload ID: %s", m.UploadID) uri := fmt.Sprintf("v2/imagefile/%s/_multipart_abort", m.ImageID) - c.Logger.Logf("abortMultipartUpload calling %s", uri) + c.logger.Logf("abortMultipartUpload calling %s", uri) body := AbortMultipartUploadRequest{ UploadID: m.UploadID, } if _, err := c.apiUpdate(ctx, uri, body); err != nil { - c.Logger.Logf("error aborting multipart upload: %v", err) + c.logger.Logf("error aborting multipart upload: %v", err) return err } return nil diff --git a/client/restclient.go b/client/restclient.go index 2ee1839..766502c 100644 --- a/client/restclient.go +++ b/client/restclient.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2019, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE.md file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -22,17 +22,17 @@ import ( var ErrNotFound = errors.New("not found") func (c *Client) apiGet(ctx context.Context, path string) (objJSON []byte, err error) { - c.Logger.Logf("apiGet calling %s", path) + c.logger.Logf("apiGet calling %s", path) return c.doGETRequest(ctx, path) } func (c *Client) apiCreate(ctx context.Context, url string, o interface{}) (objJSON []byte, err error) { - c.Logger.Logf("apiCreate calling %s", url) + c.logger.Logf("apiCreate calling %s", url) return c.doPOSTRequest(ctx, url, o) } func (c *Client) apiUpdate(ctx context.Context, url string, o interface{}) (objJSON []byte, err error) { - c.Logger.Logf("apiUpdate calling %s", url) + c.logger.Logf("apiUpdate calling %s", url) return c.doPUTRequest(ctx, url, o) } @@ -75,7 +75,7 @@ func (c *Client) commonRequestHandler(ctx context.Context, method string, path s return []byte{}, fmt.Errorf("error creating %s request:\n\t%v", method, err) } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return []byte{}, fmt.Errorf("error making request to server:\n\t%v", err) } diff --git a/client/version.go b/client/version.go index cd5e57e..f4c0cd1 100644 --- a/client/version.go +++ b/client/version.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019, Sylabs Inc. All rights reserved. +// Copyright (c) 2019-2023, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file // distributed with the sources of this project regarding your rights to use or distribute this // software. @@ -34,7 +34,7 @@ func (c *Client) GetVersion(ctx context.Context) (vi VersionInfo, err error) { return VersionInfo{}, err } - res, err := c.HTTPClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { return VersionInfo{}, err } @@ -53,17 +53,17 @@ func (c *Client) apiAtLeast(ctx context.Context, reqVersion string) bool { if err != nil || vi.APIVersion == "" { // unable to get cloud-library server API version, fallback to lowest // common denominator - c.Logger.Logf("Unable to determine remote API version: %v", err) + c.logger.Logf("Unable to determine remote API version: %v", err) return false } v, err := semver.Make(vi.APIVersion) if err != nil { - c.Logger.Logf("Unable to decode remote API version: %v", err) + c.logger.Logf("Unable to decode remote API version: %v", err) return false } minRequiredVers, err := semver.Make(reqVersion) if err != nil { - c.Logger.Logf("Unable to decode minimum required version: %v", err) + c.logger.Logf("Unable to decode minimum required version: %v", err) return false } return v.GTE(minRequiredVers)