1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-18 22:44:00 +03:00

Refactoring packages under types

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell 2021-12-29 14:43:13 -05:00
parent f7e5e6601a
commit 218b1ccd4f
19 changed files with 2265 additions and 0 deletions

7
types/blob/blob.go Normal file
View File

@ -0,0 +1,7 @@
package blob
// Blob interface is used for returning blobs
type Blob interface {
Common
RawBody() ([]byte, error)
}

74
types/blob/common.go Normal file
View File

@ -0,0 +1,74 @@
package blob
import (
"net/http"
"strconv"
"github.com/opencontainers/go-digest"
"github.com/regclient/regclient/types/ref"
)
// Common interface is provided by all Blob implementations
type Common interface {
Digest() digest.Digest
Length() int64
MediaType() string
Response() *http.Response
RawHeaders() http.Header
SetMeta(r ref.Ref, d digest.Digest, cl int64)
SetResp(resp *http.Response)
}
type common struct {
r ref.Ref
digest digest.Digest
cl int64
mt string
blobSet bool
rawHeader http.Header
resp *http.Response
}
// Digest returns the provided or calculated digest of the blob
func (b *common) Digest() digest.Digest {
return b.digest
}
// Length returns the provided or calculated length of the blob
func (b *common) Length() int64 {
return b.cl
}
// MediaType returns the Content-Type header received from the registry
func (b *common) MediaType() string {
return b.mt
}
// RawHeaders returns the headers received from the registry
func (b *common) RawHeaders() http.Header {
return b.rawHeader
}
// Response returns the response associated with the blob
func (b *common) Response() *http.Response {
return b.resp
}
// SetMeta sets the various blob metadata (reference, digest, and content length)
func (b *common) SetMeta(ref ref.Ref, d digest.Digest, cl int64) {
b.r = ref
b.digest = d
b.cl = cl
}
// SetResp sets the response header data when pulling from a registry
func (b *common) SetResp(resp *http.Response) {
if resp == nil {
return
}
b.resp = resp
b.rawHeader = resp.Header
cl, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
b.cl = int64(cl)
b.mt = resp.Header.Get("Content-Type")
}

51
types/blob/ociconfig.go Normal file
View File

@ -0,0 +1,51 @@
package blob
import (
"encoding/json"
"fmt"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// OCIConfig wraps an OCI Config struct extracted from a Blob
type OCIConfig interface {
Blob
GetConfig() ociv1.Image
}
// ociConfig includes an OCI Config struct extracted from a Blob
// Image is included as an anonymous field to facilitate json and templating calls transparently
type ociConfig struct {
common
rawBody []byte
ociv1.Image
}
// NewOCIConfig creates a new BlobOCIConfig from an OCI Image
func NewOCIConfig(ociImage ociv1.Image) OCIConfig {
bc := common{
blobSet: true,
}
b := ociConfig{
common: bc,
Image: ociImage,
}
return &b
}
// GetConfig returns the original body from the request
func (b *ociConfig) GetConfig() ociv1.Image {
return b.Image
}
// RawBody returns the original body from the request
func (b *ociConfig) RawBody() ([]byte, error) {
var err error
if !b.blobSet {
return []byte{}, fmt.Errorf("Blob is not defined")
}
if len(b.rawBody) == 0 {
b.rawBody, err = json.Marshal(b.Image)
}
return b.rawBody, err
}

129
types/blob/reader.go Normal file
View File

@ -0,0 +1,129 @@
package blob
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/types/ref"
)
// Reader is an unprocessed Blob with an available ReadCloser for reading the Blob
type Reader interface {
Blob
io.ReadCloser
ToOCIConfig() (OCIConfig, error)
}
// reader is the internal struct implementing BlobReader
type reader struct {
common
readBytes int64
reader io.Reader
origRdr io.ReadCloser
digester digest.Digester
// io.ReadCloser
}
// NewReader creates a new reader
func NewReader(rdr io.ReadCloser) Reader {
digester := digest.Canonical.Digester()
digestRdr := io.TeeReader(rdr, digester.Hash())
bc := common{
r: ref.Ref{},
}
if rdr != nil {
bc.blobSet = true
}
br := reader{
common: bc,
reader: digestRdr,
origRdr: rdr,
digester: digester,
}
return &br
}
func (b *reader) Close() error {
if b.origRdr == nil {
return nil
}
return b.origRdr.Close()
}
// RawBody returns the original body from the request
func (b *reader) RawBody() ([]byte, error) {
return ioutil.ReadAll(b)
}
// Read passes through the read operation while computing the digest and tracking the size
func (b *reader) Read(p []byte) (int, error) {
size, err := b.reader.Read(p)
b.readBytes = b.readBytes + int64(size)
if err == io.EOF {
// check/save size
if b.cl == 0 {
b.cl = b.readBytes
} else if b.readBytes != b.cl {
err = fmt.Errorf("Expected size mismatch [expected %d, received %d]: %w", b.cl, b.readBytes, err)
}
// check/save digest
if b.digest == "" {
b.digest = b.digester.Digest()
} else if b.digest != b.digester.Digest() {
err = fmt.Errorf("Expected digest mismatch [expected %s, calculated %s]: %w", b.digest.String(), b.digester.Digest().String(), err)
}
}
return size, err
}
// Seek passes through the seek operation, reseting or invalidating the digest
func (b *reader) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == io.SeekCurrent {
return b.readBytes, nil
}
// cannot do an arbitrary seek and still digest without a lot more complication
if offset != 0 || whence != io.SeekStart {
return b.readBytes, fmt.Errorf("Unable to seek to arbitrary position")
}
rdrSeek, ok := b.origRdr.(io.Seeker)
if !ok {
return b.readBytes, fmt.Errorf("Seek unsupported")
}
o, err := rdrSeek.Seek(offset, whence)
if err != nil || o != 0 {
return b.readBytes, err
}
// reset internal offset and digest calculation
digester := digest.Canonical.Digester()
digestRdr := io.TeeReader(b.origRdr, digester.Hash())
b.digester = digester
b.readBytes = 0
b.reader = digestRdr
return 0, nil
}
// ToOCIConfig converts a blobReader to a BlobOCIConfig
func (b *reader) ToOCIConfig() (OCIConfig, error) {
if !b.blobSet {
return nil, fmt.Errorf("Blob is not defined")
}
if b.readBytes != 0 {
return nil, fmt.Errorf("Unable to convert after read has been performed")
}
blobBody, err := ioutil.ReadAll(b)
if err != nil {
return nil, fmt.Errorf("Error reading image config for %s: %w", b.r.CommonName(), err)
}
var ociImage ociv1.Image
err = json.Unmarshal(blobBody, &ociImage)
if err != nil {
return nil, fmt.Errorf("Error parsing image config for %s: %w", b.r.CommonName(), err)
}
// return the resulting blobOCIConfig, reuse blobCommon, setting rawBody read above, and the unmarshaled OCI image config
return &ociConfig{common: b.common, rawBody: blobBody, Image: ociImage}, nil
}

58
types/error.go Normal file
View File

