Initial version, needs work

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2017-01-26 17:58:00 +01:00
parent 8b7705022a
commit 03b2e90dba
9 changed files with 1132 additions and 0 deletions

96
cmd/stevedore/commit.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"encoding/json"
"fmt"
"github.com/containers/image/copy"
"github.com/containers/image/signature"
"github.com/containers/image/transports"
"github.com/urfave/cli"
)
var (
commitFlags = []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "name of the working container",
},
cli.StringFlag{
Name: "root",
Usage: "root directory of the working container",
},
cli.StringFlag{
Name: "link",
Usage: "symlink to the root directory of the working container",
},
cli.StringFlag{
Name: "output",
Usage: "image to create",
},
}
)
func commitCmd(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return err
}
name := ""
if c.IsSet("name") {
name = c.String("name")
}
root := ""
if c.IsSet("root") {
root = c.String("root")
}
link := ""
if c.IsSet("link") {
link = c.String("link")
}
output := ""
if c.IsSet("output") {
output = c.String("output")
}
if output == "" {
return fmt.Errorf("the --output flag must be specified")
}
if name == "" && root == "" && link == "" {
return fmt.Errorf("either --name or --root or --link, or some combination, must be specified")
}
container, err := lookupContainer(store, name, root, link)
if err != nil {
return err
}
mdata, err := store.GetMetadata(container.ID)
if err != nil {
return err
}
metadata := ContainerMetadata{}
err = json.Unmarshal([]byte(mdata), &metadata)
if err != nil {
return err
}
policy, err := signature.DefaultPolicy(getSystemContext(c))
if err != nil {
return err
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return err
}
destRef, err := transports.ParseImageName(output)
if err != nil {
return fmt.Errorf("error parsing output image name %q: %v", output, err)
}
config := updateConfig(c, metadata.Config)
err = copy.Image(policyContext, destRef, makeContainerImageRef(store, container, string(config)), getCopyOptions())
return err
}

118
cmd/stevedore/common.go Normal file
View File

@ -0,0 +1,118 @@
package main
import (
"encoding/json"
"fmt"
"github.com/containers/image/copy"
is "github.com/containers/image/storage"
"github.com/containers/image/types"
"github.com/containers/storage/storage"
"github.com/urfave/cli"
)
const (
ContainerType = "stevedore 0.0.0"
)
type ContainerMetadata struct {
Type string
Config []byte
Manifest []byte
Links []string
Mounts []string
}
func getStore(c *cli.Context) (storage.Store, error) {
options := storage.DefaultStoreOptions
if c.GlobalIsSet("root") || c.GlobalIsSet("runroot") {
options.GraphRoot = c.GlobalString("root")
options.RunRoot = c.GlobalString("runroot")
}
if c.GlobalIsSet("storage-driver") {
options.GraphDriverName = c.GlobalString("storage-driver")
}
if c.GlobalIsSet("storage-options") {
opts := c.GlobalStringSlice("storage-options")
if len(opts) > 0 {
options.GraphDriverOptions = opts
}
}
store, err := storage.GetStore(options)
if store != nil {
is.Transport.SetStore(store)
}
return store, err
}
func getSystemContext(c *cli.Context) *types.SystemContext {
sc := &types.SystemContext{}
if c.GlobalIsSet("signature-policy") {
sc.SignaturePolicyPath = c.GlobalString("signature-policy")
}
return sc
}
func getCopyOptions() *copy.Options {
return &copy.Options{}
}
func lookupContainer(store storage.Store, name, root, link string) (*storage.Container, error) {
containers, err := store.Containers()
if err != nil {
return nil, fmt.Errorf("error listing containers: %v", err)
}
for _, c := range containers {
if name != "" {
matches := false
for _, n := range c.Names {
if name == n {
matches = true
break
}
}
if !matches {
continue
}
}
metadata := ContainerMetadata{}
if root != "" || link != "" {
mdata, err := store.GetMetadata(c.ID)
if err != nil || mdata == "" {
// probably not one of ours
continue
}
err = json.Unmarshal([]byte(mdata), &metadata)
if err != nil {
// probably not one of ours
continue
}
}
if root != "" {
matches := false
for _, m := range metadata.Mounts {
if m == root {
matches = true
break
}
}
if !matches {
continue
}
}
if link != "" {
matches := false
for _, l := range metadata.Links {
if l == link {
matches = true
break
}
}
if !matches {
continue
}
}
return &c, nil
}
return nil, fmt.Errorf("no matching container found")
}

