reggie

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2023 License: Apache-2.0 Imports: 15 Imported by: 4

README

Reggie

GitHub Actions status GoDoc

Reggie is a dead simple Go HTTP client designed to be used against OCI Distribution, built on top of Resty.

There is also built-in support for both basic auth and "Docker-style" token auth.

Note: Authentication/authorization is not part of the distribution spec, but it has been implemented similarly across registry providers targeting the Docker client.

Getting Started

First import the library:

import "github.com/bloodorangeio/reggie"

Then construct a client:

client, err := reggie.NewClient("http://localhost:5000")

You may also construct the client with a number of options related to authentication, etc:

client, err := reggie.NewClient("https://r.mysite.io",
    reggie.WithUsernamePassword("myuser", "mypass"),  // registry credentials
    reggie.WIthDefaultName("myorg/myrepo"),           // default repo name
    reggie.WithInsecureSkipTLSVerify(true),           // skip TLS verification
    reggie.WithDebug(true))                           // enable debug logging

Making Requests

Reggie uses a domain-specific language to supply various parts of the URI path in order to provide visual parity with the spec.

For example, to list all tags for the repo megacorp/superapp, you might do the following:

req := client.NewRequest(reggie.GET, "/v2/<name>/tags/list",
    reggie.WithName("megacorp/superapp"))

This will result in a request object built for GET /v2/megacorp/superapp/tags/list.

Finally, execute the request, which will return a response object:

resp, err := client.Do(req)
fmt.Println("Status Code:", resp.StatusCode())

Path Substitutions

Below is a table of all of the possible URI parameter substitutions and associated methods:

URI Parameter Description Option method
<name> Namespace of a repository within a registry WithDefaultName (Client) or
WithName (Request)
<digest> Content-addressable identifier WithDigest (Request)
<reference> Tag or digest WithReference (Request)
<session_id> Session ID for upload WithSessionID (Request)

Auth

All requests are first attempted without any authentication. If an endpoint returns a 401 Unauthorized, and the client has been constructed with a username and password (via reggie.WithUsernamePassword), the request is retried with an Authorization header.

Included in the 401 response, registries should return a Www-Authenticate header describing how to to authenticate.

For more info about the Www-Authenticate header and general HTTP auth topics, please see IETF RFCs 7235 and 6749.

Basic Auth

If the Www-Authenticate header contains the string "Basic", then the header used in the retried request will be formatted as Authorization: Basic <credentials>, where credentials is the base64 encoding of the username and password joined by a single colon.

"Docker-style" Token Auth

Note: most commercial registries use this method.

If theWww-Authenticate contains the string "Bearer", an attempt is made to retrieve a token from an authorization service endpoint, the URL of which should be provided in the Realm field of the header. The header then used in the retried request will be formatted as Authorization: Bearer <token>, where token is the one returned from the token endpoint.

Here is a visual of this auth flow copied from the Docker docs:

Custom Auth Scope

It may be necessary to override the scope obtained from the Www-Authenticate header in the registry's response. This can be done on the client level:

client, err := reggie.NewClient("http://localhost:5000",
   reggie.WithAuthScope("repository:mystuff/myrepo:pull,push"))

Other Features

Method Chaining

Each of the types provided by this package (Client, Request, & Response) are all built on top of types provided by Resty. In most cases, methods provided by Resty should just work on these objects (see the godoc for more info).

The following commonly-used methods have been wrapped in order to allow for method chaining:

  • req.Header
  • req.SetQueryParam
  • req.SetBody

The following is an example of using method chaining to build a request:

req := client.NewRequest(reggie.PUT, lastResponse.GetRelativeLocation()).
    SetHeader("Content-Length", configContentLength).
    SetHeader("Content-Type", "application/octet-stream").
    SetQueryParam("digest", configDigest).
    SetBody(configContent)
Location Header Parsing

For certain types of requests, such as chunked uploads, the Location header is needed in order to make follow-up requests.

Reggie provides two helper methods to obtain the redirect location:

fmt.Println("Relative location:", resp.GetRelativeLocation())  // /v2/...
fmt.Println("Absolute location:", resp.GetAbsoluteLocation())  // https://...
Error Parsing

On the response object, you may call the Errors() method which will attempt to parse the response body into a list of OCI ErrorInfo objects:

for _, e := range resp.Errors() {
    fmt.Println("Code:",    e.Code)
    fmt.Println("Message:", e.Message)
    fmt.Println("Detail:",  e.Detail)
}
HTTP Method Constants

Simply-named constants are provided for the following HTTP request methods:

reggie.GET     // "GET"
reggie.PUT     // "PUT"
reggie.PATCH   // "PATCH"
reggie.DELETE  // "DELETE"
reggie.POST    // "POST"
reggie.HEAD    // "HEAD"
reggie.OPTIONS // "OPTIONS"
Custom User-Agent

By default, requests made by Reggie will use a default value for the User-Agent header in order for registry providers to identify incoming requests:

