Skip to content

Latest commit

 

History

174 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kagami 🪞

A simple Maven repository mirror server built with Spring Boot. Kagami (鏡, meaning "mirror" in Japanese) provides efficient caching and proxying of Maven artifacts from multiple remote repositories.

Image Image Image Image

Features

  • Local Caching: Automatically caches artifacts from remote repositories to reduce download times
  • Pluggable Storage: Local file system (default) or Amazon S3 / S3-compatible object storage
  • Multiple Repository Support: Configure multiple remote repositories with individual settings
  • Private Repository Support: JWT-based authentication for secure repository access
  • REST API: Simple REST endpoints for artifact retrieval and cache management
  • Web Dashboard: Server-rendered UI built with htmx for repository browsing and management with unified header navigation
  • Authentication: Form-based authentication or OIDC/OAuth2 login for web UI access with styled login/logout pages
  • Token Management: Web-based JWT token generation with configurable expiration, permissions, and build tool configuration examples
  • User Interface: Consistent header across all pages showing logged-in username, logout functionality, and token generation access
  • Security Features: OAuth2 Resource Server with JWT tokens, repository-specific access control, group-based RBAC, scope-capped token generation, CSRF protection partially disabled for API usage
  • Sigstore Attestation Support: Fetches sigstore attestation bundles distributed by the upstream as sidecar files, and verifies them in the web UI with cosign against a pinned public key or keyless identity constraints
  • OIDC Support: OpenID Connect authentication with multiple identity providers (Google, Microsoft Entra ID, etc.)

Quick Start

Option 1: Using Docker

The fastest way to get started with Kagami is using the pre-built Docker image:

# Run Kagami with Docker
docker run --rm --pull always -p 8080:8080 \
  -v /tmp/kagami:/var/kagami/storage \
  -e kagami.storage.path=/var/kagami/storage \
  -e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
  ghcr.io/making/kagami:jvm

Access the application:

  1. Open http://localhost:8080 in your browser
  2. Log in with default credentials:
    • Username: demo
    • Password: demo
  3. Access artifacts: wget http://localhost:8080/artifacts/central/org/springframework/spring-core/6.0.0/spring-core-6.0.0.jar

⚠️ Production Warning: For production deployments, you must configure kagami.jwt.private-key and kagami.jwt.public-key to enable JWT token functionality. See Private Repository Configuration for key generation instructions. The Docker Compose example below shows the recommended production setup.

Docker Configuration Options:

# With private repository with authentication
docker run --rm --pull always -p 8080:8080 \
  -v /tmp/kagami:/var/kagami/storage \
  -e kagami.storage.path=/var/kagami/storage \
  -e kagami.repositories.spring-enterprise.url=https://packages.broadcom.com/artifactory/spring-enterprise \
  -e kagami.repositories.spring-enterprise.username=your-bc-username \
  -e kagami.repositories.spring-enterprise.password=your-bc-token \
  -e kagami.repositories.spring-enterprise.is-private=true \
  ghcr.io/making/kagami:jvm

# With custom authentication
docker run --rm --pull always -p 8080:8080 \
  -v /tmp/kagami:/var/kagami/storage \
  -e kagami.storage.path=/var/kagami/storage \
  -e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
  -e spring.security.user.name=admin \
  -e spring.security.user.password='{noop}mypassword' \
  ghcr.io/making/kagami:jvm

# With S3 storage instead of a mounted volume
docker run --rm --pull always -p 8080:8080 \
  -e kagami.storage.type=s3 \
  -e kagami.storage.s3.bucket=kagami-mirror \
  -e spring.cloud.aws.region.static=ap-northeast-1 \
  -e spring.cloud.aws.credentials.access-key=your-access-key \
  -e spring.cloud.aws.credentials.secret-key=your-secret-key \
  -e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
  ghcr.io/making/kagami:jvm

Using Docker Compose:

For production deployments with JWT token support, create a docker-compose.yml file:

