diff --git a/.gitignore b/.gitignore index 5dbca12..fe4e8e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /oci-cas /oci-create-runtime-bundle /oci-image-validate +/oci-refs /oci-unpack diff --git a/Makefile b/Makefile index 5acb469..a76ee9e 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ TOOLS := \ oci-cas \ oci-create-runtime-bundle \ oci-image-validate \ + oci-refs \ oci-unpack default: help diff --git a/cmd/oci-refs/get.go b/cmd/oci-refs/get.go new file mode 100644 index 0000000..0994ef1 --- /dev/null +++ b/cmd/oci-refs/get.go @@ -0,0 +1,78 @@ +// 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 ( + "encoding/json" + "fmt" + "os" + + "github.com/opencontainers/image-tools/image/refs/layout" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type getCmd struct { + path string + name string +} + +func newGetCmd() *cobra.Command { + state := &getCmd{} + + return &cobra.Command{ + Use: "get PATH NAME", + Short: "Retrieve a reference from the store", + Run: state.Run, + } +} + +func (state *getCmd) Run(cmd *cobra.Command, args []string) { + if len(args) != 2 { + fmt.Fprintln(os.Stderr, "both PATH and NAME must be provided") + if err := cmd.Usage(); err != nil { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) + } + + state.path = args[0] + state.name = 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() + + descriptor, err := engine.Get(ctx, state.name) + if err != nil { + return err + } + + return json.NewEncoder(os.Stdout).Encode(&descriptor) +} diff --git a/cmd/oci-refs/list.go b/cmd/oci-refs/list.go new file mode 100644 index 0000000..96e7e43 --- /dev/null +++ b/cmd/oci-refs/list.go @@ -0,0 +1,78 @@ +// 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/opencontainers/image-tools/image/refs/layout" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type listCmd struct { + path string +} + +func newListCmd() *cobra.Command { + state := &listCmd{} + + return &cobra.Command{ + Use: "list PATH", + Short: "Return available names from the store.", + Run: state.Run, + } +} + +func (state *listCmd) Run(cmd *cobra.Command, args []string) { + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "PATH must be provided") + if err := cmd.Usage(); err != nil { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) + } + + state.path = args[0] + + err := state.run() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + os.Exit(0) +} + +func (state *listCmd) run() (err error) { + ctx := context.Background() + + engine, err := layout.NewEngine(state.path) + if err != nil { + return err + } + defer engine.Close() + + return engine.List(ctx, "", -1, 0, state.printName) +} + +func (state *listCmd) printName(ctx context.Context, name string) (err error) { + n, err = fmt.Fprintln(os.Stdout, name) + if n < len(name) { + return fmt.Errorf("wrote %d of %d name", n, len(name)) + } + return err +} diff --git a/cmd/oci-refs/main.go b/cmd/oci-refs/main.go new file mode 100644 index 0000000..cb2cc7d --- /dev/null +++ b/cmd/oci-refs/main.go @@ -0,0 +1,38 @@ +// 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-refs", + Short: "Name-based reference manipulation", + } + + cmd.AddCommand(newGetCmd()) + cmd.AddCommand(newListCmd()) + + err := cmd.Execute() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/image/refs/interface.go b/image/refs/interface.go new file mode 100644 index 0000000..92472eb --- /dev/null +++ b/image/refs/interface.go @@ -0,0 +1,79 @@ +// 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 refs implements generic name-based reference access. +package refs + +import ( + "github.com/opencontainers/image-spec/specs-go" + "golang.org/x/net/context" +) + +// ListNameCallback templates an Engine.List callback used for +// processing names. See Engine.List for more details. +type ListNameCallback func(ctx context.Context, name string) (err error) + +// Engine represents a name-based reference storage engine. +type Engine interface { + + // Put adds a new reference to the store. The action is idempotent; + // a nil return means "that descriptor is stored at NAME" without + // implying "because of your Put()". + Put(ctx context.Context, name string, descriptor *specs.Descriptor) (err error) + + // Get returns a reference from the store. Returns os.ErrNotExist + // if the name is not found. + Get(ctx context.Context, name string) (descriptor *specs.Descriptor, err error) + + // List returns available names from the store. + // + // Results are sorted alphabetically. + // + // Arguments: + // + // * ctx: gives callers a way to gracefully cancel a long-running + // list. + // * prefix: limits the result set to names starting with that + // value. + // * size: limits the length of the result set to the first 'size' + // matches. A value of -1 means "all results". + // * from: shifts the result set to start from the 'from'th match. + // * nameCallback: called for every matching name. List returns any + // errors returned by nameCallback and aborts further listing. + // + // For example, a store with names like: + // + // * 123 + // * abcd + // * abce + // * abcf + // * abcg + // + // will have the following call/result pairs: + // + // * List(ctx, "", -1, 0, printName) -> "123", "abcd", "abce", "abcf", "abcg" + // * List(ctx, "", 2, 0, printName) -> "123", "abcd" + // * List(ctx, "", 2, 1, printName) -> "abcd", "abce" + // * List(ctx,"abc", 2, 1, printName) -> "abce", "abcf" + List(ctx context.Context, prefix string, size int, from int, nameCallback ListNameCallback) (err error) + + // Delete removes a reference from the store. The action is + // idempotent; a nil return means "that reference is not in the + // store" without implying "because of your Delete()". + Delete(ctx context.Context, name string) (err error) + + // Close releases resources held by the engine. Subsequent engine + // method calls will fail. + Close() (err error) +} diff --git a/image/refs/layout/main.go b/image/refs/layout/main.go new file mode 100644 index 0000000..4e42d79 --- /dev/null +++ b/image/refs/layout/main.go @@ -0,0 +1,36 @@ +// 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 refs interface using the image-spec's +// image-layout [1]. +// +// [1]: https://github.com/opencontainers/image-spec/blob/master/image-layout.md +package layout + +import ( + "os" + + "github.com/opencontainers/image-tools/image/refs" +) + +// NewEngine instantiates an engine with the appropriate backend (tar, +// HTTP, ...). +func NewEngine(path string) (engine refs.Engine, err error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + + return NewTarEngine(file) +} diff --git a/image/refs/layout/tar.go b/image/refs/layout/tar.go new file mode 100644 index 0000000..18f8776 --- /dev/null +++ b/image/refs/layout/tar.go @@ -0,0 +1,139 @@ +// 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" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/opencontainers/image-spec/specs-go" + caslayout "github.com/opencontainers/image-tools/image/cas/layout" + "github.com/opencontainers/image-tools/image/refs" + "golang.org/x/net/context" +) + +// TarEngine is a refs.Engine backed by a tar file. +type TarEngine struct { + file caslayout.ReadWriteSeekCloser +} + +// NewTarEngine returns a new TarEngine. +func NewTarEngine(file caslayout.ReadWriteSeekCloser) (engine refs.Engine, err error) { + engine = &TarEngine{ + file: file, + } + + return engine, nil +} + +// Put adds a new reference to the store. +func (engine *TarEngine) Put(ctx context.Context, name string, descriptor *specs.Descriptor) (err error) { + // FIXME + return errors.New("TarEngine.Put is not supported yet") +} + +// Get returns a reference from the store. +func (engine *TarEngine) Get(ctx context.Context, name string) (descriptor *specs.Descriptor, err error) { + targetName := fmt.Sprintf("./refs/%s", name) + + _, 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 { + decoder := json.NewDecoder(tarReader) + var desc specs.Descriptor + err = decoder.Decode(&desc) + if err != nil { + return nil, err + } + return &desc, nil + } + } +} + +// List returns available names from the store. +func (engine *TarEngine) List(ctx context.Context, prefix string, size int, from int, nameCallback refs.ListNameCallback) (err error) { + var i = 0 + + _, err = engine.file.Seek(0, os.SEEK_SET) + if err != nil { + return nil + } + + pathPrefix := fmt.Sprintf("./refs/%s", prefix) + + tarReader := tar.NewReader(engine.file) + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + var header *tar.Header + header, err = tarReader.Next() + if err == io.EOF { + return nil + } else if err != nil { + return err + } + + if strings.HasPrefix(header.Name, pathPrefix) && len(header.Name) > 7 { + i++ + if i > from { + err = nameCallback(ctx, header.Name[7:]) + if err != nil { + return err + } + if i-from == size { + return nil + } + } + } + } +} + +// Delete removes a reference from the store. +func (engine *TarEngine) Delete(ctx context.Context, name 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() +}