embulk-output-gcs 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,30 +1,40 @@
1
1
  package org.embulk.output;
2
2
 
3
3
  import com.google.api.client.http.InputStreamContent;
4
+ import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64;
4
5
  import com.google.api.services.storage.Storage;
5
6
  import com.google.api.services.storage.model.StorageObject;
6
7
  import com.google.common.base.Function;
7
8
  import com.google.common.base.Optional;
8
9
  import com.google.common.base.Throwables;
9
- import org.embulk.config.TaskReport;
10
10
  import org.embulk.config.Config;
11
11
  import org.embulk.config.ConfigDefault;
12
12
  import org.embulk.config.ConfigDiff;
13
- import org.embulk.config.ConfigSource;
14
13
  import org.embulk.config.ConfigException;
14
+ import org.embulk.config.ConfigSource;
15
15
  import org.embulk.config.Task;
16
+ import org.embulk.config.TaskReport;
16
17
  import org.embulk.config.TaskSource;
17
18
  import org.embulk.spi.Buffer;
18
19
  import org.embulk.spi.Exec;
19
20
  import org.embulk.spi.FileOutputPlugin;
20
21
  import org.embulk.spi.TransactionalFileOutput;
21
22
  import org.embulk.spi.unit.LocalFile;
23
+ import org.embulk.spi.util.RetryExecutor.RetryGiveupException;
24
+ import org.embulk.spi.util.RetryExecutor.Retryable;
22
25
  import org.slf4j.Logger;
26
+ import static org.embulk.spi.util.RetryExecutor.retryExecutor;
23
27
 
28
+ import java.io.BufferedInputStream;
29
+ import java.io.BufferedOutputStream;
30
+ import java.io.File;
31
+ import java.io.FileInputStream;
32
+ import java.io.FileOutputStream;
24
33
  import java.io.IOException;
25
- import java.io.PipedInputStream;
26
- import java.io.PipedOutputStream;
34
+ import java.io.InterruptedIOException;
27
35
  import java.security.GeneralSecurityException;
36
+ import java.security.MessageDigest;
37
+ import java.security.NoSuchAlgorithmException;
28
38
  import java.util.ArrayList;
29
39
  import java.util.List;
30
40
  import java.util.concurrent.Callable;
@@ -33,278 +43,398 @@ import java.util.concurrent.ExecutorService;
33
43
  import java.util.concurrent.Executors;
34
44
  import java.util.concurrent.Future;
35
45
 
