8000 [DRAFT] Demo operator CRD with generated ServerConfiguration by andreaTP · Pull Request #11806 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions operator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,19 @@
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-operator-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>generate-server-config</id>
<goals>
<goal>keycloak-operator-codegen</goal>
Comment thread
pedroigor marked this conversation as resolved.
Resolved
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
Expand Down Expand Up @@ -222,6 +235,7 @@
<configuration>
<sources>
<source>${basedir}/target/keycloak-core</source>
<source>${basedir}/target/generated-sources/java</source>
</sources>
</configuration>
</execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.annotation.JsonPropertyDescription;

import org.keycloak.operator.Constants;
import org.keycloak.codegen.configuration.ServerConfig;
import org.keycloak.operator.v2alpha1.crds.keycloakspec.Unsupported;

import javax.validation.constraints.NotNull;
Expand All @@ -35,6 +36,8 @@ public class KeycloakSpec {
"expressed as a keys (reference: https://www.keycloak.org/server/all-config) and values that can be either direct values or references to secrets.")
private List<ValueOrSecret> serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316

private ServerConfig serverConfig;

@NotNull
@JsonPropertyDescription("Hostname for the Keycloak server.\n" +
"The special value `" + Constants.INSECURE_DISABLE + "` disables the hostname strict resolution.")
Expand Down
62 changes: 62 additions & 0 deletions quarkus/maven-plugin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ 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
~
~ http://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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-docs-parent</artifactId>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know it's just a demo but wanted to point out that we're inheriting from docs module for some reason. Is that intended?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not intended, will be removed if/when we bring this forward

<groupId>org.keycloak</groupId>
<version>999-SNAPSHOT</version>
<relativePath>../../operator/pom.xml</relativePath>
</parent>

<modelVersion>4.0.0</modelVersion>

<name>Keycloak Operator Maven Plugin</name>
<artifactId>keycloak-operator-maven-plugin</artifactId>
Comment thread
pedroigor marked this conversation as resolved.
Resolved
<description>Keycloak Operator Maven Plugin</description>
<packaging>maven-plugin</packaging>

<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-project</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-quarkus-server</artifactId>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
Comment thread
pedroigor marked this conversation as resolved.
Resolved
<version>3.24.2</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.keycloak.operator.codegen;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.utils.StringEscapeUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.Map;

import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;

