mirror of
https://github.com/regclient/regclient.git
synced 2025-07-29 09:01:11 +03:00
Refactoring the blob package
The main goal was to remove unnecessary interfaces. To avoid breaking users, type aliases were used on the old interface names. Comments were updated to better align with the godoc style. Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
@ -1,19 +1,39 @@
|
|||||||
// Package blob is the underlying type for pushing and pulling blobs
|
// Package blob is the underlying type for pushing and pulling blobs.
|
||||||
package blob
|
package blob
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/regclient/regclient/types"
|
"github.com/regclient/regclient/types"
|
||||||
v1 "github.com/regclient/regclient/types/oci/v1"
|
v1 "github.com/regclient/regclient/types/oci/v1"
|
||||||
"github.com/regclient/regclient/types/ref"
|
"github.com/regclient/regclient/types/ref"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blob interface is used for returning blobs
|
// Blob interface is used for returning blobs.
|
||||||
type Blob interface {
|
type Blob interface {
|
||||||
Common
|
// GetDescriptor returns the descriptor associated with the blob.
|
||||||
|
GetDescriptor() types.Descriptor
|
||||||
|
// RawBody returns the raw content of the blob.
|
||||||
RawBody() ([]byte, error)
|
RawBody() ([]byte, error)
|
||||||
|
// RawHeaders returns the headers received from the registry.
|
||||||
|
RawHeaders() http.Header
|
||||||
|
// Response returns the response associated with the blob.
|
||||||
|
Response() *http.Response
|
||||||
|
|
||||||
|
// Digest returns the provided or calculated digest of the blob.
|
||||||
|
//
|
||||||
|
// Deprecated: Digest should be replaced by GetDescriptor().Digest.
|
||||||
|
Digest() digest.Digest
|
||||||
|
// Length returns the provided or calculated length of the blob.
|
||||||
|
//
|
||||||
|
// Deprecated: Length should be replaced by GetDescriptor().Size.
|
||||||
|
Length() int64
|
||||||
|
// MediaType returns the Content-Type header received from the registry.
|
||||||
|
//
|
||||||
|
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType.
|
||||||
|
MediaType() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type blobConfig struct {
|
type blobConfig struct {
|
||||||
@ -26,51 +46,52 @@ type blobConfig struct {
|
|||||||
rawBody []byte
|
rawBody []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opts is used for options to create a new blob.
|
||||||
type Opts func(*blobConfig)
|
type Opts func(*blobConfig)
|
||||||
|
|
||||||
// WithDesc specifies the descriptor associated with the blob
|
// WithDesc specifies the descriptor associated with the blob.
|
||||||
func WithDesc(d types.Descriptor) Opts {
|
func WithDesc(d types.Descriptor) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.desc = d
|
bc.desc = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHeader defines the headers received when pulling a blob
|
// WithHeader defines the headers received when pulling a blob.
|
||||||
func WithHeader(header http.Header) Opts {
|
func WithHeader(header http.Header) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.header = header
|
bc.header = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithImage provides the OCI Image config needed for config blobs
|
// WithImage provides the OCI Image config needed for config blobs.
|
||||||
func WithImage(image v1.Image) Opts {
|
func WithImage(image v1.Image) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.image = &image
|
bc.image = &image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithRawBody defines the raw blob contents for OCIConfig
|
// WithRawBody defines the raw blob contents for OCIConfig.
|
||||||
func WithRawBody(raw []byte) Opts {
|
func WithRawBody(raw []byte) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.rawBody = raw
|
bc.rawBody = raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithReader defines the reader for a new blob
|
// WithReader defines the reader for a new blob.
|
||||||
func WithReader(rc io.Reader) Opts {
|
func WithReader(rc io.Reader) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.rdr = rc
|
bc.rdr = rc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithRef specifies the reference where the blob was pulled from
|
// WithRef specifies the reference where the blob was pulled from.
|
||||||
func WithRef(r ref.Ref) Opts {
|
func WithRef(r ref.Ref) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.r = r
|
bc.r = r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithResp includes the http response, which is used to extract the headers and reader
|
// WithResp includes the http response, which is used to extract the headers and reader.
|
||||||
func WithResp(resp *http.Response) Opts {
|
func WithResp(resp *http.Response) Opts {
|
||||||
return func(bc *blobConfig) {
|
return func(bc *blobConfig) {
|
||||||
bc.resp = resp
|
bc.resp = resp
|
||||||
|
@ -257,16 +257,11 @@ func TestReader(t *testing.T) {
|
|||||||
if i != bl {
|
if i != bl {
|
||||||
t.Errorf("read length, expected %d, received %d", bl, i)
|
t.Errorf("read length, expected %d, received %d", bl, i)
|
||||||
}
|
}
|
||||||
bSeek, ok := b.(io.Seeker)
|
_, err = b.Seek(5, io.SeekStart)
|
||||||
if !ok {
|
|
||||||
t.Errorf("seek interface missing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = bSeek.Seek(5, io.SeekStart)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("seek to non-zero position did not fail")
|
t.Errorf("seek to non-zero position did not fail")
|
||||||
}
|
}
|
||||||
pos, err := bSeek.Seek(0, io.SeekStart)
|
pos, err := b.Seek(0, io.SeekStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("seek err: %v", err)
|
t.Errorf("seek err: %v", err)
|
||||||
return
|
return
|
||||||
@ -298,12 +293,7 @@ func TestReader(t *testing.T) {
|
|||||||
if i != bl {
|
if i != bl {
|
||||||
t.Errorf("read length, expected %d, received %d", bl, i)
|
t.Errorf("read length, expected %d, received %d", bl, i)
|
||||||
}
|
}
|
||||||
bSeek, ok = b.(io.Seeker)
|
_, err = b.Seek(0, io.SeekStart)
|
||||||
if !ok {
|
|
||||||
t.Errorf("seek interface missing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = bSeek.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("seek err: %v", err)
|
t.Errorf("seek err: %v", err)
|
||||||
return
|
return
|
||||||
@ -372,10 +362,12 @@ func TestOCI(t *testing.T) {
|
|||||||
t.Errorf("failed to unmarshal exBlob: %v", err)
|
t.Errorf("failed to unmarshal exBlob: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
opts []Opts
|
opts []Opts
|
||||||
|
fromJSON []byte
|
||||||
wantRaw []byte
|
wantRaw []byte
|
||||||
|
wantJSON []byte
|
||||||
wantDesc types.Descriptor
|
wantDesc types.Descriptor
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -386,6 +378,16 @@ func TestOCI(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantDesc: exDesc,
|
wantDesc: exDesc,
|
||||||
wantRaw: exBlob,
|
wantRaw: exBlob,
|
||||||
|
wantJSON: exBlob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSONMarshal",
|
||||||
|
opts: []Opts{
|
||||||
|
WithDesc(exDesc),
|
||||||
|
},
|
||||||
|
fromJSON: exBlob,
|
||||||
|
wantDesc: exDesc,
|
||||||
|
wantJSON: exBlob,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Config with Default Desc",
|
name: "Config with Default Desc",
|
||||||
@ -404,27 +406,43 @@ func TestOCI(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tc := range tt {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
oc := NewOCIConfig(tt.opts...)
|
oc := NewOCIConfig(tc.opts...)
|
||||||
|
|
||||||
if tt.wantDesc.Digest != "" && tt.wantDesc.Digest != oc.GetDescriptor().Digest {
|
if len(tc.fromJSON) > 0 {
|
||||||
t.Errorf("digest, expected %s, received %s", tt.wantDesc.Digest, oc.GetDescriptor().Digest)
|
err := oc.UnmarshalJSON(tc.fromJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to unmarshal json: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if tt.wantDesc.MediaType != "" && tt.wantDesc.MediaType != oc.GetDescriptor().MediaType {
|
if tc.wantDesc.Digest != "" && tc.wantDesc.Digest != oc.GetDescriptor().Digest {
|
||||||
t.Errorf("media type, expected %s, received %s", tt.wantDesc.MediaType, oc.GetDescriptor().MediaType)
|
t.Errorf("digest, expected %s, received %s", tc.wantDesc.Digest, oc.GetDescriptor().Digest)
|
||||||
}
|
}
|
||||||
if tt.wantDesc.Size > 0 && tt.wantDesc.Size != oc.GetDescriptor().Size {
|
if tc.wantDesc.MediaType != "" && tc.wantDesc.MediaType != oc.GetDescriptor().MediaType {
|
||||||
t.Errorf("size, expected %d, received %d", tt.wantDesc.Size, oc.GetDescriptor().Size)
|
t.Errorf("media type, expected %s, received %s", tc.wantDesc.MediaType, oc.GetDescriptor().MediaType)
|
||||||
}
|
}
|
||||||
if len(tt.wantRaw) > 0 {
|
if tc.wantDesc.Size > 0 && tc.wantDesc.Size != oc.GetDescriptor().Size {
|
||||||
|
t.Errorf("size, expected %d, received %d", tc.wantDesc.Size, oc.GetDescriptor().Size)
|
||||||
|
}
|
||||||
|
if len(tc.wantRaw) > 0 {
|
||||||
raw, err := oc.RawBody()
|
raw, err := oc.RawBody()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("config rawbody: %v", err)
|
t.Errorf("config rawbody: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !bytes.Equal(tt.wantRaw, raw) {
|
if !bytes.Equal(tc.wantRaw, raw) {
|
||||||
t.Errorf("config bytes, expected %s, received %s", string(tt.wantRaw), string(raw))
|
t.Errorf("config bytes, expected %s, received %s", string(tc.wantRaw), string(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tc.wantJSON) > 0 {
|
||||||
|
ocJSON, err := oc.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("json marshal: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !bytes.Equal(tc.wantJSON, ocJSON) {
|
||||||
|
t.Errorf("json marshal, expected %s, received %s", string(tc.wantJSON), string(ocJSON))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -476,7 +494,7 @@ func TestTarReader(t *testing.T) {
|
|||||||
fh.Close()
|
fh.Close()
|
||||||
dig := digger.Digest()
|
dig := digger.Digest()
|
||||||
|
|
||||||
tests := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
opts []Opts
|
opts []Opts
|
||||||
errClose bool
|
errClose bool
|
||||||
@ -507,14 +525,14 @@ func TestTarReader(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tc := range tt {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
fh, err := os.Open(fileLayer)
|
fh, err := os.Open(fileLayer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to open test data: %v", err)
|
t.Errorf("failed to open test data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts := append(tt.opts, WithReader(fh))
|
opts := append(tc.opts, WithReader(fh))
|
||||||
btr := NewTarReader(opts...)
|
btr := NewTarReader(opts...)
|
||||||
tr, err := btr.GetTarReader()
|
tr, err := btr.GetTarReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -542,9 +560,9 @@ func TestTarReader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = btr.Close()
|
err = btr.Close()
|
||||||
if !tt.errClose && err != nil {
|
if !tc.errClose && err != nil {
|
||||||
t.Errorf("failed to close tar reader: %v", err)
|
t.Errorf("failed to close tar reader: %v", err)
|
||||||
} else if tt.errClose && err == nil {
|
} else if tc.errClose && err == nil {
|
||||||
t.Errorf("close did not fail")
|
t.Errorf("close did not fail")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -552,7 +570,7 @@ func TestTarReader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReadFile(t *testing.T) {
|
func TestReadFile(t *testing.T) {
|
||||||
tests := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
filename string
|
filename string
|
||||||
content string
|
content string
|
||||||
@ -600,20 +618,20 @@ func TestReadFile(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
blobDigest := digest.FromBytes(fileBytes)
|
blobDigest := digest.FromBytes(fileBytes)
|
||||||
for _, tt := range tests {
|
for _, tc := range tt {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
fh, err := os.Open(fileLayerWH)
|
fh, err := os.Open(fileLayerWH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to open test data: %v", err)
|
t.Errorf("failed to open test data: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
btr := NewTarReader(WithReader(fh), WithDesc(types.Descriptor{Size: int64(len(fileBytes)), Digest: blobDigest, MediaType: types.MediaTypeOCI1Layer}))
|
btr := NewTarReader(WithReader(fh), WithDesc(types.Descriptor{Size: int64(len(fileBytes)), Digest: blobDigest, MediaType: types.MediaTypeOCI1Layer}))
|
||||||
th, rdr, err := btr.ReadFile(tt.filename)
|
th, rdr, err := btr.ReadFile(tc.filename)
|
||||||
if tt.expectErr != nil {
|
if tc.expectErr != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("ReadFile did not fail")
|
t.Errorf("ReadFile did not fail")
|
||||||
} else if !errors.Is(err, tt.expectErr) && err.Error() != tt.expectErr.Error() {
|
} else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
|
||||||
t.Errorf("unexpected error, expected %v, received %v", tt.expectErr, err)
|
t.Errorf("unexpected error, expected %v, received %v", tc.expectErr, err)
|
||||||
}
|
}
|
||||||
err = btr.Close()
|
err = btr.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -640,8 +658,8 @@ func TestReadFile(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed reading file: %v", err)
|
t.Errorf("failed reading file: %v", err)
|
||||||
}
|
}
|
||||||
if tt.content != string(content) {
|
if tc.content != string(content) {
|
||||||
t.Errorf("file content mismatch: expected %s, received %s", tt.content, string(content))
|
t.Errorf("file content mismatch: expected %s, received %s", tc.content, string(content))
|
||||||
}
|
}
|
||||||
err = btr.Close()
|
err = btr.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -12,21 +12,11 @@ import (
|
|||||||
"github.com/regclient/regclient/types/ref"
|
"github.com/regclient/regclient/types/ref"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common interface is provided by all Blob implementations
|
// Common was previously an interface. A type alias is provided for upgrades.
|
||||||
type Common interface {
|
type Common = *BCommon
|
||||||
GetDescriptor() types.Descriptor
|
|
||||||
Response() *http.Response
|
|
||||||
RawHeaders() http.Header
|
|
||||||
|
|
||||||
// Deprecated: Digest should be replaced by GetDescriptor().Digest
|
// BCommon is a common struct for all blobs which includes various shared methods.
|
||||||
Digest() digest.Digest
|
type BCommon struct {
|
||||||
// Deprecated: Length should be replaced by GetDescriptor().Size
|
|
||||||
Length() int64
|
|
||||||
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType
|
|
||||||
MediaType() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type common struct {
|
|
||||||
r ref.Ref
|
r ref.Ref
|
||||||
desc types.Descriptor
|
desc types.Descriptor
|
||||||
blobSet bool
|
blobSet bool
|
||||||
@ -34,32 +24,38 @@ type common struct {
|
|||||||
resp *http.Response
|
resp *http.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDescriptor returns the descriptor associated with the blob
|
// GetDescriptor returns the descriptor associated with the blob.
|
||||||
func (b *common) GetDescriptor() types.Descriptor {
|
func (c *BCommon) GetDescriptor() types.Descriptor {
|
||||||
return b.desc
|
return c.desc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Digest returns the provided or calculated digest of the blob
|
// Digest returns the provided or calculated digest of the blob.
|
||||||
func (b *common) Digest() digest.Digest {
|
//
|
||||||
return b.desc.Digest
|
// Deprecated: Digest should be replaced by GetDescriptor().Digest.
|
||||||
|
func (c *BCommon) Digest() digest.Digest {
|
||||||
|
return c.desc.Digest
|
||||||
}
|
}
|
||||||
|
|
||||||
// Length returns the provided or calculated length of the blob
|
// Length returns the provided or calculated length of the blob.
|
||||||
func (b *common) Length() int64 {
|
//
|
||||||
return b.desc.Size
|
// Deprecated: Length should be replaced by GetDescriptor().Size.
|
||||||
|
func (c *BCommon) Length() int64 {
|
||||||
|
return c.desc.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaType returns the Content-Type header received from the registry
|
// MediaType returns the Content-Type header received from the registry.
|
||||||
func (b *common) MediaType() string {
|
//
|
||||||
return b.desc.MediaType
|
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType.
|
||||||
|
func (c *BCommon) MediaType() string {
|
||||||
|
return c.desc.MediaType
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawHeaders returns the headers received from the registry
|
// RawHeaders returns the headers received from the registry.
|
||||||
func (b *common) RawHeaders() http.Header {
|
func (c *BCommon) RawHeaders() http.Header {
|
||||||
return b.rawHeader
|
return c.rawHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response returns the response associated with the blob
|
// Response returns the response associated with the blob.
|
||||||
func (b *common) Response() *http.Response {
|
func (c *BCommon) Response() *http.Response {
|
||||||
return b.resp
|
return c.resp
|
||||||
}
|
}
|
||||||
|
@ -13,23 +13,19 @@ import (
|
|||||||
v1 "github.com/regclient/regclient/types/oci/v1"
|
v1 "github.com/regclient/regclient/types/oci/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OCIConfig wraps an OCI Config struct extracted from a Blob
|
// OCIConfig was previously an interface. A type alias is provided for upgrading.
|
||||||
type OCIConfig interface {
|
type OCIConfig = *BOCIConfig
|
||||||
Blob
|
|
||||||
GetConfig() v1.Image
|
|
||||||
SetConfig(v1.Image)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ociConfig includes an OCI Config struct extracted from a Blob
|
// BOCIConfig includes an OCI Image Config struct that may be extracted from or pushed to a blob.
|
||||||
// Image is included as an anonymous field to facilitate json and templating calls transparently
|
type BOCIConfig struct {
|
||||||
type ociConfig struct {
|
BCommon
|
||||||
common
|
|
||||||
rawBody []byte
|
rawBody []byte
|
||||||
v1.Image
|
image v1.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOCIConfig creates a new BlobOCIConfig from an OCI Image
|
// NewOCIConfig creates a new BOCIConfig.
|
||||||
func NewOCIConfig(opts ...Opts) OCIConfig {
|
// When created from an existing blob, a BOCIConfig will be created using BReader.ToOCIConfig().
|
||||||
|
func NewOCIConfig(opts ...Opts) *BOCIConfig {
|
||||||
bc := blobConfig{}
|
bc := blobConfig{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(&bc)
|
opt(&bc)
|
||||||
@ -56,49 +52,76 @@ func NewOCIConfig(opts ...Opts) OCIConfig {
|
|||||||
bc.desc.MediaType = types.MediaTypeOCI1ImageConfig
|
bc.desc.MediaType = types.MediaTypeOCI1ImageConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
b := BOCIConfig{
|
||||||
c := common{
|
BCommon: BCommon{
|
||||||
desc: bc.desc,
|
desc: bc.desc,
|
||||||
r: bc.r,
|
r: bc.r,
|
||||||
rawHeader: bc.header,
|
rawHeader: bc.header,
|
||||||
resp: bc.resp,
|
resp: bc.resp,
|
||||||
}
|
},
|
||||||
b := ociConfig{
|
|
||||||
common: c,
|
|
||||||
rawBody: bc.rawBody,
|
rawBody: bc.rawBody,
|
||||||
}
|
}
|
||||||
if bc.image != nil {
|
if bc.image != nil {
|
||||||
b.Image = *bc.image
|
b.image = *bc.image
|
||||||
b.blobSet = true
|
b.blobSet = true
|
||||||
}
|
}
|
||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig returns OCI config
|
// GetConfig returns OCI config.
|
||||||
func (b *ociConfig) GetConfig() v1.Image {
|
func (oc *BOCIConfig) GetConfig() v1.Image {
|
||||||
return b.Image
|
return oc.image
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawBody returns the original body from the request
|
// RawBody returns the original body from the request.
|
||||||
func (b *ociConfig) RawBody() ([]byte, error) {
|
func (oc *BOCIConfig) RawBody() ([]byte, error) {
|
||||||
var err error
|
var err error
|
||||||
if !b.blobSet {
|
if !oc.blobSet {
|
||||||
return []byte{}, fmt.Errorf("Blob is not defined")
|
return []byte{}, fmt.Errorf("Blob is not defined")
|
||||||
}
|
}
|
||||||
if len(b.rawBody) == 0 {
|
if len(oc.rawBody) == 0 {
|
||||||
b.rawBody, err = json.Marshal(b.Image)
|
oc.rawBody, err = json.Marshal(oc.image)
|
||||||
}
|
}
|
||||||
return b.rawBody, err
|
return oc.rawBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig updates the config, including raw body and descriptor
|
// SetConfig updates the config, including raw body and descriptor.
|
||||||
func (b *ociConfig) SetConfig(c v1.Image) {
|
func (oc *BOCIConfig) SetConfig(image v1.Image) {
|
||||||
b.Image = c
|
oc.image = image
|
||||||
b.rawBody, _ = json.Marshal(b.Image)
|
oc.rawBody, _ = json.Marshal(oc.image)
|
||||||
if b.desc.MediaType == "" {
|
if oc.desc.MediaType == "" {
|
||||||
b.desc.MediaType = types.MediaTypeOCI1ImageConfig
|
oc.desc.MediaType = types.MediaTypeOCI1ImageConfig
|
||||||
}
|
}
|
||||||
b.desc.Digest = digest.FromBytes(b.rawBody)
|
oc.desc.Digest = digest.FromBytes(oc.rawBody)
|
||||||
b.desc.Size = int64(len(b.rawBody))
|
oc.desc.Size = int64(len(oc.rawBody))
|
||||||
b.blobSet = true
|
oc.blobSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON passes through the marshalling to the underlying image if rawBody is not available.
|
||||||
|
func (oc *BOCIConfig) MarshalJSON() ([]byte, error) {
|
||||||
|
if !oc.blobSet {
|
||||||
|
return []byte{}, fmt.Errorf("Blob is not defined")
|
||||||
|
}
|
||||||
|
if len(oc.rawBody) > 0 {
|
||||||
|
return oc.rawBody, nil
|
||||||
|
}
|
||||||
|
return json.Marshal(oc.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON extracts json content and populates the content.
|
||||||
|
func (oc *BOCIConfig) UnmarshalJSON(data []byte) error {
|
||||||
|
image := v1.Image{}
|
||||||
|
err := json.Unmarshal(data, &image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oc.rawBody = make([]byte, len(data))
|
||||||
|
copy(oc.rawBody, data)
|
||||||
|
if oc.desc.MediaType == "" {
|
||||||
|
oc.desc.MediaType = types.MediaTypeOCI1ImageConfig
|
||||||
|
}
|
||||||
|
oc.desc.Digest = digest.FromBytes(oc.rawBody)
|
||||||
|
oc.desc.Size = int64(len(oc.rawBody))
|
||||||
|
oc.blobSet = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -14,25 +14,20 @@ import (
|
|||||||
"github.com/regclient/regclient/types"
|
"github.com/regclient/regclient/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reader is an unprocessed Blob with an available ReadCloser for reading the Blob
|
// Reader was previously an interface. A type alias is provided for upgrading.
|
||||||
type Reader interface {
|
type Reader = *BReader
|
||||||
Blob
|
|
||||||
io.ReadSeekCloser
|
|
||||||
ToOCIConfig() (OCIConfig, error)
|
|
||||||
ToTarReader() (TarReader, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reader is the internal struct implementing BlobReader
|
// BReader is used to read blobs.
|
||||||
type reader struct {
|
type BReader struct {
|
||||||
common
|
BCommon
|
||||||
readBytes int64
|
readBytes int64
|
||||||
reader io.Reader
|
reader io.Reader
|
||||||
origRdr io.Reader
|
origRdr io.Reader
|
||||||
digester digest.Digester
|
digester digest.Digester
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReader creates a new reader
|
// NewReader creates a new BReader.
|
||||||
func NewReader(opts ...Opts) Reader {
|
func NewReader(opts ...Opts) *BReader {
|
||||||
bc := blobConfig{}
|
bc := blobConfig{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(&bc)
|
opt(&bc)
|
||||||
@ -59,14 +54,13 @@ func NewReader(opts ...Opts) Reader {
|
|||||||
bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest"))
|
bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c := common{
|
br := BReader{
|
||||||
r: bc.r,
|
BCommon: BCommon{
|
||||||
desc: bc.desc,
|
r: bc.r,
|
||||||
rawHeader: bc.header,
|
desc: bc.desc,
|
||||||
resp: bc.resp,
|
rawHeader: bc.header,
|
||||||
}
|
resp: bc.resp,
|
||||||
br := reader{
|
},
|
||||||
common: c,
|
|
||||||
origRdr: bc.rdr,
|
origRdr: bc.rdr,
|
||||||
}
|
}
|
||||||
if bc.rdr != nil {
|
if bc.rdr != nil {
|
||||||
@ -84,119 +78,121 @@ func NewReader(opts ...Opts) Reader {
|
|||||||
return &br
|
return &br
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *reader) Close() error {
|
// Close attempts to close the reader and populates/validates the digest.
|
||||||
if b.origRdr == nil {
|
func (r *BReader) Close() error {
|
||||||
|
if r.origRdr == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// attempt to close if available in original reader
|
// attempt to close if available in original reader
|
||||||
bc, ok := b.origRdr.(io.Closer)
|
bc, ok := r.origRdr.(io.Closer)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return bc.Close()
|
return bc.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawBody returns the original body from the request
|
// RawBody returns the original body from the request.
|
||||||
func (b *reader) RawBody() ([]byte, error) {
|
func (r *BReader) RawBody() ([]byte, error) {
|
||||||
return io.ReadAll(b)
|
return io.ReadAll(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read passes through the read operation while computing the digest and tracking the size
|
// Read passes through the read operation while computing the digest and tracking the size.
|
||||||
func (b *reader) Read(p []byte) (int, error) {
|
func (r *BReader) Read(p []byte) (int, error) {
|
||||||
if b.reader == nil {
|
if r.reader == nil {
|
||||||
return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF)
|
return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF)
|
||||||
}
|
}
|
||||||
size, err := b.reader.Read(p)
|
size, err := r.reader.Read(p)
|
||||||
b.readBytes = b.readBytes + int64(size)
|
r.readBytes = r.readBytes + int64(size)
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// check/save size
|
// check/save size
|
||||||
if b.desc.Size == 0 {
|
if r.desc.Size == 0 {
|
||||||
b.desc.Size = b.readBytes
|
r.desc.Size = r.readBytes
|
||||||
} else if b.readBytes < b.desc.Size {
|
} else if r.readBytes < r.desc.Size {
|
||||||
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, b.desc.Size, b.readBytes, err)
|
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, r.desc.Size, r.readBytes, err)
|
||||||
} else if b.readBytes > b.desc.Size {
|
} else if r.readBytes > r.desc.Size {
|
||||||
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, b.desc.Size, b.readBytes, err)
|
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, r.desc.Size, r.readBytes, err)
|
||||||
}
|
}
|
||||||
// check/save digest
|
// check/save digest
|
||||||
if b.desc.Digest == "" {
|
if r.desc.Digest == "" {
|
||||||
b.desc.Digest = b.digester.Digest()
|
r.desc.Digest = r.digester.Digest()
|
||||||
} else if b.desc.Digest != b.digester.Digest() {
|
} else if r.desc.Digest != r.digester.Digest() {
|
||||||
err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, b.desc.Digest.String(), b.digester.Digest().String(), err)
|
err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, r.desc.Digest.String(), r.digester.Digest().String(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return size, err
|
return size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek passes through the seek operation, reseting or invalidating the digest
|
// Seek passes through the seek operation, reseting or invalidating the digest
|
||||||
func (b *reader) Seek(offset int64, whence int) (int64, error) {
|
func (r *BReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
if offset == 0 && whence == io.SeekCurrent {
|
if offset == 0 && whence == io.SeekCurrent {
|
||||||
return b.readBytes, nil
|
return r.readBytes, nil
|
||||||
}
|
}
|
||||||
// cannot do an arbitrary seek and still digest without a lot more complication
|
// cannot do an arbitrary seek and still digest without a lot more complication
|
||||||
if offset != 0 || whence != io.SeekStart {
|
if offset != 0 || whence != io.SeekStart {
|
||||||
return b.readBytes, fmt.Errorf("unable to seek to arbitrary position")
|
return r.readBytes, fmt.Errorf("unable to seek to arbitrary position")
|
||||||
}
|
}
|
||||||
rdrSeek, ok := b.origRdr.(io.Seeker)
|
rdrSeek, ok := r.origRdr.(io.Seeker)
|
||||||
if !ok {
|
if !ok {
|
||||||
return b.readBytes, fmt.Errorf("Seek unsupported")
|
return r.readBytes, fmt.Errorf("Seek unsupported")
|
||||||
}
|
}
|
||||||
o, err := rdrSeek.Seek(offset, whence)
|
o, err := rdrSeek.Seek(offset, whence)
|
||||||
if err != nil || o != 0 {
|
if err != nil || o != 0 {
|
||||||
return b.readBytes, err
|
return r.readBytes, err
|
||||||
}
|
}
|
||||||
// reset internal offset and digest calculation
|
// reset internal offset and digest calculation
|
||||||
rdr := b.origRdr
|
rdr := r.origRdr
|
||||||
if b.desc.Size > 0 {
|
if r.desc.Size > 0 {
|
||||||
rdr = &limitread.LimitRead{
|
rdr = &limitread.LimitRead{
|
||||||
Reader: rdr,
|
Reader: rdr,
|
||||||
Limit: b.desc.Size,
|
Limit: r.desc.Size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
digester := digest.Canonical.Digester()
|
digester := digest.Canonical.Digester()
|
||||||
b.reader = io.TeeReader(rdr, digester.Hash())
|
r.reader = io.TeeReader(rdr, digester.Hash())
|
||||||
b.digester = digester
|
r.digester = digester
|
||||||
b.readBytes = 0
|
r.readBytes = 0
|
||||||
|
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToOCIConfig converts a blobReader to a BlobOCIConfig
|
// ToOCIConfig converts a BReader to a BOCIConfig.
|
||||||
func (b *reader) ToOCIConfig() (OCIConfig, error) {
|
func (r *BReader) ToOCIConfig() (*BOCIConfig, error) {
|
||||||
if !b.blobSet {
|
if !r.blobSet {
|
||||||
return nil, fmt.Errorf("blob is not defined")
|
return nil, fmt.Errorf("blob is not defined")
|
||||||
}
|
}
|
||||||
if b.readBytes != 0 {
|
if r.readBytes != 0 {
|
||||||
return nil, fmt.Errorf("unable to convert after read has been performed")
|
return nil, fmt.Errorf("unable to convert after read has been performed")
|
||||||
}
|
}
|
||||||
blobBody, err := io.ReadAll(b)
|
blobBody, err := io.ReadAll(r)
|
||||||
errC := b.Close()
|
errC := r.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error reading image config for %s: %w", b.r.CommonName(), err)
|
return nil, fmt.Errorf("error reading image config for %s: %w", r.r.CommonName(), err)
|
||||||
}
|
}
|
||||||
if errC != nil {
|
if errC != nil {
|
||||||
return nil, fmt.Errorf("error closing blob reader: %w", err)
|
return nil, fmt.Errorf("error closing blob reader: %w", err)
|
||||||
}
|
}
|
||||||
return NewOCIConfig(
|
return NewOCIConfig(
|
||||||
WithDesc(b.desc),
|
WithDesc(r.desc),
|
||||||
WithHeader(b.rawHeader),
|
WithHeader(r.rawHeader),
|
||||||
WithRawBody(blobBody),
|
WithRawBody(blobBody),
|
||||||
WithRef(b.r),
|
WithRef(r.r),
|
||||||
WithResp(b.resp),
|
WithResp(r.resp),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *reader) ToTarReader() (TarReader, error) {
|
// ToTarReader converts a BReader to a BTarReader
|
||||||
if !b.blobSet {
|
func (r *BReader) ToTarReader() (*BTarReader, error) {
|
||||||
|
if !r.blobSet {
|
||||||
return nil, fmt.Errorf("blob is not defined")
|
return nil, fmt.Errorf("blob is not defined")
|
||||||
}
|
}
|
||||||
if b.readBytes != 0 {
|
if r.readBytes != 0 {
|
||||||
return nil, fmt.Errorf("unable to convert after read has been performed")
|
return nil, fmt.Errorf("unable to convert after read has been performed")
|
||||||
}
|
}
|
||||||
return NewTarReader(
|
return NewTarReader(
|
||||||
WithDesc(b.desc),
|
WithDesc(r.desc),
|
||||||
WithHeader(b.rawHeader),
|
WithHeader(r.rawHeader),
|
||||||
WithRef(b.r),
|
WithRef(r.r),
|
||||||
WithResp(b.resp),
|
WithResp(r.resp),
|
||||||
WithReader(b.reader),
|
WithReader(r.reader),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
@ -14,36 +14,32 @@ import (
|
|||||||
"github.com/regclient/regclient/types"
|
"github.com/regclient/regclient/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TarReader reads or writes to a blob with tar contents and optional compression
|
// TarReader was previously an interface. A type alias is provided for upgrading.
|
||||||
type TarReader interface {
|
type TarReader = *BTarReader
|
||||||
Blob
|
|
||||||
io.Closer
|
|
||||||
GetTarReader() (*tar.Reader, error)
|
|
||||||
ReadFile(filename string) (*tar.Header, io.Reader, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type tarReader struct {
|
// BTarReader is used to read individual files from an image layer.
|
||||||
common
|
type BTarReader struct {
|
||||||
|
BCommon
|
||||||
origRdr io.Reader
|
origRdr io.Reader
|
||||||
reader io.Reader
|
reader io.Reader
|
||||||
digester digest.Digester
|
digester digest.Digester
|
||||||
tr *tar.Reader
|
tr *tar.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTarReader creates a TarReader
|
// NewTarReader creates a BTarReader.
|
||||||
func NewTarReader(opts ...Opts) TarReader {
|
// Typically a BTarReader will be created using BReader.ToTarReader().
|
||||||
|
func NewTarReader(opts ...Opts) *BTarReader {
|
||||||
bc := blobConfig{}
|
bc := blobConfig{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(&bc)
|
opt(&bc)
|
||||||
}
|
}
|
||||||
c := common{
|
tr := BTarReader{
|
||||||
desc: bc.desc,
|
BCommon: BCommon{
|
||||||
r: bc.r,
|
desc: bc.desc,
|
||||||
rawHeader: bc.header,
|
r: bc.r,
|
||||||
resp: bc.resp,
|
rawHeader: bc.header,
|
||||||
}
|
resp: bc.resp,
|
||||||
tr := tarReader{
|
},
|
||||||
common: c,
|
|
||||||
origRdr: bc.rdr,
|
origRdr: bc.rdr,
|
||||||
}
|
}
|
||||||
if bc.rdr != nil {
|
if bc.rdr != nil {
|
||||||
@ -61,8 +57,8 @@ func NewTarReader(opts ...Opts) TarReader {
|
|||||||
return &tr
|
return &tr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close attempts to close the reader and populates/validates the digest
|
// Close attempts to close the reader and populates/validates the digest.
|
||||||
func (tr *tarReader) Close() error {
|
func (tr *BTarReader) Close() error {
|
||||||
// attempt to close if available in original reader
|
// attempt to close if available in original reader
|
||||||
if trc, ok := tr.origRdr.(io.Closer); ok && trc != nil {
|
if trc, ok := tr.origRdr.(io.Closer); ok && trc != nil {
|
||||||
return trc.Close()
|
return trc.Close()
|
||||||
@ -70,8 +66,8 @@ func (tr *tarReader) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTarReader returns the tar.Reader for the blob
|
// GetTarReader returns the tar.Reader for the blob.
|
||||||
func (tr *tarReader) GetTarReader() (*tar.Reader, error) {
|
func (tr *BTarReader) GetTarReader() (*tar.Reader, error) {
|
||||||
if tr.reader == nil {
|
if tr.reader == nil {
|
||||||
return nil, fmt.Errorf("blob has no reader defined")
|
return nil, fmt.Errorf("blob has no reader defined")
|
||||||
}
|
}
|
||||||
@ -85,8 +81,8 @@ func (tr *tarReader) GetTarReader() (*tar.Reader, error) {
|
|||||||
return tr.tr, nil
|
return tr.tr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawBody returns the original body from the request
|
// RawBody returns the original body from the request.
|
||||||
func (tr *tarReader) RawBody() ([]byte, error) {
|
func (tr *BTarReader) RawBody() ([]byte, error) {
|
||||||
if !tr.blobSet {
|
if !tr.blobSet {
|
||||||
return []byte{}, fmt.Errorf("Blob is not defined")
|
return []byte{}, fmt.Errorf("Blob is not defined")
|
||||||
}
|
}
|
||||||
@ -109,8 +105,8 @@ func (tr *tarReader) RawBody() ([]byte, error) {
|
|||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile parses the tar to find a file
|
// ReadFile parses the tar to find a file.
|
||||||
func (tr *tarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) {
|
func (tr *BTarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) {
|
||||||
if strings.HasPrefix(filename, ".wh.") {
|
if strings.HasPrefix(filename, ".wh.") {
|
||||||
return nil, nil, fmt.Errorf(".wh. prefix is reserved for whiteout files")
|
return nil, nil, fmt.Errorf(".wh. prefix is reserved for whiteout files")
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user