services:
  kagami:
    image: ghcr.io/making/kagami:jvm
    pull_policy: always
    ports:
      - "8080:8080"
    volumes:
      - ./kagami-storage:/var/kagami/storage
      - ./kagami-private.pem:/etc/kagami/kagami-private.pem:ro
      - ./kagami-public.pem:/etc/kagami/kagami-public.pem:ro
    environment:
      kagami.storage.path: /var/kagami/storage
      kagami.repositories.central.url: https://repo.maven.apache.org/maven2
      # Configure private repository (requires JWT keys)
      kagami.repositories.private-repo.url: https://internal.repo.com/maven2
      kagami.repositories.private-repo.is-private: true
      # JWT key configuration
      kagami.jwt.private-key: file:/etc/kagami/kagami-private.pem
      kagami.jwt.public-key: file:/etc/kagami/kagami-public.pem
      # Optional: Custom authentication
      # spring.security.user.name: admin
      # spring.security.user.password: '{noop}mypassword'

Before running:

  1. Generate JWT key pair following the instructions in Private Repository Configuration
  2. Place kagami-private.pem and kagami-public.pem in the same directory as your docker-compose.yml
  3. Set appropriate file permissions:
    mkdir -p ./kagami-storage
    chmod a+w ./kagami-storage
    chmod a+r ./kagami-private.pem ./kagami-public.pem
  4. Run with: docker compose up -d

Option 2: Building from Source

Prerequisites

  • Java 25 or later

Running

  1. Clone the repository:
git clone https://github.com/making/kagami.git
cd kagami
  1. Build and run:
./mvnw spring-boot:run
  1. Access the web dashboard:
open http://localhost:8080
  1. Log in to the web dashboard:

    • Default username: demo
    • Default password: demo
  2. Access artifacts via HTTP:

wget http://localhost:8080/artifacts/central/org/springframework/spring-core/6.0.0/spring-core-6.0.0.jar

Configuration

Configure repositories and settings in application.properties or environment variables:

Basic Repository Configuration

# Storage path for cached artifacts
kagami.storage.path=/var/kagami/storage

# Public repositories
kagami.repositories.central.url=https://repo.maven.apache.org/maven2
kagami.repositories.jcenter.url=https://jcenter.bintray.com

S3 Storage

Artifacts can be mirrored into Amazon S3 or any S3-compatible object storage (MinIO, RustFS, etc.) instead of the local file system. The bucket must exist; Kagami does not create it.

kagami.storage.type=s3
kagami.storage.s3.bucket=kagami-mirror
# Optional prefix in front of every key, which is <prefix>/<repository id>/<artifact path>
kagami.storage.s3.key-prefix=mirror

# Region and credentials are resolved by Spring Cloud AWS; the default chain
# (environment variables, instance profile, ...) works without any of these.
spring.cloud.aws.region.static=ap-northeast-1
spring.cloud.aws.credentials.access-key=...
spring.cloud.aws.credentials.secret-key=...

For an S3-compatible server, point the client at its endpoint and use path-style access:

spring.cloud.aws.s3.endpoint=http://minio.example.com:9000
spring.cloud.aws.s3.path-style-access-enabled=true

Notes:

  • kagami.storage.type defaults to local; then the S3 client is not created and no AWS settings are needed.
  • With s3, the disk space health indicator and metric that point at kagami.storage.path are disabled automatically.
  • The storage type is a bean condition, so a GraalVM native image is bound to the type it was built with. Set kagami.storage.type=s3 at build time to build a native image for S3.

Repository with Authentication

# Private repository with Basic authentication
kagami.repositories.private.url=https://private.repo.example.com/maven2
kagami.repositories.private.username=your-username
kagami.repositories.private.password=your-password

Private Repository Configuration

# Mark repository as private (requires JWT authentication)
kagami.repositories.private-repo.url=https://internal.repo.com/maven2
kagami.repositories.private-repo.is-private=true

# Optional display priority (default 0); higher priorities are listed first
# on the web UI and in the generated configuration examples
kagami.repositories.private-repo.priority=10

# JWT key pair configuration
kagami.jwt.private-key=classpath:kagami-private.pem
kagami.jwt.public-key=classpath:kagami-public.pem