@Mojo(name = "keycloak-operator-codegen", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class CodegenMojo extends AbstractMojo {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Was wondering if we should not have have it the other way around. Some POJO with few custom annotations on top of field as the source of truth. This would get translated into the mappers. Might be a bit cleaner than a maven plugin for basically generating classes on the fly?

In any case, might be worth exploring what Store.X team will be doing. As mentioned, I think it is related to this, so might make sense to align on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Was wondering if we should not have have it the other way around. Some POJO with few custom annotations on top of field as the source of truth.

This would require a pretty invasive change on how the CLI and configuration work as of today, I would like to hear from @pedroigor and @DGuhr

But, also, let's first open a GH "discussion" about the desired outcome and tackle the technical implementation as a separate subject.

@DGuhr DGuhr May 4, 2022 •
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My PoC actually had a POJO inside that could be used/extended as source of truth (the pojo is actually applicable to all sorts of "PortConfiguration", so the actual configurationkey POJO would extend it and add e.g. name, description, and so on), and I am pretty paranoid due to bad experiences when it comes to code generators, so +1 from my side. Nevertheless I totally agree this is more invasive, needs a bit of boilerplate work and should be discussed first.

For the problem space / the discussion part, a primer:
e.g. I thought the goal here was not only for the operator to get the config classes, but to extract a configuration package with its own api/interface, that could be used by any frontend, be it tools like https://github.com/adorsys/keycloak-config-cli or "I want to write my own keycloak configuration service using e.g. jbang", or "i want to set keycloak configuration using an existing tool like spring cloud config server" or even "I want to have an intellij / vscode plugin to show me when I have an invalid configuration even before starting up anything, so immediately when typing e.g. proxy=reencrypt, http-enabled=true". This, together with a --no-validate flag or sth around these lines that skips validation at startup bc. "hey, i know what i am doing and it was validated beforehand", would lead to the outcome that configuring keycloak would be much more flexible to do with whatever tool you want, would be decoupled from the actual runtime keycloak lives in, as the config is "self-contained" (given we also abstract away the "to" part from the mappers, this could even lead to a wildfly configuration frontend ;) ) bc validating could be done without even invoking keycloak, so at startup we would have nearly no impact on performance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@DGuhr +1 to that it'd also be beneficial in general – decoupling our configuration logic from Quarkus.


@Parameter(property = "operator.codegen.target", defaultValue = "${basedir}/target/generated-sources/java")
private File target;

private Map<String, Set<Property>> configurations = new HashMap<>();

private void addKeyValue(String k, Property v) {
if (configurations.containsKey(k)) {
Set<Property> tmpValue = configurations.get(k);
tmpValue.add(v);
configurations.put(k, tmpValue);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a nitpick (for when this is not a demo anymore :)). I believe you don’t need to put the Set back again into the Map. It's already a reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yup

} else {
Set<Property> configs = new HashSet<>();
configs.add(v);
configurations.put(k, configs);
}
}

private void writeToFile(File file, String str) throws IOException {
try (FileWriter fileWriter = new FileWriter(file);
PrintWriter printWriter = new PrintWriter(fileWriter)) {
printWriter.println(str);
}
}

@Override
public void execute() throws MojoFailureException {
try {
Log log = getLog();

// TODO: if custom image should we remove build time configurations?
PropertyMappers.getMappers().forEach(c -> {
addKeyValue(
c.getCategory().getHeading(),
new Property(
c.getCliFormat().replaceFirst("--", ""),
c.getType().getCanonicalName(),
c.getDescription())
);
});

Map<String, CompilationUnit> cus = new HashMap<>();

CompilationUnit cu = new CompilationUnit();
cu.setPackageDeclaration("org.keycloak.codegen.configuration");

ClassOrInterfaceDeclaration serverConfig = cu.addClass("ServerConfig");
cus.put("ServerConfig", cu);

Path destination = Paths.get(target.getAbsolutePath(), "org", "keycloak", "codegen", "configuration");
destination.toFile().mkdirs();

configurations.forEach((k, v) -> {
String className = k.replace("/", "_");
serverConfig.addField(className, className.toLowerCase(Locale.ROOT));

CompilationUnit innerCu = new CompilationUnit();
innerCu.setPackageDeclaration("org.keycloak.codegen.configuration");
ClassOrInterfaceDeclaration clazz = innerCu.addClass(className);
cus.put(className, innerCu);

v.forEach(p -> {
String fieldName = replaceNonAlphanumericByUnderscores(p.getName());

if (!fieldName.equals("null")) { // edge case in Proxy configuration
FieldDeclaration field = cl 10BC8 azz.addField(p.getType(), fieldName, Modifier.Keyword.PUBLIC);
field.addSingleMemberAnnotation(
JsonProperty.class,
new StringLiteralExpr(p.getName()));
if (p.getDescription() != null) {
field.addSingleMemberAnnotation(
JsonPropertyDescription.class,
new StringLiteralExpr(StringEscapeUtils.escapeJava(p.getDescription())));
}
}
});
});

cus.forEach((k, v) -> {
try {
writeToFile(destination.resolve(k + ".java").toFile(), v.toString());
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
throw new MojoFailureException("Failed to generate code", e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.keycloak.operator.codegen;

import java.util.Objects;

public class Property {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if this type should be added to the configuration API itself. We are using a similar structure when generating docs. See

.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was wondering if I should try to re-use the docs generation, but I have no problems in including Property/Options as an extra class in the org.keycloak.quarkus.runtime.configuration package.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think that would be awesome but I don't think it is a blocker.

private final String name;
private final String type;
private final String description;

public Property(String name, String type, String description) {
this.name = name;
this.type = type;
this.description = description;
}

public String getName() {
return name;
}

public String getType() {
return type;
}

public String getDescription() {
return description;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Property property = (Property) o;
return Objects.equals(name, property.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}
1 change: 1 addition & 0 deletions quarkus/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
<module>server</module>
<module>dist</module>
<module>tests</module>
<module>maven-plugin</module>
</modules>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ public static CommandLine createCommandLine(List<String> cliArgs) {

CommandLine cmd = new CommandLine(spec);

// TODO: check if really needed
for (PropertyMapper mapper: PropertyMappers.getMappers()) {
if (mapper.getConverter() != null) {
cmd.registerConverter(mapper.getType(), PicocliConverterAdapter.build(mapper.getConverter()));
}
}

cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setHelpFactory(new HelpFactory());
Expand Down Expand Up @@ -408,15 +415,21 @@ private static void addMappedOptionsToArgGroups(CommandSpec cSpec, List<Property
String defaultValue = mapper.getDefaultValue();
Iterable<String> expectedValues = mapper.getExpectedValues();

argGroupBuilder.addArg(OptionSpec.builder(name)
.defaultValue(defaultValue)
.description(description)
.paramLabel(mapper.getParamLabel())
.completionCandidates(expectedValues)
.parameterConsumer(PropertyMapperParameterConsumer.INSTANCE)
.type(String.class)
.hidden(mapper.isHidden())
.build());
OptionSpec.Builder optBuilder = OptionSpec.builder(name)
.defaultValue(defaultValue)
.description(description)
.paramLabel(mapper.getParamLabel())
.completionCandidates(expectedValues)
.type(mapper.getType())
.hidden(mapper.isHidden());

if (mapper.getConverter() == null) {
optBuilder.parameterConsumer(PropertyMapperParameterConsumer.INSTANCE);
} else {
optBuilder.converters(PicocliConverterAdapter.build(mapper.getConverter()));
}

argGroupBuilder.addArg(optBuilder.build());
}

cSpec.addArgGroup(argGroupBuilder.build());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.keycloak.quarkus.runtime.cli;

import picocli.CommandLine;

import java.util.function.Function;

public class PicocliConverterAdapter {

public static class ConverterAdapter<T> implements CommandLine.ITypeConverter<T> {
private final Function<String, T> conversionFn;

public ConverterAdapter(Function<String, T> fn) {
this.conversionFn = fn;
}

@Override
public T convert(String s) throws Exception {
try {
return this.conversionFn.apply(s);
} catch (Exception e) {
throw new CommandLine.TypeConversionException(e.getMessage());
}
}
}

public static <T> CommandLine.ITypeConverter<T> build(Function<String, T> fn) {
return new ConverterAdapter<T>(fn);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ private static BiFunction<String, ConfigSourceInterceptorContext, String> toData
};
}

private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(ConfigCategory.DATABASE);
private static <T> PropertyMapper.Builder<T> builder() {
return PropertyMapper.<T> builder(ConfigCategory.DATABASE);
}

private static String resolveUsername(String value, ConfigSourceInterceptorContext context) {
Expand Down
Loading
0