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:
parent
f7e5e6601a
commit
218b1ccd4f
7
types/blob/blob.go
Normal file
7
types/blob/blob.go
Normal 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
74
types/blob/common.go
Normal 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
51
types/blob/ociconfig.go
Normal 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
129
types/blob/reader.go
Normal 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
58
types/error.go
Normal 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
116
types/manifest/common.go
Normal 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
121
types/manifest/docker1.go
Normal 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
180
types/manifest/docker2.go
Normal 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
14
types/manifest/error.go
Normal 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
297
types/manifest/manifest.go
Normal 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,
|
||||
}
|
||||
}
|
488
types/manifest/manifest_test.go
Normal file
488
types/manifest/manifest_test.go
Normal 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
165
types/manifest/oci1.go
Normal 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
50
types/manifest/unknown.go
Normal 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
28
types/mediatype.go
Normal 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
8
types/ratelimit.go
Normal 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
102
types/ref/ref.go
Normal 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
148
types/ref/ref_test.go
Normal 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
132
types/repo/repolist.go
Normal 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
|
||||
}
|
97
types/repo/repolist_test.go
Normal file
97
types/repo/repolist_test.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user