To generate the JWT key pair, run the following commands:

# Generate RSA private key
openssl genrsa -out private.pem 2048

# Extract public key
openssl rsa -in private.pem -outform PEM -pubout -out kagami-public.pem

# Convert private key to PKCS#8 format
openssl pkcs8 -topk8 -inform PEM -in private.pem -out kagami-private.pem -nocrypt

# Clean up temporary file
rm -f private.pem

Place the generated kagami-private.pem and kagami-public.pem files in your src/main/resources directory.

Alternative Configuration Methods

Besides file references, you can configure JWT keys using the following methods:

File path (recommended for Docker/containers):

kagami.jwt.private-key=file:/path/to/kagami-private.pem
kagami.jwt.public-key=file:/path/to/kagami-public.pem

Base64 encoded strings (useful when file mounting is difficult):

kagami.jwt.private-key=base64:LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t...
kagami.jwt.public-key=base64:LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0K...

To generate base64 encoded values:

# For private key
echo "kagami.jwt.private-key=base64:$(cat kagami-private.pem | base64 -w0)"

# For public key  
echo "kagami.jwt.public-key=base64:$(cat kagami-public.pem | base64 -w0)"

The base64 format is particularly useful in container environments where file mounting is challenging, such as certain cloud platforms like Cloud Foundry.

For more information on configuration value conversion, see the Spring Boot documentation.

Web UI Authentication

Simple Authentication (Form-based)

# Configure web UI authentication (default: demo/demo)
kagami.authentication.type=simple
spring.security.user.name=your-username
spring.security.user.password={noop}plainpassword
# For production, use encoded password:
# spring.security.user.password={bcrypt}$2a$10$...

Only one user can be configured with this method. For multiple users, use OIDC authentication.

OIDC Authentication

Configure OIDC authentication for enterprise single sign-on:

# Enable OIDC authentication
kagami.authentication.type=oidc

# Restrict access to specific email patterns (regex)
kagami.authentication.allowed-name-patterns=.*@example\\.com,.*@example\\.org

# Configure Google as identity provider
spring.security.oauth2.client.provider.google.issuer-uri=https://accounts.google.com
spring.security.oauth2.client.provider.google.user-name-attribute=email
spring.security.oauth2.client.registration.google.client-id=your-google-client-id
spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret
spring.security.oauth2.client.registration.google.client-name=Google
spring.security.oauth2.client.registration.google.scope=openid,email

# Configure Microsoft Entra ID (formerly Azure AD)
spring.security.oauth2.client.provider.microsoft-entra-id.issuer-uri=https://sts.windows.net/{tenant-id}/
spring.security.oauth2.client.provider.microsoft-entra-id.user-name-attribute=email
spring.security.oauth2.client.registration.microsoft-entra-id.client-id=your-client-id
spring.security.oauth2.client.registration.microsoft-entra-id.client-secret=your-client-secret
spring.security.oauth2.client.registration.microsoft-entra-id.client-name=Microsoft Entra ID
spring.security.oauth2.client.registration.microsoft-entra-id.scope=openid,email

Notes:

  • When OIDC is enabled, users will see provider-specific login buttons instead of username/password fields
  • The allowed-name-patterns property restricts access to users whose name matches the specified patterns
  • Multiple identity providers can be configured simultaneously
  • Users must have matching email patterns to be allowed to log in

Group claim for RBAC

Group-based RBAC for OIDC users reads the IdP group memberships from a claim in the ID token. The claim name defaults to groups and can be changed with kagami.rbac.groups-claim. Most providers do not include group claims by default; you must request them with a scope and/or configure the provider to embed the claim in the ID token:

# Claim Kagami reads the IdP group memberships from (default: groups)
kagami.rbac.groups-claim=groups

# Request the claim via a scope. Examples:
# - Keycloak: add the "groups" scope of the dedicated "groups" client scope mapper,
#   or create a protocol mapper that adds the user's groups to the ID token
spring.security.oauth2.client.registration.keycloak.scope=openid,email,groups
# - Microsoft Entra ID: request "Group.Read.All" so tokens carry the user's groups
spring.security.oauth2.client.registration.microsoft-entra-id.scope=openid,email,Group.Read.All