28
cmd/stevedore/config.go Normal file
View File

@ -0,0 +1,28 @@
package main
import (
"bytes"
"github.com/Sirupsen/logrus"
"github.com/opencontainers/runtime-tools/generate"
)
var (
configFlags = []cli.Flag{}
)
func updateConfig(c *cli.Context, config []byte) []byte {
buffer := bytes.Buffer{}
g, err := generate.NewFromTemplate(bytes.NewReader(config))
if err != nil {
logrus.Errorf("error importing template configuration, using original configuration")
return config
}
options := generate.ExportOptions{}
err = g.Save(buffer, options)
if err != nil {
logrus.Errorf("error exporting updated configuration, using original configuration")
return config
}
return buffer.Bytes()
}

88
cmd/stevedore/delete.go Normal file
View File

@ -0,0 +1,88 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/urfave/cli"
)
var (
deleteFlags = []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "name of the working container",
},
cli.StringFlag{
Name: "root",
Usage: "root directory of the working container",
},
cli.StringFlag{
Name: "link",
Usage: "symlink to the root directory of the working container",
},
}
)
func deleteCmd(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return err
}
name := ""
if c.IsSet("name") {
name = c.String("name")
}
root := ""
if c.IsSet("root") {
root = c.String("root")
}
link := ""
if c.IsSet("link") {
link = c.String("link")
}
if name == "" && root == "" && link == "" {
return fmt.Errorf("either --name or --root or --link, or some combination, must be specified")
}
container, err := lookupContainer(store, name, root, link)
if err != nil {
return err
}
mdata, err := store.GetMetadata(container.ID)
if err != nil {
return err
}
metadata := ContainerMetadata{}
err = json.Unmarshal([]byte(mdata), &metadata)
if err != nil {
return err
}
for _, link := range metadata.Links {
err = os.Remove(link)
if err != nil {
return fmt.Errorf("error removing symlink %q: %v", link, err)
}
}
metadata.Links = nil
mdata2, err := json.Marshal(&metadata)
if err != nil {
return err
}
err = store.SetMetadata(container.ID, string(mdata2))
if err != nil {
return err
}
err = store.DeleteContainer(container.ID)
if err != nil {
return fmt.Errorf("error deleting container: %v", err)
}
return nil
}

240
cmd/stevedore/from.go Normal file
View File