@ -0,0 +1,58 @@
package types
import "errors"
var (
// ErrAllRequestsFailed when there are no mirrors left to try
ErrAllRequestsFailed = errors.New("All requests failed")
// ErrAPINotFound if an api is not available for the host
ErrAPINotFound = errors.New("API not found")
// ErrBackoffLimit maximum backoff attempts reached
ErrBackoffLimit = errors.New("Backoff limit reached")
// ErrCanceled if the context was canceled
ErrCanceled = errors.New("Context was canceled")
// ErrDigestMismatch if the expected digest wasn't received
ErrDigestMismatch = errors.New("Digest mismatch")
// ErrEmptyChallenge indicates an issue with the received challenge in the WWW-Authenticate header
ErrEmptyChallenge = errors.New("Empty challenge header")
// ErrHttpStatus if the http status code was unexpected
ErrHttpStatus = errors.New("Unexpected http status code")
// ErrInvalidChallenge indicates an issue with the received challenge in the WWW-Authenticate header
ErrInvalidChallenge = errors.New("Invalid challenge header")
// ErrMissingDigest returned when image reference does not include a digest
ErrMissingDigest = errors.New("Digest missing from image reference")
// ErrMissingLocation returned when the location header is missing
ErrMissingLocation = errors.New("Location header missing")
// ErrMissingName returned when name missing for host
ErrMissingName = errors.New("Name missing")
// ErrMissingTag returned when image reference does not include a tag
ErrMissingTag = errors.New("Tag missing from image reference")
// ErrMissingTagOrDigest returned when image reference does not include a tag or digest
ErrMissingTagOrDigest = errors.New("Tag or Digest missing from image reference")
// ErrMountReturnedLocation when a blob mount fails but a location header is received
ErrMountReturnedLocation = errors.New("Blob mount returned a location to upload")
// ErrNoNewChallenge indicates a challenge update did not result in any change
ErrNoNewChallenge = errors.New("No new challenge")
// ErrNotFound isn't there, search for your value elsewhere
ErrNotFound = errors.New("Not found")
// ErrNotImplemented returned when method has not been implemented yet
ErrNotImplemented = errors.New("Not implemented")
// ErrParsingFailed when a string cannot be parsed
ErrParsingFailed = errors.New("Parsing failed")
// ErrRateLimit when requests exceed server rate limit
ErrRateLimit = errors.New("Rate limit exceeded")
// ErrRetryNeeded indicates a request needs to be retried
ErrRetryNeeded = errors.New("Retry needed")
// ErrUnavailable when a requested value is not available
ErrUnavailable = errors.New("Unavailable")
// ErrUnauthorized when authentication fails
ErrUnauthorized = errors.New("Unauthorized")
// ErrUnsupported indicates the request was unsupported
ErrUnsupported = errors.New("Unsupported")
// ErrUnsupportedAPI happens when an API is not supported on a registry
ErrUnsupportedAPI = errors.New("Unsupported API")
// ErrUnsupportedConfigVersion happens when config file version is greater than this command supports
ErrUnsupportedConfigVersion = errors.New("Unsupported config version")
// ErrUnsupportedMediaType returned when media type is unknown or unsupported
ErrUnsupportedMediaType = errors.New("Unsupported media type")
)

116
types/manifest/common.go Normal file
View File

@ -0,0 +1,116 @@
package manifest
import (
"net/http"
"strconv"
"strings"
digest "github.com/opencontainers/go-digest"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/ref"
)
type common struct {
r ref.Ref
digest digest.Digest
mt string
manifSet bool
ratelimit types.RateLimit
rawHeader http.Header
rawBody []byte
}
// GetDigest returns the digest
func (m *common) GetDigest() digest.Digest {
return m.digest
}
// GetMediaType returns the media type
func (m *common) GetMediaType() string {
return m.mt
}
// GetRateLimit returns the rate limit when the manifest was pulled from a registry.
// This supports the headers used by Docker Hub.
func (m *common) GetRateLimit() types.RateLimit {
return m.ratelimit
}
// GetRef returns the reference from the upstream registry
func (m *common) GetRef() ref.Ref {
return m.r
}
// HasRateLimit indicates if the rate limit is set
func (m *common) HasRateLimit() bool {
return m.ratelimit.Set
}
// IsList indicates if the manifest is a docker Manifest List or OCI Index
func (m *common) IsList() bool {
switch m.mt {
case MediaTypeDocker2ManifestList, MediaTypeOCI1ManifestList:
return true
default:
return false
}
}
// IsSet indicates if the manifest is defined.
// A false indicates this is from a HEAD request, providing the digest, media-type, and other headers, but no body.
func (m *common) IsSet() bool {
return m.manifSet
}
// RawBody returns the raw body from the manifest if available.
func (m *common) RawBody() ([]byte, error) {
if len(m.rawBody) == 0 {
return m.rawBody, ErrUnavailable
}
return m.rawBody, nil
}
// RawHeaders returns any headers included when manifest was pulled from a registry.
func (m *common) RawHeaders() (http.Header, error) {
return m.rawHeader, nil
}
func (m *common) setRateLimit(header http.Header) {
// check for rate limit headers
rlLimit := header.Get("RateLimit-Limit")
rlRemain := header.Get("RateLimit-Remaining")
rlReset := header.Get("RateLimit-Reset")
if rlLimit != "" {
lpSplit := strings.Split(rlLimit, ",")
lSplit := strings.Split(lpSplit[0], ";")
rlLimitI, err := strconv.Atoi(lSplit[0])
if err != nil {
m.ratelimit.Limit = 0
} else {
m.ratelimit.Limit = rlLimitI
}
if len(lSplit) > 1 {
m.ratelimit.Policies = lpSplit
} else if len(lpSplit) > 1 {
m.ratelimit.Policies = lpSplit[1:]
}
}
if rlRemain != "" {
rSplit := strings.Split(rlRemain, ";")
rlRemainI, err := strconv.Atoi(rSplit[0])
if err != nil {
m.ratelimit.Remain = 0
} else {
m.ratelimit.Remain = rlRemainI
m.ratelimit.Set = true
}
}
if rlReset != "" {
rlResetI, err := strconv.Atoi(rlReset)
if err != nil {
m.ratelimit.Reset = 0
} else {
m.ratelimit.Reset = rlResetI
}
}
}

121
types/manifest/docker1.go Normal file
View File

