Skip to content

Commit

Permalink
image/cas: Add a generic CAS interface
Browse files Browse the repository at this point in the history
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 <wking@tremily.us>
  • Loading branch information
wking committed Nov 18, 2016
1 parent f24d27b commit fbae6d9
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/oci-cas
/oci-create-runtime-bundle
/oci-unpack
/oci-image-validate
/oci-unpack
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ COMMIT=$(shell git rev-parse HEAD 2> /dev/null || true)

EPOCH_TEST_COMMIT ?= v0.2.0
TOOLS := \
oci-cas \
oci-create-runtime-bundle \
oci-image-validate \
oci-unpack
Expand Down
93 changes: 93 additions & 0 deletions cmd/oci-cas/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2016 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"fmt"
"io/ioutil"
"os"

"github.com/opencontainers/image-tools/image/cas/layout"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)

type getCmd struct {
path string
digest string
}

func newGetCmd() *cobra.Command {
state := &getCmd{}

return &cobra.Command{
Use: "get PATH DIGEST",
Short: "Retrieve a blob from the store",
Long: "Retrieve a blob from the store and write it to stdout.",
Run: state.Run,
}
}

func (state *getCmd) Run(cmd *cobra.Command, args []string) {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "both PATH and DIGEST must be provided")
if err := cmd.Usage(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
os.Exit(1)
}

state.path = args[0]
state.digest = args[1]

err := state.run()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

os.Exit(0)
}

func (state *getCmd) run() (err error) {
ctx := context.Background()

engine, err := layout.NewEngine(state.path)
if err != nil {
return err
}
defer engine.Close()

reader, err := engine.Get(ctx, state.digest)
if err != nil {
return err
}
defer reader.Close()

bytes, err := ioutil.ReadAll(reader)
if err != nil {
return err
}

n, err := os.Stdout.Write(bytes)
if err != nil {
return err
}
if n < len(bytes) {
return fmt.Errorf("wrote %d of %d bytes", n, len(bytes))
}

return nil
}
37 changes: 37 additions & 0 deletions cmd/oci-cas/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2016 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

func main() {
cmd := &cobra.Command{
Use: "oci-cas",
Short: "Content-addressable storage manipulation",
}

cmd.AddCommand(newGetCmd())

err := cmd.Execute()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
44 changes: 44 additions & 0 deletions image/cas/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2016 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cas implements generic content-addressable storage.
package cas

import (
"io"

"golang.org/x/net/context"
)

// Engine represents a content-addressable storage engine.
type Engine interface {

// Put adds a new blob to the store. The action is idempotent; a
// nil return means "that content is stored at DIGEST" without
// implying "because of your Put()".
Put(ctx context.Context, reader io.Reader) (digest string, err error)

// Get returns a reader for retrieving a blob from the store.
// Returns os.ErrNotExist if the digest is not found.
Get(ctx context.Context, digest string) (reader io.ReadCloser, err error)

// Delete removes a blob from the store. The action is idempotent; a
// nil return means "that content is not in the store" without
// implying "because of your Delete()".
Delete(ctx context.Context, digest string) (err error)

// Close releases resources held by the engine. Subsequent engine
// method calls will fail.
Close() (err error)
}
25 changes: 25 additions & 0 deletions image/cas/layout/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2016 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package layout

import (
"io"
)

// ReadWriteSeekCloser wraps the Read, Write, Seek, and Close methods.
type ReadWriteSeekCloser interface {
io.ReadWriteSeeker
io.Closer
}
51 changes: 51 additions & 0 deletions image/cas/layout/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2016 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package layout implements the cas interface using the image-spec's
// image-layout [1].
//
// [1]: https://github.com/opencontainers/image-spec/blob/master/image-layout.md
package layout

import (
"fmt"
"os"
"strings"

"github.com/opencontainers/image-tools/image/cas"
)

// NewEngine instantiates an engine with the appropriate backend (tar,
// HTTP, ...).
func NewEngine(path string) (engine cas.Engine, err error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}

return NewTarEngine(file)
}

// blobPath returns the PATH to the DIGEST blob. SEPARATOR selects
// the path separator used between components.
func blobPath(digest string, separator string) (path string, err error) {
fields := strings.SplitN(digest, ":", 2)
if len(fields) != 2 {
return "", fmt.Errorf("invalid digest: %q, %v", digest, fields)
}
algorithm := fields[0]
hash := fields[1]
components := []string{".", "blobs", algorithm, hash}
return strings.Join(components, separator), nil
}
90 changes: 90 additions & 0 deletions image/cas/layout/tar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2016 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package layout

import (
"archive/tar"
"errors"
"io"
"io/ioutil"
"os"

"github.com/opencontainers/image-tools/image/cas"
"golang.org/x/net/context"
)

// TarEngine is a cas.Engine backed by a tar file.
type TarEngine struct {
file ReadWriteSeekCloser
}

// NewTarEngine returns a new TarEngine.
func NewTarEngine(file ReadWriteSeekCloser) (engine cas.Engine, err error) {
engine = &TarEngine{
file: file,
}

return engine, nil
}

// Put adds a new blob to the store.
func (engine *TarEngine) Put(ctx context.Context, reader io.Reader) (digest string, err error) {
// FIXME
return "", errors.New("TarEngine.Put is not supported yet")
}

// Get returns a reader for retrieving a blob from the store.
func (engine *TarEngine) Get(ctx context.Context, digest string) (reader io.ReadCloser, err error) {
targetName, err := blobPath(digest, "/")
if err != nil {
return nil, err
}

_, err = engine.file.Seek(0, os.SEEK_SET)
if err != nil {
return nil, err
}

tarReader := tar.NewReader(engine.file)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}

header, err := tarReader.Next()
if err == io.EOF {
return nil, os.ErrNotExist
} else if err != nil {
return nil, err
}

if header.Name == targetName {
return ioutil.NopCloser(tarReader), nil
}
}
}

// Delete removes a blob from the store.
func (engine *TarEngine) Delete(ctx context.Context, digest string) (err error) {
// FIXME
return errors.New("TarEngine.Delete is not supported yet")
}

// Close releases resources held by the engine.
func (engine *TarEngine) Close() (err error) {
return engine.file.Close()
}

0 comments on commit fbae6d9

Please sign in to comment.