| package plugin // import "github.com/docker/docker/plugin" | |
| import ( | |
| "archive/tar" | |
| "bytes" | |
| "compress/gzip" | |
| "context" | |
| "encoding/json" | |
| "io" | |
| "net/http" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "strings" | |
| "time" | |
| "github.com/containerd/containerd/content" | |
| "github.com/containerd/containerd/images" | |
| "github.com/containerd/containerd/platforms" | |
| "github.com/containerd/containerd/remotes" | |
| "github.com/containerd/containerd/remotes/docker" | |
| "github.com/docker/distribution/manifest/schema2" | |
| "github.com/docker/distribution/reference" | |
| "github.com/docker/docker/api/types" | |
| "github.com/docker/docker/api/types/filters" | |
| "github.com/docker/docker/api/types/registry" | |
| "github.com/docker/docker/dockerversion" | |
| "github.com/docker/docker/errdefs" | |
| "github.com/docker/docker/pkg/authorization" | |
| "github.com/docker/docker/pkg/chrootarchive" | |
| "github.com/docker/docker/pkg/containerfs" | |
| "github.com/docker/docker/pkg/pools" | |
| "github.com/docker/docker/pkg/progress" | |
| "github.com/docker/docker/pkg/stringid" | |
| v2 "github.com/docker/docker/plugin/v2" | |
| "github.com/moby/sys/mount" | |
| "github.com/opencontainers/go-digest" | |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" | |
| "github.com/pkg/errors" | |
| "github.com/sirupsen/logrus" | |
| ) | |
| var acceptedPluginFilterTags = map[string]bool{ | |
| "enabled": true, | |
| "capability": true, | |
| } | |
| // Disable deactivates a plugin. This means resources (volumes, networks) cant use them. | |
| func (pm *Manager) Disable(refOrID string, config *types.PluginDisableConfig) error { | |
| p, err := pm.config.Store.GetV2Plugin(refOrID) | |
| if err != nil { | |
| return err | |
| } | |
| pm.mu.RLock() | |
| c := pm.cMap[p] | |
| pm.mu.RUnlock() | |
| if !config.ForceDisable && p.GetRefCount() > 0 { | |
| return errors.WithStack(inUseError(p.Name())) | |
| } | |
| for _, typ := range p.GetTypes() { | |
| if typ.Capability == authorization.AuthZApiImplements { | |
| pm.config.AuthzMiddleware.RemovePlugin(p.Name()) | |
| } | |
| } | |
| if err := pm.disable(p, c); err != nil { | |
| return err | |
| } | |
| pm.publisher.Publish(EventDisable{Plugin: p.PluginObj}) | |
| pm.config.LogPluginEvent(p.GetID(), refOrID, "disable") | |
| return nil | |
| } | |
| // Enable activates a plugin, which implies that they are ready to be used by containers. | |
| func (pm *Manager) Enable(refOrID string, config *types.PluginEnableConfig) error { | |
| p, err := pm.config.Store.GetV2Plugin(refOrID) | |
| if err != nil { | |
| return err | |
| } | |
| c := &controller{timeoutInSecs: config.Timeout} | |
| if err := pm.enable(p, c, false); err != nil { | |
| return err | |
| } | |
| pm.publisher.Publish(EventEnable{Plugin: p.PluginObj}) | |
| pm.config.LogPluginEvent(p.GetID(), refOrID, "enable") | |
| return nil | |
| } | |
| // Inspect examines a plugin config | |
| func (pm *Manager) Inspect(refOrID string) (tp *types.Plugin, err error) { | |
| p, err := pm.config.Store.GetV2Plugin(refOrID) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &p.PluginObj, nil | |
| } | |
| func computePrivileges(c types.PluginConfig) types.PluginPrivileges { | |
| var privileges types.PluginPrivileges | |
| if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "network", | |
| Description: "permissions to access a network", | |
| Value: []string{c.Network.Type}, | |
| }) | |
| } | |
| if c.IpcHost { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "host ipc namespace", | |
| Description: "allow access to host ipc namespace", | |
| Value: []string{"true"}, | |
| }) | |
| } | |
| if c.PidHost { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "host pid namespace", | |
| Description: "allow access to host pid namespace", | |
| Value: []string{"true"}, | |
| }) | |
| } | |
| for _, mnt := range c.Mounts { | |
| if mnt.Source != nil { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "mount", | |
| Description: "host path to mount", | |
| Value: []string{*mnt.Source}, | |
| }) | |
| } | |
| } | |
| for _, device := range c.Linux.Devices { | |
| if device.Path != nil { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "device", | |
| Description: "host device to access", | |
| Value: []string{*device.Path}, | |
| }) | |
| } | |
| } | |
| if c.Linux.AllowAllDevices { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "allow-all-devices", | |
| Description: "allow 'rwm' access to all devices", | |
| Value: []string{"true"}, | |
| }) | |
| } | |
| if len(c.Linux.Capabilities) > 0 { | |
| privileges = append(privileges, types.PluginPrivilege{ | |
| Name: "capabilities", | |
| Description: "list of additional capabilities required", | |
| Value: c.Linux.Capabilities, | |
| }) | |
| } | |
| return privileges | |
| } | |
| // Privileges pulls a plugin config and computes the privileges required to install it. | |
| func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHeader http.Header, authConfig *registry.AuthConfig) (types.PluginPrivileges, error) { | |
| var ( | |
| config types.PluginConfig | |
| configSeen bool | |
| ) | |
| h := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { | |
| switch desc.MediaType { | |
| case schema2.MediaTypeManifest, ocispec.MediaTypeImageManifest: | |
| data, err := content.ReadBlob(ctx, pm.blobStore, desc) | |
| if err != nil { | |
| return nil, errors.Wrapf(err, "error reading image manifest from blob store for %s", ref) | |
| } | |
| var m ocispec.Manifest | |
| if err := json.Unmarshal(data, &m); err != nil { | |
| return nil, errors.Wrapf(err, "error unmarshaling image manifest for %s", ref) | |
| } | |
| return []ocispec.Descriptor{m.Config}, nil | |
| case schema2.MediaTypePluginConfig: | |
| configSeen = true | |
| data, err := content.ReadBlob(ctx, pm.blobStore, desc) | |
| if err != nil { | |
| return nil, errors.Wrapf(err, "error reading plugin config from blob store for %s", ref) | |
| } | |
| if err := json.Unmarshal(data, &config); err != nil { | |
| return nil, errors.Wrapf(err, "error unmarshaling plugin config for %s", ref) | |
| } | |
| } | |
| return nil, nil | |
| } | |
| if err := pm.fetch(ctx, ref, authConfig, progress.DiscardOutput(), metaHeader, images.HandlerFunc(h)); err != nil { | |
| return types.PluginPrivileges{}, nil | |
| } | |
| if !configSeen { | |
| return types.PluginPrivileges{}, errors.Errorf("did not find plugin config for specified reference %s", ref) | |
| } | |
| return computePrivileges(config), nil | |
| } | |
| // Upgrade upgrades a plugin | |
| // | |
| // TODO: replace reference package usage with simpler url.Parse semantics | |
| func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) { | |
| p, err := pm.config.Store.GetV2Plugin(name) | |
| if err != nil { | |
| return err | |
| } | |
| if p.IsEnabled() { | |
| return errors.Wrap(enabledError(p.Name()), "plugin must be disabled before upgrading") | |
| } | |
| // revalidate because Pull is public | |
| if _, err := reference.ParseNormalizedNamed(name); err != nil { | |
| return errors.Wrapf(errdefs.InvalidParameter(err), "failed to parse %q", name) | |
| } | |
| pm.muGC.RLock() | |
| defer pm.muGC.RUnlock() | |
| tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs") | |
| if err != nil { | |
| return errors.Wrap(err, "error creating tmp dir for plugin rootfs") | |
| } | |
| var md fetchMeta | |
| ctx, cancel := context.WithCancel(ctx) | |
| out, waitProgress := setupProgressOutput(outStream, cancel) | |
| defer waitProgress() | |
| if err := pm.fetch(ctx, ref, authConfig, out, metaHeader, storeFetchMetadata(&md), childrenHandler(pm.blobStore), applyLayer(pm.blobStore, tmpRootFSDir, out)); err != nil { | |
| return err | |
| } | |
| pm.config.LogPluginEvent(reference.FamiliarString(ref), name, "pull") | |
| if err := validateFetchedMetadata(md); err != nil { | |
| return err | |
| } | |
| if err := pm.upgradePlugin(p, md.config, md.manifest, md.blobs, tmpRootFSDir, &privileges); err != nil { | |
| return err | |
| } | |
| p.PluginObj.PluginReference = ref.String() | |
| return nil | |
| } | |
| // Pull pulls a plugin, check if the correct privileges are provided and install the plugin. | |
| // | |
| // TODO: replace reference package usage with simpler url.Parse semantics | |
| func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *registry.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer, opts ...CreateOpt) (err error) { | |
| pm.muGC.RLock() | |
| defer pm.muGC.RUnlock() | |
| // revalidate because Pull is public | |
| nameref, err := reference.ParseNormalizedNamed(name) | |
| if err != nil { | |
| return errors.Wrapf(errdefs.InvalidParameter(err), "failed to parse %q", name) | |
| } | |
| name = reference.FamiliarString(reference.TagNameOnly(nameref)) | |
| if err := pm.config.Store.validateName(name); err != nil { | |
| return errdefs.InvalidParameter(err) | |
| } | |
| tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs") | |
| if err != nil { | |
| return errors.Wrap(errdefs.System(err), "error preparing upgrade") | |
| } | |
| defer os.RemoveAll(tmpRootFSDir) | |
| var md fetchMeta | |
| ctx, cancel := context.WithCancel(ctx) | |
| out, waitProgress := setupProgressOutput(outStream, cancel) | |
| defer waitProgress() | |
| if err := pm.fetch(ctx, ref, authConfig, out, metaHeader, storeFetchMetadata(&md), childrenHandler(pm.blobStore), applyLayer(pm.blobStore, tmpRootFSDir, out)); err != nil { | |
| return err | |
| } | |
| pm.config.LogPluginEvent(reference.FamiliarString(ref), name, "pull") | |
| if err := validateFetchedMetadata(md); err != nil { | |
| return err | |
| } | |
| refOpt := func(p *v2.Plugin) { | |
| p.PluginObj.PluginReference = ref.String() | |
| } | |
| optsList := make([]CreateOpt, 0, len(opts)+1) | |
| optsList = append(optsList, opts...) | |
| optsList = append(optsList, refOpt) | |
| // TODO: tmpRootFSDir is empty but should have layers in it | |
| p, err := pm.createPlugin(name, md.config, md.manifest, md.blobs, tmpRootFSDir, &privileges, optsList...) | |
| if err != nil { | |
| return err | |
| } | |
| pm.publisher.Publish(EventCreate{Plugin: p.PluginObj}) | |
| return nil | |
| } | |
| // List displays the list of plugins and associated metadata. | |
| func (pm *Manager) List(pluginFilters filters.Args) ([]types.Plugin, error) { | |
| if err := pluginFilters.Validate(acceptedPluginFilterTags); err != nil { | |
| return nil, err | |
| } | |
| enabledOnly := false | |
| disabledOnly := false | |
| if pluginFilters.Contains("enabled") { | |
| enabledFilter, err := pluginFilters.GetBoolOrDefault("enabled", false) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if enabledFilter { | |
| enabledOnly = true | |
| } else { | |
| disabledOnly = true | |
| } | |
| } | |
| plugins := pm.config.Store.GetAll() | |
| out := make([]types.Plugin, 0, len(plugins)) | |
| next: | |
| for _, p := range plugins { | |
| if enabledOnly && !p.PluginObj.Enabled { | |
| continue | |
| } | |
| if disabledOnly && p.PluginObj.Enabled { | |
| continue | |
| } | |
| if pluginFilters.Contains("capability") { | |
| for _, f := range p.GetTypes() { | |
| if !pluginFilters.Match("capability", f.Capability) { | |
| continue next | |
| } | |
| } | |
| } | |
| out = append(out, p.PluginObj) | |
| } | |
| return out, nil | |
| } | |
| // Push pushes a plugin to the registry. | |
| func (pm *Manager) Push(ctx context.Context, name string, metaHeader http.Header, authConfig *registry.AuthConfig, outStream io.Writer) error { | |
| p, err := pm.config.Store.GetV2Plugin(name) | |
| if err != nil { | |
| return err | |
| } | |
| ref, err := reference.ParseNormalizedNamed(p.Name()) | |
| if err != nil { | |
| return errors.Wrapf(err, "plugin has invalid name %v for push", p.Name()) | |
| } | |
| statusTracker := docker.NewInMemoryTracker() | |
| resolver, err := pm.newResolver(ctx, statusTracker, authConfig, metaHeader, false) | |
| if err != nil { | |
| return err | |
| } | |
| pusher, err := resolver.Pusher(ctx, ref.String()) | |
| if err != nil { | |
| return errors.Wrap(err, "error creating plugin pusher") | |
| } | |
| pj := newPushJobs(statusTracker) | |
| ctx, cancel := context.WithCancel(ctx) | |
| out, waitProgress := setupProgressOutput(outStream, cancel) | |
| defer waitProgress() | |
| progressHandler := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { | |
| logrus.WithField("mediaType", desc.MediaType).WithField("digest", desc.Digest.String()).Debug("Preparing to push plugin layer") | |
| id := stringid.TruncateID(desc.Digest.String()) | |
| pj.add(remotes.MakeRefKey(ctx, desc), id) | |
| progress.Update(out, id, "Preparing") | |
| return nil, nil | |
| }) | |
| desc, err := pm.getManifestDescriptor(ctx, p) | |
| if err != nil { | |
| return errors.Wrap(err, "error reading plugin manifest") | |
| } | |
| progress.Messagef(out, "", "The push refers to repository [%s]", reference.FamiliarName(ref)) | |
| // TODO: If a layer already exists on the registry, the progress output just says "Preparing" | |
| go func() { | |
| timer := time.NewTimer(100 * time.Millisecond) | |
| defer timer.Stop() | |
| if !timer.Stop() { | |
| <-timer.C | |
| } | |
| var statuses []contentStatus | |
| for { | |
| timer.Reset(100 * time.Millisecond) | |
| select { | |
| case <-ctx.Done(): | |
| return | |
| case <-timer.C: | |
| statuses = pj.status() | |
| } | |
| for _, s := range statuses { | |
| out.WriteProgress(progress.Progress{ID: s.Ref, Current: s.Offset, Total: s.Total, Action: s.Status, LastUpdate: s.Offset == s.Total}) | |
| } | |
| } | |
| }() | |
| // Make sure we can authenticate the request since the auth scope for plugin repos is different than a normal repo. | |
| ctx = docker.WithScope(ctx, scope(ref, true)) | |
| if err := remotes.PushContent(ctx, pusher, desc, pm.blobStore, nil, nil, func(h images.Handler) images.Handler { | |
| return images.Handlers(progressHandler, h) | |
| }); err != nil { | |
| // Try fallback to http. | |
| // This is needed because the containerd pusher will only attempt the first registry config we pass, which would | |
| // typically be https. | |
| // If there are no http-only host configs found we'll error out anyway. | |
| resolver, _ := pm.newResolver(ctx, statusTracker, authConfig, metaHeader, true) | |
| if resolver != nil { | |
| pusher, _ := resolver.Pusher(ctx, ref.String()) | |
| if pusher != nil { | |
| logrus.WithField("ref", ref).Debug("Re-attmpting push with http-fallback") | |
| err2 := remotes.PushContent(ctx, pusher, desc, pm.blobStore, nil, nil, func(h images.Handler) images.Handler { | |
| return images.Handlers(progressHandler, h) | |
| }) | |
| if err2 == nil { | |
| err = nil | |
| } else { | |
| logrus.WithError(err2).WithField("ref", ref).Debug("Error while attempting push with http-fallback") | |
| } | |
| } | |
| } | |
| if err != nil { | |
| return errors.Wrap(err, "error pushing plugin") | |
| } | |
| } | |
| // For blobs that already exist in the registry we need to make sure to update the progress otherwise it will just say "pending" | |
| // TODO: How to check if the layer already exists? Is it worth it? | |
| for _, j := range pj.jobs { | |
| progress.Update(out, pj.names[j], "Upload complete") | |
| } | |
| // Signal the client for content trust verification | |
| progress.Aux(out, types.PushResult{Tag: ref.(reference.Tagged).Tag(), Digest: desc.Digest.String(), Size: int(desc.Size)}) | |
| return nil | |
| } | |
| // manifest wraps an OCI manifest, because... | |
| // Historically the registry does not support plugins unless the media type on the manifest is specifically schema2.MediaTypeManifest | |
| // So the OCI manifest media type is not supported. | |
| // Additionally, there is extra validation for the docker schema2 manifest than there is a mediatype set on the manifest itself | |
| // even though this is set on the descriptor | |
| // The OCI types do not have this field. | |
| type manifest struct { | |
| ocispec.Manifest | |
| MediaType string `json:"mediaType,omitempty"` | |
| } | |
| func buildManifest(ctx context.Context, s content.Manager, config digest.Digest, layers []digest.Digest) (manifest, error) { | |
| var m manifest | |
| m.MediaType = images.MediaTypeDockerSchema2Manifest | |
| m.SchemaVersion = 2 | |
| configInfo, err := s.Info(ctx, config) | |
| if err != nil { | |
| return m, errors.Wrapf(err, "error reading plugin config content for digest %s", config) | |
| } | |
| m.Config = ocispec.Descriptor{ | |
| MediaType: mediaTypePluginConfig, | |
| Size: configInfo.Size, | |
| Digest: configInfo.Digest, | |
| } | |
| for _, l := range layers { | |
| info, err := s.Info(ctx, l) | |
| if err != nil { | |
| return m, errors.Wrapf(err, "error fetching info for content digest %s", l) | |
| } | |
| m.Layers = append(m.Layers, ocispec.Descriptor{ | |
| MediaType: images.MediaTypeDockerSchema2LayerGzip, // TODO: This is assuming everything is a gzip compressed layer, but that may not be true. | |
| Digest: l, | |
| Size: info.Size, | |
| }) | |
| } | |
| return m, nil | |
| } | |
| // getManifestDescriptor gets the OCI descriptor for a manifest | |
| // It will generate a manifest if one does not exist | |
| func (pm *Manager) getManifestDescriptor(ctx context.Context, p *v2.Plugin) (ocispec.Descriptor, error) { | |
| logger := logrus.WithField("plugin", p.Name()).WithField("digest", p.Manifest) | |
| if p.Manifest != "" { | |
| info, err := pm.blobStore.Info(ctx, p.Manifest) | |
| if err == nil { | |
| desc := ocispec.Descriptor{ | |
| Size: info.Size, | |
| Digest: info.Digest, | |
| MediaType: images.MediaTypeDockerSchema2Manifest, | |
| } | |
| return desc, nil | |
| } | |
| logger.WithError(err).Debug("Could not find plugin manifest in content store") | |
| } else { | |
| logger.Info("Plugin does not have manifest digest") | |
| } | |
| logger.Info("Building a new plugin manifest") | |
| manifest, err := buildManifest(ctx, pm.blobStore, p.Config, p.Blobsums) | |
| if err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| desc, err := writeManifest(ctx, pm.blobStore, &manifest) | |
| if err != nil { | |
| return desc, err | |
| } | |
| if err := pm.save(p); err != nil { | |
| logger.WithError(err).Error("Could not save plugin with manifest digest") | |
| } | |
| return desc, nil | |
| } | |
| func writeManifest(ctx context.Context, cs content.Store, m *manifest) (ocispec.Descriptor, error) { | |
| platform := platforms.DefaultSpec() | |
| desc := ocispec.Descriptor{ | |
| MediaType: images.MediaTypeDockerSchema2Manifest, | |
| Platform: &platform, | |
| } | |
| data, err := json.Marshal(m) | |
| if err != nil { | |
| return desc, errors.Wrap(err, "error encoding manifest") | |
| } | |
| desc.Digest = digest.FromBytes(data) | |
| desc.Size = int64(len(data)) | |
| if err := content.WriteBlob(ctx, cs, remotes.MakeRefKey(ctx, desc), bytes.NewReader(data), desc); err != nil { | |
| return desc, errors.Wrap(err, "error writing plugin manifest") | |
| } | |
| return desc, nil | |
| } | |
| // Remove deletes plugin's root directory. | |
| func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error { | |
| p, err := pm.config.Store.GetV2Plugin(name) | |
| pm.mu.RLock() | |
| c := pm.cMap[p] | |
| pm.mu.RUnlock() | |
| if err != nil { | |
| return err | |
| } | |
| if !config.ForceRemove { | |
| if p.GetRefCount() > 0 { | |
| return inUseError(p.Name()) | |
| } | |
| if p.IsEnabled() { | |
| return enabledError(p.Name()) | |
| } | |
| } | |
| if p.IsEnabled() { | |
| if err := pm.disable(p, c); err != nil { | |
| logrus.Errorf("failed to disable plugin '%s': %s", p.Name(), err) | |
| } | |
| } | |
| defer func() { | |
| go pm.GC() | |
| }() | |
| id := p.GetID() | |
| pluginDir := filepath.Join(pm.config.Root, id) | |
| if err := mount.RecursiveUnmount(pluginDir); err != nil { | |
| return errors.Wrap(err, "error unmounting plugin data") | |
| } | |
| if err := atomicRemoveAll(pluginDir); err != nil { | |
| return err | |
| } | |
| pm.config.Store.Remove(p) | |
| pm.config.LogPluginEvent(id, name, "remove") | |
| pm.publisher.Publish(EventRemove{Plugin: p.PluginObj}) | |
| return nil | |
| } | |
| // Set sets plugin args | |
| func (pm *Manager) Set(name string, args []string) error { | |
| p, err := pm.config.Store.GetV2Plugin(name) | |
| if err != nil { | |
| return err | |
| } | |
| if err := p.Set(args); err != nil { | |
| return err | |
| } | |
| return pm.save(p) | |
| } | |
| // CreateFromContext creates a plugin from the given pluginDir which contains | |
| // both the rootfs and the config.json and a repoName with optional tag. | |
| func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *types.PluginCreateOptions) (err error) { | |
| pm.muGC.RLock() | |
| defer pm.muGC.RUnlock() | |
| ref, err := reference.ParseNormalizedNamed(options.RepoName) | |
| if err != nil { | |
| return errors.Wrapf(err, "failed to parse reference %v", options.RepoName) | |
| } | |
| if _, ok := ref.(reference.Canonical); ok { | |
| return errors.Errorf("canonical references are not permitted") | |
| } | |
| name := reference.FamiliarString(reference.TagNameOnly(ref)) | |
| if err := pm.config.Store.validateName(name); err != nil { // fast check, real check is in createPlugin() | |
| return err | |
| } | |
| tmpRootFSDir, err := os.MkdirTemp(pm.tmpDir(), ".rootfs") | |
| if err != nil { | |
| return errors.Wrap(err, "failed to create temp directory") | |
| } | |
| defer os.RemoveAll(tmpRootFSDir) | |
| var configJSON []byte | |
| rootFS := splitConfigRootFSFromTar(tarCtx, &configJSON) | |
| rootFSBlob, err := pm.blobStore.Writer(ctx, content.WithRef(name)) | |
| if err != nil { | |
| return err | |
| } | |
| defer rootFSBlob.Close() | |
| gzw := gzip.NewWriter(rootFSBlob) | |
| rootFSReader := io.TeeReader(rootFS, gzw) | |
| if err := chrootarchive.Untar(rootFSReader, tmpRootFSDir, nil); err != nil { | |
| return err | |
| } | |
| if err := rootFS.Close(); err != nil { | |
| return err | |
| } | |
| if configJSON == nil { | |
| return errors.New("config not found") | |
| } | |
| if err := gzw.Close(); err != nil { | |
| return errors.Wrap(err, "error closing gzip writer") | |
| } | |
| var config types.PluginConfig | |
| if err := json.Unmarshal(configJSON, &config); err != nil { | |
| return errors.Wrap(err, "failed to parse config") | |
| } | |
| if err := pm.validateConfig(config); err != nil { | |
| return err | |
| } | |
| pm.mu.Lock() | |
| defer pm.mu.Unlock() | |
| if err := rootFSBlob.Commit(ctx, 0, ""); err != nil { | |
| return err | |
| } | |
| defer func() { | |
| if err != nil { | |
| go pm.GC() | |
| } | |
| }() | |
| config.Rootfs = &types.PluginConfigRootfs{ | |
| Type: "layers", | |
| DiffIds: []string{rootFSBlob.Digest().String()}, | |
| } | |
| config.DockerVersion = dockerversion.Version | |
| configBlob, err := pm.blobStore.Writer(ctx, content.WithRef(name+"-config.json")) | |
| if err != nil { | |
| return err | |
| } | |
| defer configBlob.Close() | |
| if err := json.NewEncoder(configBlob).Encode(config); err != nil { | |
| return errors.Wrap(err, "error encoding json config") | |
| } | |
| if err := configBlob.Commit(ctx, 0, ""); err != nil { | |
| return err | |
| } | |
| configDigest := configBlob.Digest() | |
| layers := []digest.Digest{rootFSBlob.Digest()} | |
| manifest, err := buildManifest(ctx, pm.blobStore, configDigest, layers) | |
| if err != nil { | |
| return err | |
| } | |
| desc, err := writeManifest(ctx, pm.blobStore, &manifest) | |
| if err != nil { | |
| return | |
| } | |
| p, err := pm.createPlugin(name, configDigest, desc.Digest, layers, tmpRootFSDir, nil) | |
| if err != nil { | |
| return err | |
| } | |
| p.PluginObj.PluginReference = name | |
| pm.publisher.Publish(EventCreate{Plugin: p.PluginObj}) | |
| pm.config.LogPluginEvent(p.PluginObj.ID, name, "create") | |
| return nil | |
| } | |
| func (pm *Manager) validateConfig(config types.PluginConfig) error { | |
| return nil // TODO: | |
| } | |
| func splitConfigRootFSFromTar(in io.ReadCloser, config *[]byte) io.ReadCloser { | |
| pr, pw := io.Pipe() | |
| go func() { | |
| tarReader := tar.NewReader(in) | |
| tarWriter := tar.NewWriter(pw) | |
| defer in.Close() | |
| hasRootFS := false | |
| for { | |
| hdr, err := tarReader.Next() | |
| if err == io.EOF { | |
| if !hasRootFS { | |
| pw.CloseWithError(errors.Wrap(err, "no rootfs found")) | |
| return | |
| } | |
| // Signals end of archive. | |
| tarWriter.Close() | |
| pw.Close() | |
| return | |
| } | |
| if err != nil { | |
| pw.CloseWithError(errors.Wrap(err, "failed to read from tar")) | |
| return | |
| } | |
| content := io.Reader(tarReader) | |
| name := path.Clean(hdr.Name) | |
| if path.IsAbs(name) { | |
| name = name[1:] | |
| } | |
| if name == configFileName { | |
| dt, err := io.ReadAll(content) | |
| if err != nil { | |
| pw.CloseWithError(errors.Wrapf(err, "failed to read %s", configFileName)) | |
| return | |
| } | |
| *config = dt | |
| } | |
| if parts := strings.Split(name, "/"); len(parts) != 0 && parts[0] == rootFSFileName { | |
| hdr.Name = path.Clean(path.Join(parts[1:]...)) | |
| if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(strings.ToLower(hdr.Linkname), rootFSFileName+"/") { | |
| hdr.Linkname = hdr.Linkname[len(rootFSFileName)+1:] | |
| } | |
| if err := tarWriter.WriteHeader(hdr); err != nil { | |
| pw.CloseWithError(errors.Wrap(err, "error writing tar header")) | |
| return | |
| } | |
| if _, err := pools.Copy(tarWriter, content); err != nil { | |
| pw.CloseWithError(errors.Wrap(err, "error copying tar data")) | |
| return | |
| } | |
| hasRootFS = true | |
| } else { | |
| io.Copy(io.Discard, content) | |
| } | |
| } | |
| }() | |
| return pr | |
| } | |
| func atomicRemoveAll(dir string) error { | |
| renamed := dir + "-removing" | |
| err := os.Rename(dir, renamed) | |
| switch { | |
| case os.IsNotExist(err), err == nil: | |
| // even if `dir` doesn't exist, we can still try and remove `renamed` | |
| case os.IsExist(err): | |
| // Some previous remove failed, check if the origin dir exists | |
| if e := containerfs.EnsureRemoveAll(renamed); e != nil { | |
| return errors.Wrap(err, "rename target already exists and could not be removed") | |
| } | |
| if _, err := os.Stat(dir); os.IsNotExist(err) { | |
| // origin doesn't exist, nothing left to do | |
| return nil | |
| } | |
| // attempt to rename again | |
| if err := os.Rename(dir, renamed); err != nil { | |
| return errors.Wrap(err, "failed to rename dir for atomic removal") | |
| } | |
| default: | |
| return errors.Wrap(err, "failed to rename dir for atomic removal") | |
| } | |
| if err := containerfs.EnsureRemoveAll(renamed); err != nil { | |
| os.Rename(renamed, dir) | |
| return err | |
| } | |
| return nil | |
| } |