Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created kubernetes client from openapi spec #762

Merged
merged 16 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
<!-- files="DefaultBeanContext.java|BeanDefinitionWriter.java|DefaultHttpClient.java"/> -->

<suppress checks="MissingJavadocType" files=".*doc-examples.*" />
<suppress checks="." files="generated[\\/]openapi[\\/]" />
</suppressions>
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ githubSlug=micronaut-projects/micronaut-kubernetes
developers=Alvaro Sanchez-Mariscal, Pavol Gressa

org.gradle.caching=true
org.gradle.jvmargs=-Xmx2g

systemProp.maxYamlCodePoints=10485760
10 changes: 8 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
micronaut = "4.5.4"
micronaut-platform = "4.5.1"
micronaut = "4.6.5"
micronaut-platform = "4.6.2"
micronaut-docs = "2.0.0"
micronaut-test = "4.1.0"
micronaut-gradle-plugin = "4.4.3"
Expand All @@ -13,12 +13,15 @@ shadow = '8.0.0'
system-lambda = '1.2.1'
io-kubernetes-client-java = '19.0.1'
io-fabric8-kubernetes-client = '6.13.4'
netty-incubator-codec-http3 = '0.0.28.Final'
testcontainers-k3s = '1.20.2'

micronaut-discovery = "4.3.0"
micronaut-logging = "1.1.2"
micronaut-reactor = "3.4.1"
micronaut-serde = '2.10.2'
micronaut-validation = '4.6.1'
micronaut-openapi = '6.11.1'

[libraries]
# Core
Expand All @@ -34,10 +37,13 @@ system-lambda = { module = "com.github.stefanbirkner:system-lambda", version.ref
io-kubernetes-client-java = { module = "io.kubernetes:client-java", version.ref = "io-kubernetes-client-java" }
io-kubernetes-client-java-extended = { module = "io.kubernetes:client-java-extended", version.ref = "io-kubernetes-client-java" }
io-fabric8-kubernetes-client = { module = "io.fabric8:kubernetes-client", version.ref = "io-fabric8-kubernetes-client" }
netty-incubator-codec-http3 = { module = "io.netty.incubator:netty-incubator-codec-http3", version.ref = "netty-incubator-codec-http3" }
testcontainers-k3s = { module = "org.testcontainers:k3s", version.ref = "testcontainers-k3s" }

micronaut-reactor = { module = 'io.micronaut.reactor:micronaut-reactor-bom', version.ref = "micronaut-reactor" }
micronaut-serde = { module = 'io.micronaut.serde:micronaut-serde-bom', version.ref = 'micronaut-serde'}
micronaut-validation = { module = 'io.micronaut.validation:micronaut-validation-bom', version.ref = 'micronaut-validation'}
micronaut-openapi = { module = 'io.micronaut.openapi:micronaut-openapi-bom', version.ref = 'micronaut-openapi'}

micronaut-discovery-client = { module = "io.micronaut.discovery:micronaut-discovery-client", version.ref = "micronaut-discovery" }

Expand Down
63 changes: 63 additions & 0 deletions kubernetes-client-openapi/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
plugins {
id 'io.micronaut.build.internal.kubernetes-module'
id 'io.micronaut.openapi' version libs.versions.micronaut.gradle.plugin
}

micronautBuild {
binaryCompatibility {
enabled.set(false)
}
}

micronaut {
openapi {
client(layout.buildDirectory.file("openapi.yaml").get().asFile) {
apiPackageName = "io.micronaut.kubernetes.client.openapi.api"
modelPackageName = "io.micronaut.kubernetes.client.openapi.model"
clientId = "kubernetes-client"
useReactive = false
}
}
}

dependencies {
annotationProcessor(mnValidation.micronaut.validation.processor)
annotationProcessor(mnSerde.micronaut.serde.processor)
annotationProcessor(mn.micronaut.inject.java)
implementation(mnValidation.micronaut.validation)
implementation(mnSerde.micronaut.serde.jackson)
implementation(mnOpenapi.micronaut.openapi)
compileOnly(mn.micronaut.http.client)
compileOnly(mn.micronaut.json.core)
compileOnly(libs.netty.incubator.codec.http3) // ClientSslBuilderImpl doesn't compile without it
testImplementation(libs.testcontainers.k3s)
testImplementation(mn.micronaut.http.client)
testImplementation(mn.micronaut.http.server.netty)
}

tasks.register("prepareOpenapiSpec") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melix can you review this Gradle logic?

inputs.property("kubernetes-client-version", libs.versions.io.kubernetes.client.java)
def outputFile = layout.buildDirectory.file("openapi.yaml")
doLast {
def clientVersion = inputs.getProperties().get("kubernetes-client-version")
def uri = uri("https://raw.githubusercontent.com/kubernetes-client/java/refs/tags/v${clientVersion}/kubernetes/api/openapi.yaml")
try (BufferedReader reader = new BufferedReader(new InputStreamReader(uri.toURL().openStream()))
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile.get().asFile)))) {
println "Downloading kubernetes client spec file: ${uri}"
String line
while ((line = reader.readLine()) != null) {
if (line.contains("x-implements")) {
// skip lines which contains x-implements because we don't want that generated classes implement
// io.kubernetes.client.common.KubernetesObject and io.kubernetes.client.common.KubernetesListObject
reader.readLine() // skip one more line because it contains KubernetesObject interface
} else {
writer.writeLine(line)
}
}
}
}
outputs.file(outputFile)
}

