Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1st draft for review #1

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

1st draft for review #1

wants to merge 3 commits into from

Conversation

rnorth
Copy link
Owner

@rnorth rnorth commented Nov 11, 2017

As requested, I've converted this into a PR for easier review.

```java
// Building a simple container object not linked to any test framework
final Container container = Container.builder()
.withImage("alpine:3.5")
Copy link
Collaborator

Choose a reason for hiding this comment

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

since image is a mandatory thing maybe we should require it as a parameter of builder() method?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yep, makes sense.

public class SimpleJUnit4Test {

@Rule
public ContainerTestRule alpine = ContainerTestRule.builder()
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we do it this way, we will have to duplicate each and every withXXX method. What if we use something like I proposed with Networks? i.e.:

Container.builder()
   .withSomething()
   .build(asJUnit4Rule());

Container.builder()
   .withSomething()
   .build(asJUnit5Rule());

Container.builder()
   .withSomething()
   .build(); // aka GenericContainer

Copy link
Collaborator

Choose a reason for hiding this comment

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

What about composing it the other way around?
Something like:

ContainerTestRule.builder(
   Container.builder()
   .withSomething()
).build();

Copy link
Collaborator

Choose a reason for hiding this comment

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

@kiview I thought about it as well, but (compared to my example) it adds more code around it without providing any visible benefits IMO. Also, if .build() accepts an interface, it will be possible to easily discover all available adapters like JUnit4/5 and others

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 for the discoverability, makes sense.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Actually as-is we don't have to duplicate the with methods... If you look, the junit.ContainerTestRuleBuilder class extends BaseContainerBuilder, which does the generic self-typed-return thing we currently do in Testcontainers 1.x. (This helps allow for a fluent interface).

containercore.ContainerBuilder also uses this same base class as well.

As such, we don't need to duplicate the methods.
The downside is that self-typed-returns are a bit confusing, but at least they're isolated to the builder classes now. I'm not sure how you feel about keeping this technique in use, though.

@bsideup I'm all for the approach you suggest if we don't go for this model above. The only reason I didn't go for that straight away is a slight discoverability worry.

Copy link
Collaborator

Choose a reason for hiding this comment

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

TBH I myself don't really understand the type hack 😅
It's for keeping the this type when calling methods from the parent class?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Hehe, yes, it's so SomeSubclass::withSomeMethodFromParent returns as SomeSubclass rather than the parent class.
I'll consider this as a vote against 😆


@Test
public void doNothing() {
alpine.exec("date");
Copy link
Collaborator

Choose a reason for hiding this comment

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

re exec and others. Since the number of such "helpers" will grow, how about introducing a concept of "recipes"? i.e. ExecRecipe, GetFileRecipe, etc?

Copy link
Collaborator

@kiview kiview Nov 28, 2017

Choose a reason for hiding this comment

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

I think in general we want a OO abstraction of docker-java, so I assume we will have all basic docker API methods available, like:

  • cp
  • exec
  • diff
  • ...

I always wonder how much to deviate from the known Docker API or if it would be kind of enough to conveniently expose these methods.

Of course we should name these methods in a Java idiomatic way.

Your example recipes are now basically Docker API methods, can you image more complex helper methods?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure that this is the goal actually :D The goal is to have a high-level API to start and manipulate the containers, and cp/exec/diff/etc are too low level for that

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, this is a good thing to discuss. The way I see it, the library should be 'narrow but deep':

Narrow: we don't need to cover every possible method of the docker API (probably >50%, but definitely <100%)
Deep: we should aim to do as much as possible for the user to hide the 'accidental complexity' of docker - e.g. all the things around file mounting, copying, and network port mapping.

By hiding some of this complexity we do reduce the flexibility of the docker API, but this is unavoidable.

There might be people who want a 100% wide abstraction, but TBH I think these people need the finer-grained, lower-level tools that docker-java provides, so we're OK to avoid this area.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@bsideup I don't think cp and exec are too low level per se, since both of your examples are basically renamed calls to these methods, aren't they ? ;)

There might be more convenient API composed on top of the basic APIs, but I don't see any pain in exposing them in a public API, still more convenient to use in a OO manner then docker-java.

```java
final Container containerWithPlugin = Container.builder()
.withImage("alpine:3.5")
.withPlugin(new FakeTimePlugin("2010-01-01 00:00:00"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe extension would be a better name? So that we extend container by composing different extensions, but the system itself is not pluggable, at least not with this API

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think both terms are fine and apply here, the Wikipedia definition of plugin:

In computing, a plug-in (or plugin, add-in, addin, add-on, addon, or extension) is a software component that adds a specific feature to an existing computer program.

We add functionality or behavior to the container, but we could also extend the existing feature set ;)

There is also the plugin definition by Martin Fowler in Patterns of Enterprise Application Architecture which would fit to this implementation I think (basically meaning DI).

Copy link
Owner Author

Choose a reason for hiding this comment

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

I don't mind; 'plugin' seemed more natural to me.

One thing which occurred to me is that this isn't essential functionality from day one (it's a new, rather than existing, feature).

