InClusterApiServer.java
package io.github.baokhang83.mnemo.warden.agent.resize;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.function.Function;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
/**
* Coordinates for reaching the Kubernetes API server from inside the pod, using the credentials
* every pod is given automatically — no RBAC beyond what {@link PodResizeClient}'s own
* calls need, and no external HTTP client library.
*
* <p>Verified against a real pod: {@code KUBERNETES_SERVICE_HOST}/{@code
* KUBERNETES_SERVICE_PORT} are injected by the kubelet into every pod's environment, and the
* bound service-account token, its namespace, and the cluster CA certificate are projected at
* {@code /var/run/secrets/kubernetes.io/serviceaccount} (see {@link #SERVICE_ACCOUNT_DIR}) by
* the default {@code kube-api-access-*} volume — no explicit {@code volumeMounts} needed
* in the pod spec.
*
* <p>{@link #bearerToken()} re-reads the token file on every call rather than caching it: bound
* service-account tokens are time-limited (an hour, by default — confirmed via a real
* token's {@code expirationSeconds: 3607}) and the kubelet rotates the file in place before
* expiry, so a long-lived sidecar that cached the token at startup would eventually start sending
* an expired one. This rotation-survival behavior is standard, documented kubelet behavior, but
* wasn't independently observed over a full rotation window here.
*/
public final class InClusterApiServer {
private static final Path SERVICE_ACCOUNT_DIR = Path.of("/var/run/secrets/kubernetes.io/serviceaccount");
private static final String ENV_HOST = "KUBERNETES_SERVICE_HOST";
private static final String ENV_PORT = "KUBERNETES_SERVICE_PORT";
private final String host;
private final int port;
private final String namespace;
private final Path tokenFile;
private final SSLContext trust;
InClusterApiServer(String host, int port, String namespace, Path tokenFile, SSLContext trust) {
this.host = host;
this.port = port;
this.namespace = namespace;
this.tokenFile = tokenFile;
this.trust = trust;
}
/** Reads the standard in-cluster environment and mounted service-account volume. */
public static InClusterApiServer discover() throws IOException, GeneralSecurityException {
return discover(System::getenv, SERVICE_ACCOUNT_DIR);
}
/**
* Package-private seam so tests can supply a fake environment and a fake service-account
* directory (e.g. a {@code @TempDir} with a throwaway self-signed cert) instead of the real
* in-pod paths, which don't exist outside a real pod.
*/
static InClusterApiServer discover(Function<String, String> env, Path serviceAccountDir)
throws IOException, GeneralSecurityException {
String host = requireEnv(env, ENV_HOST);
int port = Integer.parseInt(requireEnv(env, ENV_PORT));
String namespace = Files.readString(serviceAccountDir.resolve("namespace")).trim();
Path tokenFile = serviceAccountDir.resolve("token");
SSLContext trust = trustingClusterCa(serviceAccountDir.resolve("ca.crt"));
return new InClusterApiServer(host, port, namespace, tokenFile, trust);
}
/** The namespace this pod runs in. */
public String namespace() {
return namespace;
}
/** The API server's base URI, e.g. {@code https://10.96.0.1:443}. */
public URI baseUri() {
return URI.create("https://" + host + ":" + port);
}
/** The current bearer token, read fresh from disk (see class javadoc on why it isn't cached). */
public String bearerToken() throws IOException {
return Files.readString(tokenFile).trim();
}
/** An {@link SSLContext} that trusts only the cluster's own CA, not the JDK's public trust store. */
public SSLContext sslContext() {
return trust;
}
private static String requireEnv(Function<String, String> env, String name) {
String value = env.apply(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException(name + " is not set; is this process running in a pod?");
}
return value;
}
private static SSLContext trustingClusterCa(Path caCertFile) throws IOException, GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate caCert;
try (InputStream in = Files.newInputStream(caCertFile)) {
caCert = (X509Certificate) certificateFactory.generateCertificate(in);
}
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setCertificateEntry("cluster-ca", caCert);
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
}