generateClientOpenApiApis.dependsOn("prepareOpenapiSpec")
generateClientOpenApiModels.dependsOn("prepareOpenapiSpec")
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.kubernetes.client.openapi;

import io.micronaut.buffer.netty.NettyByteBufferFactory;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Context;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.io.ResourceResolver;
import io.micronaut.http.MediaType;
import io.micronaut.http.bind.DefaultRequestBinderRegistry;
import io.micronaut.http.body.ContextlessMessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.client.DefaultHttpClientConfiguration;
import io.micronaut.http.client.LoadBalancer;
import io.micronaut.http.client.filter.ClientFilterResolutionContext;
import io.micronaut.http.client.filter.DefaultHttpClientFilterResolver;
import io.micronaut.http.client.netty.DefaultHttpClient;
import io.micronaut.http.client.netty.NettyClientCustomizer;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.netty.body.NettyByteBufMessageBodyHandler;
import io.micronaut.http.netty.body.NettyCharSequenceBodyWriter;
import io.micronaut.http.netty.body.NettyJsonHandler;
import io.micronaut.http.netty.body.NettyJsonStreamHandler;
import io.micronaut.http.netty.body.NettyWritableBodyWriter;
import io.micronaut.json.JsonMapper;
import io.micronaut.json.codec.JsonMediaTypeCodec;
import io.micronaut.json.codec.JsonStreamMediaTypeCodec;
import io.micronaut.kubernetes.client.openapi.config.KubeConfigLoader;
import io.micronaut.kubernetes.client.openapi.ssl.KubernetesClientSslBuilder;
import io.micronaut.kubernetes.client.openapi.ssl.KubernetesPrivateKeyLoader;
import io.micronaut.kubernetes.client.openapi.config.KubeConfig;
import io.micronaut.kubernetes.client.openapi.config.KubernetesClientConfiguration;
import io.micronaut.runtime.ApplicationConfiguration;
import io.micronaut.websocket.context.WebSocketBeanRegistry;
import io.netty.channel.Channel;
import io.netty.channel.MultithreadEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

import java.net.URI;
import java.util.Collections;

/**
* Factory for kubernetes http client.
*/
@Factory
@Context
@Internal
@BootstrapContextCompatible
@Requires(beans = KubernetesClientConfiguration.class)
final class KubernetesHttpClientFactory {
static final String CLIENT_ID = "kubernetes-client";

private final KubeConfig kubeConfig;
private final KubernetesPrivateKeyLoader kubernetesPrivateKeyLoader;
private final ResourceResolver resourceResolver;
private final DefaultHttpClientFilterResolver defaultHttpClientFilterResolver;

KubernetesHttpClientFactory(KubeConfigLoader kubeConfigLoader,
KubernetesPrivateKeyLoader kubernetesPrivateKeyLoader,
ResourceResolver resourceResolver,
DefaultHttpClientFilterResolver defaultHttpClientFilterResolver) {
kubeConfig = kubeConfigLoader.getKubeConfig();
this.kubernetesPrivateKeyLoader = kubernetesPrivateKeyLoader;
this.resourceResolver = resourceResolver;
this.defaultHttpClientFilterResolver = defaultHttpClientFilterResolver;
}

@Singleton
@Named(CLIENT_ID)
@BootstrapContextCompatible
protected DefaultHttpClient getKubernetesHttpClient() {
URI uri = URI.create(kubeConfig.getCluster().server());

return new DefaultHttpClient(LoadBalancer.fixed(uri),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will replace this with http client builder once micronaut-core 4.7.0 gets released

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this code now use HttpClientRegistry.getClient? Maybe we need to enhance that API to avoid references to these internal APIs? @yawkat WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpClientRegistry.getClient can't be used since we need a custom implementation of NettyClientSslBuilder (which is KubernetesClientSslBuilder). Once micronaut-core 4.7.0 gets released entire constructor will be replaced with just a few lines where we set KubernetesClientSslBuilder

null,
new DefaultHttpClientConfiguration(),
null,
defaultHttpClientFilterResolver,
defaultHttpClientFilterResolver.resolveFilterEntries(new ClientFilterResolutionContext(Collections.singletonList(CLIENT_ID), null)),
new DefaultThreadFactory(MultithreadEventLoopGroup.class),
new KubernetesClientSslBuilder(resourceResolver, kubeConfig, kubernetesPrivateKeyLoader),
createDefaultMediaTypeRegistry(),
createDefaultMessageBodyHandlerRegistry(),
WebSocketBeanRegistry.EMPTY,
new DefaultRequestBinderRegistry(ConversionService.SHARED),
null,
NioSocketChannel::new,
NioDatagramChannel::new,
new NettyClientCustomizer() {
@Override
public @NonNull NettyClientCustomizer specializeForChannel(@NonNull Channel channel, @NonNull ChannelRole role) {
return NettyClientCustomizer.super.specializeForChannel(channel, role);
}
},
null,
ConversionService.SHARED,
null);
}

private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() {
JsonMapper mapper = JsonMapper.createDefault();
ApplicationConfiguration configuration = new ApplicationConfiguration();
return MediaTypeCodecRegistry.of(
new JsonMediaTypeCodec(mapper, configuration, null),
new JsonStreamMediaTypeCodec(mapper, configuration, null)
);
}

private static MessageBodyHandlerRegistry createDefaultMessageBodyHandlerRegistry() {
ApplicationConfiguration applicationConfiguration = new ApplicationConfiguration();
ContextlessMessageBodyHandlerRegistry registry = new ContextlessMessageBodyHandlerRegistry(
applicationConfiguration,
NettyByteBufferFactory.DEFAULT,
new NettyByteBufMessageBodyHandler(),
new NettyWritableBodyWriter(applicationConfiguration)
);
JsonMapper mapper = JsonMapper.createDefault();
registry.add(MediaType.APPLICATION_JSON_TYPE, new NettyJsonHandler<>(mapper));
registry.add(MediaType.APPLICATION_JSON_TYPE, new NettyCharSequenceBodyWriter());
registry.add(MediaType.APPLICATION_JSON_STREAM_TYPE, new NettyJsonStreamHandler<>(mapper));
return registry;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would like to see how we can avoid this logic and recreating all these objects? //cc @yawkat

Copy link
Contributor Author

@msupic msupic Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All those will be removed once micronaut-core 4.7.0 gets released since that version contains http client builder implementation

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.kubernetes.client.openapi;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.annotation.ClientFilter;
import io.micronaut.http.annotation.RequestFilter;
import io.micronaut.kubernetes.client.openapi.config.KubeConfig;
import io.micronaut.kubernetes.client.openapi.config.KubeConfigLoader;
import io.micronaut.kubernetes.client.openapi.config.KubernetesClientConfiguration;
import io.micronaut.kubernetes.client.openapi.config.model.AuthInfo;
import io.micronaut.kubernetes.client.openapi.credential.KubernetesCredentialLoader;

/**
* Filter which sets the authorization request header with basic or bearer token
* if the client certificate authentication is not enabled.
*/
@ClientFilter(serviceId = KubernetesHttpClientFactory.CLIENT_ID)
@Requires(beans = KubernetesClientConfiguration.class)
@Internal
final class KubernetesHttpClientFilter {

private final KubeConfig kubeConfig;
private final KubernetesCredentialLoader kubernetesCredentialLoader;

KubernetesHttpClientFilter(KubeConfigLoader kubeConfigLoader,
KubernetesCredentialLoader kubernetesCredentialLoader) {
kubeConfig = kubeConfigLoader.getKubeConfig();
this.kubernetesCredentialLoader = kubernetesCredentialLoader;
}

@RequestFilter
void doFilter(MutableHttpRequest<?> request) {
AuthInfo user = kubeConfig.getUser();
if (user.clientCertificateData() != null && user.clientKeyData() != null) {
return;
}
if (StringUtils.isNotEmpty(user.username()) && StringUtils.isNotEmpty(user.password())) {
request.basicAuth(user.username(), user.password());
return;
}
String token = kubernetesCredentialLoader.getToken();
if (StringUtils.isEmpty(token)) {
token = user.token();
}
if (StringUtils.isNotEmpty(token)) {
request.bearerAuth(token);
}
}
}
Loading
Loading