Without a group claim in the ID token, the kagami.rbac.mappings.groups.* translations never match and OIDC users fall into the default group. Verify the claim is actually present in the token before debugging RBAC mappings.

See the Spring Boot documentation for more details on configuring OIDC authentication.

Group-based RBAC

A group is a named set of authorities and users are mapped to groups through properties. Group names are not roles: authorization rules only see the authorities a group expands into, which reuse the JWT scope vocabulary (artifacts:read, artifacts:delete, artifacts:admin), so JWT scopes and group membership satisfy the same rules. Users absent from every mapping fall into the default group, which is editors by default. The default therefore grants read and delete access but not cache administration; assign administrators or another group with artifacts:admin explicitly when maintenance access is required.

# Group definitions: group name -> authorities (same vocabulary as JWT scopes).
# The built-in groups are:
#   administrators: artifacts:read,artifacts:delete,artifacts:admin
#   editors: artifacts:read,artifacts:delete
#   viewers: artifacts:read
# Entries here override or add groups. An empty value defines a group with no authorities.
kagami.rbac.groups.administrators=artifacts:read,artifacts:delete,artifacts:admin
kagami.rbac.groups.no-access=

# Username -> groups, common to simple and OIDC authentication. Keys containing @ or .
# need the bracket notation so that relaxed binding does not mangle them.
kagami.rbac.mappings.users.demo=administrators
kagami.rbac.mappings.users[taro@example.com]=editors

# OIDC groups claim (IdP group names) -> Kagami groups.
# The claim that carries the IdP group names is "groups" by default;
# change it with kagami.rbac.groups-claim (see the OIDC section above).
kagami.rbac.mappings.groups.my-team-admins=administrators

# Group applied to users absent from every mapping (default: editors)
kagami.rbac.default-group=no-access

With this configuration, the user demo gets the authorities of administrators, taro@example.com gets those of editors, and OIDC users carrying the my-team-admins group get those of administrators. Everyone else falls into no-access, which grants nothing: such users can browse the web UI but see no delete actions and cannot issue any token.

Notes:

  • Login grants the union of the authorities of all groups the user belongs to
  • Every referenced group must be defined under kagami.rbac.groups.*; the application fails to start otherwise
  • The token generation page only offers the scopes the logged in user holds, and the token API (POST /token) rejects requests beyond that cap with 403
  • The delete actions in the web UI are only rendered for users holding artifacts:delete
  • The allowed-name-patterns OIDC gate is orthogonal to RBAC: patterns decide who may log in, RBAC decides what admitted users can do
  • artifacts:admin is required by the cache garbage collection API; it is not granted by the default editors group

Cache garbage collection API

The administrator API removes directories whose only files are either maven-metadata.xml and maven-metadata.xml.sha1, or only resolver-status.properties. All files in a candidate directory must be at least one hour old. Preview candidates first, then run the collection explicitly:

# Dry-run; olderThan accepts ISO-8601 durations such as PT30M or PT0S
curl -H "Authorization: Bearer $TOKEN" \
  'http://localhost:8080/artifacts/central/gc?olderThan=PT1H'

# Collect eligible directories
curl -X POST -H "Authorization: Bearer $TOKEN" \
  'http://localhost:8080/artifacts/central/gc?olderThan=PT1H'

The operation works with both local and S3 storage. It scans the repository before collecting, so very large S3 repositories may take time. A concurrent artifact download is preserved; the collector removes only the eligible bookkeeping files and then removes an empty local directory when possible.

Sigstore Attestation Bundles

Kagami can fetch sigstore attestation bundles distributed by the upstream as sidecar files of artifacts (e.g. lib-1.0.jar.attestation.sigstore.json) and store them next to the artifact:

kagami.repositories.tanzu.sigstore.enabled=true
# Optional: extra bundle suffixes to try; defaults to attestation.sigstore.json (Tanzu Spring)
# and sigstore.json (Maven Central)
kagami.repositories.tanzu.sigstore.bundle-suffixes[0]=attestation.sigstore.json

