Skip to content

Commit a44de0d

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 7575a09 commit a44de0d

File tree

8 files changed

+352
-1
lines changed

8 files changed

+352
-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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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/go-digest"
23+
"github.com/opencontainers/image-tools/image/cas/layout"
24+
"github.com/spf13/cobra"
25+
"golang.org/x/net/context"
26+
)
27+
28+
type getCmd struct {
29+
path string
30+
digest digest.Digest
31+
}
32+
33+
func newGetCmd() *cobra.Command {
34+
state := &getCmd{}
35+
36+
return &cobra.Command{
37+
Use: "get PATH DIGEST",
38+
Short: "Retrieve a blob from the store",
39+
Long: "Retrieve a blob from the store and write it to stdout.",
40+
Run: state.Run,
41+
}
42+
}
43+
44+
func (state *getCmd) Run(cmd *cobra.Command, args []string) {
45+
if len(args) != 2 {
46+
fmt.Fprintln(os.Stderr, "both PATH and DIGEST must be provided")
47+
if err := cmd.Usage(); err != nil {
48+
fmt.Fprintln(os.Stderr, err)
49+
}
50+
os.Exit(1)
51+
}
52+
53+
state.path = args[0]
54+
var err error
55+
state.digest, err = digest.Parse(args[1])
56+
if err != nil {
57+
fmt.Fprintln(os.Stderr, err)
58+
os.Exit(1)
59+
}
60+
61+
err = state.run()
62+
if err != nil {
63+
fmt.Fprintln(os.Stderr, err)
64+
os.Exit(1)
65+
}
66+
67+
os.Exit(0)
68+
}
69+
70+
func (state *getCmd) run() (err error) {
71+
ctx := context.Background()
72+
73+
engine, err := layout.NewEngine(state.path)
74+
if err != nil {
75+
return err
76+
}
77+
defer engine.Close()
78+
79+
reader, err := engine.Get(ctx, state.digest)
80+
if err != nil {
81+
return err
82+
}
83+
defer reader.Close()
84+
85+
bytes, err := ioutil.ReadAll(reader)
86+
if err != nil {
87+
return err
88+
}
89+
90+
n, err := os.Stdout.Write(bytes)
91+
if err != nil {
92+
return err
93+
}
94+
if n < len(bytes) {
95+
return fmt.Errorf("wrote %d of %d bytes", n, len(bytes))
96+
}
97+
98+
return nil
99+
}

cmd/oci-cas/main.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
_ "crypto/sha256"
19+
_ "crypto/sha512"
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/cobra"
24+
)
25+
26+
func main() {
27+
cmd := &cobra.Command{
28+
Use: "oci-cas",
29+
Short: "Content-addressable storage manipulation",
30+
}
31+
32+
cmd.AddCommand(newGetCmd())
33+
34+
err := cmd.Execute()
35+
if err != nil {
36+
fmt.Fprintln(os.Stderr, err)
37+
os.Exit(1)
38+
}
39+
}

image/cas/interface.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
"github.com/opencontainers/go-digest"
22+
"golang.org/x/net/context"
23+
)
24+
25+
// Engine represents a content-addressable storage engine.
26+
type Engine interface {
27+
28+
// Put adds a new blob to the store. The action is idempotent; a
29+
// nil return means "that content is stored at DIGEST" without
30+
// implying "because of your Put()".
31+
Put(ctx context.Context, reader io.Reader) (digest digest.Digest, err error)
32+
33+
// Get returns a reader for retrieving a blob from the store.
34+
// Returns os.ErrNotExist if the digest is not found.
35+
Get(ctx context.Context, digest digest.Digest) (reader io.ReadCloser, err error)
36+
37+
// Delete removes a blob from the store. The action is idempotent; a
38+
// nil return means "that content is not in the store" without
39+
// implying "because of your Delete()".
40+
Delete(ctx context.Context, digest digest.Digest) (err error)
41+
42+
// Close releases resources held by the engine. Subsequent engine
43+
// method calls will fail.
44+
Close() (err error)
45+
}

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
"os"
23+
"strings"
24+
25+
"github.com/opencontainers/go-digest"
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 digest.Digest, separator string) (path string, err error) {
43+
err = digest.Validate()
44+
if err != nil {
45+
return "", err
46+
}
47+
algorithm := digest.Algorithm().String()
48+
components := []string{".", "blobs", algorithm, digest.Hex()}
49+
return strings.Join(components, separator), nil
50+
}

image/cas/layout/tar.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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/go-digest"
25+
"github.com/opencontainers/image-tools/image/cas"
26+
"golang.org/x/net/context"
27+
)
28+
29+
// TarEngine is a cas.Engine backed by a tar file.
30+
type TarEngine struct {
31+
file ReadWriteSeekCloser
32+
}
33+
34+
// NewTarEngine returns a new TarEngine.
35+
func NewTarEngine(file ReadWriteSeekCloser) (engine cas.Engine, err error) {
36+
engine = &TarEngine{
37+
file: file,
38+
}
39+
40+
return engine, nil
41+
}
42+
43+
// Put adds a new blob to the store.
44+
func (engine *TarEngine) Put(ctx context.Context, reader io.Reader) (digest digest.Digest, err error) {
45+
// FIXME
46+
return "", errors.New("TarEngine.Put is not supported yet")
47+
}
48+
49+
// Get returns a reader for retrieving a blob from the store.
50+
func (engine *TarEngine) Get(ctx context.Context, digest digest.Digest) (reader io.ReadCloser, err error) {
51+
targetName, err := blobPath(digest, "/")
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
_, err = engine.file.Seek(0, os.SEEK_SET)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
tarReader := tar.NewReader(engine.file)
62+
for {
63+
select {
64+
case <-ctx.Done():
65+
return nil, ctx.Err()
66+
default:
67+
}
68+
69+
header, err := tarReader.Next()
70+
if err == io.EOF {
71+
return nil, os.ErrNotExist
72+
} else if err != nil {
73+
return nil, err
74+
}
75+
76+
if header.Name == targetName {
77+
return ioutil.NopCloser(tarReader), nil
78+
}
79+
}
80+
}
81+
82+
// Delete removes a blob from the store.
83+
func (engine *TarEngine) Delete(ctx context.Context, digest digest.Digest) (err error) {
84+
// FIXME
85+
return errors.New("TarEngine.Delete is not supported yet")
86+
}
87+
88+
// Close releases resources held by the engine.
89+
func (engine *TarEngine) Close() (err error) {
90+
return engine.file.Close()
91+
}

0 commit comments

Comments
 (0)