
Remove rootfs service in place of snapshot service. Adds diff service for extracting and creating diffs. Diff creation is not yet implemented. This service allows pulling or creating images without needing root access to mount. Additionally in the future this will allow containerd to ensure extractions happen safely in a chroot if needed. Signed-off-by: Derek McGowan <derek@mcgstyle.net>
190 lines
4.7 KiB
Go
190 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containerd/console"
|
|
contentapi "github.com/containerd/containerd/api/services/content"
|
|
imagesapi "github.com/containerd/containerd/api/services/images"
|
|
"github.com/containerd/containerd/content"
|
|
"github.com/containerd/containerd/images"
|
|
"github.com/containerd/containerd/remotes"
|
|
"github.com/containerd/containerd/remotes/docker"
|
|
"github.com/containerd/containerd/rootfs"
|
|
contentservice "github.com/containerd/containerd/services/content"
|
|
imagesservice "github.com/containerd/containerd/services/images"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/urfave/cli"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
var registryFlags = []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "skip-verify,k",
|
|
Usage: "Skip SSL certificate validation",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "plain-http",
|
|
Usage: "Allow connections using plain HTTP",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "user,u",
|
|
Usage: "user[:password] Registry user and password",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "refresh",
|
|
Usage: "Refresh token for authorization server",
|
|
},
|
|
}
|
|
|
|
func resolveContentStore(context *cli.Context) (content.Store, error) {
|
|
root := filepath.Join(context.GlobalString("root"), "content")
|
|
if !filepath.IsAbs(root) {
|
|
var err error
|
|
root, err = filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
conn, err := connectGRPC(context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return contentservice.NewStoreFromClient(contentapi.NewContentClient(conn)), nil
|
|
}
|
|
|
|
func resolveImageStore(clicontext *cli.Context) (images.Store, error) {
|
|
conn, err := connectGRPC(clicontext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)), nil
|
|
}
|
|
|
|
func connectGRPC(context *cli.Context) (*grpc.ClientConn, error) {
|
|
address := context.GlobalString("address")
|
|
timeout := context.GlobalDuration("connect-timeout")
|
|
return grpc.Dial(address,
|
|
grpc.WithTimeout(timeout),
|
|
grpc.WithBlock(),
|
|
grpc.WithInsecure(),
|
|
grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
|
|
return net.DialTimeout("unix", address, timeout)
|
|
}),
|
|
)
|
|
}
|
|
|
|
// getResolver prepares the resolver from the environment and options.
|
|
func getResolver(ctx context.Context, clicontext *cli.Context) (remotes.Resolver, error) {
|
|
username := clicontext.String("user")
|
|
var secret string
|
|
if i := strings.IndexByte(username, ':'); i > 0 {
|
|
secret = username[i+1:]
|
|
username = username[0:i]
|
|
}
|
|
options := docker.ResolverOptions{
|
|
PlainHTTP: clicontext.Bool("plain-http"),
|
|
}
|
|
if username != "" {
|
|
if secret == "" {
|
|
fmt.Printf("Password: ")
|
|
|
|
var err error
|
|
secret, err = passwordPrompt()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Print("\n")
|
|
}
|
|
} else if rt := clicontext.String("refresh"); rt != "" {
|
|
secret = rt
|
|
}
|
|
|
|
options.Credentials = func(host string) (string, string, error) {
|
|
// Only one host
|
|
return username, secret, nil
|
|
}
|
|
|
|
tr := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
DualStack: true,
|
|
}).DialContext,
|
|
MaxIdleConns: 10,
|
|
IdleConnTimeout: 30 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: clicontext.Bool("insecure"),
|
|
},
|
|
ExpectContinueTimeout: 5 * time.Second,
|
|
}
|
|
|
|
options.Client = &http.Client{
|
|
Transport: tr,
|
|
}
|
|
|
|
return docker.NewResolver(options), nil
|
|
}
|
|
|
|
func passwordPrompt() (string, error) {
|
|
c := console.Current()
|
|
defer c.Reset()
|
|
|
|
if err := c.DisableEcho(); err != nil {
|
|
return "", errors.Wrap(err, "failed to disable echo")
|
|
}
|
|
|
|
line, _, err := bufio.NewReader(c).ReadLine()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to read line")
|
|
}
|
|
return string(line), nil
|
|
}
|
|
|
|
func getImageLayers(ctx context.Context, image images.Image, cs content.Store) ([]rootfs.Layer, error) {
|
|
p, err := content.ReadBlob(ctx, cs, image.Target.Digest)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to read manifest blob")
|
|
}
|
|
|
|
var manifest ocispec.Manifest
|
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
|
return nil, errors.Wrap(err, "failed to unmarshal manifest")
|
|
}
|
|
|
|
diffIDs, err := image.RootFS(ctx, cs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to resolve rootfs")
|
|
}
|
|
|
|
if len(diffIDs) != len(manifest.Layers) {
|
|
return nil, errors.Errorf("mismatched image rootfs and manifest layers")
|
|
}
|
|
|
|
layers := make([]rootfs.Layer, len(diffIDs))
|
|
for i := range diffIDs {
|
|
layers[i].Diff = ocispec.Descriptor{
|
|
// TODO: derive media type from compressed type
|
|
MediaType: ocispec.MediaTypeImageLayer,
|
|
Digest: diffIDs[i],
|
|
}
|
|
layers[i].Blob = manifest.Layers[i]
|
|
}
|
|
|
|
return layers, nil
|
|
}
|