User-Agent: reggie/0.3.0 (https://github.com/bloodorangeio/reggie)

If you wish to use a custom value for User-Agent, such as "my-agent" for example, you can do the following:

client, err := reggie.NewClient("http://localhost:5000",
    reggie.WithUserAgent("my-agent"))

Example

The following is an example of a resumable blob upload and subsequent manifest upload:

package main

import (
	"fmt"

	"github.com/bloodorangeio/reggie"
	godigest "github.com/opencontainers/go-digest"
)

func main() {
	// construct client pointing to your registry
	client, err := reggie.NewClient("http://localhost:5000",
		reggie.WithDefaultName("myorg/myrepo"),
		reggie.WithDebug(true))
	if err != nil {
		panic(err)
	}

	// get the session URL
	req := client.NewRequest(reggie.POST, "/v2/<name>/blobs/uploads/")
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}

	// a blob for an empty manifest config, separated into 2 chunks ("{" and "}")
	blob := []byte("{}")
	blobChunk1 := blob[:1]
	blobChunk1Range := fmt.Sprintf("0-%d", len(blobChunk1)-1)
	blobChunk2 := blob[1:]
	blobChunk2Range := fmt.Sprintf("%d-%d", len(blobChunk1), len(blob)-1)
	blobDigest := godigest.FromBytes(blob).String()

	// upload the first chunk
	req = client.NewRequest(reggie.PATCH, resp.GetRelativeLocation()).
		SetHeader("Content-Type", "application/octet-stream").
		SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk1))).
		SetHeader("Content-Range", blobChunk1Range).
		SetBody(blobChunk1)
	resp, err = client.Do(req)
	if err != nil {
		panic(err)
	}

	// upload the final chunk and close the session
	req = client.NewRequest(reggie.PUT, resp.GetRelativeLocation()).
		SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk2))).
		SetHeader("Content-Range", blobChunk2Range).
		SetHeader("Content-Type", "application/octet-stream").
		SetQueryParam("digest", blobDigest).
		SetBody(blobChunk2)
	resp, err = client.Do(req)
	if err != nil {
		panic(err)
	}

	// validate the uploaded blob content
	req = client.NewRequest(reggie.GET, "/v2/<name>/blobs/<digest>",
		reggie.WithDigest(blobDigest))
	resp, err = client.Do(req)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Blob content:\n%s\n", resp.String())

	// upload the manifest (referencing the uploaded blob)
	ref := "mytag"
	manifest := []byte(fmt.Sprintf(
		"{ \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\", \"config\":  { \"digest\": \"%s\", "+
			"\"mediaType\": \"application/vnd.oci.image.config.v1+json\","+" \"size\": %d }, \"layers\": [], "+
			"\"schemaVersion\": 2 }",
		blobDigest, len(blob)))
	req = client.NewRequest(reggie.PUT, "/v2/<name>/manifests/<reference>",
		reggie.WithReference(ref)).
		SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
		SetBody(manifest)
	resp, err = client.Do(req)
	if err != nil {
		panic(err)
	}

	// validate the uploaded manifest content
	req = client.NewRequest(reggie.GET, "/v2/<name>/manifests/<reference>",
		reggie.WithReference(ref)).
		SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json")
	resp, err = client.Do(req)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Manifest content:\n%s\n", resp.String())
}

Development

To develop bloodorangeio/reggie, you will need to have Go installed. You should then fork the repository, clone the fork, and checkout a new branch.

git clone https://github.com/<username>/reggie
cd reggie 
git checkout -b add/my-new-feature

You can then make changes to the code, and build as needed. But if you are just making local changes that you want to test alongside the library, you should edit client_test.go and then run tests.

$ go test
2020/10/19 15:11:34.226399 WARN RESTY Using Basic Auth in HTTP mode is not secure, use HTTPS
2020/10/19 15:11:34.227171 WARN RESTY Using Basic Auth in HTTP mode is not secure, use HTTPS
2020/10/19 15:11:34.227620 WARN RESTY Using Basic Auth in HTTP mode is not secure, use HTTPS
2020/10/19 15:11:34.228231 WARN RESTY Using Basic Auth in HTTP mode is not secure, use HTTPS
2020/10/19 15:11:34.228650 WARN RESTY Using Basic Auth in HTTP mode is not secure, use HTTPS
2020/10/19 15:11:34.229178 WARN RESTY Using Basic Auth in HTTP mode is not secure, use HTTPS
PASS
ok  	github.com/bloodorangeio/reggie	0.006s

Before submitting a pull request, make sure to format the code.

go fmt

Documentation

Index

Constants

View Source
const (
	// GET represents the HTTP GET method.
	GET = resty.MethodGet

	// PUT represents the HTTP PUT method.
	PUT = resty.MethodPut

	// PATCH represents the HTTP PATCH method.
	PATCH = resty.MethodPatch

	// DELETE represents the HTTP DELETE method.
	DELETE = resty.MethodDelete

	// POST represents the HTTP POST method.
	POST = resty.MethodPost

	// HEAD represents the HTTP HEAD method.
	HEAD = resty.MethodHead

	// OPTIONS represents the HTTP OPTIONS method.
	OPTIONS = resty.MethodOptions
)
View Source
const (
	DefaultUserAgent = "reggie/0.3.0 (https://github.com/bloodorangeio/reggie)"
)