@ -0,0 +1,121 @@
package manifest
import (
"bytes"
"encoding/json"
"fmt"
dockerSchema1 "github.com/docker/distribution/manifest/schema1"
digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/internal/wraperr"
)
const (
// MediaTypeDocker1Manifest deprecated media type for docker schema1 manifests
MediaTypeDocker1Manifest = "application/vnd.docker.distribution.manifest.v1+json"
// MediaTypeDocker1ManifestSigned is a deprecated schema1 manifest with jws signing
MediaTypeDocker1ManifestSigned = "application/vnd.docker.distribution.manifest.v1+prettyjws"
)
type docker1Manifest struct {
common
dockerSchema1.Manifest
}
type docker1SignedManifest struct {
common
dockerSchema1.SignedManifest
}
func (m *docker1Manifest) GetConfigDescriptor() (ociv1.Descriptor, error) {
return ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1Manifest) GetConfigDigest() (digest.Digest, error) {
return "", wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1SignedManifest) GetConfigDescriptor() (ociv1.Descriptor, error) {
return ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1SignedManifest) GetConfigDigest() (digest.Digest, error) {
return "", wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1Manifest) GetDescriptorList() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Platform descriptor list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1SignedManifest) GetDescriptorList() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Platform descriptor list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1Manifest) GetLayers() ([]ociv1.Descriptor, error) {
var dl []ociv1.Descriptor
for _, sd := range m.FSLayers {
dl = append(dl, ociv1.Descriptor{
Digest: sd.BlobSum,
})
}
return dl, nil
}
func (m *docker1SignedManifest) GetLayers() ([]ociv1.Descriptor, error) {
var dl []ociv1.Descriptor
for _, sd := range m.FSLayers {
dl = append(dl, ociv1.Descriptor{
Digest: sd.BlobSum,
})
}
return dl, nil
}
func (m *docker1Manifest) GetOrigManifest() interface{} {
return m.Manifest
}
func (m *docker1SignedManifest) GetOrigManifest() interface{} {
return m.SignedManifest
}
func (m *docker1Manifest) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
return nil, wraperr.New(fmt.Errorf("Platform lookup not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1SignedManifest) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
return nil, wraperr.New(fmt.Errorf("Platform lookup not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1Manifest) GetPlatformList() ([]*ociv1.Platform, error) {
return nil, wraperr.New(fmt.Errorf("Platform list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1SignedManifest) GetPlatformList() ([]*ociv1.Platform, error) {
return nil, wraperr.New(fmt.Errorf("Platform list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker1Manifest) MarshalJSON() ([]byte, error) {
if !m.manifSet {
return []byte{}, wraperr.New(fmt.Errorf("Manifest unavailable, perform a ManifestGet first"), ErrUnavailable)
}
if len(m.rawBody) > 0 {
return m.rawBody, nil
}
return json.Marshal((m.Manifest))
}
func (m *docker1SignedManifest) MarshalJSON() ([]byte, error) {
return m.SignedManifest.MarshalJSON()
}
func (m *docker1Manifest) MarshalPretty() ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(m.Manifest)
return buf.Bytes(), nil
}
func (m *docker1SignedManifest) MarshalPretty() ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(m.SignedManifest)
return buf.Bytes(), nil
}

180
types/manifest/docker2.go Normal file
View File

@ -0,0 +1,180 @@
package manifest
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/platforms"
dockerManifestList "github.com/docker/distribution/manifest/manifestlist"
dockerSchema2 "github.com/docker/distribution/manifest/schema2"
digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/internal/wraperr"
)
const (
// MediaTypeDocker2Manifest is the media type when pulling manifests from a v2 registry
MediaTypeDocker2Manifest = dockerSchema2.MediaTypeManifest
// MediaTypeDocker2ManifestList is the media type when pulling a manifest list from a v2 registry
MediaTypeDocker2ManifestList = dockerManifestList.MediaTypeManifestList
)
type docker2Manifest struct {
common
dockerSchema2.Manifest
}
type docker2ManifestList struct {
common
dockerManifestList.ManifestList
}
func (m *docker2Manifest) GetConfigDescriptor() (ociv1.Descriptor, error) {
return ociv1.Descriptor{
MediaType: m.Config.MediaType,
Digest: m.Config.Digest,
Size: m.Config.Size,
URLs: m.Config.URLs,
Annotations: m.Config.Annotations,
Platform: m.Config.Platform,
}, nil
}
func (m *docker2Manifest) GetConfigDigest() (digest.Digest, error) {
return m.Config.Digest, nil
}
func (m *docker2ManifestList) GetConfigDescriptor() (ociv1.Descriptor, error) {
return ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker2ManifestList) GetConfigDigest() (digest.Digest, error) {
return "", wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker2Manifest) GetDescriptorList() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Platform descriptor list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker2ManifestList) GetDescriptorList() ([]ociv1.Descriptor, error) {
dl := []ociv1.Descriptor{}
for _, d := range m.Manifests {
dl = append(dl, *dl2oDescriptor(d))
}
return dl, nil
}
func (m *docker2Manifest) GetLayers() ([]ociv1.Descriptor, error) {
var dl []ociv1.Descriptor
for _, sd := range m.Layers {
dl = append(dl, *d2oDescriptor(sd))
}
return dl, nil
}
func (m *docker2ManifestList) GetLayers() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Layers are not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker2Manifest) GetOrigManifest() interface{} {
return m.Manifest
}
func (m *docker2ManifestList) GetOrigManifest() interface{} {
return m.ManifestList
}
func (m *docker2Manifest) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
return nil, wraperr.New(fmt.Errorf("Platform lookup not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker2ManifestList) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
dl, err := m.GetDescriptorList()
if err != nil {
return nil, err
}
return getPlatformDesc(p, dl)
}
func (m *docker2Manifest) GetPlatformList() ([]*ociv1.Platform, error) {
return nil, wraperr.New(fmt.Errorf("Platform list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *docker2ManifestList) GetPlatformList() ([]*ociv1.Platform, error) {
dl, err := m.GetDescriptorList()
if err != nil {
return nil, err
}
return getPlatformList(dl)
}
func (m *docker2Manifest) MarshalJSON() ([]byte, error) {
if !m.manifSet {
return []byte{}, wraperr.New(fmt.Errorf("Manifest unavailable, perform a ManifestGet first"), ErrUnavailable)
}
if len(m.rawBody) > 0 {
return m.rawBody, nil
}
return json.Marshal((m.Manifest))
}
func (m *docker2ManifestList) MarshalJSON() ([]byte, error) {
if !m.manifSet {
return []byte{}, wraperr.New(fmt.Errorf("Manifest unavailable, perform a ManifestGet first"), ErrUnavailable)
}
if len(m.rawBody) > 0 {
return m.rawBody, nil
}
return json.Marshal((m.ManifestList))
}
func (m *docker2Manifest) MarshalPretty() ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(m.Manifest)
return buf.Bytes(), nil
}
func (m *docker2ManifestList) MarshalPretty() ([]byte, error) {
if m == nil {
return []byte{}, nil
}
buf := &bytes.Buffer{}
tw := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
if m.r.Reference != "" {
fmt.Fprintf(tw, "Name:\t%s\n", m.r.Reference)
}
fmt.Fprintf(tw, "MediaType:\t%s\n", m.mt)
fmt.Fprintf(tw, "Digest:\t%s\n", m.digest.String())
fmt.Fprintf(tw, "\t\n")
fmt.Fprintf(tw, "Manifests:\t\n")
for _, d := range m.Manifests {
fmt.Fprintf(tw, "\t\n")
dRef := m.r
if dRef.Reference != "" {
dRef.Digest = d.Digest.String()
fmt.Fprintf(tw, " Name:\t%s\n", dRef.CommonName())
} else {
fmt.Fprintf(tw, " Digest:\t%s\n", string(d.Digest))
}
fmt.Fprintf(tw, " MediaType:\t%s\n", d.MediaType)
if p := d.Platform; p.OS != "" {
fmt.Fprintf(tw, " Platform:\t%s\n", platforms.Format(*dlp2Platform(p)))
if p.OSVersion != "" {
fmt.Fprintf(tw, " OSVersion:\t%s\n", p.OSVersion)
}
if len(p.OSFeatures) > 0 {
fmt.Fprintf(tw, " OSFeatures:\t%s\n", strings.Join(p.OSFeatures, ", "))
}
}
if len(d.URLs) > 0 {
fmt.Fprintf(tw, " URLs:\t%s\n", strings.Join(d.URLs, ", "))
}
if d.Annotations != nil {
fmt.Fprintf(tw, " Annotations:\t\n")
for k, v := range d.Annotations {
fmt.Fprintf(tw, " %s:\t%s\n", k, v)
}
}
}
tw.Flush()
return buf.Bytes(), nil
}

14
types/manifest/error.go Normal file
View File

@ -0,0 +1,14 @@
package manifest
import "errors"
var (
// ErrNotFound isn't there, search for your value elsewhere
ErrNotFound = errors.New("Not found")
// ErrNotImplemented returned when method has not been implemented yet
ErrNotImplemented = errors.New("Not implemented")
// ErrUnavailable when a requested value is not available
ErrUnavailable = errors.New("Unavailable")
// ErrUnsupportedMediaType returned when media type is unknown or unsupported
ErrUnsupportedMediaType = errors.New("Unsupported media type")
)

297
types/manifest/manifest.go Normal file
View File

@ -0,0 +1,297 @@
package manifest
import (
"encoding/json"
"fmt"
"net/http"
"github.com/containerd/containerd/platforms"
dockerDistribution "github.com/docker/distribution"
dockerManifestList "github.com/docker/distribution/manifest/manifestlist"
dockerSchema1 "github.com/docker/distribution/manifest/schema1"
dockerSchema2 "github.com/docker/distribution/manifest/schema2"
digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/internal/wraperr"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/ref"
)
// Manifest abstracts the various types of manifests that are supported
type Manifest interface {
GetConfigDescriptor() (ociv1.Descriptor, error)
GetConfigDigest() (digest.Digest, error)
GetDigest() digest.Digest
GetDescriptorList() ([]ociv1.Descriptor, error)
GetLayers() ([]ociv1.Descriptor, error)
GetMediaType() string
GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error)
GetPlatformList() ([]*ociv1.Platform, error)
GetOrigManifest() interface{}
GetRateLimit() types.RateLimit
GetRef() ref.Ref
HasRateLimit() bool
IsList() bool
IsSet() bool
MarshalJSON() ([]byte, error)
RawBody() ([]byte, error)
RawHeaders() (http.Header, error)
}
// New creates a new manifest from an unparsed raw manifest
// mediaType: should be a known media-type. If empty, resp headers will be checked
// raw: body of the manifest. If empty, unset manifest for a HEAD request is returned
// ref: reference, may be unset
// header: headers from request, used to extract content type, digest, and rate limits
func New(mediaType string, raw []byte, r ref.Ref, header http.Header) (Manifest, error) {
mc := common{
r: r,
mt: mediaType,
rawBody: raw,
}
if header != nil {
mc.rawHeader = header
if mc.mt == "" {
mc.mt = header.Get("Content-Type")
}
mc.digest, _ = digest.Parse(header.Get("Docker-Content-Digest"))
mc.setRateLimit(header)
}
return fromCommon(mc)
}
// FromDescriptor creates a new manifest from a descriptor and the raw manifest bytes.
func FromDescriptor(desc ociv1.Descriptor, mBytes []byte) (Manifest, error) {
mc := common{
digest: desc.Digest,
mt: desc.MediaType,
manifSet: true,
rawBody: mBytes,
}
return fromCommon(mc)
}
// FromOrig creates a new manifest from the original upstream manifest type.
// This method should be used if you are creating a new manifest rather than pulling one from a registry.
func FromOrig(orig interface{}) (Manifest, error) {
var mt string
var m Manifest
mj, err := json.Marshal(orig)
if err != nil {
return nil, err
}
mc := common{
digest: digest.FromBytes(mj),
rawBody: mj,
manifSet: true,
}
// create manifest based on type
switch orig.(type) {
case dockerSchema1.Manifest:
mOrig := orig.(dockerSchema1.Manifest)
mt = mOrig.MediaType
mc.mt = MediaTypeDocker1Manifest
m = &docker1Manifest{
common: mc,
Manifest: mOrig,
}
case dockerSchema1.SignedManifest:
mOrig := orig.(dockerSchema1.SignedManifest)
mt = mOrig.MediaType
// recompute digest on the canonical data
mc.digest = digest.FromBytes(mOrig.Canonical)
mc.mt = MediaTypeDocker1ManifestSigned
m = &docker1SignedManifest{
common: mc,
SignedManifest: mOrig,
}
case dockerSchema2.Manifest:
mOrig := orig.(dockerSchema2.Manifest)
mt = mOrig.MediaType
mc.mt = MediaTypeDocker2Manifest
m = &docker2Manifest{
common: mc,
Manifest: mOrig,
}
case dockerManifestList.ManifestList:
mOrig := orig.(dockerManifestList.ManifestList)
mt = mOrig.MediaType
mc.mt = MediaTypeDocker2ManifestList
m = &docker2ManifestList{
common: mc,
ManifestList: mOrig,
}
case ociv1.Manifest:
mOrig := orig.(ociv1.Manifest)
mt = mOrig.MediaType
mc.mt = MediaTypeOCI1Manifest
m = &oci1Manifest{
common: mc,
Manifest: mOrig,
}
case ociv1.Index:
mOrig := orig.(ociv1.Index)
mt = mOrig.MediaType
mc.mt = MediaTypeOCI1ManifestList
m = &oci1Index{
common: mc,
Index: orig.(ociv1.Index),
}
case UnknownData:
m = &unknown{
common: mc,
UnknownData: orig.(UnknownData),
}
default:
return nil, fmt.Errorf("Unsupported type to convert to a manifest: %T", orig)
}
// verify media type
err = verifyMT(mc.mt, mt)
if err != nil {
return nil, err
}
return m, nil
}
func fromCommon(mc common) (Manifest, error) {
var err error
var m Manifest
var mt string
// compute/verify digest
if len(mc.rawBody) > 0 {
mc.manifSet = true
if mc.mt != MediaTypeDocker1ManifestSigned {
d := digest.FromBytes(mc.rawBody)
if mc.digest == "" {
mc.digest = d
} else if mc.digest != d {
return nil, fmt.Errorf("digest mismatch, expected %s, found %s", mc.digest.String(), d.String())
}
}
}
switch mc.mt {
case MediaTypeDocker1Manifest:
var mOrig dockerSchema1.Manifest
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
mt = mOrig.MediaType
}
m = &docker1Manifest{common: mc, Manifest: mOrig}
case MediaTypeDocker1ManifestSigned:
var mOrig dockerSchema1.SignedManifest
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
mt = mOrig.MediaType
d := digest.FromBytes(mOrig.Canonical)
if mc.digest == "" {
mc.digest = d
} else if mc.digest != d {
return nil, fmt.Errorf("digest mismatch, expected %s, found %s", mc.digest.String(), d.String())
}
}
m = &docker1SignedManifest{common: mc, SignedManifest: mOrig}
case MediaTypeDocker2Manifest:
var mOrig dockerSchema2.Manifest
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
mt = mOrig.MediaType
}
m = &docker2Manifest{common: mc, Manifest: mOrig}
case MediaTypeDocker2ManifestList:
var mOrig dockerManifestList.ManifestList
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
mt = mOrig.MediaType
}
m = &docker2ManifestList{common: mc, ManifestList: mOrig}
case MediaTypeOCI1Manifest:
var mOrig ociv1.Manifest
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
mt = mOrig.MediaType
}
m = &oci1Manifest{common: mc, Manifest: mOrig}
case MediaTypeOCI1ManifestList:
var mOrig ociv1.Index
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
mt = mOrig.MediaType
}
m = &oci1Index{common: mc, Index: mOrig}
default:
var mOrig UnknownData
if len(mc.rawBody) > 0 {
err = json.Unmarshal(mc.rawBody, &mOrig)
}
m = &unknown{common: mc, UnknownData: mOrig}
}
if err != nil {
return nil, fmt.Errorf("error unmarshaling manifest for %s: %w", mc.r.CommonName(), err)
}
// verify media type
err = verifyMT(mc.mt, mt)
if err != nil {
return nil, err
}
return m, nil
}
func verifyMT(expected, received string) error {
if received != "" && expected != received {
return fmt.Errorf("manifest contains an unexpected media type: expected %s, received %s", expected, received)
}
return nil
}
func getPlatformDesc(p *ociv1.Platform, dl []ociv1.Descriptor) (*ociv1.Descriptor, error) {
platformCmp := platforms.NewMatcher(*p)
for _, d := range dl {
if d.Platform != nil && platformCmp.Match(*d.Platform) {
return &d, nil
}
}
return nil, wraperr.New(fmt.Errorf("Platform not found: %s", platforms.Format(*p)), ErrNotFound)
}
func getPlatformList(dl []ociv1.Descriptor) ([]*ociv1.Platform, error) {
var l []*ociv1.Platform
for _, d := range dl {
if d.Platform != nil {
l = append(l, d.Platform)
}
}
return l, nil
}
func d2oDescriptor(sd dockerDistribution.Descriptor) *ociv1.Descriptor {
return &ociv1.Descriptor{
MediaType: sd.MediaType,
Digest: sd.Digest,
Size: sd.Size,
URLs: sd.URLs,
Annotations: sd.Annotations,
Platform: sd.Platform,
}
}
func dl2oDescriptor(sd dockerManifestList.ManifestDescriptor) *ociv1.Descriptor {
return &ociv1.Descriptor{
MediaType: sd.MediaType,
Digest: sd.Digest,
Size: sd.Size,
URLs: sd.URLs,
Annotations: sd.Annotations,
Platform: dlp2Platform(sd.Platform),
}
}
func dlp2Platform(sp dockerManifestList.PlatformSpec) *ociv1.Platform {
return &ociv1.Platform{
Architecture: sp.Architecture,
OS: sp.OS,
Variant: sp.Variant,
OSVersion: sp.OSVersion,
OSFeatures: sp.OSFeatures,
}
}

