Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Till Krullmann committed Jul 1, 2021
0 parents commit 8968e2b
Show file tree
Hide file tree
Showing 61 changed files with 3,734 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.bat eol=crlf
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# IntelliJ IDEA
.idea/

# Gradle
.gradle/
build/
1 change: 1 addition & 0 deletions .java-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.8
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2021 Till Krullmann

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
213 changes: 213 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
:version: 0.1.0

ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:caution-caption: :fire:
:warning-caption: :warning:

:toc-placement!:
endif::[]

= AWS CodeArtifact Maven Proxy

This project contains a lightweight, embeddable proxy server for AWS CodeArtifact Maven repositories. It
automatically handles endpoint lookups and CodeArtifact authorization tokens.

== Background

AWS CodeArtifact is a great, cost-efficient service for hosting private Maven repositories. However, its
authentication mechanism with its temporary tokens, while certainly adding a degree of security, is often
cumbersome to work with:

* Developers running a build from their local machine will have to install the AWS CLI and execute some
commands to look up endpoints and retrieve authorization tokens.

* Access to the repositories is only actually needed for the initial build execution and when dependencies
have changed. For the majority of builds, the required artifacts can be served from a local cache, making
it unnecessary to even obtain an authorization token.

== How It Works

The proxy server is intended for _local_ use only. It acts as a virtual Maven repository server by forwarding
URL paths that conform to the pattern

----
/<domain>/<domain-owner>/<repo>/<group>/<artifact>/...
----

to the appropriate AWS CodeArtifact repository endpoint for `domain`, `domain-owner` and `repo`.

TIP: The special value `default` can be used for the `<domain-owner>` to use the default AWS account ID based on the
proxy server's AWS credentials.


