Skip to content

Proposal: Docker plugins MVP#3

Closed
lukemarsden wants to merge 25 commits intocpuguy83:extensions_specfrom
lukemarsden:docker-extensions-proposal
Closed

Proposal: Docker plugins MVP#3
lukemarsden wants to merge 25 commits intocpuguy83:extensions_specfrom
lukemarsden:docker-extensions-proposal

Conversation

@lukemarsden
Copy link

Here is a proposal for an MVP for docker plugins that unifies all the efforts in this direction.

Note that there are now - as of Monday :) three types of prototypical docker plugins:

  • Volume plugins - using HTTP as extension mechanism
  • Networking plugins - using an undefined extension mechanism
  • API plugins - aka "powerstrip" - using HTTP as extension mechanism

This proposal suggests that we standardize on HTTP for an initial pass on the plugins framework.
For purely local plugins using a UNIX domain socket, this is sufficiently secure.

Powerstrip has demonstrated that HTTP makes it very easy for plugin authors to get started.

I suggest we could then iterate on this choice of transport once we've got some experience prototyping these plugins.

More important is getting rough consensus and running code after agreeing the message formats that need to be sent and received between docker and plugins for volumes, networking & API extensions.

@shykes
Copy link

shykes commented Mar 5, 2015

I would prefer a UI where the user doesn't need to manually specify the type of extension point. Rather, the plugin should have a way to register extension points.

So instead of volume:foo it should be just foo.

@shykes
Copy link

shykes commented Mar 5, 2015

Also I think we should have a full-blown plugin subcommand eg. docker plugin load

@lukemarsden
Copy link
Author

Note that if this PR were to actually get merged into the docs, it would need to be split into (at least) "using plugins" and "writing plugins" documents (for different audiences). And probably it would also make sense to split it out into a document per plugin type, at least for the "writing plugins" part.

@lukemarsden
Copy link
Author

I would prefer a UI where the user doesn't need to manually specify the type of extension point. Rather, the plugin should have a way to register extension points.

@shykes OK, makes sense. This would suggest that we need to implement what we're calling "plugin negotiation" where when docker first connects to the plugin, it does a sort of handshake to figure out which extension points the plugin wants to be registered for.

Addressed in fae773b.

@lukemarsden
Copy link
Author

Also I think we should have a full-blown plugin subcommand eg. docker plugin load

@shykes OK, that's an easy change.

Addressed in fae773b.

@lukemarsden
Copy link
Author

Review comment from Adrian: perhaps better to have an explicit configuration file, so that the Docker daemon itself can drive the lifecycle rather than putting this in the hands of the user. (Order dependencies, etc.)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should match the --env/--volume semantics:

--metadata size=50G --metadata tier=ssd --metadata replication=global

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or more really the forthcoming label API

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps /var/run/docker/ should be a thing? /var/run/docker/api.sock, /var/run/docker/plugins/, etc

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use /var/lib/docker ?

On Friday, March 6, 2015, Ben Firshman [email protected] wrote:

In docs/sources/userguide/plugins.md
#3 (comment):

