spring-cloud / spring-cloud-contract

Support for Consumer Driven Contracts in Spring
https://cloud.spring.io/spring-cloud-contract
Apache License 2.0
719 stars 438 forks source link

Support different protocols of fetching stubs / contracts #580

Closed marcingrzejszczak closed 6 years ago

marcingrzejszczak commented 6 years ago

Introduction

We already support pluggable Stub Downloading via the StubDownloaderBuilder (https://cloud.spring.io/spring-cloud-static/Edgware.SR2/multi/multi__using_the_pluggable_architecture.html#_using_the_custom_stub_downloader). That means that if someone wants to download stubs / contracts from e.g. S3 or git then it's possible to be done.

The StubDownloaderBuilder interface, that is responsible for building a StubDownloader looks like this

public interface StubDownloaderBuilder {

    StubDownloader build(StubRunnerOptions stubRunnerOptions);
}

Let's take a look at StubDownloader:

public interface StubDownloader {

    /**
     * Returns a mapping of updated StubConfiguration (it will contain the resolved version) and the location of the downloaded JAR.
     * If there was no artifact this method will return {@code null}.
     */
    Map.Entry<StubConfiguration,File> downloadAndUnpackStubJar(StubConfiguration stubConfiguration);
}

The interface returns a folder with contracts and stubs for the given coordinates of a stub.

The Problem

Currently, you can have only one way of fetching contracts / stubs at a time. It would be great to easily allow plugging in some new extension mechanisms without interrupting the current flow. Also we could add some MVP for 2.0.0 to allow some other mechanisms. E.g. git and s3.

Proposed Solution

If StubDownloaderBuilder had another method like boolean isAccepted(StubRunnerOptions options) then we could iterate over all builders and only build a StubDownloader for these components that can accept it. What would the logic be like? For example, for S3StubDownloader we would have sth like this:

boolean isAccepted(StubRunnerOptions options) {
    return options.getRepositoryRoot().startsWith("s3://");
}

The current method downloadAndUnpackStubJar would get renamed to sth like contractsAndStubsLocation() (please comment your ideas).

Map.Entry<StubConfiguration,File> contractsAndStubsLocation(StubConfiguration stubConfiguration);

The implementations of the builder and stub downloader would need to have some properties passed. Remember, that the builder is used both within the plugins and the apps (not necessarily those that have Spring Context setup). The implementations could try reading the properties (like usernames & passwords) from env variables, but sometimes it's not enough. That's why we could allow passing of custom, additional properties to the plugins. E.g.

.Maven

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
                 <additionalProperties>
                                  <username>foo</username>
                                  <password>env.PASSWORD</password>
                 </additionalProperties>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-s3-downloader</artifactId>
            <version>0.1.2.RELEASE</version>
        </dependency>
    </dependencies>
</plugin>

.Gradle

contracts {
    ...
    additionalProperties {
       addProperty([username: "foo", bar: "bar"])
       addProperty("baz", "baz")
    }
}

What about stub / contract uploading?

By default the plugins prepare the stubs jar and hook into the default uploading mechanism. If this mechanism was to be changed, the user would have to disable the current mechanism (there's a switch for that). Then, some other plugin would have to take care of uploading the artifact. Examples of plugins that do that (after 1 minute search)

S3 Upload Maven Plugin: https://github.com/bazaarvoice/s3-upload-maven-plugin S3 Upload Gradle Plugin: https://github.com/skhatri/gradle-s3-plugin

abshkd commented 6 years ago

The S3 or similar storage downloader is very useful and likely we would need to generate signed URL or authenticate before we can access the object similar to the maven upload optional parameters. Same thing for git private repos (access token or ssh key)

dstepanov commented 6 years ago

Technically implementation of StubDownloader can check if it supports repository and if not return empty map. You would need to add main implementation of StubDownloader that aggregates results of multiple downloaders. That will allow to select particular downloader based on a specific artifact if use-case like that realistic.

BTW I would try to use java.nio.file.Path in the signature of Map.Entry<StubConfiguration,File> instead of File.

marcingrzejszczak commented 6 years ago

Yeah, technically everything can already be done. I personally don't like returning nulls (OTOH I've already mentioned in the API that it can return one).

I had a chat with @dsyer and maybe we'll try to tackle it via the Resource abstraction.

Example of a Maven Resource - https://github.com/spring-cloud/spring-cloud-deployer/tree/master/spring-cloud-deployer-resource-maven And example usage: https://github.com/projectriff/java-function-invoker/blob/master/src/main/java/io/projectriff/invoker/FunctionConfiguration.java#L108

TYsewyn commented 6 years ago

Will it be possible to execute multiple StubDownloaderBuilders?

Imagine that your project has a dependency on an internal application, and those stubs/contracts are hosted using a pact broker. But the same project also has a dependency with a 3rd party application which exposes his stubs using a private git repo.

Based on the current implementation of the StubDownloaderBuilderProvider, which will only use the first one it can find in its list of StubDownloaderBuilders, this is not possible. Changing this behaviour will not interrupt the current flow/implementation, so I think this is something we should keep in mind.

The way you'll try to tackle it using the Resource abstraction looks to be the most elegant/clean way if I check out the code from both Spring Cloud Deployer and Project Riff! Really looking forward to these API changes!

Apart from that, I'm up for the implementation of a StubDownloader connecting to a pact broker if help is needed!

marcingrzejszczak commented 6 years ago

Time for feedback! https://github.com/spring-cloud/spring-cloud-contract/pull/593

I didn't change File to Path yet. Don't see a lot of value in that yet @dstepanov .