.Fowarding example
====
For example, assuming that the account `123456789012` has a CodeArtifact domain `my-domain` containing a repository
`my-repo` in the region `eu-west-1`, the proxy server forwards the request
----
GET /my-domain/123456789012/my-repo/com/example/my-package/1.2.3/my-package-1.2.3.jar
----
to
----
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
----
The forwarded request will also contain an appropriate `Authorization` header containing
(The actual hostname is retrieved using the
[https://docs.aws.amazon.com/codeartifact/latest/APIReference/API_GetRepositoryEndpoint.html] API.)
====

It uses the standard AWS SDK authentication strategies (e.g., `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
environment variables). The AWS APIs are only called on demand.

Authorization tokens are cached for the duration indicated by the AWS CodeArtifact API (maximum 12 hours). After
that, the proxy server will automatically request a new token. To the user of the proxy server, this is completely
transparent.

Caching is in-memory only, so cached tokens are lost when the proxy server is shut down or restarted. There is no
disk cache, both for security reasons and because the proxy's own AWS credentials might change between runs, making
validation of cache entries about as expensive as just requesting new tokens.


== Usage

=== As Embedded Server (JVM)

==== Prerequisites

- JDK 1.8+
- Kotlin: The server library is written in Kotlin and compiled against the Kotlin stdlib 1.5.20. If your
code uses a different version of Kotlin, there might be some compatibility issues.

==== Steps

- Include the artifact on your classpath:
+
.Maven (pom.xml)
[source,xml,subs="+attributes"]
----
<dependency>
<groupId>org.unbroken-dome.aws-codeartifact-maven-proxy</groupId>
<artifactId>aws-codeartifact-maven-proxy</artifactId>
<version>{version}</version>
</dependency>
----
+
.Gradle (build.gradle / build.gradle.kts)
[source,kotlin,subs="+attributes"]
----
dependencies {
implementation("org.unbroken-dome.aws-codeartifact-maven-proxy:aws-codeartifact-maven-proxy:{version}")
}
----
+
The artifact is available on Maven Central.

- Create an instance of `Options`

- Call `CodeArtifactMavenProxyServer.start(options)`, which returns a `CompletableFuture` to the server
object allowing to `stop` it later. Synchronous/blocking variants `startSync` and `stopSync` are available as well.

- The port can be configured in the `Options`, or set to `0` (default) to assign a random port. In the latter case,
the actual port on which the server is listening can be queried using the `actualPort` property.


=== Using the CLI

- Download the latest `aws-codeartifact-maven-proxy-cli` archive from the releases page and extract it

- Run `./aws-codeartifact-maven-proxy` to start the server. Ctrl+C to stop.

If started without any arguments, the server will start listening on a random port, which can be retrieved from the
logs.

The following command-line arguments are available:


|===
| Option | Description

| `--bind <address>`

`-b <address>`
| Bind to the given address instead of `localhost` / `127.0.0.1`.

| `--port <port>`

`-p <port>`
| Local port to listen on. Set to `0` to choose a random port.

| `--debug`
| Show DEBUG-level logs.

| `--aws-debug`
| Show DEBUG-level logs for the AWS SDK.

| `--token-ttl <duration>`

`-t <duration>`
| TTL to request for authorization tokens from AWS CodeArtifact. This can be specified as a number of seconds
(e.g. `300`) or as a duration string like `1h30m`.

A value of `0` (zero) will set the expiration of the authorization token to the same
expiration of the user's role's temporary credentials.

If not set, uses the defaults of the service (currently 12 hours).

| `--endpoint-ttl <duration>`
| TTL for caching AWS CodeArtifact repository endpoints. By default, these will be cached
indefinitely (until the server is stopped).

| `--eager-init`
| If this flag is used, certain setup tasks (like initializing the AWS clients) are done when
the server starts. By default, all initialization is done lazily when it is actually needed,
i.e. on the first request.

| `--wiretap [ all \| targets ]`
| Specify a list of targets to enable "wiretap" logging on TRACE level. Valid targets are
`raw`, `http` and `ssl`.

Multiple targets can be specified as a comma-separated list, e.g.
`--wiretap raw,http`.

The value `all` (or just `--wiretap`) will enable wiretap logging
for all targets.

|===



=== Using a Docker image

Currently, the Docker image is not published to a public registry, but you can easily create it on your local Docker
host with:

----
./gradlew :cli:jibDockerBuild
----

The environment variables or files for the desired AWS authentication strategy must be passed to the Docker image,
and the port should be forwarded to the host. (Remember to bind to 127.0.0.1 on the host, otherwise the server will
be public in your network!)

----
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=...
docker run -d --name aws-codeartifact-maven-proxy \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION \
-p 127.0.0.1:8080:8080 \
unbroken-dome:aws-codeartifact-maven-proxy:<version> -b 0.0.0.0 -p 8080
----

Other CLI arguments can be used as described above.
42 changes: 42 additions & 0 deletions aws-codeartifact-maven-proxy-cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
application
kotlin("jvm")
id("com.google.cloud.tools.jib") version "3.1.1"
}


dependencies {
implementation(project(":aws-codeartifact-maven-proxy"))
implementation(libs.joptsimple)
implementation(libs.bundles.log4j)
}


application {
applicationName = "aws-codeartifact-maven-proxy"
mainClass.set("org.unbrokendome.awscodeartifact.mavenproxy.cli.CodeArtifactMavenProxyCli")
}


tasks.named<Jar>("jar") {
manifest {
attributes("Main-Class" to application.mainClass.get())
}
}


tasks.named<Tar>("distTar") {
compression = Compression.GZIP
archiveExtension.set("tar.gz")
}


jib {
from {
image = "adoptopenjdk:11.0.11_9-jre-openj9-0.26.0-focal"
}
to {
image = "unbroken-dome/aws-codeartifact-maven-proxy"
tags = setOf(project.version.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.unbrokendome.awscodeartifact.mavenproxy.cli

import joptsimple.OptionParser
import joptsimple.util.InetAddressConverter
import java.io.OutputStream
import java.net.InetAddress
import java.time.Duration
import java.util.*


internal data class CliOptions(
val showHelp: Boolean,
val bindAddress: InetAddress,
val port: Int,
val logging: Logging,
val tokenTtl: Duration?,
val endpointCacheTtl: Duration?,
val eagerInit: Boolean,
) {
data class Logging(
val debug: Boolean,
val awsDebug: Boolean,
val wiretapTargets: Set<WiretapTarget>
) {

val isSimpleLogging: Boolean
get() = !debug && !awsDebug && wiretapTargets.isEmpty()
}


fun printHelpOn(output: OutputStream) {
parser.printHelpOn(output)
}


companion object {

private val parser = OptionParser()

private val bindOption = parser
.acceptsAll(listOf("bind", "b"), "Host name or IP address to listen on")
.withRequiredArg().withValuesConvertedBy(InetAddressConverter())
.defaultsTo(InetAddress.getLoopbackAddress())

private val portOption = parser
.acceptsAll(listOf("port", "p"), "HTTP port to listen on")
.withRequiredArg().ofType(Int::class.java)
.defaultsTo(0)

private val debugOption = parser
.accepts("debug", "Enable DEBUG-level logging")

private val awsDebugOption = parser
.accepts("aws-debug", "Enable DEBUG-level logging for AWS SDK clients")

private val tokenTtlOption = parser
.acceptsAll(listOf("token-ttl", "t"), "Time-to-live for CodeArtifact authorization tokens")
.withRequiredArg().withValuesConvertedBy(DurationValueConverter)

private val endpointTtlOption = parser
.accepts("endpoint-ttl", "Cache TTL for CodeArtifact repository endpoints")
.withRequiredArg().withValuesConvertedBy(DurationValueConverter)

private val eagerInitOption = parser
.accepts("eager-init", "Initialize eagerly on startup (not lazily on first request)")

private val wiretapOption = parser
.accepts(
"wiretap", "Traffic to wire-tap (monitor) in logs. Must be \"all\" (default if" +
" no argument is given) or a comma-separated list of targets \"raw\", \"http\", \"ssl\""
)
.withOptionalArg()
.withValuesSeparatedBy(',')

private val helpOption = parser
.acceptsAll(listOf("help", "h"), "Show this help message and exit")
.forHelp()


fun parse(args: Array<String>): CliOptions {

val parsedOptions = parser.parse(*args)

return CliOptions(
showHelp = parsedOptions.has(helpOption),
bindAddress = parsedOptions.valueOf(bindOption),
port = parsedOptions.valueOf(portOption),
logging = Logging(
debug = parsedOptions.has(debugOption),
awsDebug = parsedOptions.has(awsDebugOption),
wiretapTargets = if (parsedOptions.has(wiretapOption)) {
if (parsedOptions.hasArgument(wiretapOption)) {
WiretapTarget.parse(parsedOptions.valuesOf(wiretapOption))
} else {
EnumSet.allOf(WiretapTarget::class.java)
}
} else emptySet()
),
tokenTtl = parsedOptions.valueOf(tokenTtlOption),
endpointCacheTtl = parsedOptions.valueOf(endpointTtlOption),
eagerInit = parsedOptions.has(eagerInitOption)
)
}
}
}

Loading

0 comments on commit 8968e2b

Please sign in to comment.