nacx requested changes on this pull request.
Many thanks @jawnclarke!
I've added several comments. The PR looks good, but let's keep it simple to set
the right foundation so we can easily add more APIs and build stuff on top of
this without having to revisit it.
Figuring out the base stuff is the difficult part, and things will move faster
once this is merged.
> + @Override
+ public Builder toBuilder() {
+ return new Builder().fromApiMetadata(this);
+ }
+
+ public DimensionDataCloudControlApiMetadata() {
+ this(new Builder());
+ }
+
+ protected DimensionDataCloudControlApiMetadata(Builder builder) {
+ super(builder);
+ }
+
+ public static Properties defaultProperties() {
+ Properties properties = BaseHttpApiMetadata.defaultProperties();
+ properties.setProperty(Constants.PROPERTY_CONNECTION_TIMEOUT,
"1200000"); // 15 minutes
Can we avoid setting this? The connection timeout configures the time to get a
connection opened, not the time required to read all the data. It looks
excessive. Let's better remove this default and let users configure it as
needed.
> + properties.setProperty(Constants.PROPERTY_CONNECTION_TIMEOUT,
> "1200000"); // 15 minutes
+ return properties;
+ }
+
+ public static class Builder extends
BaseHttpApiMetadata.Builder<DimensionDataCloudControlApi, Builder> {
+
+ protected Builder() {
+ id("dimensiondata-cloudcontrol").name("DimensionData CloudControl
API").identityName("user name")
+ .credentialName("user password")
+
.documentation(URI.create("http://www.dimensiondata.com/en-US/Solutions/Cloud"))
+
.defaultEndpoint("https://api-REGION.dimensiondata.com/").version("2.4")
+
.defaultProperties(DimensionDataCloudControlApiMetadata.defaultProperties())
+ .view(typeToken(ComputeServiceContext.class)).defaultModules(
+ ImmutableSet.<Class<? extends
Module>>builder().add(DimensionDataCloudControlHttpApiModule.class)
+ .add(DimensionDataCloudControlParserModule.class)
+
.add(DimensionDataComputeServiceContextModule.class).build());
For now let's remove the ComputeServiceAdapter module, the implementation and
all other transformation functions. They are not implemented and return `null`
and placeholders, and provide no value and just add complexity to the code and
the PR. Let's remove all that stuff and start with a clean API to set a good
foundation for the provider.
That will help and allow further PRs to be easier and faster.
> +
+ @Override
+ protected void bindErrorHandlers() {
+ //TODO Implement Method
+ }
+
+ @Override
+ protected void bindRetryHandlers() {
+ //TODO Implement Method
+ }
+
+ @Override
+ protected void installLocations() {
+ install(new LocationModule());
+
bind(ImplicitLocationSupplier.class).to(OnlyLocationOrFirstZone.class).in(Scopes.SINGLETON);
+ }
Don't override this yet. Let's remove it for now and we'll add it back (if it
turns needed) when implementing the compute service adapter.
> +import org.jclouds.location.suppliers.implicit.OnlyLocationOrFirstZone;
+import org.jclouds.rest.ConfiguresHttpApi;
+import org.jclouds.rest.config.HttpApiModule;
+
+@ConfiguresHttpApi
+public class DimensionDataCloudControlHttpApiModule extends
HttpApiModule<DimensionDataCloudControlApi> {
+
+ @Override
+ protected void bindErrorHandlers() {
+ //TODO Implement Method
+ }
+
+ @Override
+ protected void bindRetryHandlers() {
+ //TODO Implement Method
+ }
You can remove this, as jclouds provides sane default retry handlers.
> +
+import com.google.inject.Scopes;
+import org.jclouds.dimensiondata.cloudcontrol.DimensionDataCloudControlApi;
+import org.jclouds.location.config.LocationModule;
+import org.jclouds.location.suppliers.ImplicitLocationSupplier;
+import org.jclouds.location.suppliers.implicit.OnlyLocationOrFirstZone;
+import org.jclouds.rest.ConfiguresHttpApi;
+import org.jclouds.rest.config.HttpApiModule;
+
+@ConfiguresHttpApi
+public class DimensionDataCloudControlHttpApiModule extends
HttpApiModule<DimensionDataCloudControlApi> {
+
+ @Override
+ protected void bindErrorHandlers() {
+ //TODO Implement Method
+ }
Remove this method if you're not defining any custom error handler.
> + * limitations under the License.
+ */
+package org.jclouds.dimensiondata.cloudcontrol.config;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+import org.jclouds.Constants;
+import org.jclouds.json.config.GsonModule;
+
+import java.util.Collections;
+
+public class DimensionDataCloudControlParserModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ Names.bindProperties(binder(),
Collections.singletonMap(Constants.PROPERTY_PRETTY_PRINT_PAYLOADS, "true"));
There is no need to bind this. It is already bound to the context and users
will be able to configure this property when creating it. Let's remove this
line.
>
- Account() {
+ @Nullable
+ public abstract AccountPhoneNumber phone();
+
+ public abstract AccountOrganization organization();
+
+ @SerializedNames({ "userName", "fullName", "firstName", "lastName",
"emailAddress", "roles", "phone", "department",
+ "customDefined1", "customDefined2", "organization", "state" })
+ public static Account create(String userName, String fullName, String
firstName, String lastName,
+ String emailAddress, List<RoleType> roles, AccountPhoneNumber phone,
String department, String customDefined1,
+ String customDefined2, AccountOrganization organization, String
state) {
+ return
builder().userName(userName).fullName(fullName).firstName(firstName).lastName(lastName)
+ .emailAddress(emailAddress)
+ .roles(roles == null ? ImmutableList.<RoleType>of() :
ImmutableList.copyOf(roles)).phone(phone)
Just pass the list here as it comes from the parameter, and move the
immutability logic to the builder following [this
pattern](https://github.com/jclouds/jclouds-labs/blob/master/dimensiondata/src/main/java/org/jclouds/dimensiondata/cloudcontrol/domain/Backup.java#L53-L62).
>
- Account() {
Why has this been removed? Providing a non-visible default constructor is a
best-practice to enforce using the builders. Let's add the constructors back.
>
- Account() {
+ @Nullable
+ public abstract AccountPhoneNumber phone();
+
+ public abstract AccountOrganization organization();
+
+ @SerializedNames({ "userName", "fullName", "firstName", "lastName",
"emailAddress", "roles", "phone", "department",
+ "customDefined1", "customDefined2", "organization", "state" })
jclouds makes some assumptions when serializing and deserializing. The order of
the parameters in this annotation **must be the same** than the order in which
the variables are declared in the class. Please, reorder accordingly.
> +import org.jclouds.http.HttpRequestFilter;
+
+import java.util.List;
+
+/**
+ * Accepts requests and modifies the endpoint path so that it is injected with
the organisation id.
+ * Handles both oec and caas based URLs.
+ */
+@Singleton
+public class OrganisationIdFilter implements HttpRequestFilter {
+
+ private static final int ORGANIZATION_ID_INDEX = 3;
+ private final Supplier<String> organisationIdSupplier;
+
+ @Inject
+ public OrganisationIdFilter(@Memoized Supplier<String>
organisationIdSupplier) {
Remove the `public` modifier. It is a best-practice in Guice to make all
injection constructors visible just to the default scope.
> +
+ // to have the compute service adapter override default locations
+ install(new
LocationsFromComputeServiceAdapterModule<ServerWithExternalIp, BaseImage,
BaseImage, Datacenter>() {
+ });
+
+ super.configure();
+ }
+
+ @Provides
+ @Singleton
+ @Memoized
+ public final Supplier<String>
getOrganisationIdForAccount(AtomicReference<AuthorizationException>
authException,
+ @Named(PROPERTY_SESSION_INTERVAL) long seconds,
DimensionDataCloudControlApi api) {
+ return MemoizedRetryOnTimeOutButNotOnAuthorizationExceptionSupplier
+ .create(authException, new OrganisationIdForAccount(api), seconds,
TimeUnit.SECONDS);
+ }
Better move this supplier to the HttpApi module, since it is generic for the
provider APIs, not specifically about the compute abstraction.
> +import org.jclouds.logging.Logger;
+
+import javax.annotation.Resource;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Singleton
+public class OrganisationIdForAccount implements Supplier<String> {
+ @Resource
+ @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+ protected Logger logger = Logger.NULL;
+
+ private final AccountApi api;
+
+ @Inject
+ public OrganisationIdForAccount(DimensionDataCloudControlApi api) {
Remove the `public` modifier.
> +import com.google.inject.name.Named;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.dimensiondata.cloudcontrol.DimensionDataCloudControlApi;
+import org.jclouds.dimensiondata.cloudcontrol.domain.Account;
+import org.jclouds.dimensiondata.cloudcontrol.features.AccountApi;
+import org.jclouds.logging.Logger;
+
+import javax.annotation.Resource;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Singleton
+public class OrganisationIdForAccount implements Supplier<String> {
+ @Resource
+ @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+ protected Logger logger = Logger.NULL;
better remove if unused.
> +
+ private static final int ORGANIZATION_ID_INDEX = 3;
+ private final Supplier<String> organisationIdSupplier;
+
+ @Inject
+ public OrganisationIdFilter(@Memoized Supplier<String>
organisationIdSupplier) {
+ this.organisationIdSupplier = organisationIdSupplier;
+ }
+
+ @Override
+ public HttpRequest filter(HttpRequest request) throws HttpException {
+ return
request.toBuilder().replacePath(injectOrganisationId(request.getEndpoint().getPath())).build();
+ }
+
+ @VisibleForTesting
+ public String injectOrganisationId(String path) {
Don't make it public if it is just visible for testing. Remove the public
modifier and leave it in default scope.
> + assertNotNull(account);
+ assertEquals("devlab1-online-vendor-devuser1", account.userName());
+ assertEquals("Donal Lunny", account.fullName());
+ assertEquals("Donal", account.firstName());
+ assertEquals("Lunny", account.lastName());
+ assertEquals("[email protected]",
account.emailAddress());
+ assertThat(account.roles().contains(Account.RoleType.create("storage")));
+ assertThat(account.roles().contains(Account.RoleType.create("primary
administrator")));
+ assertEquals("1", account.phone().countryCode());
+ assertEquals("1234567", account.phone().number());
+ assertEquals("6ac1e746-b1ea-4da5-a24e-caf1a978789d",
account.organization().id());
+ assertEquals("Eng Customer of DevLab1-homed online sign-up Vendor",
account.organization().name());
+ assertEquals("DevLab1Geo1", account.organization().homeGeoName());
+ assertEquals("apidevlab1.opsourcecloud.net",
account.organization().homeGeoApiHost());
+ assertEquals("DEVLAB1GEO1", account.organization().homeGeoId());
+ assertEquals("NORMAL", account.state());
Values seem to be of a particular user. Change them for `notNull` asserts or
something that can be run by anyone.
> + * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.dimensiondata.cloudcontrol.features;
+
+import org.jclouds.dimensiondata.cloudcontrol.domain.Account;
+import
org.jclouds.dimensiondata.cloudcontrol.internal.BaseDimensionDataCloudControlApiLiveTest;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.AssertJUnit.assertEquals;
+
+@Test(groups = "live", testName = "AccountApiLiveTest", singleThreaded = true)
Looks like it does not need to be single-threaded.
> +import org.testng.annotations.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.AssertJUnit.assertEquals;
+
+@Test(groups = "unit", testName = "AccountApiMockTest", singleThreaded = true)
+public class AccountApiMockTest extends BaseDimensionDataCloudControlMockTest {
+
+ @BeforeMethod
+ @Override
+ public void start() throws IOException {
+ super.start();
+ }
No need to override this.
> + assertEquals("Donal", account.firstName());
+ assertEquals("Lunny", account.lastName());
+ assertEquals("[email protected]",
account.emailAddress());
+ assertThat(account.roles().contains(Account.RoleType.create("storage")));
+ assertThat(account.roles().contains(Account.RoleType.create("primary
administrator")));
+ assertEquals("1", account.phone().countryCode());
+ assertEquals("1234567", account.phone().number());
+ assertEquals("Marketing", account.department());
+ assertEquals("custom defined 1", account.customDefined1());
+ assertEquals("custom defined 2", account.customDefined2());
+ assertEquals("6ac1e746-b1ea-4da5-a24e-caf1a978789d",
account.organization().id());
+ assertEquals("Eng Customer of DevLab1-homed online sign-up Vendor",
account.organization().name());
+ assertEquals("DevLab1Geo1", account.organization().homeGeoName());
+ assertEquals("apidevlab1.opsourcecloud.net",
account.organization().homeGeoApiHost());
+ assertEquals("DEVLAB1GEO1", account.organization().homeGeoId());
+ assertEquals("NORMAL", account.state());
There is no need to verify the response deserialization in mock tests, so you
can remove all these asserts. We just want to make sure the generated requests
are the expected ones.
That method uses basic authentication (via its filter), so let's modify the
`assertSent` method in the base mock test class to verify that the
authentication header is sent too.
> + assertThat(account.roles().contains(Account.RoleType.create("primary
> administrator")));
+ assertEquals("1", account.phone().countryCode());
+ assertEquals("1234567", account.phone().number());
+ assertEquals("Marketing", account.department());
+ assertEquals("custom defined 1", account.customDefined1());
+ assertEquals("custom defined 2", account.customDefined2());
+ assertEquals("6ac1e746-b1ea-4da5-a24e-caf1a978789d",
account.organization().id());
+ assertEquals("Eng Customer of DevLab1-homed online sign-up Vendor",
account.organization().name());
+ assertEquals("DevLab1Geo1", account.organization().homeGeoName());
+ assertEquals("apidevlab1.opsourcecloud.net",
account.organization().homeGeoApiHost());
+ assertEquals("DEVLAB1GEO1", account.organization().homeGeoId());
+ assertEquals("NORMAL", account.state());
+
+ assertSent(server, "GET", "/caas/2.4/user/myUser");
+ }
+
Add another test method that simulates a 404 response, to verify that the
method returns `null`.
Since you are defining an explicit `@Fallback` in the api method, we'd better
verify it behaves as expected on the affected responses.
> + * limitations under the License.
+ */
+package org.jclouds.dimensiondata.cloudcontrol.filters;
+
+import com.google.common.base.Supplier;
+import
org.jclouds.dimensiondata.cloudcontrol.compute.suppliers.OrganisationIdForAccount;
+import
org.jclouds.dimensiondata.cloudcontrol.internal.BaseDimensionDataCloudControlMockTest;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+
+import static org.testng.Assert.assertEquals;
+
+@Test(groups = "unit", testName = "OrganisationIdFilterTest", singleThreaded =
true)
+public class OrganisationIdFilterTest extends
BaseDimensionDataCloudControlMockTest {
There is no need for this test to be single-threaded nor to extend the base
mock test class. You don't need to actually start a mock web server to test
this class.
> +import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+
+import static org.testng.Assert.assertEquals;
+
+@Test(groups = "unit", testName = "OrganisationIdFilterTest", singleThreaded =
true)
+public class OrganisationIdFilterTest extends
BaseDimensionDataCloudControlMockTest {
+
+ @BeforeMethod
+ @Override
+ public void start() throws IOException {
+ super.start();
+ server.enqueue(xmlResponse("/account.json"));
+ }
Also remove this.
> +
+import static org.testng.Assert.assertEquals;
+
+@Test(groups = "unit", testName = "OrganisationIdFilterTest", singleThreaded =
true)
+public class OrganisationIdFilterTest extends
BaseDimensionDataCloudControlMockTest {
+
+ @BeforeMethod
+ @Override
+ public void start() throws IOException {
+ super.start();
+ server.enqueue(xmlResponse("/account.json"));
+ }
+
+ @Test
+ public void testCaasUrl() {
+ Supplier<String> orgIdSupplier = new OrganisationIdForAccount(api);
You can just return a mock supplier for this test:
`Suppliers.ofInstance("6ac1e746-b1ea-4da5-a24e-caf1a978789d/")`.
This way you don't need the overhead of starting a mock server, creating the
context, etc.
> + public void testCaasUrl() {
+ Supplier<String> orgIdSupplier = new OrganisationIdForAccount(api);
+ String expectedPath =
"/caas/2.4/6ac1e746-b1ea-4da5-a24e-caf1a978789d/server/0896551e-4fe3-4450-a627-ad5548e7e83a?clone=trevor-test2&desc=trevor-description2";
+ String updatedPath = new
OrganisationIdFilter(orgIdSupplier).injectOrganisationId(
+
"/caas/2.4/server/0896551e-4fe3-4450-a627-ad5548e7e83a?clone=trevor-test2&desc=trevor-description2");
+ assertEquals(updatedPath, expectedPath);
+ }
+
+ @Test
+ public void testDontAddIfAlreadyPresent() {
+ Supplier<String> orgIdSupplier = new OrganisationIdForAccount(api);
+ String expectedPath =
"/caas/2.4/6ac1e746-b1ea-4da5-a24e-caf1a978789d/server/0896551e-4fe3-4450-a627-ad5548e7e83a";
+ String updatedPath = new
OrganisationIdFilter(orgIdSupplier).injectOrganisationId(
+
"/caas/2.4/6ac1e746-b1ea-4da5-a24e-caf1a978789d/server/0896551e-4fe3-4450-a627-ad5548e7e83a");
+ assertEquals(updatedPath, expectedPath);
+ }
Also add a test to verify the behavior when the path segments are < 3 ?
> +@Test(groups = "live")
+public class BaseDimensionDataCloudControlApiLiveTest extends
BaseApiLiveTest<DimensionDataCloudControlApi> {
+
+ public BaseDimensionDataCloudControlApiLiveTest() {
+ provider = "dimensiondata-cloudcontrol";
+ }
+
+ @Override
+ protected ApiMetadata createApiMetadata() {
+ return new DimensionDataCloudControlApiMetadata();
+
+ }
+
+ @Override
+ protected Iterable<Module> setupModules() {
+ return ImmutableSet.<Module>of(getLoggingModule(), new
SshjSshClientModule());
The SSH module is only needed for the compute service tests, so you could
remove this override method.
> + protected ApiMetadata createApiMetadata() {
+ return new DimensionDataCloudControlApiMetadata();
+
+ }
+
+ @Override
+ protected Iterable<Module> setupModules() {
+ return ImmutableSet.<Module>of(getLoggingModule(), new
SshjSshClientModule());
+ }
+
+ @Override
+ protected Properties setupProperties() {
+ Properties overrides = super.setupProperties();
+ overrides.setProperty("jclouds.ssh.retry-auth", "true");
+ return overrides;
+ }
SSH config is only needed for compute tests, so you should be able to remove
this method too.
> +
+ protected Properties overrides() {
+ return new Properties();
+ }
+
+ protected String url(String path) {
+ return server.getUrl(path).toString();
+ }
+
+ protected MockResponse jsonResponse(String resource) {
+ return new MockResponse().addHeader("Content-Type",
"application/json").setBody(stringFromResource(resource));
+ }
+
+ protected MockResponse xmlResponse(String resource) {
+ return new MockResponse().addHeader("Content-Type",
"application/xml").setBody(stringFromResource(resource));
+ }
Remove this? Looks like we're just consuming a json api.
> + return new MockResponse().addHeader("Content-Type",
> "application/json").setBody(stringFromResource(resource));
+ }
+
+ protected MockResponse xmlResponse(String resource) {
+ return new MockResponse().addHeader("Content-Type",
"application/xml").setBody(stringFromResource(resource));
+ }
+
+ protected MockResponse responseUnexpectedError() {
+ return new MockResponse().setResponseCode(400).setStatus("HTTP/1.1 400
Bad Request")
+ .setBody("content:
[{\"operation\":\"OPERATION\",\"responseCode\":\"UNEXPECTED_ERROR\"}]");
+ }
+
+ protected MockResponse responseResourceNotFound() {
+ return new MockResponse().setResponseCode(400).setStatus("HTTP/1.1 400
Bad Request")
+ .setBody("content:
[{\"operation\":\"OPERATION\",\"responseCode\":\"RESOURCE_NOT_FOUND\"}]");
+ }
Hmmm, upon a 400 response, is this what the DimensionData API returns? Do
clients need to actually read the response body to really know the cause of the
failure?
In any case, it looks like we need a custom error handler to propagate the
right exception upon failed requests. jclouds by default throws
`HttpResponseExceptions`, but there are other exceptions that better represent
the different response codes:
* IllegalArgumentException - for 400 Bad Request or 409 Conflict.
* ResourceNotFoundException - for 404 Not Found.
* AuthorizationException - for 403 Unauthorized.
Those can be handled by creating a `DimensionDataCloudControlErrorHandler` (you
can take [the Packet
one](https://github.com/jclouds/jclouds-labs/blob/master/packet/src/main/java/org/jclouds/packet/handlers/PacketErrorHandler.java)
as an example) and declaring it in the http api module (as in
[here](https://github.com/jclouds/jclouds-labs/blob/master/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java#L48-L53)).
--
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/jclouds/jclouds-labs/pull/375#pullrequestreview-28841227