Bundles that reached the storage are shown in the browse UI and can be verified with the external cosign binary, which must be installed in the Kagami runtime environment (configurable via kagami.sigstore.cosign-path, default cosign). Verification runs entirely on trust anchors from the repository configuration; user input is never passed to cosign.

Two verification modes exist, configured per repository:

# Pinned key mode: verify against a public key, skipping transparency log verification
kagami.repositories.tanzu.sigstore.verification=key
kagami.repositories.tanzu.sigstore.public-key-url=https://storage.googleapis.com/tanzu-signing-bucket/build-factory/public-key.pem

# Keyless mode: verify the Fulcio certificate embedded in the bundle, with full
# transparency log verification
kagami.repositories.central.sigstore.verification=keyless
kagami.repositories.central.sigstore.certificate-identity-regexp=https://github.com/<owner>/<repo>/.*
kagami.repositories.central.sigstore.certificate-oidc-issuer=https://token.actions.githubusercontent.com

The verification result (including the cosign output on failure) is shown in the file information dialog of the browse UI, together with the equivalent command line for running the verification with a locally installed cosign.

The Docker image built by CI embeds a cosign binary in the application jar (Maven profile embedded-cosign); the embedded binary takes precedence over the one on the PATH when kagami.sigstore.cosign-path is left at its default, so the image needs no cosign installation.

HTTP Proxy Configuration

Kagami sends every outgoing request through the configured proxy, both the artifact downloads performed by Maven Resolver and the direct downloads of the files Maven Resolver does not handle, such as maven-metadata.xml.

# Proxy for http repositories, also used for https repositories unless kagami.proxy.https-url is set
kagami.proxy.url=http://proxy.company.com:8080
# Proxy for https repositories
kagami.proxy.https-url=http://proxy.company.com:8443
# Credentials for proxies that require Basic authentication
kagami.proxy.username=user
kagami.proxy.password=password
# Hosts to reach without the proxy, sub domains included
kagami.proxy.non-proxy-hosts=localhost,127.0.0.1,.internal.company.com

Alternatively, use the standard environment variables:

export http_proxy=http://proxy.company.com:8080
export https_proxy=http://proxy.company.com:8080
export no_proxy=localhost,127.0.0.1,.internal.company.com

Notes:

  • Properties take precedence over environment variables, and lower case environment variables take precedence over upper case ones (http_proxy before HTTP_PROXY)
  • Credentials can also be embedded in the URL, e.g. kagami.proxy.url=http://user:password@proxy.company.com:8080. Reserved characters must be percent encoded
  • The scheme may be omitted, e.g. kagami.proxy.url=proxy.company.com:8080
  • Only http proxies are supported, which is what legacy proxies use even for https repositories, where the connection is tunneled with CONNECT
  • The JDK refuses to send Basic credentials over a tunneled connection by default. If an https repository is behind a proxy requiring authentication, start Kagami with -Djdk.http.auth.tunneling.disabledSchemes=

API Usage

See the API documentation for details on available endpoints.

Maven Client Configuration

Configure your Maven settings to use Kagami as a mirror:

Public Repository Access

<settings>
  <profiles>
    <profile>
      <id>kagami-public</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <repositories>
        <repository>
          <id>kagami-central</id>
          <name>Kagami Public Repository</name>
          <url>http://localhost:8080/artifacts/central</url>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>kagami-central</id>
          <name>Kagami Public Repository</name>
          <url>http://localhost:8080/artifacts/central</url>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  </profiles>
  
  <!-- ALTERNATIVE: Mirror configuration -->
  <!-- Use mirrors when you want to redirect ALL Maven repository requests through Kagami -->
  <!-- This is useful for: -->
  <!-- - Corporate environments where all external access must go through a proxy -->
  <!-- - Offline environments where only Kagami has access to external repositories -->
  <!-- - Performance optimization when Kagami has better network access to upstream repos -->
  <!--
  <mirrors>
    <mirror>
      <id>kagami</id>
      <mirrorOf>*</mirrorOf>
      <name>Kagami Mirror</name>
      <url>http://localhost:8080/artifacts/central</url>
    </mirror>
  </mirrors>
  -->
