embulk-input-dynamodb 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 +4 -4
- data/.github/workflows/master.yml +34 -0
- data/.github/workflows/test.yml +30 -0
- data/.scalafmt.conf +5 -0
- data/CHANGELOG.md +49 -0
- data/README.md +204 -54
- data/build.gradle +53 -44
- data/example/config-deprecated.yml +20 -0
- data/example/config-query-as-json.yml +18 -0
- data/example/config-query.yml +22 -0
- data/example/config-scan.yml +18 -0
- data/example/prepare_dynamodb_table.sh +67 -0
- data/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +1 -2
- data/gradlew +67 -48
- data/gradlew.bat +20 -10
- data/{test/run_dynamodb_local.sh → run_dynamodb_local.sh} +2 -1
- data/settings.gradle +1 -0
- data/src/main/scala/org/embulk/input/dynamodb/DeprecatedDynamodbInputPlugin.scala +73 -0
- data/src/main/scala/org/embulk/input/dynamodb/DynamodbInputPlugin.scala +76 -25
- data/src/main/scala/org/embulk/input/dynamodb/PluginTask.scala +132 -32
- data/src/main/scala/org/embulk/input/dynamodb/aws/Aws.scala +44 -0
- data/src/main/scala/org/embulk/input/dynamodb/aws/AwsClientConfiguration.scala +37 -0
- data/src/main/scala/org/embulk/input/dynamodb/aws/AwsCredentials.scala +240 -0
- data/src/main/scala/org/embulk/input/dynamodb/aws/AwsDynamodbConfiguration.scala +35 -0
- data/src/main/scala/org/embulk/input/dynamodb/aws/AwsEndpointConfiguration.scala +79 -0
- data/src/main/scala/org/embulk/input/dynamodb/aws/HttpProxy.scala +61 -0
- data/src/main/scala/org/embulk/input/dynamodb/deprecated/AttributeValueHelper.scala +72 -0
- data/src/main/scala/org/embulk/input/dynamodb/{Filter.scala → deprecated/Filter.scala} +3 -3
- data/src/main/scala/org/embulk/input/dynamodb/{FilterConfig.scala → deprecated/FilterConfig.scala} +13 -13
- data/src/main/scala/org/embulk/input/dynamodb/{ope → deprecated/ope}/AbstractOperation.scala +36 -18
- data/src/main/scala/org/embulk/input/dynamodb/{ope → deprecated/ope}/QueryOperation.scala +21 -13
- data/src/main/scala/org/embulk/input/dynamodb/{ope → deprecated/ope}/ScanOperation.scala +20 -13
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbAttributeValue.scala +154 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbAttributeValueEmbulkTypeTransformable.scala +245 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbAttributeValueType.scala +33 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbItemColumnVisitor.scala +50 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbItemConsumer.scala +40 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbItemIterator.scala +19 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbItemReader.scala +64 -0
- data/src/main/scala/org/embulk/input/dynamodb/item/DynamodbItemSchema.scala +135 -0
- data/src/main/scala/org/embulk/input/dynamodb/operation/AbstractDynamodbOperation.scala +169 -0
- data/src/main/scala/org/embulk/input/dynamodb/operation/DynamodbOperationProxy.scala +59 -0
- data/src/main/scala/org/embulk/input/dynamodb/operation/DynamodbQueryOperation.scala +72 -0
- data/src/main/scala/org/embulk/input/dynamodb/operation/DynamodbScanOperation.scala +93 -0
- data/src/main/scala/org/embulk/input/dynamodb/operation/EmbulkDynamodbOperation.scala +15 -0
- data/src/main/scala/org/embulk/input/dynamodb/package.scala +4 -9
- data/src/test/scala/org/embulk/input/dynamodb/AttributeValueHelperTest.scala +245 -101
- data/src/test/scala/org/embulk/input/dynamodb/AwsCredentialsTest.scala +150 -97
- data/src/test/scala/org/embulk/input/dynamodb/DynamodbQueryOperationTest.scala +188 -0
- data/src/test/scala/org/embulk/input/dynamodb/DynamodbScanOperationTest.scala +181 -0
- data/src/test/scala/org/embulk/input/dynamodb/testutil/EmbulkTestBase.scala +85 -0
- metadata +73 -49
- data/circle.yml +0 -16
- data/config/checkstyle/checkstyle.xml +0 -128
- data/config/checkstyle/default.xml +0 -108
- data/src/main/scala/org/embulk/input/dynamodb/AttributeValueHelper.scala +0 -41
- data/src/main/scala/org/embulk/input/dynamodb/AwsCredentials.scala +0 -63
- data/src/main/scala/org/embulk/input/dynamodb/DynamoDBClient.scala +0 -23
- data/src/test/resources/yaml/authMethodBasic.yml +0 -21
- data/src/test/resources/yaml/authMethodBasic_Error.yml +0 -19
- data/src/test/resources/yaml/authMethodEnv.yml +0 -19
- data/src/test/resources/yaml/authMethodProfile.yml +0 -20
- data/src/test/resources/yaml/dynamodb-local-query.yml +0 -25
- data/src/test/resources/yaml/dynamodb-local-scan.yml +0 -23
- data/src/test/resources/yaml/notSetAuthMethod.yml +0 -20
- data/src/test/scala/org/embulk/input/dynamodb/ope/QueryOperationTest.scala +0 -83
- data/src/test/scala/org/embulk/input/dynamodb/ope/ScanOperationTest.scala +0 -83
- data/test/create_table.sh +0 -16
- data/test/put_items.sh +0 -25
@@ -2,46 +2,97 @@ package org.embulk.input.dynamodb
|
|
2
2
|
|
3
3
|
import java.util.{List => JList}
|
4
4
|
|
5
|
-
import
|
6
|
-
import org.embulk.
|
7
|
-
import org.embulk.input.dynamodb.
|
8
|
-
import org.embulk.
|
5
|
+
import org.embulk.config.{ConfigDiff, ConfigSource, TaskReport, TaskSource}
|
6
|
+
import org.embulk.input.dynamodb.aws.Aws
|
7
|
+
import org.embulk.input.dynamodb.item.DynamodbItemSchema
|
8
|
+
import org.embulk.input.dynamodb.operation.DynamodbOperationProxy
|
9
|
+
import org.embulk.spi.{Exec, InputPlugin, PageBuilder, PageOutput, Schema}
|
9
10
|
|
10
11
|
class DynamodbInputPlugin extends InputPlugin {
|
11
|
-
def transaction(config: ConfigSource, control: InputPlugin.Control): ConfigDiff = {
|
12
|
-
val task: PluginTask = config.loadConfig(classOf[PluginTask])
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
override def transaction(
|
14
|
+
config: ConfigSource,
|
15
|
+
control: InputPlugin.Control
|
16
|
+
): ConfigDiff = {
|
17
|
+
val task: PluginTask = PluginTask.load(config)
|
18
|
+
if (isDeprecatedOperationRequired(task))
|
19
|
+
return DeprecatedDynamodbInputPlugin.transaction(config, control)
|
16
20
|
|
17
|
-
|
18
|
-
|
21
|
+
val schema: Schema = DynamodbItemSchema(task).getEmbulkSchema
|
22
|
+
val taskCount: Int = DynamodbOperationProxy(task).getEmbulkTaskCount
|
19
23
|
|
20
|
-
|
21
|
-
control.run(taskSource, schema, taskCount)
|
24
|
+
control.run(task.dump(), schema, taskCount)
|
22
25
|
Exec.newConfigDiff()
|
23
26
|
}
|
24
27
|
|
25
|
-
def
|
26
|
-
|
28
|
+
override def resume(
|
29
|
+
taskSource: TaskSource,
|
30
|
+
schema: Schema,
|
31
|
+
taskCount: Int,
|
32
|
+
control: InputPlugin.Control
|
33
|
+
): ConfigDiff = {
|
34
|
+
val task: PluginTask = PluginTask.load(taskSource)
|
35
|
+
if (isDeprecatedOperationRequired(task))
|
36
|
+
return DeprecatedDynamodbInputPlugin.resume(
|
37
|
+
taskSource,
|
38
|
+
schema,
|
39
|
+
taskCount,
|
40
|
+
control
|
41
|
+
)
|
42
|
+
throw new UnsupportedOperationException
|
43
|
+
}
|
27
44
|
|
28
|
-
|
45
|
+
override def run(
|
46
|
+
taskSource: TaskSource,
|
47
|
+
schema: Schema,
|
48
|
+
taskIndex: Int,
|
49
|
+
output: PageOutput
|
50
|
+
): TaskReport = {
|
51
|
+
val task: PluginTask = PluginTask.load(taskSource)
|
52
|
+
if (isDeprecatedOperationRequired(task))
|
53
|
+
return DeprecatedDynamodbInputPlugin.run(
|
54
|
+
taskSource,
|
55
|
+
schema,
|
56
|
+
taskIndex,
|
57
|
+
output
|
58
|
+
)
|
29
59
|
|
30
|
-
val
|
31
|
-
case "scan" => new ScanOperation(client)
|
32
|
-
case "query" => new QueryOperation(client)
|
33
|
-
}
|
34
|
-
ope.execute(task, schema, output)
|
60
|
+
val pageBuilder = new PageBuilder(task.getBufferAllocator, schema, output)
|
35
61
|
|
62
|
+
Aws(task).withDynamodb { dynamodb =>
|
63
|
+
DynamodbOperationProxy(task).run(
|
64
|
+
dynamodb,
|
65
|
+
taskIndex,
|
66
|
+
DynamodbItemSchema(task).getItemsConsumer(pageBuilder)
|
67
|
+
)
|
68
|
+
}
|
69
|
+
pageBuilder.finish()
|
36
70
|
Exec.newTaskReport()
|
37
71
|
}
|
38
72
|
|
39
|
-
def cleanup(
|
40
|
-
|
73
|
+
override def cleanup(
|
74
|
+
taskSource: TaskSource,
|
75
|
+
schema: Schema,
|
76
|
+
taskCount: Int,
|
77
|
+
successTaskReports: JList[TaskReport]
|
78
|
+
): Unit = {
|
79
|
+
val task: PluginTask = PluginTask.load(taskSource)
|
80
|
+
if (isDeprecatedOperationRequired(task))
|
81
|
+
DeprecatedDynamodbInputPlugin.cleanup(
|
82
|
+
taskSource,
|
83
|
+
schema,
|
84
|
+
taskCount,
|
85
|
+
successTaskReports
|
86
|
+
)
|
41
87
|
}
|
42
88
|
|
43
|
-
def guess(config: ConfigSource): ConfigDiff = {
|
44
|
-
|
45
|
-
|
89
|
+
override def guess(config: ConfigSource): ConfigDiff = {
|
90
|
+
val task: PluginTask = PluginTask.load(config)
|
91
|
+
if (isDeprecatedOperationRequired(task))
|
92
|
+
return DeprecatedDynamodbInputPlugin.guess(config)
|
93
|
+
throw new UnsupportedOperationException
|
46
94
|
}
|
95
|
+
|
96
|
+
private def isDeprecatedOperationRequired(task: PluginTask): Boolean =
|
97
|
+
task.getOperation.isPresent
|
47
98
|
}
|
@@ -1,55 +1,71 @@
|
|
1
1
|
package org.embulk.input.dynamodb
|
2
2
|
|
3
|
-
import
|
4
|
-
import org.embulk.config.{Config, ConfigDefault, ConfigInject, Task}
|
5
|
-
import org.embulk.spi.{BufferAllocator, SchemaConfig}
|
3
|
+
import java.util.Optional
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def getProfileName: Optional[String]
|
5
|
+
import org.embulk.config.{
|
6
|
+
Config,
|
7
|
+
ConfigDefault,
|
8
|
+
ConfigException,
|
9
|
+
ConfigInject,
|
10
|
+
ConfigSource,
|
11
|
+
Task,
|
12
|
+
TaskSource
|
13
|
+
}
|
14
|
+
import org.embulk.input.dynamodb.aws.Aws
|
15
|
+
import org.embulk.input.dynamodb.deprecated.Filter
|
16
|
+
import org.embulk.input.dynamodb.item.DynamodbItemSchema
|
17
|
+
import org.embulk.input.dynamodb.operation.DynamodbOperationProxy
|
18
|
+
import org.embulk.spi.BufferAllocator
|
19
|
+
import zio.macros.annotation.delegate
|
23
20
|
|
24
|
-
|
25
|
-
@ConfigDefault("null")
|
26
|
-
def getRegion: Optional[String]
|
21
|
+
import scala.util.chaining._
|
27
22
|
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
trait PluginTask
|
24
|
+
extends Task
|
25
|
+
with Aws.Task
|
26
|
+
with DynamodbItemSchema.Task
|
27
|
+
with DynamodbOperationProxy.Task {
|
31
28
|
|
29
|
+
@deprecated(
|
30
|
+
message = "Use #getScan() or #getQuery() instead.",
|
31
|
+
since = "0.3.0"
|
32
|
+
)
|
32
33
|
@Config("operation")
|
33
|
-
|
34
|
+
@ConfigDefault("null")
|
35
|
+
def getOperation: Optional[String]
|
34
36
|
|
37
|
+
@deprecated(
|
38
|
+
message =
|
39
|
+
"Use DynamodbQueryOperation.Task#getBatchSize() or DynamodbScanOperation.Task#getBatchSize() instead.",
|
40
|
+
since = "0.3.0"
|
41
|
+
)
|
35
42
|
@Config("limit")
|
36
43
|
@ConfigDefault("0")
|
37
44
|
def getLimit: Long
|
38
45
|
|
46
|
+
@deprecated(
|
47
|
+
message =
|
48
|
+
"Use DynamodbQueryOperation.Task#getBatchSize() or DynamodbScanOperation.Task#getBatchSize() instead.",
|
49
|
+
since = "0.3.0"
|
50
|
+
)
|
39
51
|
@Config("scan_limit")
|
40
52
|
@ConfigDefault("0")
|
41
53
|
def getScanLimit: Long
|
42
54
|
|
55
|
+
@deprecated(
|
56
|
+
message =
|
57
|
+
"Use DynamodbQueryOperation.Task#getLimit() or DynamodbScanOperation.Task#getLimit() instead.",
|
58
|
+
since = "0.3.0"
|
59
|
+
)
|
43
60
|
@Config("record_limit")
|
44
61
|
@ConfigDefault("0")
|
45
62
|
def getRecordLimit: Long
|
46
63
|
|
47
|
-
@
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
@deprecated(
|
65
|
+
message =
|
66
|
+
"Use DynamodbQueryOperation.Task#getFilterExpression() or DynamodbScanOperation.Task#getFilterExpression() instead.",
|
67
|
+
since = "0.3.0"
|
68
|
+
)
|
53
69
|
@Config("filters")
|
54
70
|
@ConfigDefault("null")
|
55
71
|
def getFilters: Optional[Filter]
|
@@ -57,3 +73,87 @@ trait PluginTask extends Task {
|
|
57
73
|
@ConfigInject
|
58
74
|
def getBufferAllocator: BufferAllocator
|
59
75
|
}
|
76
|
+
|
77
|
+
case class PluginTaskCompat(@delegate task: PluginTask) extends PluginTask {
|
78
|
+
|
79
|
+
override def getOperation: Optional[String] = {
|
80
|
+
task.getOperation.ifPresent { op =>
|
81
|
+
logger.warn(
|
82
|
+
"[Deprecated] The option \"operation\" is deprecated. Use \"scan\" or \"query\" option instead."
|
83
|
+
)
|
84
|
+
|
85
|
+
op.toLowerCase match {
|
86
|
+
case "scan" | "query" => // do nothing
|
87
|
+
case x =>
|
88
|
+
throw new ConfigException(
|
89
|
+
s"Operation '$x' is unsupported. Available values are 'scan' or 'query'."
|
90
|
+
)
|
91
|
+
}
|
92
|
+
|
93
|
+
if (getScan.isPresent || getQuery.isPresent)
|
94
|
+
throw new ConfigException(
|
95
|
+
"The option \"operation\" must not be used together with either \"scan\" or \"query\" options."
|
96
|
+
)
|
97
|
+
}
|
98
|
+
task.getOperation
|
99
|
+
}
|
100
|
+
|
101
|
+
override def getLimit: Long = {
|
102
|
+
logger.warn(
|
103
|
+
"[Deprecated] The option \"limit\" is deprecated. Use \"query.batch_size\" or \"scan.batch_size\" instead."
|
104
|
+
)
|
105
|
+
task.getLimit
|
106
|
+
}
|
107
|
+
|
108
|
+
override def getScanLimit: Long = {
|
109
|
+
logger.warn(
|
110
|
+
"[Deprecated] The option \"scan_limit\" is deprecated. Use \"query.batch_size\" or \"scan.batch_size\" instead."
|
111
|
+
)
|
112
|
+
task.getScanLimit
|
113
|
+
}
|
114
|
+
|
115
|
+
override def getRecordLimit: Long = {
|
116
|
+
logger.warn(
|
117
|
+
"[Deprecated] The option \"record_limit\" is deprecated. Use \"query.limit\" or \"scan.limit\" instead."
|
118
|
+
)
|
119
|
+
task.getRecordLimit
|
120
|
+
}
|
121
|
+
|
122
|
+
override def getFilters: Optional[Filter] = {
|
123
|
+
logger.warn(
|
124
|
+
"[Deprecated] The option \"filters\" is deprecated. Use \"query.filter_expression\" or \"scan.filter_expression\" instead."
|
125
|
+
)
|
126
|
+
task.getFilters
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
object PluginTask {
|
131
|
+
|
132
|
+
def load(configSource: ConfigSource): PluginTask = {
|
133
|
+
configSource
|
134
|
+
.loadConfig(classOf[PluginTask])
|
135
|
+
.pipe(PluginTaskCompat)
|
136
|
+
.tap(configure)
|
137
|
+
}
|
138
|
+
|
139
|
+
def load(taskSource: TaskSource): PluginTask = {
|
140
|
+
taskSource
|
141
|
+
.loadTask(classOf[PluginTask])
|
142
|
+
.pipe(PluginTaskCompat)
|
143
|
+
.tap(configure)
|
144
|
+
}
|
145
|
+
|
146
|
+
private def configure(task: PluginTask): Unit = {
|
147
|
+
if (!task.getOperation.isPresent && !task.getScan.isPresent && !task.getQuery.isPresent) {
|
148
|
+
// NOTE: "operation" option is deprecated, so this is not shown the message.
|
149
|
+
throw new ConfigException(
|
150
|
+
"Either \"scan\" or \"query\" option is required."
|
151
|
+
)
|
152
|
+
}
|
153
|
+
if (task.getOperation.isPresent && (task.getScan.isPresent || task.getQuery.isPresent)) {
|
154
|
+
throw new ConfigException(
|
155
|
+
"[Deprecated] You must not use \"scan\" or \"query\" option with \"operation\" option."
|
156
|
+
)
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
package org.embulk.input.dynamodb.aws
|
2
|
+
|
3
|
+
import com.amazonaws.client.builder.AwsClientBuilder
|
4
|
+
import com.amazonaws.services.dynamodbv2.{
|
5
|
+
AmazonDynamoDB,
|
6
|
+
AmazonDynamoDBClientBuilder
|
7
|
+
}
|
8
|
+
|
9
|
+
object Aws {
|
10
|
+
|
11
|
+
trait Task
|
12
|
+
extends AwsCredentials.Task
|
13
|
+
with AwsEndpointConfiguration.Task
|
14
|
+
with AwsClientConfiguration.Task
|
15
|
+
with AwsDynamodbConfiguration.Task
|
16
|
+
|
17
|
+
def apply(task: Task): Aws = {
|
18
|
+
new Aws(task)
|
19
|
+
}
|
20
|
+
|
21
|
+
}
|
22
|
+
|
23
|
+
class Aws(task: Aws.Task) {
|
24
|
+
|
25
|
+
def withDynamodb[A](f: AmazonDynamoDB => A): A = {
|
26
|
+
val builder: AmazonDynamoDBClientBuilder =
|
27
|
+
AmazonDynamoDBClientBuilder.standard()
|
28
|
+
AwsDynamodbConfiguration(task).configureAmazonDynamoDBClientBuilder(builder)
|
29
|
+
val svc = createService(builder)
|
30
|
+
try f(svc)
|
31
|
+
finally svc.shutdown()
|
32
|
+
|
33
|
+
}
|
34
|
+
|
35
|
+
def createService[S <: AwsClientBuilder[S, T], T](
|
36
|
+
builder: AwsClientBuilder[S, T]
|
37
|
+
): T = {
|
38
|
+
AwsEndpointConfiguration(task).configureAwsClientBuilder(builder)
|
39
|
+
AwsClientConfiguration(task).configureAwsClientBuilder(builder)
|
40
|
+
builder.setCredentials(AwsCredentials(task).createAwsCredentialsProvider)
|
41
|
+
|
42
|
+
builder.build()
|
43
|
+
}
|
44
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
package org.embulk.input.dynamodb.aws
|
2
|
+
|
3
|
+
import java.util.Optional
|
4
|
+
|
5
|
+
import com.amazonaws.ClientConfiguration
|
6
|
+
import com.amazonaws.client.builder.AwsClientBuilder
|
7
|
+
import org.embulk.config.{Config, ConfigDefault}
|
8
|
+
import org.embulk.input.dynamodb.aws.AwsClientConfiguration.Task
|
9
|
+
|
10
|
+
object AwsClientConfiguration {
|
11
|
+
|
12
|
+
trait Task {
|
13
|
+
|
14
|
+
@Config("http_proxy")
|
15
|
+
@ConfigDefault("null")
|
16
|
+
def getHttpProxy: Optional[HttpProxy.Task]
|
17
|
+
|
18
|
+
}
|
19
|
+
|
20
|
+
def apply(task: Task): AwsClientConfiguration = {
|
21
|
+
new AwsClientConfiguration(task)
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
class AwsClientConfiguration(task: Task) {
|
26
|
+
|
27
|
+
def configureAwsClientBuilder[S <: AwsClientBuilder[S, T], T](
|
28
|
+
builder: AwsClientBuilder[S, T]
|
29
|
+
): Unit = {
|
30
|
+
task.getHttpProxy.ifPresent { v =>
|
31
|
+
val cc = new ClientConfiguration
|
32
|
+
HttpProxy(v).configureClientConfiguration(cc)
|
33
|
+
builder.setClientConfiguration(cc)
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
}
|
@@ -0,0 +1,240 @@
|
|
1
|
+
package org.embulk.input.dynamodb.aws
|
2
|
+
|
3
|
+
import java.util.Optional
|
4
|
+
|
5
|
+
import com.amazonaws.auth.{
|
6
|
+
AnonymousAWSCredentials,
|
7
|
+
AWSCredentialsProvider,
|
8
|
+
AWSStaticCredentialsProvider,
|
9
|
+
BasicAWSCredentials,
|
10
|
+
BasicSessionCredentials,
|
11
|
+
DefaultAWSCredentialsProviderChain,
|
12
|
+
EC2ContainerCredentialsProviderWrapper,
|
13
|
+
EnvironmentVariableCredentialsProvider,
|
14
|
+
STSAssumeRoleSessionCredentialsProvider,
|
15
|
+
SystemPropertiesCredentialsProvider,
|
16
|
+
WebIdentityTokenCredentialsProvider
|
17
|
+
}
|
18
|
+
import com.amazonaws.auth.profile.{
|
19
|
+
ProfileCredentialsProvider,
|
20
|
+
ProfilesConfigFile
|
21
|
+
}
|
22
|
+
import org.embulk.config.{Config, ConfigDefault, ConfigException}
|
23
|
+
import org.embulk.input.dynamodb.aws.AwsCredentials.Task
|
24
|
+
import org.embulk.input.dynamodb.logger
|
25
|
+
import org.embulk.spi.unit.LocalFile
|
26
|
+
import zio.macros.annotation.delegate
|
27
|
+
|
28
|
+
object AwsCredentials {
|
29
|
+
|
30
|
+
trait Task {
|
31
|
+
|
32
|
+
@Config("auth_method")
|
33
|
+
@ConfigDefault("\"default\"")
|
34
|
+
def getAuthMethod: String
|
35
|
+
|
36
|
+
@deprecated(message = "Use #getAccessKeyId() instead.", since = "0.3.0")
|
37
|
+
@Config("access_key")
|
38
|
+
@ConfigDefault("null")
|
39
|
+
def getAccessKey: Optional[String]
|
40
|
+
|
41
|
+
@Config("access_key_id")
|
42
|
+
@ConfigDefault("null")
|
43
|
+
def getAccessKeyId: Optional[String]
|
44
|
+
|
45
|
+
@deprecated(message = "Use #getSecretAccessKey() instead.", since = "0.3.0")
|
46
|
+
@Config("secret_key")
|
47
|
+
@ConfigDefault("null")
|
48
|
+
def getSecretKey: Optional[String]
|
49
|
+
|
50
|
+
@Config("secret_access_key")
|
51
|
+
@ConfigDefault("null")
|
52
|
+
def getSecretAccessKey: Optional[String]
|
53
|
+
|
54
|
+
@Config("session_token")
|
55
|
+
@ConfigDefault("null")
|
56
|
+
def getSessionToken: Optional[String]
|
57
|
+
|
58
|
+
@Config("profile_file")
|
59
|
+
@ConfigDefault("null")
|
60
|
+
def getProfileFile: Optional[LocalFile]
|
61
|
+
|
62
|
+
@Config("profile_name")
|
63
|
+
@ConfigDefault("\"default\"")
|
64
|
+
def getProfileName: String
|
65
|
+
|
66
|
+
@Config("role_arn")
|
67
|
+
@ConfigDefault("null")
|
68
|
+
def getRoleArn: Optional[String]
|
69
|
+
|
70
|
+
@Config("role_session_name")
|
71
|
+
@ConfigDefault("null")
|
72
|
+
def getRoleSessionName: Optional[String]
|
73
|
+
|
74
|
+
@Config("role_external_id")
|
75
|
+
@ConfigDefault("null")
|
76
|
+
def getRoleExternalId: Optional[String]
|
77
|
+
|
78
|
+
@Config("role_session_duration_seconds")
|
79
|
+
@ConfigDefault("null")
|
80
|
+
def getRoleSessionDurationSeconds: Optional[Int]
|
81
|
+
|
82
|
+
@Config("scope_down_policy")
|
83
|
+
@ConfigDefault("null")
|
84
|
+
def getScopeDownPolicy: Optional[String]
|
85
|
+
|
86
|
+
@Config("web_identity_token_file")
|
87
|
+
@ConfigDefault("null")
|
88
|
+
def getWebIdentityTokenFile: Optional[String]
|
89
|
+
}
|
90
|
+
|
91
|
+
def apply(task: Task): AwsCredentials = {
|
92
|
+
new AwsCredentials(AwsCredentialsTaskCompat(task))
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
case class AwsCredentialsTaskCompat(@delegate task: Task) extends Task {
|
97
|
+
|
98
|
+
override def getAccessKey: Optional[String] = {
|
99
|
+
throw new NotImplementedError()
|
100
|
+
}
|
101
|
+
|
102
|
+
override def getSecretKey: Optional[String] = {
|
103
|
+
throw new NotImplementedError()
|
104
|
+
}
|
105
|
+
|
106
|
+
override def getAuthMethod: String = {
|
107
|
+
if (getAccessKeyId.isPresent && getSecretAccessKey.isPresent) {
|
108
|
+
if (task.getAuthMethod != "basic") {
|
109
|
+
logger.warn(
|
110
|
+
"[Deprecated] The default value of \"auth_method\" option is \"default\", " +
|
111
|
+
"but currently use \"basic\" auth_method for backward compatibility " +
|
112
|
+
"because you set \"access_key_id\" and \"secret_access_key\" options. " +
|
113
|
+
"Please set \"basic\" to \"auth_method\" option expressly."
|
114
|
+
)
|
115
|
+
return "basic"
|
116
|
+
}
|
117
|
+
}
|
118
|
+
task.getAuthMethod
|
119
|
+
}
|
120
|
+
|
121
|
+
override def getAccessKeyId: Optional[String] = {
|
122
|
+
if (task.getAccessKeyId.isPresent && task.getAccessKey.isPresent)
|
123
|
+
throw new ConfigException(
|
124
|
+
"You cannot use both \"access_key_id\" option and \"access_key\" option. Use \"access_key_id\" option."
|
125
|
+
)
|
126
|
+
if (task.getAccessKey.isPresent) {
|
127
|
+
logger.warn(
|
128
|
+
"[Deprecated] \"access_key\" option is deprecated. Use \"access_key_id\" option instead."
|
129
|
+
)
|
130
|
+
return task.getAccessKey
|
131
|
+
}
|
132
|
+
task.getAccessKeyId
|
133
|
+
}
|
134
|
+
|
135
|
+
override def getSecretAccessKey: Optional[String] = {
|
136
|
+
if (task.getSecretAccessKey.isPresent && task.getSecretKey.isPresent)
|
137
|
+
throw new ConfigException(
|
138
|
+
"You cannot use both \"secret_access_key\" option and \"secret_key\" option. Use \"secret_access_key\" option."
|
139
|
+
)
|
140
|
+
if (task.getSecretKey.isPresent) {
|
141
|
+
logger.warn(
|
142
|
+
"[Deprecated] \"secret_key\" option is deprecated. Use \"secret_access_key\" option instead."
|
143
|
+
)
|
144
|
+
return task.getSecretKey
|
145
|
+
}
|
146
|
+
task.getSecretAccessKey
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
class AwsCredentials(task: Task) {
|
151
|
+
|
152
|
+
def createAwsCredentialsProvider: AWSCredentialsProvider = {
|
153
|
+
task.getAuthMethod match {
|
154
|
+
case "basic" =>
|
155
|
+
new AWSStaticCredentialsProvider(
|
156
|
+
new BasicAWSCredentials(
|
157
|
+
getRequiredOption(task.getAccessKeyId, "access_key_id"),
|
158
|
+
getRequiredOption(task.getSecretAccessKey, "secret_access_key")
|
159
|
+
)
|
160
|
+
)
|
161
|
+
|
162
|
+
case "env" =>
|
163
|
+
new EnvironmentVariableCredentialsProvider
|
164
|
+
|
165
|
+
case "instance" =>
|
166
|
+
// NOTE: combination of InstanceProfileCredentialsProvider and ContainerCredentialsProvider
|
167
|
+
new EC2ContainerCredentialsProviderWrapper
|
168
|
+
|
169
|
+
case "profile" =>
|
170
|
+
if (task.getProfileFile.isPresent) {
|
171
|
+
val pf: ProfilesConfigFile = new ProfilesConfigFile(
|
172
|
+
task.getProfileFile.get().getFile
|
173
|
+
)
|
174
|
+
new ProfileCredentialsProvider(pf, task.getProfileName)
|
175
|
+
}
|
176
|
+
else new ProfileCredentialsProvider(task.getProfileName)
|
177
|
+
|
178
|
+
case "properties" =>
|
179
|
+
new SystemPropertiesCredentialsProvider
|
180
|
+
|
181
|
+
case "anonymous" =>
|
182
|
+
new AWSStaticCredentialsProvider(new AnonymousAWSCredentials)
|
183
|
+
|
184
|
+
case "session" =>
|
185
|
+
new AWSStaticCredentialsProvider(
|
186
|
+
new BasicSessionCredentials(
|
187
|
+
getRequiredOption(task.getAccessKeyId, "access_key_id"),
|
188
|
+
getRequiredOption(task.getSecretAccessKey, "secret_access_key"),
|
189
|
+
getRequiredOption(task.getSessionToken, "session_token")
|
190
|
+
)
|
191
|
+
)
|
192
|
+
|
193
|
+
case "assume_role" =>
|
194
|
+
// NOTE: Are http_proxy, endpoint, region required when assuming role?
|
195
|
+
val builder = new STSAssumeRoleSessionCredentialsProvider.Builder(
|
196
|
+
getRequiredOption(task.getRoleArn, "role_arn"),
|
197
|
+
getRequiredOption(task.getRoleSessionName, "role_session_name")
|
198
|
+
)
|
199
|
+
task.getRoleExternalId.ifPresent(v => builder.withExternalId(v))
|
200
|
+
task.getRoleSessionDurationSeconds.ifPresent(v =>
|
201
|
+
builder.withRoleSessionDurationSeconds(v)
|
202
|
+
)
|
203
|
+
task.getScopeDownPolicy.ifPresent(v => builder.withScopeDownPolicy(v))
|
204
|
+
|
205
|
+
builder.build()
|
206
|
+
|
207
|
+
case "web_identity_token" =>
|
208
|
+
WebIdentityTokenCredentialsProvider
|
209
|
+
.builder()
|
210
|
+
.roleArn(getRequiredOption(task.getRoleArn, "role_arn"))
|
211
|
+
.roleSessionName(
|
212
|
+
getRequiredOption(task.getRoleSessionName, "role_session_name")
|
213
|
+
)
|
214
|
+
.webIdentityTokenFile(
|
215
|
+
getRequiredOption(
|
216
|
+
task.getWebIdentityTokenFile,
|
217
|
+
"web_identity_token_file"
|
218
|
+
)
|
219
|
+
)
|
220
|
+
.build()
|
221
|
+
|
222
|
+
case "default" =>
|
223
|
+
new DefaultAWSCredentialsProviderChain
|
224
|
+
|
225
|
+
case am =>
|
226
|
+
throw new ConfigException(
|
227
|
+
s"'$am' is unsupported: `auth_method` must be one of ['basic', 'env', 'instance', 'profile', 'properties', 'anonymous', 'session', 'assume_role', 'default']."
|
228
|
+
)
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
private def getRequiredOption[A](o: Optional[A], name: String): A = {
|
233
|
+
o.orElseThrow(() =>
|
234
|
+
new ConfigException(
|
235
|
+
s"`$name` must be set when `auth_method` is ${task.getAuthMethod}."
|
236
|
+
)
|
237
|
+
)
|
238
|
+
}
|
239
|
+
|
240
|
+
}
|