Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KeyKosh Java SDK (com.k2platform:k2-sdk-java)

A client for reading configuration from a self-hosted KeyKosh (K2) platform. The core has one runtime dependency (Jackson) and uses the JDK's java.net.http. Spring support is optional and lives in the same jar — plain-JVM apps never pull Spring transitively. Java 17+.

The SDK talks to exactly one host — your own k2-app. There is no vendor default URL and no callback home; baseUrl is required.

Requirements

Minimum
Core K2Client (Mode 3, plain JVM) Java 17
Spring modes (Modes 1 & 2) Java 17 — Spring Boot 3 is itself 17+

The jar is compiled with --release 17, so its class files are major version 61 and load on any Java 17 or newer JVM. The floor is enforced at build time: maven-enforcer-plugin fails the build if the target is raised, since raising it is a breaking change for every consumer on 17.

The two halves are worth stating separately, because only one is really a "floor" question:

  • The core client uses no language feature newer than Java 16 (records, pattern-matching instanceof), so 17 is simply the nearest supported LTS — see CHANGELOG.md → Java baseline.
  • The optional Spring integration compiles against Spring Boot 3, which is itself Java 17+. Spring mode therefore requires 17 regardless of what the core could support, and lowering the core baseline would not move this number.

On Java 17–20, use 1.1.2 or later. Both 1.1.0 and 1.1.1 were published with Java 21 bytecode (1.1.1 only partially — 20 of 35 classes, including K2Client) and fail at class load with UnsupportedClassVersionError. 1.1.2 is verified major 61 throughout.

Install

<dependency>
  <groupId>com.k2platform</groupId>
  <artifactId>k2-sdk-java</artifactId>
  <version>1.1.2</version>
</dependency>

Three ways to use it

Mode For What you write
1. PropertySource (Spring) Spring Boot apps — replace application.yml nothing in code; 2 bootstrap props
2. @K2Config proxy (Spring) typed, live-refreshing config beans one interface
3. Core K2Client plain JVM / batch / non-Spring direct calls

Mode 1 — K2 as a high-precedence Spring PropertySource (the headline)

K2 replaces application.yml as the source of truth. Just add the dependency and set the bootstrap trio; every existing @Value("${...}") / @ConfigurationProperties resolves from K2 first, with application.yml as fallback. No code change.

# application.yml — now just bootstrap + fallback defaults
k2:
  base-url: http://localhost:8080      # https://k2.acme.com in prod
  env: prod
  property-source:
    precedence: highest                # highest = beats application.yml AND -D/OS env
                                       # above-application-yaml = operators keep -D/env override
db:
  url: jdbc:postgresql://localhost/app # FALLBACK only — K2's "db.url" wins on a name match
export K2_TOKEN=k2_live_...            # the one secret — never commit it
@Service
class MyService {
    @Value("${db.url}") String dbUrl;  // ← resolved FROM K2 at startup; falls back to yaml
}

Hot-reload note: the K2PropertySource is refreshed live (see Hot reload), but @Value / @ConfigurationProperties bind once at startup and keep their initial value — that's Spring's injection model. For values that must follow a live change, read through Environment or use Mode 2. If k2-app is unreachable at startup, Mode 1 boots from k2config-<env>.json (then application.yml defaults) — it never blocks boot.

Mode 2 — @K2Config typed proxy (live per-call)

For the handful of properties you need live without a refresh:

@K2Config(prefix = "feature")
public interface FeatureFlags {
    @K2ConfigProperty(key = "x", defaultValue = "false")
    boolean x();                       // reads the K2 cache on every call
}

@SpringBootApplication
@EnableK2Config(basePackages = "com.acme.config")
public class MyApp { }
@Service
class Gate {
    private final FeatureFlags flags;
    Gate(FeatureFlags flags) { this.flags = flags; }   // injected like any bean
}

Method→key: explicit key(), else getXxx/isXxxxxx, else camelCase → dotted; prefixed with @K2Config.prefix. Return types String/int/long/boolean/double are coerced; defaultValue applies when the key is absent.

In both Spring modes the bootstrap props bind from your existing application.yml (and K2_BASE_URL/ K2_TOKEN/K2_ENV env vars via Spring relaxed binding) — no separate k2.yml, and you never declare an app name (the token carries app+env scope).


Mode 3 — core K2Client (plain JVM, no Spring)

K2Client k2 = K2Client.builder()
        .baseUrl("http://localhost:8080")   // your K2 URL — https://k2.acme.com in prod
        .token("k2_live_...")               // SDK token from the admin UI (Tokens tab)
        .build();

// Full config for an environment
K2Configuration cfg = k2.getConfiguration("prod");
String dbUrl = cfg.getString("db.url", "jdbc:postgresql://localhost/app");
int    pool  = cfg.getInt("db.pool", 10);
boolean flag = cfg.getBoolean("feature.x", false);

// Or a single property
Object value = k2.getProperty("prod", "db.url");

The token is sent as both Authorization: Bearer <token> and X-API-Token: <token>, so it works regardless of which header your platform build expects. The token's environment scope is enforced server-side.

API mapping

SDK call Endpoint
getConfiguration(env) GET /api/config/token/{env}/current
getProperty(env, key) GET /api/config/token/{env}/properties/{key}