</settings>

Private Repository Access

For private repositories, generate a JWT token using the web interface:

  1. Log in to the web dashboard at http://localhost:8080
  2. Click "Generate Token" in the header navigation
  3. Select repositories and permissions using checkboxes
  4. Set token expiration with human-friendly units (default: 6 months)
  5. Click "Generate Token" and copy the generated JWT
  6. Use the provided Maven, Gradle Groovy, or Gradle Kotlin configuration examples

Note: JWT tokens cannot be used to generate new tokens. You must use the web interface.

Two authentication methods are supported for build tools:

  • Username/Password (Basic authentication): the username can be any value and the password is the JWT token
  • Bearer token: send the JWT token in the Authorization: Bearer header

Then configure Maven with the JWT token. With the standard username/password configuration:

<settings>
  <servers>
    <server>
      <id>kagami-private</id>
      <username>any-username</username>
      <password>YOUR_JWT_TOKEN</password>
    </server>
  </servers>
  <profiles>
    <profile>
      <id>kagami-private</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <repositories>
        <repository>
          <id>kagami-private</id>
          <name>Kagami Private Repository</name>
          <url>http://localhost:8080/artifacts/private-repo</url>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>kagami-private</id>
          <name>Kagami Private Repository</name>
          <url>http://localhost:8080/artifacts/private-repo</url>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  </profiles>
  
  <!-- ALTERNATIVE: Mirror configuration -->
  <!-- Use mirrors when you want to redirect ALL Maven repository requests through Kagami -->
  <!-- This is useful for: -->
  <!-- - Corporate environments where all external access must go through a proxy -->
  <!-- - Offline environments where only Kagami has access to external repositories -->
  <!-- - Performance optimization when Kagami has better network access to upstream repos -->
  <!--
  <mirrors>
    <mirror>
      <id>kagami-private</id>
      <mirrorOf>*</mirrorOf>
      <name>Kagami Private Mirror</name>
      <url>http://localhost:8080/artifacts/private-repo</url>
    </mirror>
  </mirrors>
  -->
</settings>

Alternatively, configure the Bearer token method by replacing the <server> element above with:

<server>
  <id>kagami-private</id>
  <configuration>
    <httpHeaders>
      <property>
        <name>Authorization</name>
        <value>Bearer YOUR_JWT_TOKEN</value>
      </property>
    </httpHeaders>
  </configuration>
</server>

Gradle Configuration

Public Repository

repositories {
    maven {
        url 'http://localhost:8080/artifacts/central'
    }
}

Private Repository

The examples below use the username/password method. The Bearer token method is also available (see the end of this section).

Groovy DSL ($HOME/.gradle/init.gradle)

// $HOME/.gradle/init.gradle - Groovy DSL version

def repoUrl = "http://localhost:8080/artifacts/private-repo"
def repoToken = "YOUR_JWT_TOKEN"

// For regular dependencies (legacy projects)
allprojects {
    repositories {
        maven {
            url = repoUrl
            allowInsecureProtocol = true
            credentials {
                username = "kagami" // can be any value
                password = repoToken
            }
        }
        mavenCentral() // fallback
    }
}

// Configure settings.gradle
settingsEvaluated { settings ->
    // For plugin resolution
    settings.pluginManagement {
        repositories {
            maven {
                url = repoUrl
                allowInsecureProtocol = true
                credentials {
                    username = "kagami" // can be any value
                    password = repoToken
                }
            }
            gradlePluginPortal() // fallback
            mavenCentral() // fallback
        }
    }
    
    // Dependency resolution management (Gradle 6.8+)
    settings.dependencyResolutionManagement {
        // Ignore repositories defined in build.gradle
        repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
        
        repositories {
            maven {
                url = repoUrl
                allowInsecureProtocol = true
                credentials {
                    username = "kagami" // can be any value
                    password = repoToken
                }
            }
            mavenCentral()
        }
    }
}

Kotlin DSL ($HOME/.gradle/init.gradle.kts)