View File

@ -0,0 +1,488 @@
package manifest
import (
"encoding/json"
"fmt"
"net/http"
"testing"
dockerSchema1 "github.com/docker/distribution/manifest/schema1"
dockerSchema2 "github.com/docker/distribution/manifest/schema2"
"github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/types/ref"
)
var (
rawDockerSchema2 = []byte(`
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:10fdcbb8eac53c686023468e307adb6c0da03fc904f6739ee543143a2365be41",
"size": 3023
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:f6e2d7fa40092cf3d9817bf6ff54183d68d108a47fdf5a5e476c612626c80e14",
"size": 941
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:92365f35877078c3e558e9a66ac083fe9a8d44bdb3150bdac058380054b05972",
"size": 122412
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:fa98de7a23a1c3debba4398c982decfd8b31bcfad1ac6e5e7d800375cefbd42f",
"size": 146
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:9767ed5c27ebed39ff76afe979043e52dc7714c78d1dda8a8581965e06be2535",
"size": 3535944
}
]
}
`)
rawDockerSchema2List = []byte(`
{
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:69168abe0494a1f1e619725d23a8f85cb156a8986f342c7dc86915b551f5a711",
"size": 1152,
"platform": {
"architecture": "386",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:41b9947d8f19e154a5415c88ef71b851d37fa3ceb1de56ffe88d1b616ce503d9",
"size": 1152,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:e8baa0ddeed304ed91e91f155392462fcfab79df67f1052f92a377305dd521b6",
"size": 1152,
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v6"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:5536e52b2508b905c7f37bf120435c3c75684bab53c04467b61904be1febe5f8",
"size": 1152,
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v7"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:b302f648065bb2ba542dc75167db065781f296ef72bb504585d652b27b5079ad",
"size": 1152,
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:2d6a26eeb5a58c3c2534470f201b471778cc2ed37352775c9632e60880339e24",
"size": 1152,
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:201dd5b2dcc8793566b3d2cfa4d32eb3963028d20cc7befb3260de6d7ceac8a4",
"size": 1152,
"platform": {
"architecture": "s390x",
"os": "linux"
}
}
]
}
`)
rawAmbiguousOCI = []byte(`
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 733,
"digest": "sha256:35481f6488745b7eb5748f759b939deb063f458e9c3f9f998abc423e6652ece5"
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 657696,
"digest": "sha256:b49b96595fd4bd6de7cb7253fe5e89d242d0eb4f993b2b8280c0581c3a62ddc2"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 127,
"digest": "sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 1136676,
"digest": "sha256:c6690738d95e2b3d3c9ddfd34aa88ddce6e8d6e31c826989b869c25f8888f158"
}
],
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 659,
"digest": "sha256:bdde23183a221cc31fb66df0d93b834b11f2a0c2e8a03e6304c5e17d3cd5038f",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}
`)
rawOCIImage = []byte(`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 733,
"digest": "sha256:35481f6488745b7eb5748f759b939deb063f458e9c3f9f998abc423e6652ece5"
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 657696,
"digest": "sha256:b49b96595fd4bd6de7cb7253fe5e89d242d0eb4f993b2b8280c0581c3a62ddc2"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 127,
"digest": "sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 1136676,
"digest": "sha256:c6690738d95e2b3d3c9ddfd34aa88ddce6e8d6e31c826989b869c25f8888f158"
}
],
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 659,
"digest": "sha256:bdde23183a221cc31fb66df0d93b834b11f2a0c2e8a03e6304c5e17d3cd5038f",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}
`)
rawOCIIndex = []byte(`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 733,
"digest": "sha256:35481f6488745b7eb5748f759b939deb063f458e9c3f9f998abc423e6652ece5"
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 657696,
"digest": "sha256:b49b96595fd4bd6de7cb7253fe5e89d242d0eb4f993b2b8280c0581c3a62ddc2"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 127,
"digest": "sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 1136676,
"digest": "sha256:c6690738d95e2b3d3c9ddfd34aa88ddce6e8d6e31c826989b869c25f8888f158"
}
],
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 659,
"digest": "sha256:bdde23183a221cc31fb66df0d93b834b11f2a0c2e8a03e6304c5e17d3cd5038f",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}
`)
// signed schemas are white space sensitive, contents here must be indented with 3 spaces, no tabs
rawDockerSchema1Signed = []byte(`
{
"schemaVersion": 1,
"name": "library/debian",
"tag": "6",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:069873d23334d65630bbe5e303ced0c68181b694c7f5506b54bf5d8115b5af20"
}
],
"history": [
{
"v1Compatibility": "{\"id\":\"ff11dd0897b8ded12196819a787b5bd6d5bf886d9a7836c21b070efb5d9e77e4\",\"parent\":\"4e507d091336a8ec91e1b0fd0e33f11625d8bf3494765d3dbec37ec17387cbf5\",\"created\":\"2016-02-16T21:25:24.035599122Z\",\"container\":\"0fd99658f7a77c1170f8ff325c14437eaced7bab6b3152264cb1946d8d018e2e\",\"container_config\":{\"Hostname\":\"71f62d8ce24c\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/bin/bash\\\"]\"],\"Image\":\"4e507d091336a8ec91e1b0fd0e33f11625d8bf3494765d3dbec37ec17387cbf5\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"71f62d8ce24c\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/bash\"],\"Image\":\"4e507d091336a8ec91e1b0fd0e33f11625d8bf3494765d3dbec37ec17387cbf5\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}"
},
{
"v1Compatibility": "{\"id\":\"4e507d091336a8ec91e1b0fd0e33f11625d8bf3494765d3dbec37ec17387cbf5\",\"created\":\"2016-02-16T21:25:21.747984969Z\",\"container\":\"71f62d8ce24cd81b2835a2a4457e9e745f775a225cb2e75a5e76fc8b5f44874c\",\"container_config\":{\"Hostname\":\"71f62d8ce24c\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:09d717d62608e18d79af6b6cd5aae36f675bd5c4f34452ab1693b56bfbfe2520 in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"71f62d8ce24c\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":76534288}"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "FD6K:7VOX:ZVOM:34T7:2ZT5:753N:ZM4C:RJIF:WPOO:NPC2:7VPJ:3TVM",
"kty": "EC",
"x": "kHg6ZEbadXH4gC5ggkduHEAeJP40vdudo7tekiigA00",
"y": "K5r269kJQV1ERenXMuEQbY7_hrbxy1JnTnSOBR0bvTg"
},
"alg": "ES256"
},
"signature": "mtuG3ORjrX8o7lqyx78tX_JIX-JuiBAWX2sEvf60t4zXzLB61gNecwasp56Mn3LT7fxmJzC3-IcHW-UryDm6uw",
"protected": "eyJmb3JtYXRMZW5ndGgiOjI3NDYsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMS0xMi0xM1QxMzo0OTozNFoifQ"
}
]
}
`)
)
var ()
func TestNewManifest(t *testing.T) {
digestML := digest.FromBytes(rawDockerSchema2List)
digestInvalid := digest.FromString("invalid")
r, _ := ref.New("localhost:5000/test:latest")
var tests = []struct {
name string
mt string
raw []byte
r ref.Ref
header http.Header
wantE error
}{
{
name: "Docker Schema 2 Manifest",
mt: MediaTypeDocker2Manifest,
raw: rawDockerSchema2,
r: r,
wantE: nil,
},
{
name: "Docker Schema 2 List from Http",
header: http.Header{
"Content-Type": []string{MediaTypeDocker2ManifestList},
"Docker-Content-Digest": []string{digestML.String()},
},
raw: rawDockerSchema2List,
r: r,
wantE: nil,
},
{
name: "Docker Schema 1 Signed",
mt: MediaTypeDocker1ManifestSigned,
raw: rawDockerSchema1Signed,
r: r,
wantE: nil,
},
{
name: "Invalid Http Digest",
header: http.Header{
"Content-Type": []string{MediaTypeDocker2ManifestList},
"Docker-Content-Digest": []string{digestInvalid.String()},
},
raw: rawDockerSchema2List,
r: r,
wantE: fmt.Errorf("digest mismatch, expected %s, found %s", digestInvalid, digestML),
},
{
name: "Ambiguous OCI Image",
mt: MediaTypeOCI1Manifest,
raw: rawAmbiguousOCI,
r: r,
wantE: nil,
},
{
name: "Ambiguous OCI Index",
mt: MediaTypeOCI1ManifestList,
raw: rawAmbiguousOCI,
r: r,
wantE: nil,
},
{
name: "Invalid OCI Index",
mt: MediaTypeOCI1ManifestList,
raw: rawOCIImage,
r: r,
wantE: fmt.Errorf("manifest contains an unexpected media type: expected %s, received %s", MediaTypeOCI1ManifestList, MediaTypeOCI1Manifest),
},
{
name: "Invalid OCI Image",
mt: MediaTypeOCI1Manifest,
raw: rawOCIIndex,
r: r,
wantE: fmt.Errorf("manifest contains an unexpected media type: expected %s, received %s", MediaTypeOCI1Manifest, MediaTypeOCI1ManifestList),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(tt.mt, tt.raw, tt.r, tt.header)
if tt.wantE == nil && err != nil {
t.Errorf("failed creating manifest, err: %v", err)
} else if tt.wantE != nil && (err == nil || (tt.wantE != err && tt.wantE.Error() != err.Error())) {
t.Errorf("expected error not received, expected %v, received %v", tt.wantE, err)
}
})
}
}
func TestFromDescriptor(t *testing.T) {
digestInvalid := digest.FromString("invalid")
digestDockerSchema2 := digest.FromBytes(rawDockerSchema2)
digestDockerSchema1Signed, err := digest.Parse("sha256:f3ef067962554c3352dc0c659ca563f73cc396fe0dea2a2c23a7964c6290f782")
if err != nil {
t.Fatalf("failed to parse docker schema1 signed digest string: %v", err)
}
digestOCIImage := digest.FromBytes(rawOCIImage)
var tests = []struct {
name string
desc ociv1.Descriptor
raw []byte
wantE error
}{
{
name: "Docker Schema 2 Manifest",
desc: ociv1.Descriptor{
MediaType: MediaTypeDocker2Manifest,
Digest: digestDockerSchema2,
Size: int64(len(rawDockerSchema2)),
},
raw: rawDockerSchema2,
wantE: nil,
},
{
name: "Docker Schema 1 Signed Manifest",
desc: ociv1.Descriptor{
MediaType: MediaTypeDocker1ManifestSigned,
Digest: digestDockerSchema1Signed,
Size: int64(len(rawDockerSchema1Signed)),
},
raw: rawDockerSchema1Signed,
wantE: nil,
},
{
name: "Invalid digest",
desc: ociv1.Descriptor{
MediaType: MediaTypeDocker2Manifest,
Digest: digestInvalid,
Size: int64(len(rawDockerSchema2)),
},
raw: rawDockerSchema2,
wantE: fmt.Errorf("digest mismatch, expected %s, found %s", digestInvalid, digestDockerSchema2),
},
{
name: "Invalid Media Type",
desc: ociv1.Descriptor{
MediaType: MediaTypeOCI1ManifestList,
Digest: digestOCIImage,
Size: int64(len(rawOCIImage)),
},
raw: rawOCIImage,
wantE: fmt.Errorf("manifest contains an unexpected media type: expected %s, received %s", MediaTypeOCI1ManifestList, MediaTypeOCI1Manifest),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := FromDescriptor(tt.desc, tt.raw)
if tt.wantE == nil && err != nil {
t.Errorf("failed creating manifest, err: %v", err)
} else if tt.wantE != nil && (err == nil || (tt.wantE != err && tt.wantE.Error() != err.Error())) {
t.Errorf("expected error not received, expected %v, received %v", tt.wantE, err)
}
})
}
}
func TestFromOrig(t *testing.T) {
var manifestDockerSchema2, manifestInvalid dockerSchema2.Manifest
var manifestDockerSchema1Signed dockerSchema1.SignedManifest
err := json.Unmarshal(rawDockerSchema2, &manifestDockerSchema2)
if err != nil {
t.Fatalf("failed to unmarshal docker schema2 json: %v", err)
}
err = json.Unmarshal(rawDockerSchema2, &manifestInvalid)
if err != nil {
t.Fatalf("failed to unmarshal docker schema2 json: %v", err)
}
manifestInvalid.MediaType = MediaTypeOCI1Manifest
err = json.Unmarshal(rawDockerSchema1Signed, &manifestDockerSchema1Signed)
var tests = []struct {
name string
orig interface{}
wantE error
}{
{
name: "Nil interface",
orig: nil,
wantE: fmt.Errorf("Unsupported type to convert to a manifest: %v", nil),
},
{
name: "Docker Schema2",
orig: manifestDockerSchema2,
wantE: nil,
},
{
name: "Docker Schema1 Signed",
orig: manifestDockerSchema1Signed,
wantE: nil,
},
{
name: "Invalid Media Type",
orig: manifestInvalid,
wantE: fmt.Errorf("manifest contains an unexpected media type: expected %s, received %s", MediaTypeDocker2Manifest, MediaTypeOCI1Manifest),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := FromOrig(tt.orig)
if tt.wantE == nil && err != nil {
t.Errorf("failed creating manifest, err: %v", err)
} else if tt.wantE != nil && (err == nil || (tt.wantE != err && tt.wantE.Error() != err.Error())) {
t.Errorf("expected error not received, expected %v, received %v", tt.wantE, err)
}
})
}
}

165
types/manifest/oci1.go Normal file
View File

@ -0,0 +1,165 @@
package manifest
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/platforms"
digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/internal/wraperr"
)
const (
// MediaTypeOCI1Manifest OCI v1 manifest media type
MediaTypeOCI1Manifest = ociv1.MediaTypeImageManifest
// MediaTypeOCI1ManifestList OCI v1 manifest list media type
MediaTypeOCI1ManifestList = ociv1.MediaTypeImageIndex
)
type oci1Manifest struct {
common
ociv1.Manifest
}
type oci1Index struct {
common
ociv1.Index
}
func (m *oci1Manifest) GetConfigDescriptor() (ociv1.Descriptor, error) {
return m.Config, nil
}
func (m *oci1Manifest) GetConfigDigest() (digest.Digest, error) {
return m.Config.Digest, nil
}
func (m *oci1Index) GetConfigDescriptor() (ociv1.Descriptor, error) {
return ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *oci1Index) GetConfigDigest() (digest.Digest, error) {
return "", wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *oci1Manifest) GetDescriptorList() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Platform descriptor list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *oci1Index) GetDescriptorList() ([]ociv1.Descriptor, error) {
return m.Manifests, nil
}
func (m *oci1Manifest) GetLayers() ([]ociv1.Descriptor, error) {
return m.Layers, nil
}
func (m *oci1Index) GetLayers() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Layers are not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *oci1Manifest) GetOrigManifest() interface{} {
return m.Manifest
}
func (m *oci1Index) GetOrigManifest() interface{} {
return m.Index
}
func (m *oci1Manifest) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
return nil, wraperr.New(fmt.Errorf("Platform lookup not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *oci1Index) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
dl, err := m.GetDescriptorList()
if err != nil {
return nil, err
}
return getPlatformDesc(p, dl)
}
func (m *oci1Manifest) GetPlatformList() ([]*ociv1.Platform, error) {
return nil, wraperr.New(fmt.Errorf("Platform list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *oci1Index) GetPlatformList() ([]*ociv1.Platform, error) {
dl, err := m.GetDescriptorList()
if err != nil {
return nil, err
}
return getPlatformList(dl)
}
func (m *oci1Manifest) MarshalJSON() ([]byte, error) {
if !m.manifSet {
return []byte{}, wraperr.New(fmt.Errorf("Manifest unavailable, perform a ManifestGet first"), ErrUnavailable)
}
if len(m.rawBody) > 0 {
return m.rawBody, nil
}
return json.Marshal((m.Manifest))
}
func (m *oci1Index) MarshalJSON() ([]byte, error) {
if !m.manifSet {
return []byte{}, wraperr.New(fmt.Errorf("Manifest unavailable, perform a ManifestGet first"), ErrUnavailable)
}
if len(m.rawBody) > 0 {
return m.rawBody, nil
}
return json.Marshal((m.Index))
}
func (m *oci1Manifest) MarshalPretty() ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(m.Manifest)
return buf.Bytes(), nil
}
func (m *oci1Index) MarshalPretty() ([]byte, error) {
if m == nil {
return []byte{}, nil
}
buf := &bytes.Buffer{}
tw := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
if m.r.Reference != "" {
fmt.Fprintf(tw, "Name:\t%s\n", m.r.Reference)
}
fmt.Fprintf(tw, "MediaType:\t%s\n", m.mt)
fmt.Fprintf(tw, "Digest:\t%s\n", m.digest.String())
fmt.Fprintf(tw, "\t\n")
fmt.Fprintf(tw, "Manifests:\t\n")
for _, d := range m.Manifests {
fmt.Fprintf(tw, "\t\n")
dRef := m.r
if dRef.Reference != "" {
dRef.Digest = d.Digest.String()
fmt.Fprintf(tw, " Name:\t%s\n", dRef.CommonName())
} else {
fmt.Fprintf(tw, " Digest:\t%s\n", string(d.Digest))
}
fmt.Fprintf(tw, " MediaType:\t%s\n", d.MediaType)
if d.Platform != nil {
if p := d.Platform; p.OS != "" {
fmt.Fprintf(tw, " Platform:\t%s\n", platforms.Format(*p))
if p.OSVersion != "" {
fmt.Fprintf(tw, " OSVersion:\t%s\n", p.OSVersion)
}
if len(p.OSFeatures) > 0 {
fmt.Fprintf(tw, " OSFeatures:\t%s\n", strings.Join(p.OSFeatures, ", "))
}
}
}
if len(d.URLs) > 0 {
fmt.Fprintf(tw, " URLs:\t%s\n", strings.Join(d.URLs, ", "))
}
if d.Annotations != nil {
fmt.Fprintf(tw, " Annotations:\t\n")
for k, v := range d.Annotations {
fmt.Fprintf(tw, " %s:\t%s\n", k, v)
}
}
}
tw.Flush()
return buf.Bytes(), nil
}

50
types/manifest/unknown.go Normal file
View File

@ -0,0 +1,50 @@
package manifest
import (
"fmt"
digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/internal/wraperr"
)
type unknown struct {
common
UnknownData
}
type UnknownData struct {
Data map[string]interface{}
}
func (m *unknown) GetConfigDescriptor() (ociv1.Descriptor, error) {
return ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *unknown) GetConfigDigest() (digest.Digest, error) {
return "", wraperr.New(fmt.Errorf("Config digest not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *unknown) GetDescriptorList() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Platform descriptor list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *unknown) GetLayers() ([]ociv1.Descriptor, error) {
return []ociv1.Descriptor{}, wraperr.New(fmt.Errorf("Layer list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *unknown) GetOrigManifest() interface{} {
return m.UnknownData
}
func (m *unknown) GetPlatformDesc(p *ociv1.Platform) (*ociv1.Descriptor, error) {
return nil, wraperr.New(fmt.Errorf("Platform lookup not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *unknown) GetPlatformList() ([]*ociv1.Platform, error) {
return nil, wraperr.New(fmt.Errorf("Platform list not available for media type %s", m.mt), ErrUnsupportedMediaType)
}
func (m *unknown) MarshalJSON() ([]byte, error) {
return m.rawBody, nil
}

28
types/mediatype.go Normal file
View File

@ -0,0 +1,28 @@
package types
const (
// MediaTypeDocker1Manifest deprecated media type for docker schema1 manifests
MediaTypeDocker1Manifest = "application/vnd.docker.distribution.manifest.v1+json"
// MediaTypeDocker1ManifestSigned is a deprecated schema1 manifest with jws signing
MediaTypeDocker1ManifestSigned = "application/vnd.docker.distribution.manifest.v1+prettyjws"
// MediaTypeDocker2Manifest is the media type when pulling manifests from a v2 registry
MediaTypeDocker2Manifest = "application/vnd.docker.distribution.manifest.v2+json"
// MediaTypeDocker2ManifestList is the media type when pulling a manifest list from a v2 registry
MediaTypeDocker2ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
// MediaTypeDocker2ImageConfig is for the configuration json object media type
MediaTypeDocker2ImageConfig = "application/vnd.docker.container.image.v1+json"
// MediaTypeOCI1Manifest OCI v1 manifest media type
MediaTypeOCI1Manifest = "application/vnd.oci.image.manifest.v1+json"
// MediaTypeOCI1ManifestList OCI v1 manifest list media type
MediaTypeOCI1ManifestList = "application/vnd.oci.image.index.v1+json"
// MediaTypeOCI1ImageConfig OCI v1 configuration json object media type
MediaTypeOCI1ImageConfig = "application/vnd.oci.image.config.v1+json"
// MediaTypeDocker2Layer is the default compressed layer for docker schema2
MediaTypeDocker2Layer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
// MediaTypeOCI1Layer is the uncompressed layer for OCIv1
MediaTypeOCI1Layer = "application/vnd.oci.image.layer.v1.tar"
// MediaTypeOCI1LayerGzip is the gzip compressed layer for OCI v1
MediaTypeOCI1LayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip"
// MediaTypeBuildkitCacheConfig is used by buildkit cache images
MediaTypeBuildkitCacheConfig = "application/vnd.buildkit.cacheconfig.v0"
)

8
types/ratelimit.go Normal file
View File

@ -0,0 +1,8 @@
package types
// RateLimit is returned from some http requests
type RateLimit struct {
Remain, Limit, Reset int
Set bool
Policies []string
}

102
types/ref/ref.go Normal file
View File

@ -0,0 +1,102 @@
package ref
import (
"fmt"
"regexp"
"github.com/docker/distribution/reference"
)
var (
pathS = `[/a-zA-Z0-9_\-. ]+`
tagS = `[\w][\w.-]{0,127}`
digestS = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
schemeRE = regexp.MustCompile(`^([a-z]+)://(.+)$`)
pathRE = regexp.MustCompile(`^(` + pathS + `)` +
`(?:` + regexp.QuoteMeta(`:`) + `(` + tagS + `))?` +
`(?:` + regexp.QuoteMeta(`@`) + `(` + digestS + `))?$`)
)
// Ref reference to a registry/repository
// If the tag or digest is available, it's also included in the reference.
// Reference itself is the unparsed string.
// While this is currently a struct, that may change in the future and access
// to contents should not be assumed/used.
type Ref struct {
Scheme string
Reference string // unparsed string
Registry string // server, host:port
Repository string // path on server
Tag string
Digest string
Path string
}
// New returns a reference based on the scheme, defaulting to a
func New(ref string) (Ref, error) {
scheme := ""
path := ref
matchScheme := schemeRE.FindStringSubmatch(ref)
if matchScheme != nil && len(matchScheme) == 3 {
scheme = matchScheme[1]
path = matchScheme[2]
}
ret := Ref{
Scheme: scheme,
Reference: ref,
}
switch scheme {
case "":
ret.Scheme = "reg"
parsed, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return ret, err
}
ret.Registry = reference.Domain(parsed)
ret.Repository = reference.Path(parsed)
if canonical, ok := parsed.(reference.Canonical); ok {
ret.Digest = canonical.Digest().String()
}
if tagged, ok := parsed.(reference.Tagged); ok {
ret.Tag = tagged.Tag()
}
if ret.Tag == "" && ret.Digest == "" {
ret.Tag = "latest"
}
case "ocidir", "ocifile":
matchPath := pathRE.FindStringSubmatch(path)
if matchPath == nil || len(matchPath) < 2 || matchPath[1] == "" {
return Ref{}, fmt.Errorf("invalid path for scheme \"%s\": %s", scheme, path)
}
ret.Path = matchPath[1]
if len(matchPath) > 2 && matchPath[2] != "" {
ret.Tag = matchPath[2]
}
if len(matchPath) > 3 && matchPath[3] != "" {
ret.Digest = matchPath[3]
}
default:
return Ref{}, fmt.Errorf("unhandled reference scheme \"%s\" in \"%s\"", scheme, ref)
}
return ret, nil
}
// CommonName outputs a parsable name from a reference
func (r Ref) CommonName() string {
cn := ""
if r.Registry != "" {
cn = r.Registry + "/"
}
if r.Repository == "" {
return ""
}
cn = cn + r.Repository
if r.Digest != "" {
cn = cn + "@" + r.Digest
} else if r.Tag != "" {
cn = cn + ":" + r.Tag
}
return cn
}

148
types/ref/ref_test.go Normal file
View File

@ -0,0 +1,148 @@
package ref
import (
"fmt"
"testing"
)
func TestRef(t *testing.T) {
var tests = []struct {
name string
ref string
scheme string
registry string
repository string
tag string
digest string
path string
wantE error
}{
{
name: "Docker library",
ref: "alpine",
scheme: "reg",
registry: "docker.io",
repository: "library/alpine",
tag: "latest",
digest: "",
path: "",
wantE: nil,
},
{
name: "Local registry",
ref: "localhost:5000/group/image:v42",
scheme: "reg",
registry: "localhost:5000",
repository: "group/image",
tag: "v42",
digest: "",
path: "",
wantE: nil,
},
{
name: "OCI file",
ref: "ocifile://path/to/file.tgz",
scheme: "ocifile",
registry: "",
repository: "",
tag: "",
digest: "",
path: "path/to/file.tgz",
wantE: nil,
},
{
name: "OCI file with tag",
ref: "ocifile://path/to/file.tgz:v1.2.3",
scheme: "ocifile",
registry: "",
repository: "",
tag: "v1.2.3",
digest: "",
path: "path/to/file.tgz",
wantE: nil,
},
{
name: "OCI file with digest",
ref: "ocifile://path/to/file.tgz@sha256:15f840677a5e245d9ea199eb9b026b1539208a5183621dced7b469f6aa678115",
scheme: "ocifile",
registry: "",
repository: "",
tag: "",
digest: "sha256:15f840677a5e245d9ea199eb9b026b1539208a5183621dced7b469f6aa678115",
path: "path/to/file.tgz",
wantE: nil,
},
{
name: "OCI file with invalid digest",
ref: "ocifile://path/to/file.tgz@sha256:ZZ15f840677a5e245d9ea199eb9b026b1539208a5183621dced7b469f6aa678115ZZ",
wantE: fmt.Errorf("invalid path for scheme \"ocifile\": path/to/file.tgz@sha256:ZZ15f840677a5e245d9ea199eb9b026b1539208a5183621dced7b469f6aa678115ZZ"),
},
{
name: "OCI dir",
ref: "ocidir://path/to/dir",
scheme: "ocidir",
registry: "",
repository: "",
tag: "",
digest: "",
path: "path/to/dir",
wantE: nil,
},
{
name: "OCI dir with tag",
ref: "ocidir://path/to/dir:v1.2.3",
scheme: "ocidir",
registry: "",
repository: "",
tag: "v1.2.3",
digest: "",
path: "path/to/dir",
wantE: nil,
},
{
name: "OCI dir with digest",
ref: "ocidir://path/to/dir@sha256:15f840677a5e245d9ea199eb9b026b1539208a5183621dced7b469f6aa678115",
scheme: "ocidir",
registry: "",
repository: "",
tag: "",
digest: "sha256:15f840677a5e245d9ea199eb9b026b1539208a5183621dced7b469f6aa678115",
path: "path/to/dir",
wantE: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, err := New(tt.ref)
if tt.wantE == nil && err != nil {
t.Errorf("failed creating reference, err: %v", err)
return
} else if tt.wantE != nil && (err == nil || (tt.wantE != err && tt.wantE.Error() != err.Error())) {
t.Errorf("expected error not received, expected %v, received %v", tt.wantE, err)
return
} else if tt.wantE != nil {
return
}
if tt.scheme != ref.Scheme {
t.Errorf("scheme mismatch for %s, expected %s, received %s", tt.ref, tt.scheme, ref.Scheme)
}
if tt.registry != ref.Registry {
t.Errorf("registry mismatch for %s, expected %s, received %s", tt.ref, tt.registry, ref.Registry)
}
if tt.repository != ref.Repository {
t.Errorf("repository mismatch for %s, expected %s, received %s", tt.ref, tt.repository, ref.Repository)
}
if tt.tag != ref.Tag {
t.Errorf("tag mismatch for %s, expected %s, received %s", tt.ref, tt.tag, ref.Tag)
}
if tt.digest != ref.Digest {
t.Errorf("digest mismatch for %s, expected %s, received %s", tt.ref, tt.digest, ref.Digest)
}
if tt.path != ref.Path {
t.Errorf("path mismatch for %s, expected %s, received %s", tt.ref, tt.path, ref.Path)
}
})
}
}

132
types/repo/repolist.go Normal file
View File

@ -0,0 +1,132 @@
package repo
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"github.com/regclient/regclient/types"
)
type RepoList struct {
repoCommon
RepoRegistryList
}
type repoCommon struct {
host string
mt string
orig interface{}
rawHeader http.Header
rawBody []byte
}
type repoConfig struct {
host string
mt string
raw []byte
header http.Header
}
type Opts func(*repoConfig)
func New(opts ...Opts) (*RepoList, error) {
conf := repoConfig{
mt: "application/json",
}
for _, opt := range opts {
opt(&conf)
}
rl := RepoList{}
rc := repoCommon{
mt: conf.mt,
rawHeader: conf.header,
rawBody: conf.raw,
}
mt := strings.Split(conf.mt, ";")[0] // "application/json; charset=utf-8" -> "application/json"
switch mt {
case "application/json", "text/plain":
err := json.Unmarshal(conf.raw, &rl)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w: media type: %s, hostname: %s", types.ErrUnsupportedMediaType, conf.mt, conf.host)
}
rl.repoCommon = rc
return &rl, nil
}
func WithHeaders(header http.Header) Opts {
return func(c *repoConfig) {
c.header = header
}
}
func WithHost(host string) Opts {
return func(c *repoConfig) {
c.host = host
}
}
func WithMT(mt string) Opts {
return func(c *repoConfig) {
c.mt = mt
}
}
func WithRaw(raw []byte) Opts {
return func(c *repoConfig) {
c.raw = raw
}
}
// RepoRegistryList is a list of repositories from the _catalog API
type RepoRegistryList struct {
Repositories []string `json:"repositories"`
}
func (r repoCommon) GetOrig() interface{} {
return r.orig
}
func (r repoCommon) MarshalJSON() ([]byte, error) {
if len(r.rawBody) > 0 {
return r.rawBody, nil
}
if r.orig != nil {
return json.Marshal((r.orig))
}
return []byte{}, fmt.Errorf("Json marshalling failed: %w", types.ErrNotFound)
}
func (r repoCommon) RawBody() ([]byte, error) {
return r.rawBody, nil
}
func (r repoCommon) RawHeaders() (http.Header, error) {
return r.rawHeader, nil
}
// GetRepos returns the repositories
func (rl RepoRegistryList) GetRepos() ([]string, error) {
return rl.Repositories, nil
}
// MarshalPretty is used for printPretty template formatting
func (rl RepoRegistryList) MarshalPretty() ([]byte, error) {
sort.Slice(rl.Repositories, func(i, j int) bool {
if strings.Compare(rl.Repositories[i], rl.Repositories[j]) < 0 {
return true
}
return false
})
buf := &bytes.Buffer{}
for _, tag := range rl.Repositories {
fmt.Fprintf(buf, "%s\n", tag)
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,97 @@
package repo
import (
"bytes"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"github.com/regclient/regclient/types"
)
func TestNew(t *testing.T) {
emptyRaw := []byte("{}")
registryList := []string{"library/alpine", "library/debian", "library/golang"}
registryRaw := []byte(fmt.Sprintf(`{"repositories":["%s"]}`, strings.Join(registryList, `","`)))
registryHost := "localhost:5000"
registryMT := "application/json; charset=utf-8"
registryHeaders := http.Header{
"Content-Type": []string{registryMT},
}
tests := []struct {
name string
opts []Opts
// all remaining fields are expected results from creating a tag with opts
err error
raw []byte
repos []string
}{
{
name: "Empty",
opts: []Opts{
WithRaw(emptyRaw),
},
raw: emptyRaw,
},
{
name: "Registry",
opts: []Opts{
WithHost(registryHost),
WithRaw(registryRaw),
WithHeaders(registryHeaders),
WithMT(registryMT),
},
raw: registryRaw,
repos: registryList,
},
{
name: "Unknown MT",
opts: []Opts{
WithHost(registryHost),
WithRaw(registryRaw),
WithHeaders(registryHeaders),
WithMT("application/unknown"),
},
err: types.ErrUnsupportedMediaType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rl, err := New(tt.opts...)
if tt.err != nil {
if err == nil || !errors.Is(err, tt.err) {
t.Errorf("expected error not found, expected %v, received %v", tt.err, err)
}
return
}
if err != nil {
t.Errorf("error creating tag list: %v", err)
return
}
raw, err := rl.RawBody()
if bytes.Compare(tt.raw, raw) != 0 {
t.Errorf("unexpected raw body: expected %s, received %s", tt.raw, raw)
}
repos, err := rl.GetRepos()
if cmpSliceString(tt.repos, repos) == false {
t.Errorf("unexpected repo list: expected %v, received %v", tt.repos, repos)
}
})
}
}
func cmpSliceString(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}