Skip to content
This repository was archived by the owner on Jan 15, 2026. It is now read-only.

Commit fbae6d9

Browse files
committed
image/cas: Add a generic CAS interface
And implement that interface for tarballs based on the specs image-layout. I plan on adding other backends later, but this is enough for a proof of concept. Also add a new oci-cas command so folks can access the new read functionality from the command line. In a subsequent commit, I'll replace the image/walker.go functionality with this new API. The Context interface follows the pattern recommended in [1], allowing callers to cancel long running actions (e.g. push/pull over the network for engine implementations that communicate with a remote store). blobPath's separator argument will allow us to use string(os.PathSeparator)) once we add directory support. [1]: https://blog.golang.org/context Signed-off-by: W. Trevor King <[email protected]>
1 parent f24d27b commit fbae6d9

File tree

8 files changed

+343
-1
lines changed

8 files changed

+343
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/oci-cas
12
/oci-create-runtime-bundle
2-
/oci-unpack
33
/oci-image-validate
4+
/oci-unpack

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ COMMIT=$(shell git rev-parse HEAD 2> /dev/null || true)
55

66
EPOCH_TEST_COMMIT ?= v0.2.0
77
TOOLS := \
8+
oci-cas \
89
oci-create-runtime-bundle \
910
oci-image-validate \
1011
oci-unpack

cmd/oci-cas/get.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"io/ioutil"
20+
"os"
21+
22+
"github.com/opencontainers/image-tools/image/cas/layout"
23+
"github.com/spf13/cobra"
24+
"golang.org/x/net/context"
25+
)
26+
27+
type getCmd struct {
28+
path string
29+
digest string
30+
}
31+
32+
func newGetCmd() *cobra.Command {
33+
state := &getCmd{}
34+
35+
return &cobra.Command{
36+
Use: "get PATH DIGEST",
37+
Short: "Retrieve a blob from the store",
38+
Long: "Retrieve a blob from the store and write it to stdout.",
39+
Run: state.Run,
40+
}
41+
}
42+
43+
func (state *getCmd) Run(cmd *cobra.Command, args []string) {
44+
if len(args) != 2 {
45+
fmt.Fprintln(os.Stderr, "both PATH and DIGEST must be provided")
46+
if err := cmd.Usage(); err != nil {
47+
fmt.Fprintln(os.Stderr, err)
48+
}
49+
os.Exit(1)
50+
}
51+
52+
state.path = args[0]
53+
state.digest = args[1]
54+
55+
err := state.run()
56+
if err != nil {
57+
fmt.Fprintln(os.Stderr, err)
58+
os.Exit(1)
59+
}
60+
61+
os.Exit(0)
62+
}
63+
64+
func (state *getCmd) run() (err error) {
65+
ctx := context.Background()
66+
67+
engine, err := layout.NewEngine(state.path)
68+
if err != nil {
69+
return err
70+
}
71+
defer engine.Close()
72+
73+
reader, err := engine.Get(ctx, state.digest)
74+
if err != nil {
75+
return err
76+
}
77+
defer reader.Close()
78+
79+
bytes, err := ioutil.ReadAll(reader)
80+
if err != nil {
81+
return err
82+
}
83+
84+
n, err := os.Stdout.Write(bytes)
85+
if err != nil {
86+
return err
87+
}
88+
if n < len(bytes) {
89+
return fmt.Errorf("wrote %d of %d bytes", n, len(bytes))
90+
}
91+
92+
return nil
93+
}

cmd/oci-cas/main.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"fmt"
19+
"os"
20+
21+
"github.com/spf13/cobra"
22+
)
23+
24+
func main() {
25+
cmd := &cobra.Command{
26+
Use: "oci-cas",
27+
Short: "Content-addressable storage manipulation",
28+
}
29+
30+
cmd.AddCommand(newGetCmd())
31+
32+
err := cmd.Execute()
33+
if err != nil {
34+
fmt.Fprintln(os.Stderr, err)
35+
os.Exit(1)
36+
}
37+
}

