constellation/internal/versionsapi/fetcher/fetcher.go
Paul Meyer 9dbe6033f2 versionsapi: new fetcher implementation
Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
2023-01-04 11:39:19 +01:00

111 lines
2.9 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package fetcher
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
)
// Fetcher fetches versions API resources.
//
// The fetcher is used to get information from the versions API without having to
// authenticate with AWS. It is the interface that should be used in user-facing
// application code most of the time.
type Fetcher struct {
httpc httpc
}
// NewFetcher returns a new Fetcher.
func NewFetcher() *Fetcher {
return &Fetcher{
httpc: http.DefaultClient,
}
}
// FetchVersionList fetches the given version list from the versions API.
func (f *Fetcher) FetchVersionList(ctx context.Context, list versionsapi.List) (versionsapi.List, error) {
return fetch(ctx, f.httpc, list)
}
// FetchVersionLatest fetches the latest version from the versions API.
func (f *Fetcher) FetchVersionLatest(ctx context.Context, latest versionsapi.Latest) (versionsapi.Latest, error) {
return fetch(ctx, f.httpc, latest)
}
// FetchImageInfo fetches the given image info from the versions API.
func (f *Fetcher) FetchImageInfo(ctx context.Context, imageInfo versionsapi.ImageInfo) (versionsapi.ImageInfo, error) {
return fetch(ctx, f.httpc, imageInfo)
}
type apiObject interface {
ValidateRequest() error
Validate() error
URL() (string, error)
}
func fetch[T apiObject](ctx context.Context, c httpc, obj T) (T, error) {
if err := obj.ValidateRequest(); err != nil {
return *new(T), fmt.Errorf("validating request for %T: %w", obj, err)
}
url, err := obj.URL()
if err != nil {
return *new(T), fmt.Errorf("getting URL for %T: %w", obj, err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return *new(T), fmt.Errorf("creating request for %T: %w", obj, err)
}
resp, err := c.Do(req)
if err != nil {
return *new(T), fmt.Errorf("sending request for %T: %w", obj, err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
case http.StatusNotFound:
return *new(T), &NotFoundError{fmt.Errorf("requesting resource at %s returned status code 404", url)}
default:
return *new(T), fmt.Errorf("unexpected status code %d while requesting resource", resp.StatusCode)
}
var newObj T
if err := json.NewDecoder(resp.Body).Decode(&newObj); err != nil {
return *new(T), fmt.Errorf("decoding %T: %w", obj, err)
}
if newObj.Validate() != nil {
return *new(T), fmt.Errorf("received invalid %T: %w", newObj, newObj.Validate())
}
return newObj, nil
}
// NotFoundError is an error that is returned when a resource is not found.
type NotFoundError struct {
err error
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("the requested resource was not found: %s", e.err.Error())
}
func (e *NotFoundError) Unwrap() error {
return e.err
}
type httpc interface {
Do(req *http.Request) (*http.Response, error)
}