Skip to content

Commit 20e71cb

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 'cas get' subcommand to oci-image-tool so folks can access the new read functionality from the command line. In a subsequent commit, I'll replace oci-image-tools walker functionality with this new API. Signed-off-by: W. Trevor King <[email protected]>
1 parent 2a0bfdc commit 20e71cb

File tree

7 files changed

+312
-0
lines changed

7 files changed

+312
-0
lines changed

cmd/oci-image-tool/cas.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
"io"
19+
"log"
20+
21+
"github.com/spf13/cobra"
22+
)
23+
24+
func newCASCmd(stdout io.Writer, stderr *log.Logger) *cobra.Command {
25+
cmd := &cobra.Command{
26+
Use: "cas",
27+
Short: "Content-addressable storage manipulation",
28+
}
29+
30+
cmd.AddCommand(newCASGetCmd(stdout, stderr))
31+
32+
return cmd
33+
}

cmd/oci-image-tool/cas_get.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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"
20+
"io/ioutil"
21+
"log"
22+
"os"
23+
24+
"github.com/opencontainers/image-spec/image/cas/layout"
25+
"github.com/spf13/cobra"
26+
)
27+
28+
type casGetCmd struct {
29+
stdout io.Writer
30+
stderr *log.Logger
31+
path string
32+
digest string
33+
}
34+
35+
func newCASGetCmd(stdout io.Writer, stderr *log.Logger) *cobra.Command {
36+
state := &casGetCmd{
37+
stdout: stdout,
38+
stderr: stderr,
39+
}
40+
41+
return &cobra.Command{
42+
Use: "get PATH DIGEST",
43+
Short: "Retrieve a blob from the store",
44+
Run: state.Run,
45+
}
46+
}
47+
48+
func (state *casGetCmd) Run(cmd *cobra.Command, args []string) {
49+
if len(args) != 2 {
50+
state.stderr.Print("both PATH and DIGEST must be provided")
51+
if err := cmd.Usage(); err != nil {
52+
state.stderr.Println(err)
53+
}
54+
os.Exit(1)
55+
}
56+
57+
state.path = args[0]
58+
state.digest = args[1]
59+
60+
err := state.run()
61+
if err != nil {
62+
state.stderr.Println(err)
63+
os.Exit(1)
64+
}
65+
66+
os.Exit(1)
67+
}
68+
69+
func (state *casGetCmd) run() (err error) {
70+
engine, err := layout.GetEngine(state.path)
71+
if err != nil {
72+
return err
73+
}
74+
75+
reader, err := engine.Get(state.digest)
76+
if err != nil {
77+
return err
78+
}
79+
80+
bytes, err := ioutil.ReadAll(reader)
81+
if err != nil {
82+
return err
83+
}
84+
85+
n, err := state.stdout.Write(bytes)
86+
if err != nil {
87+
return err
88+
}
89+
if n < len(bytes) {
90+
return fmt.Errorf("wrote %d of %d bytes", n, len(bytes))
91+
}
92+
93+
return engine.Close()
94+
}

cmd/oci-image-tool/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func main() {
3333
cmd.AddCommand(newValidateCmd(stdout, stderr))
3434
cmd.AddCommand(newUnpackCmd(stdout, stderr))
3535
cmd.AddCommand(newBundleCmd(stdout, stderr))
36+
cmd.AddCommand(newCASCmd(os.Stdout, stderr))
3637

3738
if err := cmd.Execute(); err != nil {
3839
stderr.Println(err)

image/cas/interface.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 cas implements generic content-addressable storage.
16+
package cas
17+
18+
import (
19+
"io"
20+
)
21+
22+
// Engine represents a content-addressable storage engine.
23+
type Engine interface {
24+
25+
// Put adds a new blob to the store.
26+
Put(writer io.Writer) (digest string, err error)
27+
28+
// Get returns a reader for retrieving a blob from the store.
29+
Get(digest string) (reader io.Reader, err error)
30+
31+
// Delete removes a blob from the store.
32+
Delete(digest string) (err error)
33+
34+
// Close releases resources held by the engine. Subsequent engine
35+
// method calls will fail.
36+
Close() (err error)
37+
}

image/cas/layout/interface.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
// ReadSeekCloser wraps the Read, Seek, and Close methods.
22+
type ReadSeekCloser interface {
23+
io.Reader
24+
io.Seeker
25+
io.Closer
26+
}

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-spec/image/cas"
25+
)
26+
27+
// GetEngine guesses the appropriate backend (tar, HTTP, ...) and
28+
// returns that engine.
29+
func GetEngine(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 GetTarEngine(file)
36+
}

image/cas/layout/tar.go

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

0 commit comments

Comments
 (0)