// $HOME/.gradle/init.gradle.kts - Kotlin DSL version

val repoUrl = "http://localhost:8080/artifacts/private-repo"
val repoToken = "YOUR_JWT_TOKEN"

// Extension function to configure repository
fun RepositoryHandler.addKagamiRepository() {
    maven {
        url = uri(repoUrl)
        isAllowInsecureProtocol = true
        credentials {
            username = "kagami" // can be any value
            password = repoToken
        }
    }
}

// For regular dependencies (legacy projects)
allprojects {
    repositories {
        addKagamiRepository()
        mavenCentral() // fallback
    }
}

// Configure settings.gradle
settingsEvaluated {
    // For plugin resolution
    pluginManagement {
        repositories {
            addKagamiRepository()
            gradlePluginPortal() // fallback
            mavenCentral() // fallback
        }
    }
    
    // Dependency resolution management (Gradle 6.8+)
    dependencyResolutionManagement {
        // Ignore repositories defined in build.gradle
        @Suppress("UnstableApiUsage")
        repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
        
        repositories {
            addKagamiRepository()
            mavenCentral() // fallback
        }
    }
}

To use the Bearer token method instead, replace each credentials block in the examples above with the following.

Groovy DSL:

authentication {
    header(HttpHeaderAuthentication)
}
credentials(HttpHeaderCredentials) {
    name = "Authorization"
    value = "Bearer YOUR_JWT_TOKEN"
}

Kotlin DSL (add the imports at the top of the init script):

import org.gradle.authentication.http.HttpHeaderAuthentication
import org.gradle.kotlin.dsl.*

// ...
authentication {
    create<HttpHeaderAuthentication>("header")
}
credentials(HttpHeaderCredentials::class) {
    name = "Authorization"
    value = "Bearer YOUR_JWT_TOKEN"
}

Deploying Kagami to Cloud Foundry

Generate the JWT key pair as documented above and paste base64-encoded values in manifest.yaml as below:

applications:
- name: kagami
  instances: 1
  memory: 768m
  docker:
    image: ghcr.io/making/kagami:jvm
  env:
    kagami.repositories.central.url: https://repo.maven.apache.org/maven2
    kagami.jwt.private-key: base64:LS0tLS1CRUd...
    kagami.jwt.public-key: base64:LS0tLS1CRUdJ...

Then deploy it:

cf push

On Cloud Foundry, applications are ephemeral by default. Therefore, all data will be lost when Kagami is restarted or updated. Kagami is a mirror repository, so even if data is lost, it will be re-downloaded the next time it is accessed. Aside from the slow initial download time, ephemeral containers are not a problem. Similarly, when scaling out, each container has its own cache with shared nothing. If initial download time or disk capacity are issues, consider using Volume Services.

Building from Source

Build and Test

# Build the application
./mvnw clean package

# Run all tests
./mvnw test

# Run with development profile
./mvnw spring-boot:run

Creating Docker Images

Kagami supports creating optimized Docker images using Spring Boot's buildpacks integration:

# Create Docker image using buildpacks (requires Docker)
./mvnw spring-boot:build-image

# The generated image will be tagged as: kagami:0.0.1-SNAPSHOT
# You can run it with:
docker run --pull always -p 8080:8080 \
  -v /tmp/kagami:/var/kagami/storage \
  -e kagami.storage.path=/var/kagami/storage \
  -e kagami.repositories.central.url=https://repo.maven.apache.org/maven2 \
  kagami:0.0.1-SNAPSHOT

# Custom image name and tag
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myregistry/kagami:latest

Logging

Enable debug logging for troubleshooting:

logging.level.am.ik.kagami=DEBUG
logging.level.org.eclipse.aether=DEBUG
# For security troubleshooting:
logging.level.org.springframework.security=DEBUG

Health Check

The application provides health check endpoints via Spring Actuator:

# Health status
curl http://localhost:8080/actuator/health

# Prometheus metrics
curl http://localhost:8080/actuator/prometheus

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

About

A simple Maven repository mirror server built with Spring Boot

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages