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.
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
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())
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 ) orWithName (Request ) |
<digest> |
Content-addressable identifier | WithDigest (Request ) |
<reference> |
Tag or digest | WithReference (Request ) |
<session_id> |
Session ID for upload | WithSessionID (Request ) |
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.
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.
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:
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"))
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)
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://...
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)
}
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"
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"))
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())
}
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