image/cas/interface.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package cas implements generic content-addressable storage.
16+
package cas
17+
18+
import (
19+
"io"
20+
21+
"golang.org/x/net/context"
22+
)
23+
24+
// Engine represents a content-addressable storage engine.
25+
type Engine interface {
26+
27+
// Put adds a new blob to the store. The action is idempotent; a
28+
// nil return means "that content is stored at DIGEST" without
29+
// implying "because of your Put()".
30+
Put(ctx context.Context, reader io.Reader) (digest string, err error)
31+
32+
// Get returns a reader for retrieving a blob from the store.
33+
// Returns os.ErrNotExist if the digest is not found.
34+
Get(ctx context.Context, digest string) (reader io.ReadCloser, err error)
35+
36+
// Delete removes a blob from the store. The action is idempotent; a
37+
// nil return means "that content is not in the store" without
38+
// implying "because of your Delete()".
39+
Delete(ctx context.Context, digest string) (err error)
40+
41+
// Close releases resources held by the engine. Subsequent engine
42+
// method calls will fail.
43+
Close() (err error)
44+
}

image/cas/layout/interface.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package layout
16+
17+
import (
18+
"io"
19+
)
20+
21+
// ReadWriteSeekCloser wraps the Read, Write, Seek, and Close methods.
22+
type ReadWriteSeekCloser interface {
23+
io.ReadWriteSeeker
24+
io.Closer
25+
}

image/cas/layout/main.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package layout implements the cas interface using the image-spec's
16+
// image-layout [1].
17+
//
18+
// [1]: https://github.com/opencontainers/image-spec/blob/master/image-layout.md
19+
package layout
20+
21+
import (
22+
"fmt"
23+
"os"
24+
"strings"
25+
26+
"github.com/opencontainers/image-tools/image/cas"
27+
)
28+
29+
// NewEngine instantiates an engine with the appropriate backend (tar,
30+
// HTTP, ...).
31+
func NewEngine(path string) (engine cas.Engine, err error) {
32+
file, err := os.Open(path)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
return NewTarEngine(file)
38+
}
39+
40+
// blobPath returns the PATH to the DIGEST blob. SEPARATOR selects
41+
// the path separator used between components.
42+
func blobPath(digest string, separator string) (path string, err error) {
43+
fields := strings.SplitN(digest, ":", 2)
44+
if len(fields) != 2 {
45+
return "", fmt.Errorf("invalid digest: %q, %v", digest, fields)
46+
}
47+
algorithm := fields[0]
48+
hash := fields[1]
49+
components := []string{".", "blobs", algorithm, hash}
50+
return strings.Join(components, separator), nil
51+
}

image/cas/layout/tar.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package layout
16+
17+
import (
18+
"archive/tar"
19+
"errors"
20+
"io"
21+
"io/ioutil"
22+
"os"
23+
24+
"github.com/opencontainers/image-tools/image/cas"
25+
"golang.org/x/net/context"
26+
)
27+
28+
// TarEngine is a cas.Engine backed by a tar file.
29+
type TarEngine struct {
30+
file ReadWriteSeekCloser
31+
}
32+
33+
// NewTarEngine returns a new TarEngine.
34+
func NewTarEngine(file ReadWriteSeekCloser) (engine cas.Engine, err error) {
35+
engine = &TarEngine{
36+
file: file,
37+
}
38+
39+
return engine, nil
40+
}
41+
42+
// Put adds a new blob to the store.
43+
func (engine *TarEngine) Put(ctx context.Context, reader io.Reader) (digest string, err error) {
44+
// FIXME
45+
return "", errors.New("TarEngine.Put is not supported yet")
46+
}
47+
48+
// Get returns a reader for retrieving a blob from the store.
49+
func (engine *TarEngine) Get(ctx context.Context, digest string) (reader io.ReadCloser, err error) {
50+
targetName, err := blobPath(digest, "/")
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
_, err = engine.file.Seek(0, os.SEEK_SET)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
tarReader := tar.NewReader(engine.file)
61+
for {
62+
select {
63+
case <-ctx.Done():
64+
return nil, ctx.Err()
65+
default:
66+
}
67+
68+
header, err := tarReader.Next()
69+
if err == io.EOF {
70+
return nil, os.ErrNotExist
71+
} else if err != nil {
72+
return nil, err
73+
}
74+
75+
if header.Name == targetName {
76+
return ioutil.NopCloser(tarReader), nil
77+
}
78+
}
79+
}
80+
81+
// Delete removes a blob from the store.
82+
func (engine *TarEngine) Delete(ctx context.Context, digest string) (err error) {
83+
// FIXME
84+
return errors.New("TarEngine.Delete is not supported yet")
85+
}
86+
87+
// Close releases resources held by the engine.
88+
func (engine *TarEngine) Close() (err error) {
89+
return engine.file.Close()
90+
}

0 commit comments

Comments
 (0)