Skip to content

Commit 8e8fcf9

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). [1]: https://blog.golang.org/context Signed-off-by: W. Trevor King <[email protected]>
1 parent 0179710 commit 8e8fcf9

File tree

8 files changed

+333
-1
lines changed

8 files changed

+333
-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
@@ -3,6 +3,7 @@ export GO15VENDOREXPERIMENT
33

44
EPOCH_TEST_COMMIT ?= v0.2.0
55
TOOLS := \
6+
oci-cas \
67
oci-create-runtime-bundle \
78
oci-image-validate \
89
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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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. Returns os.ErrNotExist if
37+
// the digest is not found.
38+
Delete(ctx context.Context, digest string) (err error)
39+
40+
// Close releases resources held by the engine. Subsequent engine
41+
// method calls will fail.
42+
Close() (err error)
43+
}

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
24+
"github.com/opencontainers/image-tools/image/cas"
25+
)
26+
27+
// NewEngine instantiates an engine with the appropriate backend (tar,
28+
// HTTP, ...).
29+
func NewEngine(path string) (engine cas.Engine, err error) {
30+
file, err := os.Open(path)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
return NewTarEngine(file)
36+
}

image/cas/layout/tar.go

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

0 commit comments

Comments
 (0)