36
- public class GcsOutputPlugin implements FileOutputPlugin {
37
- private static final Logger logger = Exec.getLogger(GcsOutputPlugin.class);
38
-
39
- public interface PluginTask extends Task {
40
- @Config("bucket")
41
- String getBucket();
42
-
43
- @Config("path_prefix")
44
- String getPathPrefix();
45
-
46
- @Config("file_ext")
47
- String getFileNameExtension();
48
-
49
- @Config("sequence_format")
50
- @ConfigDefault("\".%03d.%02d\"")
51
- String getSequenceFormat();
52
-
53
- @Config("content_type")
54
- @ConfigDefault("\"application/octet-stream\"")
55
- String getContentType();
56
-
57
- @Config("auth_method")
58
- @ConfigDefault("\"private_key\"")
59
- AuthMethod getAuthMethod();
60
-
61
- @Config("service_account_email")
62
- @ConfigDefault("null")
63
- Optional<String> getServiceAccountEmail();
64
-
65
- // kept for backward compatibility
66
- @Config("p12_keyfile_path")
67
- @ConfigDefault("null")
68
- Optional<String> getP12KeyfilePath();
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
-
79
- @Config("application_name")
80
- @ConfigDefault("\"embulk-output-gcs\"")
81
- String getApplicationName();
82
- }
83
-
84
- @Override
85
- public ConfigDiff transaction(ConfigSource config,
86
- int taskCount,
87
- FileOutputPlugin.Control control) {
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
-
111
- return resume(task.dump(), taskCount, control);
112
- }
113
-
114
- @Override
115
- public ConfigDiff resume(TaskSource taskSource,
116
- int taskCount,
117
- FileOutputPlugin.Control control) {
118
- control.run(taskSource);
119
- return Exec.newConfigDiff();
120
- }
121
-
122
- @Override
123
- public void cleanup(TaskSource taskSource,
124
- int taskCount,
125
- List<TaskReport> successTaskReports) {
126
- }
127
-
128
- @Override
129
- public TransactionalFileOutput open(TaskSource taskSource, final int taskIndex) {
130
- PluginTask task = taskSource.loadTask(PluginTask.class);
131
-
132
- Storage client = createClient(task);
133
- return new TransactionalGcsFileOutput(task, client, taskIndex);
134
- }
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
-
151
- private Storage createClient(final PluginTask task) {
152
- Storage client = null;
153
- try {
154
- GcsAuthentication auth = newGcsAuth(task);
155
- client = auth.getGcsClient(task.getBucket());
156
- } catch (ConfigException | IOException ex) {
157
- throw new ConfigException(ex);
158
- }
159
-
160
- return client;
161
- }
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
-
173
- static class TransactionalGcsFileOutput implements TransactionalFileOutput {
174
- private final int taskIndex;
175
- private final Storage client;
176
- private final String bucket;
177
- private final String pathPrefix;
178
- private final String pathSuffix;
179
- private final String sequenceFormat;
180
- private final String contentType;
181
- private final List<StorageObject> storageObjects = new ArrayList<>();
182
-
183
- private int fileIndex = 0;
184
- private int callCount = 0;
185
- private PipedOutputStream currentStream = null;
186
- private Future<StorageObject> currentUpload = null;
187
-
188
- TransactionalGcsFileOutput(PluginTask task, Storage client, int taskIndex) {
189
- this.taskIndex = taskIndex;
190
- this.client = client;
191
- this.bucket = task.getBucket();
192
- this.pathPrefix = task.getPathPrefix();
193
- this.pathSuffix = task.getFileNameExtension();
194
- this.sequenceFormat = task.getSequenceFormat();
195
- this.contentType = task.getContentType();
196
- }
197
-
198
- public void nextFile() {
199
- closeCurrentUpload();
200
- currentStream = new PipedOutputStream();
201
- String path = pathPrefix + String.format(sequenceFormat, taskIndex, fileIndex) + pathSuffix;
202
- logger.info("Uploading '{}/{}'", bucket, path);
203
- currentUpload = startUpload(path, contentType, currentStream);
204
- fileIndex++;
205
- }
206
-
207
- @Override
208
- public void add(Buffer buffer) {
209
- try {
210
- logger.debug("#add called {} times for taskIndex {}", callCount, taskIndex);
211
- currentStream.write(buffer.array(), buffer.offset(), buffer.limit());
212
- callCount++;
213
- } catch (IOException ex) {
214
- throw new RuntimeException(ex);
215
- } finally {
216
- buffer.release();
217
- }
218
- }
219
-
220
- @Override
221
- public void finish() {
222
- closeCurrentUpload();
223
- }
224
-
225
- @Override
226
- public void close() {
227
- closeCurrentUpload();
228
- }
229
-
230
- @Override
231
- public void abort() {
232
- }
233
-
234
- @Override
235
- public TaskReport commit() {
236
- TaskReport report = Exec.newTaskReport();
237
- report.set("files", storageObjects);
238
- return report;
239
- }
240
-
241
- private void closeCurrentUpload() {
242
- try {
243
- if (currentStream != null) {
244
- currentStream.close();
245
- currentStream = null;
246
- }
247
-
248
- if (currentUpload != null) {
249
- StorageObject obj = currentUpload.get();
250
- storageObjects.add(obj);
251
- logger.info("Uploaded '{}/{}' to {}bytes", obj.getBucket(), obj.getName(), obj.getSize());
252
- currentUpload = null;
253
- }
254
-
255
- callCount = 0;
256
- } catch (InterruptedException | ExecutionException | IOException ex) {
257
- throw Throwables.propagate(ex);
258
- }
259
- }
260
-
261
- private Future<StorageObject> startUpload(String path, String contentType, PipedOutputStream output) {
262
- try {
263
- final ExecutorService executor = Executors.newCachedThreadPool();
264
-
265
- PipedInputStream inputStream = new PipedInputStream(output);
266
- InputStreamContent mediaContent = new InputStreamContent(contentType, inputStream);
267
- mediaContent.setCloseInputStream(true);
268
-
269
- StorageObject objectMetadata = new StorageObject();
270
- objectMetadata.setName(path);
271
-
272
- final Storage.Objects.Insert insert = client.objects().insert(bucket, objectMetadata, mediaContent);
273
- insert.setDisableGZipContent(true);
274
- return executor.submit(new Callable<StorageObject>() {
275
- @Override
276
- public StorageObject call() throws InterruptedException {
277
- try {
278
- return insert.execute();
279
- } catch (IOException ex) {
280
- throw Throwables.propagate(ex);
281
- } finally {
282
- executor.shutdown();
283
- }
284
- }
285
- });
286
- } catch (IOException ex) {
287
- throw Throwables.propagate(ex);
288
- }
289
- }
290
- }
291
-
292
- public enum AuthMethod
293
- {
294
- private_key("private_key"),
295
- compute_engine("compute_engine"),
296
- json_key("json_key");
297
-
298
- private final String string;
299
-
300
- AuthMethod(String string)
301
- {
302
- this.string = string;
303
- }
304
-
305
- public String getString()
306
- {
307
- return string;
308
- }
309
- }
46
+ public class GcsOutputPlugin implements FileOutputPlugin
47
+ {
48
+ private static final Logger logger = Exec.getLogger(GcsOutputPlugin.class);
49
+
50
+ public interface PluginTask extends Task
51
+ {
52
+ @Config("bucket")
53
+ String getBucket();
54
+
55
+ @Config("path_prefix")
56
+ String getPathPrefix();
57
+
58
+ @Config("file_ext")
59
+ String getFileNameExtension();
60
+
61
+ @Config("sequence_format")
62
+ @ConfigDefault("\".%03d.%02d\"")
63
+ String getSequenceFormat();
64
+
65
+ @Config("content_type")
66
+ @ConfigDefault("\"application/octet-stream\"")
67
+ String getContentType();
68
+
69
+ @Config("auth_method")
70
+ @ConfigDefault("\"private_key\"")
71
+ AuthMethod getAuthMethod();
72
+
73
+ @Config("service_account_email")
74
+ @ConfigDefault("null")
75
+ Optional<String> getServiceAccountEmail();
76
+
77
+ // kept for backward compatibility
78
+ @Config("p12_keyfile_path")
79
+ @ConfigDefault("null")
80
+ Optional<String> getP12KeyfilePath();
81
+
82
+ @Config("p12_keyfile")
83
+ @ConfigDefault("null")
84
+ Optional<LocalFile> getP12Keyfile();
85
+ void setP12Keyfile(Optional<LocalFile> p12Keyfile);
86
+
87
+ @Config("json_keyfile")
88
+ @ConfigDefault("null")
89
+ Optional<LocalFile> getJsonKeyfile();
90
+
91
+ @Config("application_name")
92
+ @ConfigDefault("\"embulk-output-gcs\"")
93
+ String getApplicationName();
94
+
95
+ @Config("max_connection_retry")
96
+ @ConfigDefault("10") // 10 times retry to connect GCS server if failed.
97
+ int getMaxConnectionRetry();
98
+ }
99
+
100
+ @Override
101
+ public ConfigDiff transaction(ConfigSource config,
102
+ int taskCount,
103
+ FileOutputPlugin.Control control)
104
+ {
105
+ PluginTask task = config.loadConfig(PluginTask.class);
106
+
107
+ if (task.getP12KeyfilePath().isPresent()) {
108
+ if (task.getP12Keyfile().isPresent()) {
109
+ throw new ConfigException("Setting both p12_keyfile_path and p12_keyfile is invalid");
110
+ }
111
+ try {
112
+ task.setP12Keyfile(Optional.of(LocalFile.of(task.getP12KeyfilePath().get())));
113
+ }
114
+ catch (IOException ex) {
115
+ throw Throwables.propagate(ex);
116
+ }
117
+ }
118
+
119
+ if (task.getAuthMethod().getString().equals("json_key")) {
120
+ if (!task.getJsonKeyfile().isPresent()) {
121
+ throw new ConfigException("If auth_method is json_key, you have to set json_keyfile");
122
+ }
123
+ }
124
+ else if (task.getAuthMethod().getString().equals("private_key")) {
125
+ if (!task.getP12Keyfile().isPresent() || !task.getServiceAccountEmail().isPresent()) {
126
+ throw new ConfigException("If auth_method is private_key, you have to set both service_account_email and p12_keyfile");
127
+ }
128
+ }
129
+
130
+ return resume(task.dump(), taskCount, control);
131
+ }
132
+
133
+ @Override
134
+ public ConfigDiff resume(TaskSource taskSource,
135
+ int taskCount,
136
+ FileOutputPlugin.Control control)
137
+ {
138
+ control.run(taskSource);
139
+ return Exec.newConfigDiff();
140
+ }
141
+
142
+ @Override
143
+ public void cleanup(TaskSource taskSource,
144
+ int taskCount,
145
+ List<TaskReport> successTaskReports)
146
+ {
147
+ }
148
+
149
+ @Override
150
+ public TransactionalFileOutput open(TaskSource taskSource, final int taskIndex)
151
+ {
152
+ PluginTask task = taskSource.loadTask(PluginTask.class);
153
+
154
+ Storage client = createClient(task);
155
+ return new TransactionalGcsFileOutput(task, client, taskIndex);
156
+ }
157
+
158
+ private GcsAuthentication newGcsAuth(PluginTask task)
159
+ {
160
+ try {
161
+ return new GcsAuthentication(
162
+ task.getAuthMethod().getString(),
163
+ task.getServiceAccountEmail(),
164
+ task.getP12Keyfile().transform(localFileToPathString()),
165
+ task.getJsonKeyfile().transform(localFileToPathString()),
166
+ task.getApplicationName()
167
+ );
168
+ }
169
+ catch (GeneralSecurityException | IOException ex) {
170
+ throw new ConfigException(ex);
171
+ }
172
+ }
173
+
174
+ private Storage createClient(final PluginTask task)
175
+ {
176
+ try {
177
+ GcsAuthentication auth = newGcsAuth(task);
178
+ return auth.getGcsClient(task.getBucket(), task.getMaxConnectionRetry());
179
+ }
180
+ catch (ConfigException | IOException ex) {
181
+ throw Throwables.propagate(ex);
182
+ }
183
+ }
184
+
185
+ private Function<LocalFile, String> localFileToPathString()
186
+ {
187
+ return new Function<LocalFile, String>()
188
+ {
189
+ public String apply(LocalFile file)
190
+ {
191
+ return file.getPath().toString();
192
+ }
193
+ };
194
+ }
195
+
196
+ static class TransactionalGcsFileOutput implements TransactionalFileOutput
197
+ {
198
+ private final int taskIndex;
199
+ private final Storage client;
200
+ private final String bucket;
201
+ private final String pathPrefix;
202
+ private final String pathSuffix;
203
+ private final String sequenceFormat;
204
+ private final String contentType;
205
+ private final int maxConnectionRetry;
206
+ private final List<StorageObject> storageObjects = new ArrayList<>();
207
+
208
+ private int fileIndex = 0;
209
+ private int callCount = 0;
210
+ private BufferedOutputStream currentStream = null;
211
+ private Future<StorageObject> currentUpload = null;
212
+ private File tempFile = null;
213
+
214
+ TransactionalGcsFileOutput(PluginTask task, Storage client, int taskIndex)
215
+ {
216
+ this.taskIndex = taskIndex;
217
+ this.client = client;
218
+ this.bucket = task.getBucket();
219
+ this.pathPrefix = task.getPathPrefix();
220
+ this.pathSuffix = task.getFileNameExtension();
221
+ this.sequenceFormat = task.getSequenceFormat();
222
+ this.contentType = task.getContentType();
223
+ this.maxConnectionRetry = task.getMaxConnectionRetry();
224
+ }
225
+
226
+ public void nextFile()
227
+ {
228
+ closeCurrentUpload();
229
+ try {
230
+ tempFile = Exec.getTempFileSpace().createTempFile();
231
+ currentStream = new BufferedOutputStream(new FileOutputStream(tempFile));
232
+ fileIndex++;
233
+ }
234
+ catch (IOException ex) {
235
+ Throwables.propagate(ex);
236
+ }
237
+ }
238
+
239
+ @Override
240
+ public void add(Buffer buffer)
241
+ {
242
+ try {
243
+ logger.debug("#add called {} times for taskIndex {}", callCount, taskIndex);
244
+ currentStream.write(buffer.array(), buffer.offset(), buffer.limit());
245
+ callCount++;
246
+ }
247
+ catch (IOException ex) {
248
+ throw new RuntimeException(ex);
249
+ }
250
+ finally {
251
+ buffer.release();
252
+ }
253
+ }
254
+
255
+ @Override
256
+ public void finish()
257
+ {
258
+ String path = pathPrefix + String.format(sequenceFormat, taskIndex, fileIndex) + pathSuffix;
259
+ close();
260
+ if (tempFile != null) {
261
+ currentUpload = startUpload(path);
262
+ }
263
+
264
+ closeCurrentUpload();
265
+ }
266
+
267
+ @Override
268
+ public void close()
269
+ {
270
+ try {
271
+ if (currentStream != null) {
272
+ currentStream.close();
273
+ currentStream = null;
274
+ }
275
+ }
276
+ catch (IOException ex) {
277
+ throw Throwables.propagate(ex);
278
+ }
279
+ }
280
+
281
+ @Override
282
+ public void abort()
283
+ {
284
+ }
285
+
286
+ @Override
287
+ public TaskReport commit()
288
+ {
289
+ TaskReport report = Exec.newTaskReport();
290
+ report.set("files", storageObjects);
291
+ return report;
292
+ }
293
+
294
+ private void closeCurrentUpload()
295
+ {
296
+ try {
297
+ if (currentUpload != null) {
298
+ StorageObject obj = currentUpload.get();
299
+ storageObjects.add(obj);
300
+ logger.info("Uploaded '{}/{}' to {}bytes", obj.getBucket(), obj.getName(), obj.getSize());
301
+ currentUpload = null;
302
+ }
303
+
304
+ callCount = 0;
305
+ }
306
+ catch (InterruptedException | ExecutionException ex) {
307
+ throw Throwables.propagate(ex);
308
+ }
309
+ }
310
+
311
+ private Future<StorageObject> startUpload(final String path)
312
+ {
313
+ try {
314
+ final ExecutorService executor = Executors.newCachedThreadPool();
315
+ final String hash = getLocalMd5hash(tempFile.getAbsolutePath());
316
+
317
+ return executor.submit(new Callable<StorageObject>() {
318
+ @Override
319
+ public StorageObject call() throws IOException
320
+ {
321
+ try {
322
+ logger.info("Uploading '{}/{}'", bucket, path);
323
+ return execUploadWithRetry(path, hash);
324
+ }
325
+ finally {
326
+ executor.shutdown();
327
+ }
328
+ }
329
+ });
330
+ }
331
+ catch (IOException ex) {
332
+ throw Throwables.propagate(ex);
333
+ }
334
+ }
335
+
336
+ private StorageObject execUploadWithRetry(final String path, final String localHash) throws IOException
337
+ {
338
+ try {
339
+ return retryExecutor()
340
+ .withRetryLimit(maxConnectionRetry)
341
+ .withInitialRetryWait(500)
342
+ .withMaxRetryWait(30 * 1000)
343
+ .runInterruptible(new Retryable<StorageObject>() {
344
+ @Override
345
+ public StorageObject call() throws IOException, RetryGiveupException
346
+ {
347
+ try (final BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(tempFile))) {
348
+ InputStreamContent mediaContent = new InputStreamContent(contentType, inputStream);
349
+ mediaContent.setCloseInputStream(true);
350
+
351
+ StorageObject objectMetadata = new StorageObject();
352
+ objectMetadata.setName(path);
353
+
354
+ final Storage.Objects.Insert insert = client.objects().insert(bucket, objectMetadata, mediaContent);
355
+ insert.setDisableGZipContent(true);
356
+ StorageObject obj = insert.execute();
357
+
358
+ logger.info(String.format("Local Hash(MD5): %s / Remote Hash(MD5): %s", localHash, obj.getMd5Hash()));
359
+ return obj;
360
+ }
361
+ }
362
+
363
+ @Override
364
+ public boolean isRetryableException(Exception exception)
365
+ {
366
+ return true;
367
+ }
368
+
369
+ @Override
370
+ public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait) throws RetryGiveupException
371
+ {
372
+ String message = String.format("GCS put request failed. Retrying %d/%d after %d seconds. Message: %s: %s",
373
+ retryCount, retryLimit, retryWait / 1000, exception.getClass(), exception.getMessage());
374
+ if (retryCount % 3 == 0) {
375
+ logger.warn(message, exception);
376
+ }
377
+ else {
378
+ logger.warn(message);
379
+ }
380
+ }
381
+
382
+ @Override
383
+ public void onGiveup(Exception firstException, Exception lastException) throws RetryGiveupException
384
+ {
385
+ }
386
+ });
387
+ }
388
+ catch (RetryGiveupException ex) {
389
+ throw Throwables.propagate(ex.getCause());
390
+ }
391
+ catch (InterruptedException ex) {
392
+ throw new InterruptedIOException();
393
+ }
394
+ }
395
+
396
+ /*
397
+ MD5 hash sum on GCS bucket is encoded with base64.
398
+ You can get same hash with following commands.
399
+ $ openssl dgst -md5 -binary /path/to/file.txt | openssl enc -base64
400
+ or
401
+ $ gsutil hash -m /path/to/file.txt
402
+ */
403
+ private String getLocalMd5hash(String filePath) throws IOException
404
+ {
405
+ try {
406
+ MessageDigest md = MessageDigest.getInstance("MD5");
407
+ try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(new File(filePath)))) {
408
+ byte[] buffer = new byte[256];
409
+ int len;
410
+ while ((len = input.read(buffer, 0, buffer.length)) >= 0) {
411
+ md.update(buffer, 0, len);
412
+ }
413
+ return new String(Base64.encodeBase64(md.digest()));
414
+ }
415
+ }
416
+ catch (NoSuchAlgorithmException ex) {
417
+ throw new ConfigException("MD5 algorism not found");
418
+ }
419
+ }
420
+ }
421
+
422
+ public enum AuthMethod
423
+ {
424
+ private_key("private_key"),
425
+ compute_engine("compute_engine"),
426
+ json_key("json_key");
427
+
428
+ private final String string;
429
+
430
+ AuthMethod(String string)
431
+ {
432
+ this.string = string;
433
+ }
434
+
435
+ public String getString()
436
+ {
437
+ return string;
438
+ }
439
+ }
310
440
  }