+Plugins can be loaded using a special docker plugin load command, as follows:
+
+ +$ docker plugin load -e $ARGS clusterhq/flocker-plugin $@ +[... fetching...] +Plugin flocker:v0.1 loaded and registered with extension points: +* volumes +Plugin flocker activated. +
+
+This is really just syntactic sugar for the following:
+
+```
+$ docker run -d -e $ARGS \

  • -v /var/run/docker-plugins/flocker.sock:/var/run/plugin.sock \

Perhaps /var/run/docker/ should be a thing? /var/run/docker/api.sock,
/var/run/docker/plugins/, etc


Reply to this email directly or view it on GitHub
https://github.com/cpuguy83/docker/pull/3/files#r25964675.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've always thought that sockets and pid files live in /var/run or /usr/var/run
at least, that's a pattern I've come to expect when using linux

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the host side, I think inside /var/run/docker/plugins/<id> would be the place to put that.
In the container, /run or /var/run seems like the right thing.

@bfirsh
Copy link

bfirsh commented Mar 7, 2015

I wonder if these things should be called "drivers". A networking "extension" doesn't feel like it's extending the functionality of networking, it's more defining how networking works. In the same way I would install a "driver" for my network interface on my operating system – that thing wouldn't be an "extension" of the operating system, it's an essential piece of functionality.

You also hint at that when you say docker volume create --driver flocker.

@lukemarsden
Copy link
Author

@mrjana I worry that using a config file doesn't help either. There's nothing stopping docker from being restarting docker during a create => start window anyway. I think @shykes is right: it is on the plugin author to store state needed to guarantee its own internal consistency even when it might not receive all events.

@lukemarsden
Copy link
Author

Just to be clear I am commenting here on a best effort basis

@shykes understood, no problem.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think packaging plugins as containers is great - it saves a lot of work from the plugin authors side.
However, The question I have is about how they are distributed. Today the Docker registry doesn't contain any metadata about a container and what permissions it requires. This metadata is important from a users standpoint (a parallel would be the Google Play Store for Android) so they can make a decision of whether or not to use a plugin.

As @progrium mentions below, most agents will require --privileged or perhaps a combination of --cap-add and --cap-drop.

Perhaps a plugin should be a container + a docker-compose.yml

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is something we can work towards... basically having the image declare the permissions it needs to be able to run and error out if those perms weren't granted (ie by docker plugin load --privileged).

But I don't see this as a hard requirement for plugins.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Caveat here is that if we bind-mount the docker socket, then any plugin can elevate its own privileges. This needs more thought but not yet for an extensions MVP.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely not bind-mount the docker socket into the plugins.
Once people start writing plugins which assume this, it will be impossible
to change the design without breaking them.

On Thu, Mar 12, 2015 at 4:20 AM, lukemarsden [email protected]
wrote:

In docs/sources/userguide/plugins.md
#3 (comment):

@@ -0,0 +1,157 @@
+Docker Plugins MVP
+==================
+
+Plugins provide a mechanism for hooking into the Docker engine to extend its behavior.
+
+HTTP over a local UNIX socket is used to communicate between Docker and its plugins.
+
+Plugins are distributed as containers.

Agree. Caveat here is that if we bind-mount the docker socket, then any
plugin can elevate its own privileges. This needs more thought but not yet
for an extensions MVP.


Reply to this email directly or view it on GitHub
https://github.com/cpuguy83/docker/pull/3/files#r26293764.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with not automatically doing it for all plugins, it would be nice to request one (not using -v /var/run/docker.sock:/var/run/docker.sock) from the UI.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this @shykes. Bind-mounting (which is how we'd propose to expose the Docker API) is already arguably part of the docker permissions interface (let the container mess with this subtree on the host filesystem). Does that mean that the plugin loader interface should have -v?

To be clear, the choice is:

  • generic -v support for plugin loader.
  • some sort of specific --allow-docker-api flag to the plugin loader which special-cases bind-mounting in the docker sock.

I think @progrium preferred the latter. I'm cool with either. If you strongly prefer the former @shykes I'm fine with that.

One data point: the prototypical containerized flocker-zfs-agent I'm working on currently has 5 bind-mounts. BTW, I was imagining that the flocker extension would start the flocker-zfs-agent with its requisite bind-mounts via the docker api when it started... but we really do need the extensions to be able to "do stuff". I don't think we have time to define an interface with granular permissions. And in fact arguably the interface with granular permissions should be a later version of the Docker API!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a point of protocol, I think @progrium is working on the loader spec this/next week. It may be more productive to let him finish that and then do a review pass over it than to pre-empt his proposed design, which I'm sure will handle the use cases we have in mind. ;)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for having some way for a plugin to speak to the main docker server - it is vital for various reasons - I'm not overly concerned with how though (-v or --allow-docker-api)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My take on these:

  • I'd rather not expose docker run arguments to docker plugin for two reasons
    • As soon as we start adding some of them, we'll end up adding all of them.
    • I like that we have a dedicated docker plugin top-level command that hides the fact that it's actually pulling an image and running a container: I believe that it makes perfect sense for an MVP (as we reuse many of the infrastructure in place), but I don't think we'd like to lock ourselves in that as the only possible execution model.
  • Mounting the daemon socket should not be the default, although I agree it should be made possible through some command-line flag that makes it obvious you're doing something "privileged", such as --allow-full-daemon-access.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I like the idea of a container for plugins on the daemon-side, I'm wondering about CLI plugins. I don't think they can use the same mechanism. I wonder if it makes sense to have cli-plugins be executables that use the same API defined in there? Meaning, via some cli-specific registration mechanism (perhaps they're listed in the cli config file), the cli can start the exe's and then talk to them via HTTP using the same protocol defined here. So while we can't get consistency around distribution, we can be consistent on the API/protocol.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps "implements"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@lukemarsden
Copy link
Author

When chatting with @binocarlos and @squaremo we discussed the need to perhaps introduce a concept of "read" or "write" access to a subsystem. This might help with chaining.

@progrium
Copy link

How would you model "read" or "write" access with a native language API?

@cpuguy83
Copy link
Owner

I think if you are registering a plugin you should make sure it only does what you want it to.
If you only give a plugin read access and it was expecting to be able to write, it will break.

So basically, the plugin has to be written to only care about reads... in which case is there any sense in enforcing this?

I suppose a read-only plugin is one that is just a fire-and-forget... ie docker doesn't wait for a response from it.

@progrium
Copy link

Again, I believe this should be address in the design of the extension point interface by the subsystem author, not the responsibility of this protocol / system.

@squaremo
Copy link

we discussed the need to perhaps introduce a concept of "read" or "write" access to a subsystem

This was one way of accounting for composing extensions: if more than one plugin registers for an extension point, one can have write and the others only read, and docker breaks when loading the overreaching plugin if you get it wrong.

Another way to account for composition: don't have it. Only one plugin can be loaded for each extension point, and docker breaks when loading the extraneous plugin if you get it wrong.

Another way: Specify which extension is being called, e.g., docker run --net=weave/args ..., and docker (run) breaks if you name a plugin that's not loaded.

Something to keep in mind is how surprising these things might be to users. Because of the "register interest" handshaking, it won't necessarily be obvious to a user that e.g., the flocker plugin is a "volume" plugin and they can have only one of those (or only one "volume" write plugin).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we can replace "when" with "after" ? just to be clear about when it is called.

cpuguy83 pushed a commit that referenced this pull request Aug 1, 2015
TL;DR: check for IsExist(err) after a failed MkdirAll() is both
redundant and wrong -- so two reasons to remove it.

Quoting MkdirAll documentation:

> MkdirAll creates a directory named path, along with any necessary
> parents, and returns nil, or else returns an error. If path
> is already a directory, MkdirAll does nothing and returns nil.

This means two things:

1. If a directory to be created already exists, no error is returned.

2. If the error returned is IsExist (EEXIST), it means there exists
a non-directory with the same name as MkdirAll need to use for
directory. Example: we want to MkdirAll("a/b"), but file "a"
(or "a/b") already exists, so MkdirAll fails.

The above is a theory, based on quoted documentation and my UNIX
knowledge.

3. In practice, though, current MkdirAll implementation [1] returns
ENOTDIR in most of cases described in #2, with the exception when
there is a race between MkdirAll and someone else creating the
last component of MkdirAll argument as a file. In this very case
MkdirAll() will indeed return EEXIST.

Because of #1, IsExist check after MkdirAll is not needed.

Because of #2 and #3, ignoring IsExist error is just plain wrong,
as directory we require is not created. It's cleaner to report
the error now.

Note this error is all over the tree, I guess due to copy-paste,
or trying to follow the same usage pattern as for Mkdir(),
or some not quite correct examples on the Internet.

[v2: a separate aufs commit is merged into this one]

[1] https://github.com/golang/go/blob/f9ed2f75/src/os/path.go

Signed-off-by: Kir Kolyshkin <[email protected]>
cpuguy83 pushed a commit that referenced this pull request Sep 8, 2015
TL;DR: stop building static binary that may fail

Linker flag --unresolved-symbols=ignore-in-shared-libs was added
in commit 06d0843 two years ago for the static build case, presumably
to avoid dealing with problem of missing libraries.

For the record, this is what ld(1) man page says:

> --unresolved-symbols=method
>    Determine how to handle unresolved symbols.  There are four
>    possible values for method:
> .........
>    ignore-in-shared-libs
>        Report unresolved symbols that come from regular object files,
>        but ignore them if they come from shared libraries.  This can
>        be useful when creating a dynamic binary and it is known that
>        all the shared libraries that it should be referencing are
>        included on the linker's command line.

Here, the flag is not used for its purpose ("creating a dynamic binary")
and does more harm than good. Instead of complaining about missing symbols
as it should do if some libraries are missing from LIBS/LDFLAGS, it lets
ld create a binary with unresolved symbols, ike this:

 $ readelf -s bundles/1.7.1/binary/docker-1.7.1 | grep -w UND
 ........
 21029: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND dlopen
 .........

Such binary is working just fine -- until code calls one of those
functions, then it crashes (for apparently no reason, i.e. it is
impossible to tell why from the diagnistics printed).

In other words, adding this flag allows to build a static binary
with missing libraries, hiding the problem from both a developer
(who forgot to add a library to #cgo: LDFLAGS -- I was one such
developer a few days ago when I was working on ploop graphdriver)
and from a user (who expects the binary to work without crashing,
and it does that until the code calls a function in one of those
libraries).

Removing the flag immediately unveils the problem (as it should):

	/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/libsqlite3.a(sqlite3.o):
	In function `unixDlError':
	(.text+0x20971): undefined reference to `dlerror'
	/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/libsqlite3.a(sqlite3.o):
	In function `unixDlClose':
	(.text+0x8814): undefined reference to `dlclose'

The problem is, gosqlite package says:

	#cgo LDFLAGS: -lsqlite3

which is enough for dynamic linking, as indirect dependencies (i.e.
libraries required by libsqlite3.so) are listed in .so file and will be
resolved dynamically by ldd upon executing the binary.

For static linking though, one has to list all the required libraries,
both direct and indirect. For libraries with pkgconfig support the
list of required libraries can be obtained with pkg-config:

	$ pkg-config --libs sqlite3 # dynamic linking case
	-lsqlite3
	$ pkg-config --libs --static sqlite3 # static case
	-lsqlite3 -ldl -lpthread

It seems that all one has to do is to fix gosqlite this way:

	-#cgo LDFLAGS: -lsqlite3
	+#cgo pkg-config: sqlite3

Unfortunately, cmd/go doesn't know that it needs to pass --static
flag to pkg-config in case of static linking
(see golang/go#12058).

So, for one, one has to do one of these things:

1. Patch sqlite.go like this:

	-#cgo LDFLAGS: -lsqlite3
	+#cgo pkg-config: --static sqlite3

(this is exactly what I do in goploop, see
kolyshkin/goploop@e9aa072f51)

2. Patch sqlite.go like this:
	-#cgo LDFLAGS: -lsqlite3
	+#cgo LDFLAGS: -lsqlite3 -ldl -lpthread

(I would submit this patch to gosqlite but it seems that
https://code.google.com/p/gosqlite/ is deserted and not maintained,
and patching it here is not right as it is "vendored")

3. Explicitly add -ldl for the static link case.
This is what this patch does.

4. Fork sqlite to github and maintain it there. Personally I am not
ready for that, as I'm neither a Go expert nor gosqlite user.

Now, #3 doesn't look like a clear solution, but nevertheless it makes
the build much better than it was before.

Signed-off-by: Kir Kolyshkin <[email protected]>
@cpuguy83 cpuguy83 closed this Nov 21, 2015
cpuguy83 pushed a commit that referenced this pull request Dec 4, 2017
This subtle bug keeps lurking in because error checking for `Mkdir()`
and `MkdirAll()` is slightly different wrt to `EEXIST`/`IsExist`:

 - for `Mkdir()`, `IsExist` error should (usually) be ignored
   (unless you want to make sure directory was not there before)
   as it means "the destination directory was already there"

 - for `MkdirAll()`, `IsExist` error should NEVER be ignored.

Mostly, this commit just removes ignoring the IsExist error, as it
should not be ignored.

Also, there are a couple of cases then IsExist is handled as
"directory already exist" which is wrong. As a result, some code
that never worked as intended is now removed.

NOTE that `idtools.MkdirAndChown()` behaves like `os.MkdirAll()`
rather than `os.Mkdir()` -- so its description is amended accordingly,
and its usage is handled as such (i.e. IsExist error is not ignored).

For more details, a quote from my runc commit 6f82d4b (July 2015):

    TL;DR: check for IsExist(err) after a failed MkdirAll() is both
    redundant and wrong -- so two reasons to remove it.

    Quoting MkdirAll documentation:

    > MkdirAll creates a directory named path, along with any necessary
    > parents, and returns nil, or else returns an error. If path
    > is already a directory, MkdirAll does nothing and returns nil.

    This means two things:

    1. If a directory to be created already exists, no error is
    returned.

    2. If the error returned is IsExist (EEXIST), it means there exists
    a non-directory with the same name as MkdirAll need to use for
    directory. Example: we want to MkdirAll("a/b"), but file "a"
    (or "a/b") already exists, so MkdirAll fails.

    The above is a theory, based on quoted documentation and my UNIX
    knowledge.

    3. In practice, though, current MkdirAll implementation [1] returns
    ENOTDIR in most of cases described in #2, with the exception when
    there is a race between MkdirAll and someone else creating the
    last component of MkdirAll argument as a file. In this very case
    MkdirAll() will indeed return EEXIST.

    Because of #1, IsExist check after MkdirAll is not needed.

    Because of #2 and #3, ignoring IsExist error is just plain wrong,
    as directory we require is not created. It's cleaner to report
    the error now.

    Note this error is all over the tree, I guess due to copy-paste,
    or trying to follow the same usage pattern as for Mkdir(),
    or some not quite correct examples on the Internet.

    [1] https://github.com/golang/go/blob/f9ed2f75/src/os/path.go

Signed-off-by: Kir Kolyshkin <[email protected]>
neersighted pushed a commit that referenced this pull request Mar 21, 2024
…f v1.5.4

full diffs:

- protocolbuffers/protobuf-go@v1.31.0...v1.33.0
- golang/protobuf@v1.5.3...v1.5.4

From the Go security announcement list;

> Version v1.33.0 of the google.golang.org/protobuf module fixes a bug in
> the google.golang.org/protobuf/encoding/protojson package which could cause
> the Unmarshal function to enter an infinite loop when handling some invalid
> inputs.
>
> This condition could only occur when unmarshaling into a message which contains
> a google.protobuf.Any value, or when the UnmarshalOptions.UnmarshalUnknown
> option is set. Unmarshal now correctly returns an error when handling these
> inputs.
>
> This is CVE-2024-24786.

In a follow-up post;

> A small correction: This vulnerability applies when the UnmarshalOptions.DiscardUnknown
> option is set (as well as when unmarshaling into any message which contains a
> google.protobuf.Any). There is no UnmarshalUnknown option.
>
> In addition, version 1.33.0 of google.golang.org/protobuf inadvertently
> introduced an incompatibility with the older github.com/golang/protobuf
> module. (golang/protobuf#1596) Users of the older
> module should update to github.com/golang/[email protected].

govulncheck results in our code:

    govulncheck ./...
    Scanning your code and 1221 packages across 204 dependent modules for known vulnerabilities...

    === Symbol Results ===

    Vulnerability #1: GO-2024-2611
        Infinite loop in JSON unmarshaling in google.golang.org/protobuf
      More info: https://pkg.go.dev/vuln/GO-2024-2611
      Module: google.golang.org/protobuf
        Found in: google.golang.org/[email protected]
        Fixed in: google.golang.org/[email protected]
        Example traces found:
          #1: daemon/logger/gcplogs/gcplogging.go:154:18: gcplogs.New calls logging.Client.Ping, which eventually calls json.Decoder.Peek
          #2: daemon/logger/gcplogs/gcplogging.go:154:18: gcplogs.New calls logging.Client.Ping, which eventually calls json.Decoder.Read
          #3: daemon/logger/gcplogs/gcplogging.go:154:18: gcplogs.New calls logging.Client.Ping, which eventually calls protojson.Unmarshal

    Your code is affected by 1 vulnerability from 1 module.
    This scan found no other vulnerabilities in packages you import or modules you
    require.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.