@ -0,0 +1,240 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/Sirupsen/logrus"
"github.com/containers/image/copy"
"github.com/containers/image/signature"
is "github.com/containers/image/storage"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/containers/storage/storage"
"github.com/urfave/cli"
)
var (
fromFlags = []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "set a name for the working container",
},
cli.StringFlag{
Name: "image",
Usage: "name of the starting image",
},
cli.BoolFlag{
Name: "pull",
Usage: "pull the image if not present",
},
cli.StringFlag{
Name: "registry",
Usage: "prefix to prepend to the image name in order to pull the image",
},
cli.BoolFlag{
Name: "mount",
Usage: "mount the working container",
},
cli.StringFlag{
Name: "link",
Usage: "name of a symlink to create to the root directory of the container",
},
}
)
func pullImage(c *cli.Context, store storage.Store, sc *types.SystemContext, name string) error {
spec := name
if c.IsSet("registry") {
spec = c.String("registry") + name
}
srcRef, err := transports.ParseImageName(spec)
if err != nil {
return fmt.Errorf("error parsing image name %q: %v", spec, err)
}
if ref := srcRef.DockerReference(); ref != nil {
name = ref.FullName()
}
destRef, err := is.Transport.ParseStoreReference(store, name)
if err != nil {
return fmt.Errorf("error parsing full image name %q: %v", spec, err)
}
policy, err := signature.DefaultPolicy(getSystemContext(c))
if err != nil {
return err
}
policyContext, err := signature.NewPolicyContext(policy)
if err != nil {
return err
}
logrus.Debugf("copying %q to %q", spec, name)
err = copy.Image(policyContext, destRef, srcRef, getCopyOptions())
if err != nil {
return err
}
// Go find the image, and attach the requested name to it, so that we
// can more easily find it later, even if the destination reference
// looks different.
destImage, err := is.Transport.GetStoreImage(store, destRef)
if err != nil {
return err
}
names := append(destImage.Names, spec, name)
err = store.SetNames(destImage.ID, names)
if err != nil {
return err
}
return nil
}
func fromCmd(c *cli.Context) error {
var img *storage.Image
manifest := []byte{}
config := []byte{}
store, err := getStore(c)
if err != nil {
return err
}
image := ""
if c.IsSet("image") {
image = c.String("image")
}
pull := false
if c.IsSet("pull") {
pull = c.Bool("pull")
}
name := "working-container"
if c.IsSet("name") {
name = c.String("name")
} else {
if image != "" {
name = image + "-working-container"
}
}
if name != "" {
suffix := 1
tmpName := name
err = nil
for err != storage.ErrContainerUnknown {
_, err = store.GetContainer(tmpName)
if err == nil {
suffix++
tmpName = fmt.Sprintf("%s-%d", name, suffix)
}
}
name = tmpName
}
mount := false
if c.IsSet("mount") {
mount = c.Bool("mount")
}
link := ""
if c.IsSet("link") {
link = c.String("link")
if link == "" {
return fmt.Errorf("link location can not be empty")
}
abs, err := filepath.Abs(link)
if err != nil {
return fmt.Errorf("error converting link path %q to absolute path: %v", link, err)
}
link = abs
}
systemContext := getSystemContext(c)
if image != "" {
ref, err := is.Transport.ParseStoreReference(store, image)
if err != nil {
return fmt.Errorf("error parsing reference to image %q: %v", image, err)
}
img, err = is.Transport.GetStoreImage(store, ref)
if err != nil {
if err != storage.ErrImageUnknown || !pull {
return fmt.Errorf("no such image %q: %v", image, err)
}
err = pullImage(c, store, systemContext, image)
if err != nil {
return fmt.Errorf("error pulling image %q: %v", image, err)
}
ref, err = is.Transport.ParseStoreReference(store, image)
if err != nil {
return fmt.Errorf("error parsing reference to image %q: %v", image, err)
}
img, err = is.Transport.GetStoreImage(store, ref)
}
if err != nil {
return fmt.Errorf("no such image %q: %v", image, err)
}
image = img.ID
src, err := ref.NewImage(systemContext)
if err != nil {
return fmt.Errorf("error instantiating image: %v", err)
}
defer src.Close()
config, err = src.ConfigBlob()
if err != nil {
return fmt.Errorf("error reading image configuration: %v", err)
}
manifest, _, err = src.Manifest()
if err != nil {
return fmt.Errorf("error reading image manifest: %v", err)
}
}
metadata := &ContainerMetadata{
Type: ContainerType,
Config: config,
Manifest: manifest,
}
options := storage.ContainerOptions{}
container, err := store.CreateContainer("", []string{name}, image, "", "", &options)
if err != nil {
return fmt.Errorf("error creating container: %v", err)
}
if mount {
mountPoint, err := store.Mount(container.ID, "")
if err != nil {
return fmt.Errorf("error mounting container: %v", err)
}
metadata.Mounts = []string{mountPoint}
if link != "" {
err = os.Symlink(mountPoint, link)
if err != nil {
return fmt.Errorf("error creating symlink to %q: %v", mountPoint, err)
}
metadata.Links = []string{link}
}
}
fmt.Printf("%s\n", name)
mdata, err := json.Marshal(metadata)
if err != nil {
return fmt.Errorf("error encoding container metadata: %v", err)
}
err = store.SetMetadata(container.ID, string(mdata))
if err != nil {
return fmt.Errorf("error saving container metadata: %v", err)
}
return nil
}

252
cmd/stevedore/image.go Normal file
View File