K2Exception carries a stable getCode() (a K2ErrorCode — branch on this, not on the message) plus getStatusCode(): 401/403 token rejected, 404 config not found, 421 baseUrl host not licensed (must match the platform's K2_PUBLIC_HOST), -1 transport/config/file failure. isAvailabilityError() is true only for K2_UNREACHABLE, K2_TIMEOUT and K2_SERVER_ERROR — the codes eligible for the local file. K2ErrorCode.kind() groups the codes as CONFIG / FILE / AUTH / AVAILABILITY / OTHER; K2_REQUEST_FAILED moved AUTHOTHER in 1.1.1 (classification only — it has never served the local file).

Credentials

Precedence: .token(...)K2_TOKEN.tokenFile(...) / k2.token-file / K2_TOKEN_FILE.tokenEnc(...) / K2_TOKEN_ENC. A token file is the Docker/Kubernetes secret-mount shape: its contents are stripped of surrounding whitespace (mounted secrets end in a newline) and read once, at token resolution — not per request. A missing, unreadable or blank file throws K2_TOKEN_FILE_UNREADABLE naming the path, never a silent fallthrough to "no token". Supported by the core client since 1.1.1; before that only the Spring integration read it. The Node and Python SDKs honour the same variable from 1.2.0.

Configuring from the environment

K2Client k2 = K2Client.fromEnv();          // everything from K2_*
K2Client k2b = K2Client.builderFromEnv()   // ... or override before building
        .requestTimeout(Duration.ofSeconds(30))
        .build();

New in 1.1.1. fromEnv() is the Mode-3 counterpart of Node's createClient() and Python's K2Client.from_env(): it reads K2_BASE_URL, K2_TOKEN, K2_TOKEN_FILE, K2_TOKEN_ENC, K2_ENV, K2_ORG, K2_APP, K2_OFFLINE, K2_OFFLINE_CACHE, K2_HOT_RELOAD, K2_CONFIG_DIR, K2_CONFIG_FILE, K2_OFFLINE_MAX_AGE, K2_CACHE_TTL and K2_STS_ENABLED — the same set as the other two SDKs, blank counting as unset. builder() remains how you configure a client in code; fromEnv() is how you configure one from a container's environment, and a builder() client reads no environment variable.

fromEnv() does not honour the deprecated K2_SOURCE / K2_CACHE_DIR aliases (Node and Python still do, for one more minor release). The core never read either, so nothing regresses. Use K2_OFFLINE and K2_CONFIG_DIR.

Cross-language note: Node and Python raise K2Error (not K2Exception), have no K2ErrorCode.Kind, have no snapshot(env) or getOfflineCacheAllowed(), honour the deprecated env aliases this SDK's fromEnv() drops, and are zero-dependency where this SDK needs Jackson. Codes and variable names are otherwise identical across all three. See USER_MANUAL.md → Cross-language differences.

Resilience & config knobs

Concern Builder Spring property Default
Token from a mounted secret file .tokenFile(path) k2.token-file / K2_TOKEN_FILE
TTL cache (collapses repeat reads) .cacheTtl(Duration) k2.cache.ttl-seconds 300s (Spring) / off (core)
Never contact the server .offline(true) k2.offline false
Keep a local config file at all .offlineCache(false) k2.offline-cache true
Hot reload over SSE .hotReload(false) k2.hot-reload on when online
Local file location .configDir(...) / .configFile(...) k2.config-dir, k2.config-file ~/.k2/config
Hard staleness limit .offlineMaxAge(Duration) k2.offline-max-age none
PropertySource on/off + precedence k2.property-source.enabled, k2.property-source.precedence enabled, highest

k2config-<env>.json is written on every successful fetch and read only on an availability failure (unreachable, timeout, or 5xx) — 401/403/404/421 always surface, because an auth failure is not an outage. k2.offline=true inverts this: the file becomes the source of truth and no server is ever contacted.

The file is plaintext and holds secret values in clear. Gitignore k2config-*.json.

Hot reload

AutoCloseable sub = client.watch("prod", cfg -> pool.resize(cfg.getInt("pool.size", 10)));

Subscribes over Server-Sent Events and fires when an admin edits config — no polling. The stream carries a signal, not values, so the SDK re-fetches on each event (secrets never sit on a long-lived connection). Reconnects with backoff, and falls back to polling on cacheTtl if the platform predates the stream or a proxy strips SSE.

Under Spring, K2ConfigWatcher does this for you and swaps fresh values into the K2PropertySource. Note: a field injected with @Value is resolved once at bean creation and keeps its original value — that is Spring's injection model, not an SDK limitation. @K2Config proxies and Environment reads do track live changes.

Try it against a running platform

K2_BASE_URL=http://localhost:8080 \
K2_TOKEN=k2_live_xxx \
K2_ENV=prod \
mvn -q exec:java -Dexec.mainClass=com.k2platform.sdk.K2SdkDemo

Add K2_KEY=some.property to also fetch a single property. Mint the token first in the admin UI: log in → workspace → Tokens → generate, scoped to the app + env.

Build & test

mvn clean test     # 50 tests — model, HTTP (in-JVM fake server), config file, SSE, both Spring modes
mvn package        # builds the jar

All tests run offline against an in-process fake k2-app (FakeK2Server) — no network, no external platform.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages