Skip to content

Commit 8968e2b

Browse files
author
Till Krullmann
committed
Initial commit
0 parents  commit 8968e2b

61 files changed

Lines changed: 3734 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.bat eol=crlf

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# IntelliJ IDEA
2+
.idea/
3+
4+
# Gradle
5+
.gradle/
6+
build/

.java-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.8

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Till Krullmann
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.adoc

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
:version: 0.1.0
2+
3+
ifdef::env-github[]
4+
:tip-caption: :bulb:
5+
:note-caption: :information_source:
6+
:important-caption: :heavy_exclamation_mark:
7+
:caution-caption: :fire:
8+
:warning-caption: :warning:
9+
10+
:toc-placement!:
11+
endif::[]
12+
13+
= AWS CodeArtifact Maven Proxy
14+
15+
This project contains a lightweight, embeddable proxy server for AWS CodeArtifact Maven repositories. It
16+
automatically handles endpoint lookups and CodeArtifact authorization tokens.
17+
18+
== Background
19+
20+
AWS CodeArtifact is a great, cost-efficient service for hosting private Maven repositories. However, its
21+
authentication mechanism with its temporary tokens, while certainly adding a degree of security, is often
22+
cumbersome to work with:
23+
24+
* Developers running a build from their local machine will have to install the AWS CLI and execute some
25+
commands to look up endpoints and retrieve authorization tokens.
26+
27+
* Access to the repositories is only actually needed for the initial build execution and when dependencies
28+
have changed. For the majority of builds, the required artifacts can be served from a local cache, making
29+
it unnecessary to even obtain an authorization token.
30+
31+
== How It Works
32+
33+
The proxy server is intended for _local_ use only. It acts as a virtual Maven repository server by forwarding
34+
URL paths that conform to the pattern
35+
36+
----
37+
/<domain>/<domain-owner>/<repo>/<group>/<artifact>/...
38+
----
39+
40+
to the appropriate AWS CodeArtifact repository endpoint for `domain`, `domain-owner` and `repo`.
41+
42+
TIP: The special value `default` can be used for the `<domain-owner>` to use the default AWS account ID based on the
43+
proxy server's AWS credentials.
44+
45+
46+
.Fowarding example
47+
====
48+
49+
For example, assuming that the account `123456789012` has a CodeArtifact domain `my-domain` containing a repository
50+
`my-repo` in the region `eu-west-1`, the proxy server forwards the request
51+
52+
----
53+
GET /my-domain/123456789012/my-repo/com/example/my-package/1.2.3/my-package-1.2.3.jar
54+
----
55+
56+
to
57+
58+
----
59+
https://my-domain-123456789012.d.codeartifact.eu-west-1.amazonaws.com/maven/my-repo/com/example/my-package/1.2.3/my-package-1.2.3.jar
60+
----
61+
62+
The forwarded request will also contain an appropriate `Authorization` header containing
63+
64+
(The actual hostname is retrieved using the
65+
[https://docs.aws.amazon.com/codeartifact/latest/APIReference/API_GetRepositoryEndpoint.html] API.)
66+
67+
====
68+
69+
It uses the standard AWS SDK authentication strategies (e.g., `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
70+
environment variables). The AWS APIs are only called on demand.
71+
72+
Authorization tokens are cached for the duration indicated by the AWS CodeArtifact API (maximum 12 hours). After
73+
that, the proxy server will automatically request a new token. To the user of the proxy server, this is completely
74+
transparent.
75+
76+
Caching is in-memory only, so cached tokens are lost when the proxy server is shut down or restarted. There is no
77+
disk cache, both for security reasons and because the proxy's own AWS credentials might change between runs, making
78+
validation of cache entries about as expensive as just requesting new tokens.
79+
80+
81+
== Usage
82+
83+
=== As Embedded Server (JVM)
84+
85+
==== Prerequisites
86+
87+
- JDK 1.8+
88+
- Kotlin: The server library is written in Kotlin and compiled against the Kotlin stdlib 1.5.20. If your
89+
code uses a different version of Kotlin, there might be some compatibility issues.
90+
91+
==== Steps
92+
93+
- Include the artifact on your classpath:
94+
+
95+
.Maven (pom.xml)
96+
[source,xml,subs="+attributes"]
97+
----
98+
<dependency>
99+
<groupId>org.unbroken-dome.aws-codeartifact-maven-proxy</groupId>
100+
<artifactId>aws-codeartifact-maven-proxy</artifactId>
101+
<version>{version}</version>
102+
</dependency>
103+
----
104+
+
105+
.Gradle (build.gradle / build.gradle.kts)
106+
[source,kotlin,subs="+attributes"]
107+
----
108+
dependencies {
109+
implementation("org.unbroken-dome.aws-codeartifact-maven-proxy:aws-codeartifact-maven-proxy:{version}")
110+
}
111+
----
112+
+
113+
The artifact is available on Maven Central.
114+
115+
- Create an instance of `Options`
116+
117+
- Call `CodeArtifactMavenProxyServer.start(options)`, which returns a `CompletableFuture` to the server
118+
object allowing to `stop` it later. Synchronous/blocking variants `startSync` and `stopSync` are available as well.
119+
120+
- The port can be configured in the `Options`, or set to `0` (default) to assign a random port. In the latter case,
121+
the actual port on which the server is listening can be queried using the `actualPort` property.
122+
123+
124+
=== Using the CLI
125+
126+
- Download the latest `aws-codeartifact-maven-proxy-cli` archive from the releases page and extract it
127+
128+
- Run `./aws-codeartifact-maven-proxy` to start the server. Ctrl+C to stop.
129+
130+
If started without any arguments, the server will start listening on a random port, which can be retrieved from the
131+
logs.
132+
133+
The following command-line arguments are available:
134+
135+
136+
|===
137+
| Option | Description
138+
139+
| `--bind <address>`
140+
141+
`-b <address>`
142+
| Bind to the given address instead of `localhost` / `127.0.0.1`.
143+
144+
| `--port <port>`
145+
146+
`-p <port>`
147+
| Local port to listen on. Set to `0` to choose a random port.
148+
149+
| `--debug`
150+
| Show DEBUG-level logs.
151+
152+
| `--aws-debug`
153+
| Show DEBUG-level logs for the AWS SDK.
154+
155+
| `--token-ttl <duration>`
156+
157+
`-t <duration>`
158+
| TTL to request for authorization tokens from AWS CodeArtifact. This can be specified as a number of seconds
159+
(e.g. `300`) or as a duration string like `1h30m`.
160+
161+
A value of `0` (zero) will set the expiration of the authorization token to the same
162+
expiration of the user's role's temporary credentials.
163+
164+
If not set, uses the defaults of the service (currently 12 hours).
165+
166+
| `--endpoint-ttl <duration>`
167+
| TTL for caching AWS CodeArtifact repository endpoints. By default, these will be cached
168+
indefinitely (until the server is stopped).
169+
170+
| `--eager-init`
171+
| If this flag is used, certain setup tasks (like initializing the AWS clients) are done when
172+
the server starts. By default, all initialization is done lazily when it is actually needed,
173+
i.e. on the first request.
174+
175+
| `--wiretap [ all \| targets ]`
176+
| Specify a list of targets to enable "wiretap" logging on TRACE level. Valid targets are
177+
`raw`, `http` and `ssl`.
178+
179+
Multiple targets can be specified as a comma-separated list, e.g.
180+
`--wiretap raw,http`.
181+
182+
The value `all` (or just `--wiretap`) will enable wiretap logging
183+
for all targets.
184+
185+
|===
186+
187+
188+
189+
=== Using a Docker image
190+
191+
Currently, the Docker image is not published to a public registry, but you can easily create it on your local Docker
192+
host with:
193+
194+
----
195+
./gradlew :cli:jibDockerBuild
196+
----
197+
198+
The environment variables or files for the desired AWS authentication strategy must be passed to the Docker image,
199+
and the port should be forwarded to the host. (Remember to bind to 127.0.0.1 on the host, otherwise the server will
200+
be public in your network!)
201+
202+
----
203+
export AWS_ACCESS_KEY_ID=...
204+
export AWS_SECRET_ACCESS_KEY=...
205+
export AWS_REGION=...
206+
207+
docker run -d --name aws-codeartifact-maven-proxy \
208+
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION \
209+
-p 127.0.0.1:8080:8080 \
210+
unbroken-dome:aws-codeartifact-maven-proxy:<version> -b 0.0.0.0 -p 8080
211+
----
212+
213+
Other CLI arguments can be used as described above.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
plugins {
2+
application
3+
kotlin("jvm")
4+
id("com.google.cloud.tools.jib") version "3.1.1"
5+
}
6+
7+
8+
dependencies {
9+
implementation(project(":aws-codeartifact-maven-proxy"))
10+
implementation(libs.joptsimple)
11+
implementation(libs.bundles.log4j)
12+
}
13+
14+
15+
application {
16+
applicationName = "aws-codeartifact-maven-proxy"
17+
mainClass.set("org.unbrokendome.awscodeartifact.mavenproxy.cli.CodeArtifactMavenProxyCli")
18+
}
19+
20+
21+
tasks.named<Jar>("jar") {
22+
manifest {
23+
attributes("Main-Class" to application.mainClass.get())
24+
}
25+
}
26+
27+
28+
tasks.named<Tar>("distTar") {
29+
compression = Compression.GZIP
30+
archiveExtension.set("tar.gz")
31+
}
32+
33+
34+
jib {
35+
from {
36+
image = "adoptopenjdk:11.0.11_9-jre-openj9-0.26.0-focal"
37+
}
38+
to {
39+
image = "unbroken-dome/aws-codeartifact-maven-proxy"
40+
tags = setOf(project.version.toString())
41+
}
42+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.unbrokendome.awscodeartifact.mavenproxy.cli
2+
3+
import joptsimple.OptionParser
4+
import joptsimple.util.InetAddressConverter
5+
import java.io.OutputStream
6+
import java.net.InetAddress
7+
import java.time.Duration
8+
import java.util.*
9+
10+
11+
internal data class CliOptions(
12+
val showHelp: Boolean,
13+
val bindAddress: InetAddress,
14+
val port: Int,
15+
val logging: Logging,
16+
val tokenTtl: Duration?,
17+
val endpointCacheTtl: Duration?,
18+
val eagerInit: Boolean,
19+
) {
20+
data class Logging(
21+
val debug: Boolean,
22+
val awsDebug: Boolean,
23+
val wiretapTargets: Set<WiretapTarget>
24+
) {
25+
26+
val isSimpleLogging: Boolean
27+
get() = !debug && !awsDebug && wiretapTargets.isEmpty()
28+
}
29+
30+
31+
fun printHelpOn(output: OutputStream) {
32+
parser.printHelpOn(output)
33+
}
34+
35+
36+
companion object {
37+
38+
private val parser = OptionParser()
39+
40+
private val bindOption = parser
41+
.acceptsAll(listOf("bind", "b"), "Host name or IP address to listen on")
42+
.withRequiredArg().withValuesConvertedBy(InetAddressConverter())
43+
.defaultsTo(InetAddress.getLoopbackAddress())
44+
45+
private val portOption = parser
46+
.acceptsAll(listOf("port", "p"), "HTTP port to listen on")
47+
.withRequiredArg().ofType(Int::class.java)
48+
.defaultsTo(0)
49+
50+
private val debugOption = parser
51+
.accepts("debug", "Enable DEBUG-level logging")
52+
53+
private val awsDebugOption = parser
54+
.accepts("aws-debug", "Enable DEBUG-level logging for AWS SDK clients")
55+
56+
private val tokenTtlOption = parser
57+
.acceptsAll(listOf("token-ttl", "t"), "Time-to-live for CodeArtifact authorization tokens")
58+
.withRequiredArg().withValuesConvertedBy(DurationValueConverter)
59+
60+
private val endpointTtlOption = parser
61+
.accepts("endpoint-ttl", "Cache TTL for CodeArtifact repository endpoints")
62+
.withRequiredArg().withValuesConvertedBy(DurationValueConverter)
63+
64+
private val eagerInitOption = parser
65+
.accepts("eager-init", "Initialize eagerly on startup (not lazily on first request)")
66+
67+
private val wiretapOption = parser
68+
.accepts(
69+
"wiretap", "Traffic to wire-tap (monitor) in logs. Must be \"all\" (default if" +
70+
" no argument is given) or a comma-separated list of targets \"raw\", \"http\", \"ssl\""
71+
)
72+
.withOptionalArg()
73+
.withValuesSeparatedBy(',')
74+
75+
private val helpOption = parser
76+
.acceptsAll(listOf("help", "h"), "Show this help message and exit")
77+
.forHelp()
78+
79+
80+
fun parse(args: Array<String>): CliOptions {
81+
82+
val parsedOptions = parser.parse(*args)
83+
84+
return CliOptions(
85+
showHelp = parsedOptions.has(helpOption),
86+
bindAddress = parsedOptions.valueOf(bindOption),
87+
port = parsedOptions.valueOf(portOption),
88+
logging = Logging(
89+
debug = parsedOptions.has(debugOption),
90+
awsDebug = parsedOptions.has(awsDebugOption),
91+
wiretapTargets = if (parsedOptions.has(wiretapOption)) {
92+
if (parsedOptions.hasArgument(wiretapOption)) {
93+
WiretapTarget.parse(parsedOptions.valuesOf(wiretapOption))
94+
} else {
95+
EnumSet.allOf(WiretapTarget::class.java)
96+
}
97+
} else emptySet()
98+
),
99+
tokenTtl = parsedOptions.valueOf(tokenTtlOption),
100+
endpointCacheTtl = parsedOptions.valueOf(endpointTtlOption),
101+
eagerInit = parsedOptions.has(eagerInitOption)
102+
)
103+
}
104+
}
105+
}
106+

0 commit comments

Comments
 (0)