@ -0,0 +1,252 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/Sirupsen/logrus"
"github.com/containers/image/docker/reference"
"github.com/containers/image/image"
is "github.com/containers/image/storage"
"github.com/containers/image/types"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/ioutils"
"github.com/containers/storage/storage"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
"github.com/opencontainers/image-spec/specs-go/v1"
)
type containerImageRef struct {
store storage.Store
container *storage.Container
name reference.Named
config []byte
}
type containerImageSource struct {
path string
ref *containerImageRef
store storage.Store
container *storage.Container
config []byte
configDigest digest.Digest
manifest []byte
}
func (i *containerImageRef) NewImage(sc *types.SystemContext) (types.Image, error) {
src, err := i.NewImageSource(sc, nil)
if err != nil {
return nil, err
}
return image.FromSource(src)
}
func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestTypes []string) (src types.ImageSource, err error) {
if len(manifestTypes) > 0 {
ok := false
for _, mt := range manifestTypes {
if mt == v1.MediaTypeImageManifest {
ok = true
break
}
}
if !ok {
return nil, fmt.Errorf("no supported manifest types")
}
}
layers := []string{}
layerID := i.container.LayerID
layer, err := i.store.GetLayer(layerID)
if err != nil {
return nil, fmt.Errorf("unable to read layer %q: %v", layerID, err)
}
for layer != nil {
layers = append(append([]string{}, layerID), layers...)
layerID = layer.Parent
if layerID == "" {
err = nil
break
}
layer, err = i.store.GetLayer(layerID)
if err != nil {
return nil, fmt.Errorf("unable to read layer %q: %v", layerID, err)
}
}
path, err := ioutil.TempDir(os.TempDir(), "stevedore")
if err != nil {
return nil, err
}
defer func() {
if src == nil {
err2 := os.RemoveAll(path)
if err2 != nil {
logrus.Errorf("error removing %q: %v", path, err)
}
}
}()
manifest := v1.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Config: v1.Descriptor{
MediaType: v1.MediaTypeImageConfig,
Digest: digest.FromBytes(i.config),
Size: int64(len(i.config)),
},
Layers: []v1.Descriptor{},
}
for _, layerID := range layers {
rc, err := i.store.Diff("", layerID)
if err != nil {
return nil, fmt.Errorf("error extracting layer %q: %v", layerID, err)
}
defer rc.Close()
uncompressed, err := archive.DecompressStream(rc)
if err != nil {
return nil, fmt.Errorf("error decompressing layer %q: %v", layerID, err)
}
defer uncompressed.Close()
hasher := digest.Canonical.Digester()
reader := io.TeeReader(uncompressed, hasher.Hash())
layerFile, err := os.OpenFile(filepath.Join(path, "layer"), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return nil, fmt.Errorf("error opening file for layer %q: %v", layerID, err)
}
size, err := io.Copy(layerFile, reader)
if err != nil {
return nil, fmt.Errorf("error storing layer %q to file: %v", layerID, err)
}
layerFile.Close()
err = os.Rename(filepath.Join(path, "layer"), filepath.Join(path, hasher.Digest().String()))
layerDescriptor := v1.Descriptor{
MediaType: v1.MediaTypeImageLayer,
Digest: hasher.Digest(),
Size: size,
}
manifest.Layers = append(manifest.Layers, layerDescriptor)
}
mfest, err := json.Marshal(&manifest)
if err != nil {
return nil, err
}
logrus.Debugf("manifest = %s\n", mfest)
src = &containerImageSource{
path: path,
ref: i,
store: i.store,
container: i.container,
manifest: mfest,
config: i.config,
configDigest: manifest.Config.Digest,
}
return src, nil
}
func (i *containerImageRef) NewImageDestination(sc *types.SystemContext) (types.ImageDestination, error) {
return nil, fmt.Errorf("can't write to a container")
}
func (i *containerImageRef) DockerReference() reference.Named {
return i.name
}
func (i *containerImageRef) StringWithinTransport() string {
if len(i.container.Names) > 0 {
return i.container.Names[0]
}
return ""
}
func (i *containerImageRef) DeleteImage(*types.SystemContext) error {
// we were never here
return nil
}
func (i *containerImageRef) PolicyConfigurationIdentity() string {
return ""
}
func (i *containerImageRef) PolicyConfigurationNamespaces() []string {
return nil
}
func (i *containerImageRef) Transport() types.ImageTransport {
return is.Transport
}
func (i *containerImageSource) Close() {
err := os.RemoveAll(i.path)
if err != nil {
logrus.Errorf("error removing %q: %v", i.path, err)
}
}
func (i *containerImageSource) Reference() types.ImageReference {
return i.ref
}
func (i *containerImageSource) GetSignatures() ([][]byte, error) {
return nil, nil
}
func (i *containerImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
return []byte{}, "", fmt.Errorf("TODO")
}
func (i *containerImageSource) GetManifest() ([]byte, string, error) {
return i.manifest, v1.MediaTypeImageManifest, nil
}
func (i *containerImageSource) GetBlob(blob types.BlobInfo) (reader io.ReadCloser, size int64, err error) {
if blob.Digest == i.configDigest {
logrus.Debugf("start reading config")
reader := bytes.NewReader(i.config)
closer := func() error {
logrus.Debugf("finished reading config")
return nil
}
return ioutils.NewReadCloserWrapper(reader, closer), reader.Size(), nil
}
layerFile, err := os.OpenFile(filepath.Join(i.path, blob.Digest.String()), os.O_RDONLY, 0600)
if err != nil {
logrus.Debugf("error reading layer %q: %v", blob.Digest.String(), err)
return nil, -1, err
}
size = -1
st, err := layerFile.Stat()
if err != nil {
logrus.Warnf("error reading size of layer %q: %v", blob.Digest.String(), err)
} else {
size = st.Size()
}
logrus.Debugf("reading layer %q", blob.Digest.String())
return layerFile, size, nil
}
func makeContainerImageRef(store storage.Store, container *storage.Container, config string) types.ImageReference {
var err error
var name reference.Named
if len(container.Names) > 0 {
name, err = reference.ParseNamed(container.Names[0])
if err != nil {
name = nil
}
}
return &containerImageRef{
store: store,
container: container,
name: name,
config: []byte(config),
}
}

105
cmd/stevedore/main.go Normal file
View File

@ -0,0 +1,105 @@
package main
import (
"os"
"github.com/Sirupsen/logrus"
"github.com/containers/storage/pkg/reexec"
"github.com/urfave/cli"
)
func main() {
if reexec.Init() {
return
}
app := cli.NewApp()
app.Name = "stevedore"
app.Usage = "minimal image builder"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "root",
Usage: "storage root dir",
},
cli.StringFlag{
Name: "runroot",
Usage: "storage state dir",
},
cli.BoolFlag{
Name: "storage-driver",
Usage: "storage driver",
},
cli.StringSliceFlag{
Name: "storage-option",
Usage: "storage driver option",
},
cli.StringFlag{
Name: "signature-policy",
Usage: "signature policy path",
},
cli.BoolFlag{
Name: "debug",
Usage: "print debugging information",
},
}
app.Before = func(c *cli.Context) error {
logrus.SetLevel(logrus.ErrorLevel)
if c.GlobalIsSet("debug") {
if c.GlobalBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}
}
return nil
}
app.After = func(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return err
}
store.Shutdown(false)
return nil
}
app.Commands = []cli.Command{
{
Name: "from",
Aliases: []string{"f"},
Usage: "create a working container based on an image",
Description: "creates a working container based on an image",
Flags: fromFlags,
Action: fromCmd,
},
{
Name: "mount",
Aliases: []string{"m"},
Usage: "mount and create a symbolic link to a working container's filesystem root",
Description: "mounts and creates a symbolic link to a working container's filesystem root",
Flags: mountFlags,
Action: mountCmd,
},
{
Name: "umount",
Aliases: []string{"u", "unmount"},
Usage: "unmount and remove a symbolic link to a working container's filesystem root",
Description: "unmounts and removes a symbolic link to a working container's filesystem root",
Flags: umountFlags,
Action: umountCmd,
},
{
Name: "commit",
Aliases: []string{"c"},
Usage: "create an image from a working container",
Description: "creates an image from a working container",
Flags: append(commitFlags, configFlags...),
Action: commitCmd,
},
{
Name: "delete",
Aliases: []string{"d"},
Usage: "delete a working container",
Description: "deletes a working container",
Flags: deleteFlags,
Action: deleteCmd,
},
}
app.Run(os.Args)
}

108
cmd/stevedore/mount.go Normal file
View File

@ -0,0 +1,108 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/urfave/cli"
)
var (
mountFlags = []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "name of the working container",
},
cli.StringFlag{
Name: "root",
Usage: "a previous root directory of the working container",
},
cli.StringFlag{
Name: "link",
Usage: "name of a symlink to create",
},
}
)
func mountCmd(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return err
}
name := ""
if c.IsSet("name") {
name = c.String("name")
}
root := ""
if c.IsSet("root") {
root = c.String("root")
}
link := ""
if c.IsSet("link") {
link = c.String("link")
if link == "" {
return fmt.Errorf("link location can not be empty")
}
abs, err := filepath.Abs(link)
if err != nil {
return fmt.Errorf("error converting link path %q to absolute path: %v", link, err)
}
link = abs
}
if name == "" && root == "" {
return fmt.Errorf("either --name or --root, or both, must be specified")
}
container, err := lookupContainer(store, name, root, "")
if err != nil {
return err
}
mdata, err := store.GetMetadata(container.ID)
if err != nil {
return err
}
metadata := ContainerMetadata{}
err = json.Unmarshal([]byte(mdata), &metadata)
if err != nil {
return err
}
mountPoint, err := store.Mount(container.ID, "")
if err != nil {
return fmt.Errorf("error mounting container: %v", err)
}
present := false
for _, m := range metadata.Mounts {
if m == mountPoint {
present = true
break
}
}
if !present {
metadata.Mounts = append(append([]string{}, metadata.Mounts...), mountPoint)
}
if link != "" {
err = os.Symlink(mountPoint, link)
if err != nil {
return fmt.Errorf("error creating symlink to %q: %v", mountPoint, err)
}
metadata.Links = append(append([]string{}, metadata.Links...), link)
}
mdata2, err := json.Marshal(&metadata)
if err != nil {
return err
}
err = store.SetMetadata(container.ID, string(mdata2))
if err != nil {
return err
}
return nil
}

97
cmd/stevedore/umount.go Normal file
View File

@ -0,0 +1,97 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/urfave/cli"
)
var (
umountFlags = []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "name of the working container",
},
cli.StringFlag{
Name: "root",
Usage: "root directory of the working container",
},
cli.StringFlag{
Name: "link",
Usage: "symlink to the root directory of the working container",
},
}
)
func umountCmd(c *cli.Context) error {
store, err := getStore(c)
if err != nil {
return err
}
name := ""
if c.IsSet("name") {
name = c.String("name")
}
root := ""
if c.IsSet("root") {
root = c.String("root")
}
link := ""
if c.IsSet("link") {
link = c.String("link")
if link == "" {
return fmt.Errorf("link location can not be empty")
}
abs, err := filepath.Abs(link)
if err != nil {
return fmt.Errorf("error converting link path %q to absolute path: %v", link, err)
}
link = abs
}
if name == "" && root == "" && link == "" {
return fmt.Errorf("either --name or --root or --link, or some combination, must be specified")
}
container, err := lookupContainer(store, name, root, link)
if err != nil {
return err
}
err = store.Unmount(container.ID)
if err != nil {
return err
}
mdata, err := store.GetMetadata(container.ID)
if err != nil {
return err
}
metadata := ContainerMetadata{}
err = json.Unmarshal([]byte(mdata), &metadata)
if err != nil {
return err
}
for _, link := range metadata.Links {
err = os.Remove(link)
if err != nil {
return fmt.Errorf("error removing symlink %q: %v", link, err)
}
}
metadata.Links = nil
mdata2, err := json.Marshal(&metadata)
if err != nil {
return err
}
err = store.SetMetadata(container.ID, string(mdata2))
if err != nil {
return err
}
return nil
}