Skip to content

Commit 13bb81a

Browse files
committed
Update to address various review comments
- error out of getUserBucketUsages() if bucket param is set but userid is not - added unit tests for the same. - split some copy/paste code into a new function - added unit tests for new function which required moving the test to the right package to test a protected function. - use the base class logger - misc tidy ups.
1 parent 33c7a07 commit 13bb81a

File tree

4 files changed

+119
-60
lines changed

4 files changed

+119
-60
lines changed

plugins/integrations/cloudian/src/main/java/org/apache/cloudstack/cloudian/client/CloudianClient.java

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.cloudstack.cloudian.client;
1919

2020
import java.io.IOException;
21+
import java.io.InputStream;
2122
import java.io.PushbackInputStream;
2223
import java.net.SocketTimeoutException;
2324
import java.security.KeyManagementException;
@@ -156,6 +157,30 @@ private void throwTimeoutOrServerException(final IOException e) {
156157
}
157158
}
158159

160+
/**
161+
* Return the body content stream only if the body has bytes.
162+
*
163+
* Unfortunately, some of the responses such as listGroups() or listUsers() return
164+
* an empty body instead of returning and empty list. The only way to detect this is
165+
* to try read from the body. This method handles this and will return null if the
166+
* body was empty or a valid stream with the body content otherwise.
167+
*
168+
* @param response the response to check for the body contents.
169+
* @return a valid InputStream or null if the body was empty.
170+
*
171+
* @throws IOException some error reading from the body such as timeout etc.
172+
*/
173+
protected InputStream getNonEmptyContentStream(HttpResponse response) throws IOException {
174+
PushbackInputStream iStream = new PushbackInputStream(response.getEntity().getContent());
175+
int firstByte=iStream.read();
176+
if (firstByte == -1) {
177+
return null;
178+
} else {
179+
iStream.unread(firstByte);
180+
return iStream;
181+
}
182+
}
183+
159184
private HttpResponse delete(final String path) throws IOException {
160185
final HttpResponse response = httpClient.execute(new HttpDelete(adminApiUrl + path), httpContext);
161186
checkAuthFailure(response);
@@ -235,14 +260,17 @@ public String getServerVersion() {
235260
*
236261
* @param groupId the groupId is required (and must exist)
237262
* @param userId the userId is optional (null) and if not set all group users are returned.
238-
* @param bucket the bucket is optional (null). If set the userId must also be set.
263+
* @param bucket the bucket is optional (null). If set, the userId must also be set.
239264
* @return a list of bucket usages (possibly empty).
240265
* @throws ServerApiException on non-200 response such as unknown groupId etc or response issue.
241266
*/
242267
public List<CloudianUserBucketUsage> getUserBucketUsages(final String groupId, final String userId, final String bucket) {
243-
if (StringUtils.isBlank(groupId)) {
244-
return new ArrayList<>();
268+
if (StringUtils.isBlank(groupId) || (StringUtils.isBlank(userId) && !StringUtils.isBlank(bucket))) {
269+
String msg = String.format("Bad parameters groupId=%s userId=%s bucket=%s", groupId, userId, bucket);
270+
logger.error(msg);
271+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, msg);
245272
}
273+
246274
logger.debug("Getting bucket usages for groupId={} userId={} bucket={}", groupId, userId, bucket);
247275
StringBuilder cmd = new StringBuilder("/system/bucketusage?groupId=");
248276
cmd.append(groupId);
@@ -251,7 +279,6 @@ public List<CloudianUserBucketUsage> getUserBucketUsages(final String groupId, f
251279
cmd.append(userId);
252280
}
253281
if (! StringUtils.isBlank(bucket)) {
254-
// Assume userId is also set (or request fails).
255282
cmd.append("&bucket=");
256283
cmd.append(bucket);
257284
}
@@ -341,15 +368,10 @@ public List<CloudianUser> listUsers(final String groupId) {
341368
if (noResponseEntity(response)) {
342369
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "API error");
343370
}
344-
// The empty list case is badly behaved and returns as 200 with an empty body.
345-
// We'll try detect this by reading the first byte and then putting it back if required.
346-
PushbackInputStream iStream = new PushbackInputStream(response.getEntity().getContent());
347-
int firstByte=iStream.read();
348-
if (firstByte == -1) {
349-
return new ArrayList<>(); // EOF => empty list
371+
InputStream iStream = getNonEmptyContentStream(response);
372+
if (iStream == null) {
373+
return new ArrayList<>(); // empty body => empty list
350374
}
351-
// unread that first byte and process the JSON
352-
iStream.unread(firstByte);
353375
final ObjectMapper mapper = new ObjectMapper();
354376
return Arrays.asList(mapper.readValue(iStream, CloudianUser[].class));
355377
} catch (final IOException e) {
@@ -512,15 +534,10 @@ public List<CloudianGroup> listGroups() {
512534
if (noResponseEntity(response)) {
513535
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "API error");
514536
}
515-
// The empty list case is badly behaved and returns as 200 with an empty body.
516-
// We'll try detect this by reading the first byte and then putting it back if required.
517-
PushbackInputStream iStream = new PushbackInputStream(response.getEntity().getContent());
518-
int firstByte=iStream.read();
519-
if (firstByte == -1) {
520-
return new ArrayList<>(); // EOF => empty list
537+
InputStream iStream = getNonEmptyContentStream(response);
538+
if (iStream == null) {
539+
return new ArrayList<>(); // Empty body => empty list
521540
}
522-
// unread that first byte and process the JSON
523-
iStream.unread(firstByte);
524541
final ObjectMapper mapper = new ObjectMapper();
525542
return Arrays.asList(mapper.readValue(iStream, CloudianGroup[].class));
526543
} catch (final IOException e) {
Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
package org.apache.cloudstack.cloudian;
18+
package org.apache.cloudstack.cloudian.client;
1919

2020
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
2121
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
@@ -31,26 +31,33 @@
3131
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
3232
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
3333
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
3436

37+
import java.io.ByteArrayInputStream;
38+
import java.io.IOException;
39+
import java.io.InputStream;
3540
import java.util.ArrayList;
3641
import java.util.List;
3742

43+
import org.apache.http.HttpEntity;
44+
import org.apache.http.HttpResponse;
45+
46+
import org.apache.cloudstack.api.ApiErrorCode;
3847
import org.apache.cloudstack.api.ServerApiException;
39-
import org.apache.cloudstack.cloudian.client.CloudianClient;
40-
import org.apache.cloudstack.cloudian.client.CloudianCredential;
41-
import org.apache.cloudstack.cloudian.client.CloudianGroup;
42-
import org.apache.cloudstack.cloudian.client.CloudianUser;
43-
import org.apache.cloudstack.cloudian.client.CloudianUserBucketUsage;
4448
import org.apache.cloudstack.cloudian.client.CloudianUserBucketUsage.CloudianBucketUsage;
4549
import org.junit.Assert;
4650
import org.junit.Before;
4751
import org.junit.Rule;
4852
import org.junit.Test;
53+
import org.junit.runner.RunWith;
54+
import org.mockito.junit.MockitoJUnitRunner;
4955

5056
import com.cloud.utils.exception.CloudRuntimeException;
5157
import com.github.tomakehurst.wiremock.client.BasicCredentials;
5258
import com.github.tomakehurst.wiremock.junit.WireMockRule;
5359

60+
@RunWith(MockitoJUnitRunner.class)
5461
public class CloudianClientTest {
5562
private final int port = 14333;
5663
private final int timeout = 2;
@@ -119,6 +126,38 @@ public void testBasicAuthFailure() {
119126
client.listUser("someUserId", "somegGroupId");
120127
}
121128

129+
@Test
130+
public void getNonEmptyContentStreamEmpty() {
131+
InputStream emptyStream = new ByteArrayInputStream(new byte[]{});
132+
HttpEntity entity = mock(HttpEntity.class);
133+
HttpResponse response = mock(HttpResponse.class);
134+
when(response.getEntity()).thenReturn(entity);
135+
try {
136+
when(entity.getContent()).thenReturn(emptyStream);
137+
Assert.assertNull(client.getNonEmptyContentStream(response));
138+
} catch (IOException e) {
139+
Assert.fail("Should not be any exception here");
140+
}
141+
}
142+
143+
@Test
144+
public void getNonEmptyContentStreamWithContent() {
145+
InputStream nonEmptyStream = new ByteArrayInputStream(new byte[]{9, 8});
146+
HttpEntity entity = mock(HttpEntity.class);
147+
HttpResponse response = mock(HttpResponse.class);
148+
when(response.getEntity()).thenReturn(entity);
149+
try {
150+
when(entity.getContent()).thenReturn(nonEmptyStream);
151+
InputStream is = client.getNonEmptyContentStream(response);
152+
Assert.assertNotNull(is);
153+
Assert.assertEquals(9, is.read());
154+
Assert.assertEquals(8, is.read());
155+
Assert.assertEquals(-1, is.read());
156+
} catch (IOException e) {
157+
Assert.fail("Should not be any exception here");
158+
}
159+
}
160+
122161
/////////////////////////////////////////////////////
123162
//////////////// System API tests ///////////////////
124163
/////////////////////////////////////////////////////
@@ -135,6 +174,20 @@ public void getServerVersion() {
135174
Assert.assertEquals(expect, version);
136175
}
137176

177+
@Test
178+
public void getUserBucketUsagesBadUsageBlankGroup() {
179+
ServerApiException thrown = Assert.assertThrows(ServerApiException.class, () -> client.getUserBucketUsages(null, null, null));
180+
Assert.assertNotNull(thrown);
181+
Assert.assertEquals(ApiErrorCode.PARAM_ERROR, thrown.getErrorCode());
182+
}
183+
184+
@Test
185+
public void getUserBucketUsagesBadUsageBlankUserWithBucket() {
186+
ServerApiException thrown = Assert.assertThrows(ServerApiException.class, () -> client.getUserBucketUsages("group", "", "bucket"));
187+
Assert.assertNotNull(thrown);
188+
Assert.assertEquals(ApiErrorCode.PARAM_ERROR, thrown.getErrorCode());
189+
}
190+
138191
@Test
139192
public void getUserBucketUsagesEmptyGroup() {
140193
wireMockRule.stubFor(get(urlEqualTo("/system/bucketusage?groupId=mygroup"))

plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImpl.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@
4141
import org.apache.cloudstack.storage.object.BaseObjectStoreDriverImpl;
4242
import org.apache.cloudstack.storage.object.Bucket;
4343
import org.apache.cloudstack.storage.object.BucketObject;
44-
import org.apache.logging.log4j.LogManager;
45-
import org.apache.logging.log4j.Logger;
4644

4745
import com.amazonaws.AmazonClientException;
4846
import com.amazonaws.services.identitymanagement.AmazonIdentityManagement;
@@ -82,8 +80,6 @@
8280
import com.cloud.utils.exception.CloudRuntimeException;
8381

8482
public class CloudianHyperStoreObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
85-
protected Logger logger = LogManager.getLogger(CloudianHyperStoreObjectStoreDriverImpl.class);
86-
8783
@Inject
8884
AccountDao _accountDao;
8985

plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtil.java

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -64,33 +64,28 @@ public class CloudianHyperStoreUtil {
6464

6565
public static final String IAM_USER_USERNAME = "CloudStack";
6666
public static final String IAM_USER_POLICY_NAME = "CloudStackPolicy";
67-
public static final String IAM_USER_POLICY;
68-
static {
69-
StringBuilder sb = new StringBuilder();
70-
sb.append("{\n");
71-
sb.append(" \"Version\": \"2012-10-17\",\n");
72-
sb.append(" \"Statement\": [\n");
73-
sb.append(" {\n");
74-
sb.append(" \"Sid\": \"AllowFullS3Access\",\n");
75-
sb.append(" \"Effect\": \"Allow\",\n");
76-
sb.append(" \"Action\": [\n");
77-
sb.append(" \"s3:*\"\n");
78-
sb.append(" ],\n");
79-
sb.append(" \"Resource\": \"*\"\n");
80-
sb.append(" },\n");
81-
sb.append(" {\n");
82-
sb.append(" \"Sid\": \"ExceptBucketCreationOrDeletion\",\n");
83-
sb.append(" \"Effect\": \"Deny\",\n");
84-
sb.append(" \"Action\": [\n");
85-
sb.append(" \"s3:createBucket\",\n");
86-
sb.append(" \"s3:deleteBucket\"\n");
87-
sb.append(" ],\n");
88-
sb.append(" \"Resource\": \"*\"\n");
89-
sb.append(" }\n");
90-
sb.append(" ]\n");
91-
sb.append("}\n");
92-
IAM_USER_POLICY = sb.toString();
93-
}
67+
public static final String IAM_USER_POLICY = "{\n" +
68+
" \"Version\": \"2012-10-17\",\n" +
69+
" \"Statement\": [\n" +
70+
" {\n" +
71+
" \"Sid\": \"AllowFullS3Access\",\n" +
72+
" \"Effect\": \"Allow\",\n" +
73+
" \"Action\": [\n" +
74+
" \"s3:*\"\n" +
75+
" ],\n" +
76+
" \"Resource\": \"*\"\n" +
77+
" },\n" +
78+
" {\n" +
79+
" \"Sid\": \"ExceptBucketCreationOrDeletion\",\n" +
80+
" \"Effect\": \"Deny\",\n" +
81+
" \"Action\": [\n" +
82+
" \"s3:createBucket\",\n" +
83+
" \"s3: deleteBucket\"\n" +
84+
" ],\n" +
85+
" \"Resource\": \"*\"\n" +
86+
" }\n" +
87+
" ]\n" +
88+
"}\n";
9489

9590
/**
9691
* This method is solely for test purposes so that we can mock the timeout.
@@ -120,9 +115,7 @@ public static CloudianClient getCloudianClient(String url, String user, String p
120115
port = DEFAULT_ADMIN_PORT;
121116
}
122117
return new CloudianClient(host, port, scheme, user, pass, validateSSL, getAdminTimeoutSeconds());
123-
} catch (MalformedURLException e) {
124-
throw new CloudRuntimeException(e);
125-
} catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
118+
} catch (MalformedURLException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
126119
throw new CloudRuntimeException(e);
127120
}
128121
}

0 commit comments

Comments
 (0)