embulk-output-gcs 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 94fccd956be721058fbf74ffd57017d05caf8e5b
4
- data.tar.gz: e7e35ef7704225a633d47babdd21c91edd6ad272
3
+ metadata.gz: 7f2569f6effb1c6cd11617f367c4f9edc5c34955
4
+ data.tar.gz: a320b195255e974e5bc94872af9674930284bf11
5
5
  SHA512:
6
- metadata.gz: 2b2325cf0230d16f57fb7a247a9237428fd2d4527cd6848367a5989e3d8bc9a9da7f282b3a81ec97ad2425da9bb70cbd0a26465fcc1dd747ce3e804908cd48ad
7
- data.tar.gz: ceb4828ba593c2e2ab8cf043bce26b4509867489cf3b7eac96fd00f1ef10b5ffe8895587a1844d7b01671ea0373cd30b82d39a94da2caf8183cdee206dcdd4e7
6
+ metadata.gz: d44045b96beeed143a690ce74d93463c35183f32114990a19b48d796f40180e32885014cebda5e2551e514fdbd5c2cf092d16d7ae251d7eb6515a796eaa902d2
7
+ data.tar.gz: d32896f52c60f511eb5bcdb5c802219d4652e24f8722c2e4f85793f6aab415dc21204b588e9e1fe7e50175b5268d467c9f171e14df37cc3ad30167db67d19363
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ language: java
2
+ jdk:
3
+ - oraclejdk8
4
+ - oraclejdk7
5
+ - openjdk7
6
+ env:
7
+ global:
8
+ - GCP_EMAIL=account-2@embulk-output-gcs-test.iam.gserviceaccount.com
9
+ - GCP_P12_KEYFILE=embulk-output-gcs-test.p12
10
+ - GCP_JSON_KEYFILE=embulk-output-gcs-test.json
11
+ - GCP_BUCKET=embulk-output-gcs-test-01
12
+ - GCP_BUCKET_DIRECTORY=unittests
13
+ before_cache:
14
+ - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
15
+ cache:
16
+ directories:
17
+ - "$HOME/.gradle/caches/"
18
+ - "$HOME/.gradle/wrapper/"
19
+ before_install:
20
+ - openssl aes-256-cbc -K $encrypted_1230b5822723_key -iv $encrypted_1230b5822723_iv
21
+ -in src/test/resources/keys.tar.enc -out keys.tar -d
22
+ - tar xvf keys.tar
23
+ after_success: "./gradlew jacocoTestReport"
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/hakobera/embulk-output-gcs.svg?branch=master)](https://travis-ci.org/hakobera/embulk-output-gcs)
2
+
1
3
  # Google Cloud Storage output plugin for Embulk
2
4
 
3
5
  Google Cloud Storage output plugin for [Embulk](https://github.com/embulk/embulk).
@@ -15,9 +17,10 @@ Google Cloud Storage output plugin for [Embulk](https://github.com/embulk/embulk
15
17
  - **path_prefix**: Prefix of output keys (string, required)
16
18
  - **file_ext**: Extention of output file (string, required)
17
19
  - **content_type**: content type of output file (string, optional, default value is "application/octet-stream")
18
- - **auth_method**: Authentication method `private_key` or `compute_engine` (string, optional, default value is "private_key")
19
- - **service_account_email**: Google Cloud Platform service account email (string, required)
20
- - **p12_keyfile_path**: Private key file fullpath of Google Cloud Platform service account (string, required)
20
+ - **auth_method**: Authentication method `private_key`, `json_key` or `compute_engine` (string, optional, default value is "private_key")
21
+ - **service_account_email**: Google Cloud Platform service account email (string, required when auth_method is private_key)
22
+ - **p12_keyfile**: Private key file fullpath of Google Cloud Platform service account (string, required when auth_method is private_key)
23
+ - **json_keyfile** fullpath of json_key (string, required when auth_method is json_key)
21
24
  - **application_name**: Application name, anything you like (string, optional, default value is "embulk-output-gcs")
22
25
 
23
26
  ## Example
@@ -30,7 +33,7 @@ out:
30
33
  file_ext: .csv
31
34
  auth_method: `private_key` #default
32
35
  service_account_email: 'XYZ@developer.gserviceaccount.com'
33
- p12_keyfile_path: '/path/to/private/key.p12'
36
+ p12_keyfile: '/path/to/private/key.p12'
34
37
  formatter:
35
38
  type: csv
36
39
  encoding: UTF-8
@@ -38,18 +41,56 @@ out:
38
41
 
39
42
  ## Authentication
40
43
 
41
- There are two methods supported to fetch access token for the service account.
44
+ There are three methods supported to fetch access token for the service account.
45
+
46
+ 1. Public-Private key pair of GCP(Google Cloud Platform)'s service account
47
+ 2. JSON key of GCP(Google Cloud Platform)'s service account
48
+ 3. Pre-defined access token (Google Compute Engine only)
49
+
50
+ ### Public-Private key pair of GCP's service account
51
+
52
+ You first need to create a service account (client ID), download its private key and deploy the key with embulk.
53
+
54
+ ```yaml
55
+ out:
56
+ type: gcs
57
+ auth_method: private_key
58
+ service_account_email: ABCXYZ123ABCXYZ123.gserviceaccount.com
59
+ p12_keyfile: /path/to/p12_keyfile.p12
60
+ ```
42
61
 
43
- 1. Public-Private key pair
44
- 2. Pre-defined access token (Compute Engine only)
62
+ ### JSON key of GCP's service account
45
63
 
46
- The examples above use the first one. You first need to create a service account (client ID),
47
- download its private key and deploy the key with embulk.
64
+ You first need to create a service account (client ID), download its json key and deploy the key with embulk.
65
+
66
+ ```yaml
67
+ out:
68
+ type: gcs
69
+ auth_method: json_key
70
+ json_keyfile: /path/to/json_keyfile.json
71
+ ```
72
+
73
+ You can also embed contents of json_keyfile at config.yml.
74
+
75
+ ```yaml
76
+ out:
77
+ type: gcs
78
+ auth_method: json_key
79
+ json_keyfile:
80
+ content: |
81
+ {
82
+ "private_key_id": "123456789",
83
+ "private_key": "-----BEGIN PRIVATE KEY-----\nABCDEF",
84
+ "client_email": "..."
85
+ }
86
+ ```
87
+
88
+ ### Pre-defined access token(GCE only)
48
89
 
49
90
  On the other hand, you don't need to explicitly create a service account for embulk when you
50
- run embulk in Google Compute Engine. In this second authentication method, you need to
91
+ run embulk in Google Compute Engine. In this third authentication method, you need to
51
92
  add the API scope "https://www.googleapis.com/auth/devstorage.read_write" to the scope list of your
52
- Compute Engine instance, then you can configure embulk like this.
93
+ Compute Engine VM instance, then you can configure embulk like this.
53
94
 
54
95
  [Setting the scope of service account access for instances](https://cloud.google.com/compute/docs/authentication)
55
96
 
@@ -64,3 +105,53 @@ out:
64
105
  ```
65
106
  $ ./gradlew gem
66
107
  ```
108
+
109
+ ## Test
110
+
111
+ ```
112
+ $ ./gradlew test # -t to watch change of files and rebuild continuously
113
+ ```
114
+
115
+ To run unit tests, we need to configure the following environment variables.
116
+
117
+ When environment variables are not set, skip almost test cases.
118
+
119
+ ```
120
+ GCP_EMAIL
121
+ GCP_P12_KEYFILE
122
+ GCP_JSON_KEYFILE
123
+ GCP_BUCKET
124
+ GCP_BUCKET_DIRECTORY(optional, if needed)
125
+ ```
126
+
127
+ If you're using Mac OS X El Capitan and GUI Applications(IDE), like as follows.
128
+ ```
129
+ $ vi ~/Library/LaunchAgents/environment.plist
130
+ <?xml version="1.0" encoding="UTF-8"?>
131
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
132
+ <plist version="1.0">
133
+ <dict>
134
+ <key>Label</key>
135
+ <string>my.startup</string>
136
+ <key>ProgramArguments</key>
137
+ <array>
138
+ <string>sh</string>
139
+ <string>-c</string>
140
+ <string>
141
+ launchctl setenv GCP_EMAIL ABCXYZ123ABCXYZ123.gserviceaccount.com
142
+ launchctl setenv GCP_P12_KEYFILE /path/to/p12_keyfile.p12
143
+ launchctl setenv GCP_JSON_KEYFILE /path/to/json_keyfile.json
144
+ launchctl setenv GCP_BUCKET my-bucket
145
+ launchctl setenv GCP_BUCKET_DIRECTORY unittests
146
+ </string>
147
+ </array>
148
+ <key>RunAtLoad</key>
149
+ <true/>
150
+ </dict>
151
+ </plist>
152
+
153
+ $ launchctl load ~/Library/LaunchAgents/environment.plist
154
+ $ launchctl getenv GCP_EMAIL //try to get value.
155
+
156
+ Then start your applications.
157
+ ```
data/build.gradle CHANGED
@@ -2,6 +2,7 @@ plugins {
2
2
  id "com.jfrog.bintray" version "1.1"
3
3
  id "com.github.jruby-gradle.base" version "0.1.5"
4
4
  id "java"
5
+ id "jacoco"
5
6
  }
6
7
  import com.github.jrubygradle.JRubyExec
7
8
  repositories {
@@ -15,7 +16,7 @@ configurations {
15
16
  sourceCompatibility = 1.7
16
17
  targetCompatibility = 1.7
17
18
 
18
- version = "0.2.0"
19
+ version = "0.3.0"
19
20
 
20
21
  dependencies {
21
22
  compile "org.embulk:embulk-core:0.7.3"
@@ -24,7 +25,9 @@ dependencies {
24
25
  compile "com.google.http-client:google-http-client-jackson2:1.19.0"
25
26
  compile ("com.google.apis:google-api-services-storage:v1-rev28-1.19.1") {exclude module: "guava-jdk5"}
26
27
 
27
- testCompile "junit:junit:4.+"
28
+ testCompile "junit:junit:4.12"
29
+ testCompile "org.embulk:embulk-core:0.7.5:tests"
30
+ testCompile "org.embulk:embulk-standards:0.7.5"
28
31
  }
29
32
 
30
33
  task classpath(type: Copy, dependsOn: ["jar"]) {
@@ -60,3 +63,7 @@ Gem::Specification.new do |spec|
60
63
  end
61
64
  /$)
62
65
  }
66
+
67
+ task gempush << {
68
+ "gem push pkg/embulk-output-gcs-${project.version}.gem".execute().waitFor()
69
+ }
@@ -1,8 +1,9 @@
1
1
  package org.embulk.output;
2
2
 
3
3
  import java.io.File;
4
+ import java.io.FileInputStream;
4
5
  import java.io.IOException;
5
-
6
+ import java.util.Collections;
6
7
  import com.google.api.client.http.apache.ApacheHttpTransport;
7
8
  import com.google.api.services.storage.model.Objects;
8
9
  import com.google.common.base.Optional;
@@ -25,16 +26,19 @@ public class GcsAuthentication
25
26
  private final Logger log = Exec.getLogger(GcsAuthentication.class);
26
27
  private final Optional<String> serviceAccountEmail;
27
28
  private final Optional<String> p12KeyFilePath;
29
+ private final Optional<String> jsonKeyFilePath;
28
30
  private final String applicationName;
29
31
  private final HttpTransport httpTransport;
30
32
  private final JsonFactory jsonFactory;
31
33
  private final HttpRequestInitializer credentials;
32
34
 
33
- public GcsAuthentication(String authMethod, Optional<String> serviceAccountEmail, Optional<String> p12KeyFilePath, String applicationName)
34
- throws IOException, GeneralSecurityException
35
+ public GcsAuthentication(String authMethod, Optional<String> serviceAccountEmail,
36
+ Optional<String> p12KeyFilePath, Optional<String> jsonKeyFilePath, String applicationName)
37
+ throws IOException, GeneralSecurityException
35
38
  {
36
39
  this.serviceAccountEmail = serviceAccountEmail;
37
40
  this.p12KeyFilePath = p12KeyFilePath;
41
+ this.jsonKeyFilePath = jsonKeyFilePath;
38
42
  this.applicationName = applicationName;
39
43
 
40
44
  this.httpTransport = new ApacheHttpTransport.Builder().build();
@@ -42,6 +46,8 @@ public class GcsAuthentication
42
46
 
43
47
  if (authMethod.equals("compute_engine")) {
44
48
  this.credentials = getComputeCredential();
49
+ } else if(authMethod.toLowerCase().equals("json_key")) {
50
+ this.credentials = getServiceAccountCredentialFromJsonFile();
45
51
  } else {
46
52
  this.credentials = getServiceAccountCredential();
47
53
  }
@@ -68,6 +74,14 @@ public class GcsAuthentication
68
74
  .build();
69
75
  }
70
76
 
77
+ private GoogleCredential getServiceAccountCredentialFromJsonFile() throws IOException
78
+ {
79
+ FileInputStream stream = new FileInputStream(jsonKeyFilePath.orNull());
80
+
81
+ return GoogleCredential.fromStream(stream, httpTransport, jsonFactory)
82
+ .createScoped(Collections.singleton(StorageScopes.DEVSTORAGE_READ_WRITE));
83
+ }
84
+
71
85
  /**
72
86
  * @see http://developers.guge.io/accounts/docs/OAuth2ServiceAccount#creatinganaccount
73
87
  * @see https://developers.google.com/accounts/docs/OAuth2
@@ -3,6 +3,7 @@ package org.embulk.output;
3
3
  import com.google.api.client.http.InputStreamContent;
4
4
  import com.google.api.services.storage.Storage;
5
5
  import com.google.api.services.storage.model.StorageObject;
6
+ import com.google.common.base.Function;
6
7
  import com.google.common.base.Optional;
7
8
  import com.google.common.base.Throwables;
8
9
  import org.embulk.config.TaskReport;
@@ -17,6 +18,7 @@ import org.embulk.spi.Buffer;
17
18
  import org.embulk.spi.Exec;
18
19
  import org.embulk.spi.FileOutputPlugin;
19
20
  import org.embulk.spi.TransactionalFileOutput;
21
+ import org.embulk.spi.unit.LocalFile;
20
22
  import org.slf4j.Logger;
21
23
 
22
24
  import java.io.IOException;
@@ -60,10 +62,20 @@ public class GcsOutputPlugin implements FileOutputPlugin {
60
62
  @ConfigDefault("null")
61
63
  Optional<String> getServiceAccountEmail();
62
64
 
65
+ // kept for backward compatibility
63
66
  @Config("p12_keyfile_path")
64
67
  @ConfigDefault("null")
65
68
  Optional<String> getP12KeyfilePath();
66
69
 
70
+ @Config("p12_keyfile")
71
+ @ConfigDefault("null")
72
+ Optional<LocalFile> getP12Keyfile();
73
+ void setP12Keyfile(Optional<LocalFile> p12Keyfile);
74
+
75
+ @Config("json_keyfile")
76
+ @ConfigDefault("null")
77
+ Optional<LocalFile> getJsonKeyfile();
78
+
67
79
  @Config("application_name")
68
80
  @ConfigDefault("\"embulk-output-gcs\"")
69
81
  String getApplicationName();
@@ -74,6 +86,28 @@ public class GcsOutputPlugin implements FileOutputPlugin {
74
86
  int taskCount,
75
87
  FileOutputPlugin.Control control) {
76
88
  PluginTask task = config.loadConfig(PluginTask.class);
89
+
90
+ if (task.getP12KeyfilePath().isPresent()) {
91
+ if (task.getP12Keyfile().isPresent()) {
92
+ throw new ConfigException("Setting both p12_keyfile_path and p12_keyfile is invalid");
93
+ }
94
+ try {
95
+ task.setP12Keyfile(Optional.of(LocalFile.of(task.getP12KeyfilePath().get())));
96
+ } catch (IOException ex) {
97
+ throw Throwables.propagate(ex);
98
+ }
99
+ }
100
+
101
+ if (task.getAuthMethod().getString().equals("json_key")) {
102
+ if (!task.getJsonKeyfile().isPresent()) {
103
+ throw new ConfigException("If auth_method is json_key, you have to set json_keyfile");
104
+ }
105
+ } else if (task.getAuthMethod().getString().equals("private_key")) {
106
+ if (!task.getP12Keyfile().isPresent() || !task.getServiceAccountEmail().isPresent()) {
107
+ throw new ConfigException("If auth_method is private_key, you have to set both service_account_email and p12_keyfile");
108
+ }
109
+ }
110
+
77
111
  return resume(task.dump(), taskCount, control);
78
112
  }
79
113
 
@@ -99,18 +133,43 @@ public class GcsOutputPlugin implements FileOutputPlugin {
99
133
  return new TransactionalGcsFileOutput(task, client, taskIndex);
100
134
  }
101
135
 
136
+ private GcsAuthentication newGcsAuth(PluginTask task)
137
+ {
138
+ try {
139
+ return new GcsAuthentication(
140
+ task.getAuthMethod().getString(),
141
+ task.getServiceAccountEmail(),
142
+ task.getP12Keyfile().transform(localFileToPathString()),
143
+ task.getJsonKeyfile().transform(localFileToPathString()),
144
+ task.getApplicationName()
145
+ );
146
+ } catch (GeneralSecurityException | IOException ex) {
147
+ throw new ConfigException(ex);
148
+ }
149
+ }
150
+
102
151
  private Storage createClient(final PluginTask task) {
103
152
  Storage client = null;
104
153
  try {
105
- GcsAuthentication auth = new GcsAuthentication(task.getAuthMethod().getString(), task.getServiceAccountEmail(), task.getP12KeyfilePath(), task.getApplicationName());
154
+ GcsAuthentication auth = newGcsAuth(task);
106
155
  client = auth.getGcsClient(task.getBucket());
107
- } catch (GeneralSecurityException | IOException ex) {
156
+ } catch (ConfigException | IOException ex) {
108
157
  throw new ConfigException(ex);
109
158
  }
110
159
 
111
160
  return client;
112
161
  }
113
162
 
163
+ private Function<LocalFile, String> localFileToPathString() {
164
+ return new Function<LocalFile, String>()
165
+ {
166
+ public String apply(LocalFile file)
167
+ {
168
+ return file.getPath().toString();
169
+ }
170
+ };
171
+ }
172
+
114
173
  static class TransactionalGcsFileOutput implements TransactionalFileOutput {
115
174
  private final int taskIndex;
116
175
  private final Storage client;
@@ -233,7 +292,8 @@ public class GcsOutputPlugin implements FileOutputPlugin {
233
292
  public enum AuthMethod
234
293
  {
235
294
  private_key("private_key"),
236
- compute_engine("compute_engine");
295
+ compute_engine("compute_engine"),
296
+ json_key("json_key");
237
297
 
238
298
  private final String string;
239
299
 
@@ -0,0 +1,177 @@
1
+ package org.embulk.output;
2
+
3
+ import com.google.common.base.Optional;
4
+ import org.embulk.EmbulkTestRuntime;
5
+ import com.google.api.services.storage.Storage;
6
+ import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
7
+
8
+ import java.io.IOException;
9
+ import java.io.FileNotFoundException;
10
+ import java.security.GeneralSecurityException;
11
+ import com.google.api.client.googleapis.json.GoogleJsonResponseException;
12
+
13
+ import java.lang.reflect.Field;
14
+ import org.junit.BeforeClass;
15
+ import org.junit.Rule;
16
+ import org.junit.Test;
17
+ import static org.junit.Assert.assertEquals;
18
+ import static org.junit.Assume.assumeNotNull;
19
+
20
+ public class TestGcsAuthentication
21
+ {
22
+ private static Optional<String> GCP_EMAIL;
23
+ private static Optional<String> GCP_P12_KEYFILE;
24
+ private static Optional<String> GCP_JSON_KEYFILE;
25
+ private static String GCP_BUCKET;
26
+ private static final String GCP_APPLICATION_NAME = "embulk-output-gcs";
27
+
28
+ /*
29
+ * This test case requires environment variables
30
+ * GCP_EMAIL
31
+ * GCP_P12_KEYFILE
32
+ * GCP_JSON_KEYFILE
33
+ * GCP_BUCKET
34
+ */
35
+ @BeforeClass
36
+ public static void initializeConstant()
37
+ {
38
+ GCP_EMAIL = Optional.of(System.getenv("GCP_EMAIL"));
39
+ GCP_P12_KEYFILE = Optional.of(System.getenv("GCP_P12_KEYFILE"));
40
+ GCP_JSON_KEYFILE = Optional.of(System.getenv("GCP_JSON_KEYFILE"));
41
+ GCP_BUCKET = System.getenv("GCP_BUCKET");
42
+ // skip test cases, if environment variables are not set.
43
+ assumeNotNull(GCP_EMAIL, GCP_P12_KEYFILE, GCP_JSON_KEYFILE, GCP_BUCKET);
44
+ }
45
+
46
+ @Rule
47
+ public EmbulkTestRuntime runtime = new EmbulkTestRuntime();
48
+
49
+ @Test
50
+ public void testGetServiceAccountCredentialSuccess()
51
+ throws NoSuchFieldException, IllegalAccessException, GeneralSecurityException, IOException
52
+ {
53
+ GcsAuthentication auth = new GcsAuthentication(
54
+ "private_key",
55
+ GCP_EMAIL,
56
+ GCP_P12_KEYFILE,
57
+ null,
58
+ GCP_APPLICATION_NAME
59
+ );
60
+
61
+ Field field = GcsAuthentication.class.getDeclaredField("credentials");
62
+ field.setAccessible(true);
63
+
64
+ assertEquals(GoogleCredential.class, field.get(auth).getClass());
65
+ }
66
+
67
+ @Test(expected = FileNotFoundException.class)
68
+ public void testGetServiceAccountCredentialThrowFileNotFoundException()
69
+ throws GeneralSecurityException, IOException
70
+ {
71
+ Optional<String> notFoundP12Keyfile = Optional.of("/path/to/notfound.p12");
72
+ GcsAuthentication auth = new GcsAuthentication(
73
+ "private_key",
74
+ GCP_EMAIL,
75
+ notFoundP12Keyfile,
76
+ null,
77
+ GCP_APPLICATION_NAME
78
+ );
79
+ }
80
+
81
+ @Test
82
+ public void testGetGcsClientUsingServiceAccountCredentialSuccess()
83
+ throws NoSuchFieldException, IllegalAccessException, GeneralSecurityException, IOException
84
+ {
85
+ GcsAuthentication auth = new GcsAuthentication(
86
+ "private_key",
87
+ GCP_EMAIL,
88
+ GCP_P12_KEYFILE,
89
+ null,
90
+ GCP_APPLICATION_NAME
91
+ );
92
+
93
+ Storage client = auth.getGcsClient(GCP_BUCKET);
94
+
95
+ assertEquals(Storage.class, client.getClass());
96
+ }
97
+
98
+ @Test(expected = GoogleJsonResponseException.class)
99
+ public void testGetGcsClientUsingServiceAccountCredentialThrowJsonResponseException()
100
+ throws NoSuchFieldException, IllegalAccessException, GeneralSecurityException, IOException
101
+ {
102
+ GcsAuthentication auth = new GcsAuthentication(
103
+ "private_key",
104
+ GCP_EMAIL,
105
+ GCP_P12_KEYFILE,
106
+ null,
107
+ GCP_APPLICATION_NAME
108
+ );
109
+
110
+ Storage client = auth.getGcsClient("non-exists-bucket");
111
+
112
+ assertEquals(Storage.class, client.getClass());
113
+ }
114
+
115
+ @Test
116
+ public void testGetServiceAccountCredentialFromJsonFileSuccess()
117
+ throws NoSuchFieldException, IllegalAccessException, GeneralSecurityException, IOException
118
+ {
119
+ GcsAuthentication auth = new GcsAuthentication(
120
+ "json_key",
121
+ GCP_EMAIL,
122
+ null,
123
+ GCP_JSON_KEYFILE,
124
+ GCP_APPLICATION_NAME
125
+ );
126
+ Field field = GcsAuthentication.class.getDeclaredField("credentials");
127
+ field.setAccessible(true);
128
+
129
+ assertEquals(GoogleCredential.class, field.get(auth).getClass());
130
+ }
131
+
132
+ @Test(expected = FileNotFoundException.class)
133
+ public void testGetServiceAccountCredentialFromJsonThrowFileFileNotFoundException()
134
+ throws GeneralSecurityException, IOException
135
+ {
136
+ Optional<String> notFoundJsonKeyfile = Optional.of("/path/to/notfound.json");
137
+ GcsAuthentication auth = new GcsAuthentication(
138
+ "json_key",
139
+ GCP_EMAIL,
140
+ null,
141
+ notFoundJsonKeyfile,
142
+ GCP_APPLICATION_NAME
143
+ );
144
+ }
145
+
146
+ @Test
147
+ public void testGetServiceAccountCredentialFromJsonSuccess()
148
+ throws NoSuchFieldException, IllegalAccessException, GeneralSecurityException, IOException
149
+ {
150
+ GcsAuthentication auth = new GcsAuthentication(
151
+ "json_key",
152
+ GCP_EMAIL,
153
+ null,
154
+ GCP_JSON_KEYFILE,
155
+ GCP_APPLICATION_NAME
156
+ );
157
+
158
+ Storage client = auth.getGcsClient(GCP_BUCKET);
159
+
160
+ assertEquals(Storage.class, client.getClass());
161
+ }
162
+
163
+ @Test(expected = GoogleJsonResponseException.class)
164
+ public void testGetServiceAccountCredentialFromJsonThrowGoogleJsonResponseException()
165
+ throws NoSuchFieldException, IllegalAccessException, GeneralSecurityException, IOException
166
+ {
167
+ GcsAuthentication auth = new GcsAuthentication(
168
+ "json_key",
169
+ GCP_EMAIL,
170
+ null,
171
+ GCP_JSON_KEYFILE,
172
+ GCP_APPLICATION_NAME
173
+ );
174
+
175
+ Storage client = auth.getGcsClient("non-exists-bucket");
176
+ }
177
+ }
@@ -1,5 +1,426 @@
1
1
  package org.embulk.output;
2
2
 
3
+ import java.io.BufferedReader;
4
+ import java.io.ByteArrayOutputStream;
5
+ import java.io.FileInputStream;
6
+ import java.io.InputStream;
7
+ import java.io.InputStreamReader;
8
+ import java.util.Arrays;
9
+ import java.util.List;
10
+
11
+ import com.google.common.collect.ImmutableMap;
12
+ import com.google.common.base.Optional;
13
+ import com.google.common.collect.ImmutableList;
14
+ import com.google.common.collect.Lists;
15
+ import java.io.IOException;
16
+ import java.security.GeneralSecurityException;
17
+
18
+ import org.embulk.EmbulkTestRuntime;
19
+ import org.embulk.config.TaskReport;
20
+ import org.embulk.config.TaskSource;
21
+ import org.embulk.config.ConfigDiff;
22
+ import org.embulk.config.ConfigSource;
23
+ import org.embulk.config.ConfigException;
24
+ import org.embulk.spi.Buffer;
25
+ import org.embulk.spi.Exec;
26
+ import org.embulk.spi.FileOutputPlugin;
27
+ import org.embulk.spi.FileOutputRunner;
28
+ import org.embulk.spi.OutputPlugin;
29
+ import org.embulk.spi.TransactionalFileOutput;
30
+ import org.embulk.spi.Schema;
31
+ import org.embulk.output.GcsOutputPlugin.PluginTask;
32
+ import org.embulk.standards.CsvParserPlugin;
33
+
34
+ import org.junit.BeforeClass;
35
+ import org.junit.Before;
36
+ import org.junit.Rule;
37
+ import org.junit.Test;
38
+ import static org.junit.Assert.assertEquals;
39
+ import static org.junit.Assume.assumeNotNull;
40
+ import java.lang.reflect.Method;
41
+ import java.lang.reflect.InvocationTargetException;
42
+
43
+ import com.google.api.services.storage.Storage;
44
+
3
45
  public class TestGcsOutputPlugin
4
46
  {
47
+ private static Optional<String> GCP_EMAIL;
48
+ private static Optional<String> GCP_P12_KEYFILE;
49
+ private static Optional<String> GCP_JSON_KEYFILE;
50
+ private static String GCP_BUCKET;
51
+ private static String GCP_BUCKET_DIRECTORY;
52
+ private static String GCP_PATH_PREFIX;
53
+ private static String LOCAL_PATH_PREFIX;
54
+ private final String GCP_APPLICATION_NAME = "embulk-output-gcs";
55
+ private FileOutputRunner runner;
56
+
57
+ /*
58
+ * This test case requires environment variables
59
+ * GCP_EMAIL
60
+ * GCP_P12_KEYFILE
61
+ * GCP_JSON_KEYFILE
62
+ * GCP_BUCKET
63
+ */
64
+ @BeforeClass
65
+ public static void initializeConstant()
66
+ {
67
+ GCP_EMAIL = Optional.of(System.getenv("GCP_EMAIL"));
68
+ GCP_P12_KEYFILE = Optional.of(System.getenv("GCP_P12_KEYFILE"));
69
+ GCP_JSON_KEYFILE = Optional.of(System.getenv("GCP_JSON_KEYFILE"));
70
+ GCP_BUCKET = System.getenv("GCP_BUCKET");
71
+ // skip test cases, if environment variables are not set.
72
+ assumeNotNull(GCP_EMAIL, GCP_P12_KEYFILE, GCP_JSON_KEYFILE, GCP_BUCKET);
73
+
74
+ GCP_BUCKET_DIRECTORY = System.getenv("GCP_BUCKET_DIRECTORY") != null ? getDirectory(System.getenv("GCP_BUCKET_DIRECTORY")) : getDirectory("");
75
+ GCP_PATH_PREFIX = GCP_BUCKET_DIRECTORY + "sample_";
76
+ LOCAL_PATH_PREFIX = GcsOutputPlugin.class.getClassLoader().getResource("sample_01.csv").getPath();
77
+ }
78
+
79
+ @Rule
80
+ public EmbulkTestRuntime runtime = new EmbulkTestRuntime();
81
+ private GcsOutputPlugin plugin;
82
+
83
+ @Before
84
+ public void createResources() throws GeneralSecurityException, NoSuchMethodException, IOException
85
+ {
86
+ plugin = new GcsOutputPlugin();
87
+ runner = new FileOutputRunner(runtime.getInstance(GcsOutputPlugin.class));
88
+ }
89
+
90
+ @Test
91
+ public void checkDefaultValues()
92
+ {
93
+ ConfigSource config = Exec.newConfigSource()
94
+ .set("in", inputConfig())
95
+ .set("parser", parserConfig(schemaConfig()))
96
+ .set("type", "gcs")
97
+ .set("bucket", GCP_BUCKET)
98
+ .set("path_prefix", "my-prefix")
99
+ .set("file_ext", ".csv")
100
+ .set("formatter", formatterConfig());
101
+
102
+ GcsOutputPlugin.PluginTask task = config.loadConfig(PluginTask.class);
103
+ assertEquals("private_key", task.getAuthMethod().toString());
104
+ }
105
+
106
+ // p12_keyfile is null when auth_method is private_key
107
+ @Test(expected = ConfigException.class)
108
+ public void checkDefaultValuesP12keyNull()
109
+ {
110
+ ConfigSource config = Exec.newConfigSource()
111
+ .set("in", inputConfig())
112
+ .set("parser", parserConfig(schemaConfig()))
113
+ .set("type", "gcs")
114
+ .set("bucket", GCP_BUCKET)
115
+ .set("path_prefix", "my-prefix")
116
+ .set("file_ext", ".csv")
117
+ .set("auth_method", "private_key")
118
+ .set("service_account_email", GCP_EMAIL)
119
+ .set("p12_keyfile", null)
120
+ .set("formatter", formatterConfig());
121
+
122
+ Schema schema = config.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
123
+
124
+ runner.transaction(config, schema, 0, new Control());
125
+ }
126
+
127
+ // both p12_keyfile and p12_keyfile_path set
128
+ @Test(expected = ConfigException.class)
129
+ public void checkDefaultValuesConflictSetting()
130
+ {
131
+ ConfigSource config = Exec.newConfigSource()
132
+ .set("in", inputConfig())
133
+ .set("parser", parserConfig(schemaConfig()))
134
+ .set("type", "gcs")
135
+ .set("bucket", GCP_BUCKET)
136
+ .set("path_prefix", "my-prefix")
137
+ .set("file_ext", ".csv")
138
+ .set("auth_method", "private_key")
139
+ .set("service_account_email", GCP_EMAIL)
140
+ .set("p12_keyfile", GCP_P12_KEYFILE)
141
+ .set("p12_keyfile_path", GCP_P12_KEYFILE)
142
+ .set("formatter", formatterConfig());
143
+
144
+ Schema schema = config.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
145
+
146
+ runner.transaction(config, schema, 0, new Control());
147
+ }
148
+
149
+ // invalid p12keyfile when auth_method is private_key
150
+ @Test(expected = ConfigException.class)
151
+ public void checkDefaultValuesInvalidPrivateKey()
152
+ {
153
+ ConfigSource config = Exec.newConfigSource()
154
+ .set("in", inputConfig())
155
+ .set("parser", parserConfig(schemaConfig()))
156
+ .set("type", "gcs")
157
+ .set("bucket", GCP_BUCKET)
158
+ .set("path_prefix", "my-prefix")
159
+ .set("file_ext", ".csv")
160
+ .set("auth_method", "private_key")
161
+ .set("service_account_email", GCP_EMAIL)
162
+ .set("p12_keyfile", "invalid-key.p12")
163
+ .set("formatter", formatterConfig());
164
+
165
+ Schema schema = config.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
166
+
167
+ runner.transaction(config, schema, 0, new Control());
168
+ }
169
+
170
+ // json_keyfile is null when auth_method is json_key
171
+ @Test(expected = ConfigException.class)
172
+ public void checkDefaultValuesJsonKeyfileNull()
173
+ {
174
+ ConfigSource config = Exec.newConfigSource()
175
+ .set("in", inputConfig())
176
+ .set("parser", parserConfig(schemaConfig()))
177
+ .set("type", "gcs")
178
+ .set("bucket", GCP_BUCKET)
179
+ .set("path_prefix", "my-prefix")
180
+ .set("file_ext", ".csv")
181
+ .set("auth_method", "json_key")
182
+ .set("service_account_email", GCP_EMAIL)
183
+ .set("json_keyfile", null)
184
+ .set("formatter", formatterConfig());
185
+
186
+ Schema schema = config.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
187
+
188
+ runner.transaction(config, schema, 0, new Control());
189
+ }
190
+
191
+ @Test
192
+ public void testGcsClientCreateSuccessfully()
193
+ throws GeneralSecurityException, IOException, NoSuchMethodException,
194
+ IllegalAccessException, InvocationTargetException
195
+ {
196
+ ConfigSource configSource = config();
197
+ PluginTask task = configSource.loadConfig(PluginTask.class);
198
+ Schema schema = configSource.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
199
+ runner.transaction(configSource, schema, 0, new Control());
200
+
201
+ Method method = GcsOutputPlugin.class.getDeclaredMethod("createClient", PluginTask.class);
202
+ method.setAccessible(true);
203
+ method.invoke(plugin, task); // no errors happens
204
+ }
205
+
206
+ @Test(expected = ConfigException.class)
207
+ public void testGcsClientCreateThrowConfigException()
208
+ throws GeneralSecurityException, IOException, NoSuchMethodException,
209
+ IllegalAccessException, InvocationTargetException
210
+ {
211
+ ConfigSource config = Exec.newConfigSource()
212
+ .set("in", inputConfig())
213
+ .set("parser", parserConfig(schemaConfig()))
214
+ .set("type", "gcs")
215
+ .set("bucket", "non-exists-bucket")
216
+ .set("path_prefix", "my-prefix")
217
+ .set("file_ext", ".csv")
218
+ .set("auth_method", "json_key")
219
+ .set("service_account_email", GCP_EMAIL)
220
+ .set("json_keyfile", GCP_JSON_KEYFILE)
221
+ .set("formatter", formatterConfig());
222
+
223
+ PluginTask task = config.loadConfig(PluginTask.class);
224
+
225
+ Schema schema = config.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
226
+ runner.transaction(config, schema, 0, new Control());
227
+
228
+ Method method = GcsOutputPlugin.class.getDeclaredMethod("createClient", PluginTask.class);
229
+ method.setAccessible(true);
230
+ try {
231
+ method.invoke(plugin, task);
232
+ } catch (InvocationTargetException ex) {
233
+ throw (ConfigException) ex.getCause();
234
+ }
235
+ }
236
+
237
+ @Test
238
+ public void testResume()
239
+ {
240
+ PluginTask task = config().loadConfig(PluginTask.class);
241
+ plugin.resume(task.dump(), 0, new FileOutputPlugin.Control() // no errors happens
242
+ {
243
+ @Override
244
+ public List<TaskReport> run(TaskSource taskSource)
245
+ {
246
+ return Lists.newArrayList(Exec.newTaskReport());
247
+ }
248
+ });
249
+ }
250
+
251
+ @Test
252
+ public void testCleanup()
253
+ {
254
+ PluginTask task = config().loadConfig(PluginTask.class);
255
+ plugin.cleanup(task.dump(), 0, Lists.<TaskReport>newArrayList()); // no errors happens
256
+ }
257
+
258
+ @Test
259
+ public void testGcsFileOutputByOpen() throws Exception
260
+ {
261
+ ConfigSource configSource = config();
262
+ PluginTask task = configSource.loadConfig(PluginTask.class);
263
+ Schema schema = configSource.getNested("parser").loadConfig(CsvParserPlugin.PluginTask.class).getSchemaConfig().toSchema();
264
+ runner.transaction(configSource, schema, 0, new Control());
265
+
266
+ TransactionalFileOutput output = plugin.open(task.dump(), 0);
267
+
268
+ output.nextFile();
269
+
270
+ FileInputStream is = new FileInputStream(LOCAL_PATH_PREFIX);
271
+ byte[] bytes = convertInputStreamToByte(is);
272
+ Buffer buffer = Buffer.wrap(bytes);
273
+ output.add(buffer);
274
+
275
+ output.finish();
276
+ output.commit();
277
+
278
+ String remotePath = GCP_PATH_PREFIX + String.format(task.getSequenceFormat(), 0, 0) + task.getFileNameExtension();
279
+ assertRecords(remotePath);
280
+ }
281
+
282
+ public ConfigSource config()
283
+ {
284
+ return Exec.newConfigSource()
285
+ .set("in", inputConfig())
286
+ .set("parser", parserConfig(schemaConfig()))
287
+ .set("type", "gcs")
288
+ .set("bucket", GCP_BUCKET)
289
+ .set("path_prefix", GCP_PATH_PREFIX)
290
+ .set("last_path", "")
291
+ .set("file_ext", ".csv")
292
+ .set("auth_method", "private_key")
293
+ .set("service_account_email", GCP_EMAIL)
294
+ .set("p12_keyfile", GCP_P12_KEYFILE)
295
+ .set("json_keyfile", GCP_JSON_KEYFILE)
296
+ .set("application_name", GCP_APPLICATION_NAME)
297
+ .set("formatter", formatterConfig());
298
+ }
299
+
300
+ private class Control
301
+ implements OutputPlugin.Control
302
+ {
303
+ @Override
304
+ public List<TaskReport> run(TaskSource taskSource)
305
+ {
306
+ return Lists.newArrayList(Exec.newTaskReport());
307
+ }
308
+ }
309
+
310
+ private ImmutableMap<String, Object> inputConfig()
311
+ {
312
+ ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
313
+ builder.put("type", "file");
314
+ builder.put("path_prefix", LOCAL_PATH_PREFIX);
315
+ builder.put("last_path", "");
316
+ return builder.build();
317
+ }
318
+
319
+ private ImmutableMap<String, Object> parserConfig(ImmutableList<Object> schemaConfig)
320
+ {
321
+ ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
322
+ builder.put("type", "csv");
323
+ builder.put("newline", "CRLF");
324
+ builder.put("delimiter", ",");
325
+ builder.put("quote", "\"");
326
+ builder.put("escape", "\"");
327
+ builder.put("trim_if_not_quoted", false);
328
+ builder.put("skip_header_lines", 1);
329
+ builder.put("allow_extra_columns", false);
330
+ builder.put("allow_optional_columns", false);
331
+ builder.put("columns", schemaConfig);
332
+ return builder.build();
333
+ }
334
+
335
+ private ImmutableList<Object> schemaConfig()
336
+ {
337
+ ImmutableList.Builder<Object> builder = new ImmutableList.Builder<>();
338
+ builder.add(ImmutableMap.of("name", "id", "type", "long"));
339
+ builder.add(ImmutableMap.of("name", "account", "type", "long"));
340
+ builder.add(ImmutableMap.of("name", "time", "type", "timestamp", "format", "%Y-%m-%d %H:%M:%S"));
341
+ builder.add(ImmutableMap.of("name", "purchase", "type", "timestamp", "format", "%Y%m%d"));
342
+ builder.add(ImmutableMap.of("name", "comment", "type", "string"));
343
+ return builder.build();
344
+ }
345
+
346
+ private ImmutableMap<String, Object> formatterConfig()
347
+ {
348
+ ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
349
+ builder.put("type", "csv");
350
+ builder.put("header_line", "false");
351
+ builder.put("timezone", "Asia/Tokyo");
352
+ return builder.build();
353
+ }
354
+
355
+ private void assertRecords(String gcsPath) throws Exception
356
+ {
357
+ ImmutableList<List<String>> records = getFileContentsFromGcs(gcsPath);
358
+ assertEquals(5, records.size());
359
+ {
360
+ List<String> record = records.get(1);
361
+ assertEquals("1", record.get(0));
362
+ assertEquals("32864", record.get(1));
363
+ assertEquals("2015-01-27 19:23:49", record.get(2));
364
+ assertEquals("20150127", record.get(3));
365
+ assertEquals("embulk", record.get(4));
366
+ }
367
+
368
+ {
369
+ List<String> record = records.get(2);
370
+ assertEquals("2", record.get(0));
371
+ assertEquals("14824", record.get(1));
372
+ assertEquals("2015-01-27 19:01:23", record.get(2));
373
+ assertEquals("20150127", record.get(3));
374
+ assertEquals("embulk jruby", record.get(4));
375
+ }
376
+ }
377
+
378
+ private ImmutableList<List<String>> getFileContentsFromGcs(String path) throws Exception
379
+ {
380
+ ConfigSource config = config();
381
+
382
+ PluginTask task = config.loadConfig(PluginTask.class);
383
+
384
+ Method method = GcsOutputPlugin.class.getDeclaredMethod("createClient", PluginTask.class);
385
+ method.setAccessible(true);
386
+ Storage client = (Storage) method.invoke(plugin, task);
387
+ Storage.Objects.Get getObject = client.objects().get(GCP_BUCKET, path);
388
+
389
+ ImmutableList.Builder<List<String>> builder = new ImmutableList.Builder<>();
390
+
391
+ InputStream is = getObject.executeMediaAsInputStream();
392
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
393
+ String line;
394
+ while ((line = reader.readLine()) != null) {
395
+ List<String> records = Arrays.asList(line.split(",", 0));
396
+
397
+ builder.add(records);
398
+ }
399
+ return builder.build();
400
+ }
401
+
402
+ private static String getDirectory(String dir)
403
+ {
404
+ if (dir != null && !dir.endsWith("/")) {
405
+ dir = dir + "/";
406
+ }
407
+ if (dir.startsWith("/")) {
408
+ dir = dir.replaceFirst("/", "");
409
+ }
410
+ return dir;
411
+ }
412
+
413
+ private byte[] convertInputStreamToByte(InputStream is) throws IOException
414
+ {
415
+ ByteArrayOutputStream bo = new ByteArrayOutputStream();
416
+ byte [] buffer = new byte[1024];
417
+ while(true) {
418
+ int len = is.read(buffer);
419
+ if(len < 0) {
420
+ break;
421
+ }
422
+ bo.write(buffer, 0, len);
423
+ }
424
+ return bo.toByteArray();
425
+ }
5
426
  }
Binary file
@@ -0,0 +1,5 @@
1
+ id,account,time,purchase,comment
2
+ 1,32864,2015-01-27 19:23:49,20150127,embulk
3
+ 2,14824,2015-01-27 19:01:23,20150127,embulk jruby
4
+ 3,27559,2015-01-28 02:20:02,20150128,"Embulk ""csv"" parser plugin"
5
+ 4,11270,2015-01-29 11:54:36,20150129,NULL
@@ -0,0 +1,5 @@
1
+ id,account,time,purchase,comment
2
+ 1,32864,2015-01-27 19:23:49,20150127,embulk
3
+ 2,14824,2015-01-27 19:01:23,20150127,embulk jruby
4
+ 3,27559,2015-01-28 02:20:02,20150128,"Embulk ""csv"" parser plugin"
5
+ 4,11270,2015-01-29 11:54:36,20150129,NULL
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embulk-output-gcs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuyuki Honda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-04 00:00:00.000000000 Z
11
+ date: 2015-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -46,6 +46,7 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - .gitignore
49
+ - .travis.yml
49
50
  - LICENSE.txt
50
51
  - README.md
51
52
  - build.gradle
@@ -61,10 +62,14 @@ files:
61
62
  - lib/embulk/output/gcs.rb
62
63
  - src/main/java/org/embulk/output/GcsAuthentication.java
63
64
  - src/main/java/org/embulk/output/GcsOutputPlugin.java
65
+ - src/test/java/org/embulk/output/TestGcsAuthentication.java
64
66
  - src/test/java/org/embulk/output/TestGcsOutputPlugin.java
67
+ - src/test/resources/keys.tar.enc
68
+ - src/test/resources/sample_01.csv
69
+ - src/test/resources/sample_02.csv
65
70
  - classpath/commons-codec-1.3.jar
66
71
  - classpath/commons-logging-1.1.1.jar
67
- - classpath/embulk-output-gcs-0.2.0.jar
72
+ - classpath/embulk-output-gcs-0.3.0.jar
68
73
  - classpath/google-api-client-1.19.1.jar
69
74
  - classpath/google-api-services-storage-v1-rev28-1.19.1.jar
70
75
  - classpath/google-http-client-1.19.0.jar
Binary file