Perhaps let's not spend too much time worrying about the details for now, as long as we can agree we'd like this concept to exist in some form 😄

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, since the fluent builder API would allow us to introduce such functionality without any breaking changes we can simply defer the discussion 🙂

.build();

// later, while container is running
fakeTime.setTime("2011-01-01 00:00:00");
Copy link
Collaborator

Choose a reason for hiding this comment

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

would make it hard to reuse. How about containerWithPlugin.using(fakeTime).setTime(...)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the difference, since we still need the reference to the field?

Copy link
Collaborator

Choose a reason for hiding this comment

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

containerFoo.using(fakeTime).setTime(...)
containerBar.using(fakeTime).setTime(...)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be nice, might be problematic for stateful Plugins (which might exist).

Or maybe we could have something like:

final Container containerWithPlugin = Container.builder()
  .withImage("alpine:3.5")
  .withPlugin(FakeTimePlugin.class);

containerWithPlugin.using(FakeTimePlugin.class).setTime(...);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Stateful plugins is a pain. Even if plugin wants to store the state, it's better if it will use something like https://docs.oracle.com/javase/7/docs/api/java/util/WeakHashMap.html


---

##Runtime `Container` vs define-time `ContainerBuilder`
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should take a look at https://github.com/sundrio/sundrio/blob/master/annotations/dsl/readme.md for this. It's being used in fabric8's Docker client and seems to be very powerful DSL generator

Copy link
Owner Author

Choose a reason for hiding this comment

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

👍 Thanks for the tip - will have a look

/**
* TODO: Javadocs
*/
public class Container {
Copy link

Choose a reason for hiding this comment

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

I would like to suggest making this class AutoClosable. The interface can simply invoke stop and should override the method to remove the Exception as none is thrown.

This allows for a very comfortable usage of containers such as:

try(Container container = new MyContainer()) {
container.start();
// do something with container..
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

@raphw 👍 good catch! In fact, we already have it in TestContainers and it proved to be useful :)

Copy link
Owner Author

Choose a reason for hiding this comment

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

👍 absolutely - a small detail that adds a lot of benefit. Thanks!

* making sure all fluent setters are consistent
* adding a separation between definition-time concerns and 'run-time' concerns
* add a plugin model to allow *composition* to be used in addition to *inheritance*
* **Continue to support specialized container types via specialized classes**
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we already started the discussion in Slack - we don't want to have Docker-Compose support as part of containercore, correct?

Copy link
Owner Author

@rnorth rnorth Nov 29, 2017

Choose a reason for hiding this comment

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

Worth bringing up, and we should really add to this doc as it's a 'future of testcontainers' question. In brief, I think I'd see it as:

Testcontainers /  Test framework integrations (e.g. JUnit4/5, Spock, etc)
               \  Specialised container support (e.g. database, selenium, compose, etc) 
Containercore -- High level OO docker API

I'd hope there's nothing in containercore that has to be aware of docker-compose.
Does that make sense?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we decide to exclude docker-compose from core (which makes sense), we should IMO provide a compose-core module, which itself is decoupled from testcontainers. There are really a lot of use cases for the high level docker-compose support, even outside of test frameworks. And if I think about it, not all of the specialized containers seem to be testcontainers specific.

So maybe it would be more like this?

Testcontainers -- Test framework integrations (e.g. JUnit4/5, Spock, etc)
Containercore -- High level OO docker API                  
Containercore-modules -- Specialised container support (e.g. database, selenium, compose, etc) 

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fair point - trying out a couple of ways to achieve this now.

```java
final Container containerWithPlugin = Container.builder()
.withImage("alpine:3.5")
.withPlugin(new FakeTimePlugin("2010-01-01 00:00:00"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think both terms are fine and apply here, the Wikipedia definition of plugin:

In computing, a plug-in (or plugin, add-in, addin, add-on, addon, or extension) is a software component that adds a specific feature to an existing computer program.

We add functionality or behavior to the container, but we could also extend the existing feature set ;)

There is also the plugin definition by Martin Fowler in Patterns of Enterprise Application Architecture which would fit to this implementation I think (basically meaning DI).

.build();

// later, while container is running
fakeTime.setTime("2011-01-01 00:00:00");
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the difference, since we still need the reference to the field?

### Implementation

* `Container` holds configuration data in fields, plus methods for 'run-time' interactions such as `start`, `stop` , `exec`, etc.
* `ContainerBuilder` (and its parent class `BaseContainerBuilder`) exposes the define-time `withXYZ` fluent setter API
Copy link

Choose a reason for hiding this comment

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

What do you guys think about the immutable builder? With the current proposal builder is mutable and it could lead to some surprising behavior. For example, I want to have 2 PosgtreSQL containers who shares some common settings:

final ContainerBuilder commonBuilder = PostgreSQLContainer.builder().withImage("posgtresql:9.6");
final Container pg1 = commonBuilder.withExposedPort(1234).build();
final Container pg2 = commonBuilder.withExposedPort(2345).build();

But this code would not work as expected because of mutable nature of builders.

I think immutability is always a good default for the libraries. In testcontainers we don't need high-performant builders, so extra allocations is not a problem.

@LMnet
Copy link

LMnet commented Jan 23, 2018

The new proposal doesn't take into account tests with multiple dependent containers. With the current implementation, I had some issues and had to use some hacks. testcontainers currently expose api only for test<->container interoperability, but there is no convenient container<->container interface. And I think one of the most exciting use case for testcontainers is tests with multiple dependent containers.

I suggest adding some methods to Container to expose ports, ips and other network information for linking containers.

@bsideup
Copy link
Collaborator

bsideup commented Jan 23, 2018

@LMnet actually, we have Networks support in TestContainers for the container <-> container communication:
https://github.com/testcontainers/testcontainers-java/blob/master/core/src/test/java/org/testcontainers/containers/NetworkTest.java

@LMnet
Copy link

LMnet commented Jan 23, 2018

@bsideup Yes, I know about networks support. But it looks like container <-> container communication features are not given enough attention. For example, to obtain an IP for a container inside a network I need to do something like this:

container.getContainerInfo().getNetworkSettings().getNetworks().get(myNetworkId).getIpAddress()

Looks a bit verbose for such simple operation.

@bsideup
Copy link
Collaborator

bsideup commented Jan 23, 2018

@LMnet why would you do it at all? Just use a network alias (i.e. my-super-container) and use it instead of IP

@LMnet
Copy link

LMnet commented Jan 23, 2018

@bsideup It was just an example of API usage.

Another real-life example. In my tests, I start an HTTP mock on my host machine (from the test code). The application is inside a container, and this container is inside a custom network. The application should be able to do HTTP request to the mock. To link mock with the application I need to get network's gateway address inside the container - and it would host's address. So, to get a gateway address I need something like this:

container.getContainerInfo().getNetworkSettings().getNetworks().get(myNetworkId).getGateway()

ATM I don't know any better way to do this.

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.

5 participants