Variables

This section is empty.

Functions

func WithAuthScope added in v0.3.3

func WithAuthScope(authScope string) clientOption

WithAuthScope overrides the scope provided by the authorization server.

func WithDebug added in v0.2.0

func WithDebug(debug bool) clientOption

WithDebug enables or disables debug mode.

func WithDefaultName added in v0.2.0

func WithDefaultName(namespace string) clientOption

WithDefaultName sets the default registry namespace configuration setting.

func WithDigest added in v0.2.0

func WithDigest(digest string) requestOption

WithDigest sets the digest per a single request.

func WithInsecureSkipTLSVerify added in v0.6.0

func WithInsecureSkipTLSVerify(skip bool) clientOption

WithInsecureSkipTLSVerify configures the insecure option to skip TLS verification.

func WithName added in v0.2.0

func WithName(name string) requestOption

WithName sets the namespace per a single request.

func WithReference added in v0.2.0

func WithReference(ref string) requestOption

WithReference sets the reference per a single request.

func WithRetryCallback added in v0.5.0

func WithRetryCallback(cb RetryCallbackFunc) requestOption

WithRetryCallback specifies a callback that will be invoked before a request is retried. This is useful for, e.g., ensuring an io.Reader used for the body will produce the right content on retry.

func WithSessionID added in v0.2.0

func WithSessionID(id string) requestOption

WithSessionID sets the session ID per a single request.

func WithUserAgent added in v0.3.0

func WithUserAgent(userAgent string) clientOption

WithUserAgent overrides the client user agent

func WithUsernamePassword added in v0.2.0

func WithUsernamePassword(username string, password string) clientOption

WithUsernamePassword sets registry username and password configuration settings.

Types

type Client

type Client struct {
	*resty.Client
	Config *clientConfig
}

Client is an HTTP(s) client to make requests against an OCI registry.

func NewClient added in v0.2.0

func NewClient(address string, opts ...clientOption) (*Client, error)

NewClient builds a new Client from provided options.

func (*Client) Do

func (client *Client) Do(req *Request) (*Response, error)

Do executes a Request and returns a Response.

func (*Client) NewRequest

func (client *Client) NewRequest(method string, path string, opts ...requestOption) *Request

NewRequest builds a new Request from provided options.

func (*Client) SetDefaultName added in v0.2.0

func (client *Client) SetDefaultName(namespace string)

SetDefaultName sets the default registry namespace to use for building a Request.

type ErrorInfo added in v0.3.0

type ErrorInfo struct {
	Code    string      `json:"code"`
	Message string      `json:"message"`
	Detail  interface{} `json:"detail"`
}

ErrorInfo describes a server error returned from a registry.

type ErrorResponse added in v0.3.2

type ErrorResponse struct {
	Errors []ErrorInfo `json:"errors"`
}

type Request

type Request struct {
	*resty.Request
	// contains filtered or unexported fields
}

Request is an HTTP request to be sent to an OCI registry.

func (*Request) Execute

func (req *Request) Execute(method, url string) (*Response, error)

Execute validates a Request and executes it.

func (*Request) SetBody added in v0.2.0

func (req *Request) SetBody(body interface{}) *Request

SetBody wraps the resty SetBody and returns the request, allowing method chaining

func (*Request) SetHeader added in v0.2.1

func (req *Request) SetHeader(header, content string) *Request

SetHeader wraps the resty SetHeader and returns the request, allowing method chaining

func (*Request) SetQueryParam added in v0.2.1

func (req *Request) SetQueryParam(param, content string) *Request

SetQueryParam wraps the resty SetQueryParam and returns the request, allowing method chaining

type Response

type Response struct {
	*resty.Response
}

Response is an HTTP response returned from an OCI registry.

func (*Response) Errors added in v0.2.1

func (resp *Response) Errors() ([]ErrorInfo, error)

Errors attempts to parse a response as OCI-compliant errors array

func (*Response) GetAbsoluteLocation added in v0.2.0

func (resp *Response) GetAbsoluteLocation() string

GetAbsoluteLocation returns the full URL, including protocol and host, of the location contained in the `Location` header of the response.

func (*Response) GetRelativeLocation added in v0.2.0

func (resp *Response) GetRelativeLocation() string

GetRelativeLocation returns the path component of the URL contained in the `Location` header of the response.

func (*Response) IsUnauthorized

func (resp *Response) IsUnauthorized() bool

IsUnauthorized returns whether or not the response is a 401

type RetryCallbackFunc added in v0.5.0

type RetryCallbackFunc func(*Request) error

RetryCallbackFunc is a function that can mutate a request prior to it being retried.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL