Skip to content

feat(crd-generator): Add support for WebhookConversion and NonConversion #5875

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

Closed
wants to merge 4 commits into from
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* 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.
*/
package io.fabric8.crd.generator;

import io.fabric8.crd.generator.annotation.NoneConversion;
import io.fabric8.crd.generator.annotation.WebhookConversion;
import io.fabric8.kubernetes.client.CustomResource;
import io.sundr.utils.Strings;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Optional;
import java.util.StringJoiner;

import javax.lang.model.element.TypeElement;

public class CustomResourceConversionInfo {

private static final String ERROR_MSG_WEBHOOKCONVERSION_PREFIX = "Invalid WebhookConversion configuration: ";
private final Strategy strategy;
private final String[] conversionReviewVersions;
private final String url;
private final String serviceName;
private final String serviceNamespace;
private final String servicePath;
private final Integer servicePort;

private CustomResourceConversionInfo(Strategy strategy,
String[] conversionReviewVersions,
String url,
String serviceName,
String serviceNamespace,
String servicePath,
Integer servicePort) {
this.strategy = strategy;
this.conversionReviewVersions = conversionReviewVersions;
this.url = url;
this.serviceName = serviceName;
this.serviceNamespace = serviceNamespace;
this.servicePath = servicePath;
this.servicePort = servicePort;
}

public Strategy getStrategy() {
return strategy;
}

public String[] getConversionReviewVersions() {
return conversionReviewVersions;
}

public String getUrl() {
return url;
}

public String getServiceName() {
return serviceName;
}

public String getServiceNamespace() {
return serviceNamespace;
}

public String getServicePath() {
return servicePath;
}

public Integer getServicePort() {
return servicePort;
}

@Override
public String toString() {
return new StringJoiner(", ", CustomResourceConversionInfo.class.getSimpleName() + "[", "]")
.add("strategy:'" + strategy + "'")
.add("conversionsReviewVersions:" + Arrays.toString(conversionReviewVersions))
.add("url: '" + url + "'")
.add("serviceName: '" + serviceName + "'")
.add("serviceNamespace: '" + serviceNamespace + "'")
.add("servicePath: '" + servicePath + "'")
.add("servicePort: " + servicePort)
.toString();
}

public static Optional<CustomResourceConversionInfo> from(Class<? extends CustomResource<?, ?>> customResource) {
return from(customResource.getAnnotation(NoneConversion.class),
customResource.getAnnotation(WebhookConversion.class));
}

public static Optional<CustomResourceConversionInfo> from(TypeElement customResource) {
return from(customResource.getAnnotation(NoneConversion.class),
customResource.getAnnotation(WebhookConversion.class));
}

public static Optional<CustomResourceConversionInfo> from(
NoneConversion noneConversion, WebhookConversion webhookConversion) {
return Optional.ofNullable(Optional.ofNullable(noneConversion)
.map(CustomResourceConversionInfo::from)
.orElseGet(() -> Optional.ofNullable(webhookConversion)
.map(CustomResourceConversionInfo::from)
.orElse(null)));
}

public static CustomResourceConversionInfo from(NoneConversion noneConversion) {
return new CustomResourceConversionInfo(Strategy.None,
null, null, null, null, null, null);
}

public static CustomResourceConversionInfo from(WebhookConversion webhookConversion) {
final String[] versions = webhookConversion.versions();
final String url = mapNotEmpty(webhookConversion.url());
final String serviceName = mapNotEmpty(webhookConversion.serviceName());
final String serviceNamespace = mapNotEmpty(webhookConversion.serviceNamespace());
final String servicePath = mapNotEmpty(webhookConversion.servicePath());
final Integer servicePort = webhookConversion.servicePort() != 443 ? webhookConversion.servicePort() : null;

assertUniqueConversionReviewVersions(versions);
assertUrlOrService(url, serviceName, serviceNamespace, servicePath, servicePort);
assertValidServicePort(servicePort);
assertValidUrl(url);

return new CustomResourceConversionInfo(Strategy.Webhook,
versions,
url,
serviceName,
serviceNamespace,
servicePath,
servicePort);
}

public enum Strategy {
None,
Webhook
}

private static void assertUniqueConversionReviewVersions(String[] versions) {
if (Arrays.stream(versions).distinct().count() != versions.length) {
throw new IllegalArgumentException(
String.format("ConversionReviewVersions values must be distinct: %s", Arrays.toString(versions)));
}
}

private static void assertUrlOrService(String url,
String serviceName,
String serviceNamespace,
String servicePath,
Integer servicePort) {
if (url != null) {
// url
if (serviceName != null || serviceNamespace != null || servicePath != null || servicePort != null) {
throw new IllegalArgumentException(String.format(
ERROR_MSG_WEBHOOKCONVERSION_PREFIX
+ "Exactly one of URL or serviceNamespace/serviceName must be specified. "
+ "serviceNamespace: %s, serviceName: %s, servicePath: %s, servicePort: %s, URL: %s",
serviceNamespace, serviceName, servicePath, servicePort, url));
}
} else if (serviceName == null || serviceNamespace == null) {
// service
throw new IllegalArgumentException(String.format(
ERROR_MSG_WEBHOOKCONVERSION_PREFIX
+ "Exactly one of URL or serviceNamespace/serviceName must be specified. "
+ "serviceNamespace: %s, serviceName: %s, servicePath: %s, servicePort: %s, URL: null",
serviceNamespace, serviceName, servicePath, servicePort));
}
}

private static void assertValidServicePort(Integer servicePort) {
if (servicePort != null) {
if (servicePort < 1 || servicePort > 65535) {
throw new IllegalArgumentException(ERROR_MSG_WEBHOOKCONVERSION_PREFIX
+ "Service port must be a valid port number (1-65535, inclusive). ServicePort: " + servicePort);
}
}
}

private static void assertValidUrl(String urlString) {
if (urlString != null) {
try {
final URL url = new URL(urlString);
if (!"https".equals(url.getProtocol())) {
throw new IllegalArgumentException(
String.format(ERROR_MSG_WEBHOOKCONVERSION_PREFIX
+ "URL schema of %s is invalid. "
+ "Only https:// is allowed.", urlString));
}
if (url.getQuery() != null) {
throw new IllegalArgumentException(
String.format(ERROR_MSG_WEBHOOKCONVERSION_PREFIX + "URL %s contains query parameters "
+ "which are not allowed.", urlString));
}
if (url.getRef() != null) {
throw new IllegalArgumentException(
String.format(ERROR_MSG_WEBHOOKCONVERSION_PREFIX + "URL %s contains fragment(s) "
+ "which is not allowed.", urlString));
}
} catch (MalformedURLException e) {
throw new IllegalArgumentException(
String.format(ERROR_MSG_WEBHOOKCONVERSION_PREFIX + "Malformed URL: %s", e.getMessage()));
}
}
}

private static String mapNotEmpty(String s) {
if (Strings.isNullOrEmpty(s)) {
return null;
}
return s;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ public class CustomResourceInfo {

private final String[] annotations;
private final String[] labels;
private final CustomResourceConversionInfo conversionInfo;

public CustomResourceInfo(String group, String version, String kind, String singular,
String plural, String[] shortNames, boolean storage, boolean served, boolean deprecated, String deprecationWarning,
Scope scope, TypeDef definition, String crClassName,
String specClassName, String statusClassName, String[] annotations, String[] labels) {
public CustomResourceInfo(String group, String version, String kind,
String singular, String plural, String[] shortNames,
boolean storage, boolean served, boolean deprecated, String deprecationWarning,
Scope scope, TypeDef definition, String crClassName, String specClassName, String statusClassName,
String[] annotations, String[] labels, CustomResourceConversionInfo conversionInfo) {
this.group = group;
this.version = version;
this.kind = kind;
Expand All @@ -78,6 +80,7 @@ public CustomResourceInfo(String group, String version, String kind, String sing
this.hash = id.hashCode();
this.annotations = annotations;
this.labels = labels;
this.conversionInfo = conversionInfo;
}

public boolean storage() {
Expand Down Expand Up @@ -156,6 +159,10 @@ public String[] labels() {
return labels;
}

public Optional<CustomResourceConversionInfo> conversionInfo() {
return Optional.ofNullable(conversionInfo);
}

public static CustomResourceInfo fromClass(Class<? extends CustomResource<?, ?>> customResource) {
try {
final CustomResource<?, ?> instance = customResource.getDeclaredConstructor().newInstance();
Expand All @@ -176,13 +183,16 @@ public static CustomResourceInfo fromClass(Class<? extends CustomResource<?, ?>>
customResource.getCanonicalName());
}

final CustomResourceConversionInfo conversionInfo = CustomResourceConversionInfo.from(customResource)
.orElse(null);

return new CustomResourceInfo(instance.getGroup(), instance.getVersion(), instance.getKind(),
instance.getSingular(), instance.getPlural(), shortNames, instance.isStorage(), instance.isServed(),
instance.isDeprecated(), instance.getDeprecationWarning(),
scope, definition,
customResource.getCanonicalName(), specAndStatus.getSpecClassName(),
specAndStatus.getStatusClassName(), toStringArray(instance.getMetadata().getAnnotations()),
toStringArray(instance.getMetadata().getLabels()));
toStringArray(instance.getMetadata().getLabels()), conversionInfo);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw KubernetesClientException.launderThrowable(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.fabric8.crd.generator.v1.decorator.AddStatusSubresourceDecorator;
import io.fabric8.crd.generator.v1.decorator.AddSubresourcesDecorator;
import io.fabric8.crd.generator.v1.decorator.EnsureSingleStorageVersionDecorator;
import io.fabric8.crd.generator.v1.decorator.SetCustomResourceConversionDecorator;
import io.fabric8.crd.generator.v1.decorator.SetDeprecatedVersionDecorator;
import io.fabric8.crd.generator.v1.decorator.SetServedVersionDecorator;
import io.fabric8.crd.generator.v1.decorator.SetStorageVersionDecorator;
Expand Down Expand Up @@ -95,6 +96,9 @@ protected void addDecorators(CustomResourceInfo config, TypeDef def, Optional<St
resources.decorate(new EnsureSingleStorageVersionDecorator(name));
resources.decorate(new SortCustomResourceDefinitionVersionDecorator(name));
resources.decorate(new SortPrinterColumnsDecorator(name, version));

config.conversionInfo()
.ifPresent(conversionInfo -> resources.decorate(new SetCustomResourceConversionDecorator(name, conversionInfo)));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* 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.
*/
package io.fabric8.crd.generator.v1.decorator;

import io.fabric8.crd.generator.CustomResourceConversionInfo;
import io.fabric8.crd.generator.decorator.Decorator;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceConversion;
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionFluent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SetCustomResourceConversionDecorator
extends CustomResourceDefinitionDecorator<CustomResourceDefinitionFluent<?>> {

private static final Logger LOGGER = LoggerFactory.getLogger(SetCustomResourceConversionDecorator.class);

private final CustomResourceConversionInfo conversionInfo;

public SetCustomResourceConversionDecorator(String name, CustomResourceConversionInfo conversionInfo) {
super(name);
this.conversionInfo = conversionInfo;
}

@Override
public void andThenVisit(CustomResourceDefinitionFluent<?> item, ObjectMeta resourceMeta) {
assertConversionNotYetConfigured(item.buildSpec().getConversion(), resourceMeta);

switch (conversionInfo.getStrategy()) {
case None:
item.editSpec().withNewConversion()
.withStrategy(conversionInfo.getStrategy().name())
.endConversion()
.endSpec();
break;
case Webhook:
item.editSpec().withNewConversion()
.withStrategy(conversionInfo.getStrategy().name())
.withNewWebhook()
.withConversionReviewVersions(conversionInfo.getConversionReviewVersions())
.withNewClientConfig()
.withUrl(conversionInfo.getUrl())
.withNewService()
.withName(conversionInfo.getServiceName())
.withNamespace(conversionInfo.getServiceNamespace())
.withPath(conversionInfo.getServicePath())
.withPort(conversionInfo.getServicePort())
.endService()
.endClientConfig()
.endWebhook()
.endConversion()
.endSpec();
break;
default:
LOGGER.warn("Unknown CustomResourceConversion strategy: {}", conversionInfo.getStrategy());
break;
}
}

@Override
public Class<? extends Decorator>[] after() {
return new Class[] { CustomResourceDefinitionDecorator.class };
}

private void assertConversionNotYetConfigured(CustomResourceConversion existing, ObjectMeta resourceMeta) {
if (existing != null) {
throw new IllegalStateException(String.format(
"'%s' custom resource contains a conversion configuration and it has already been configured. " +
"CustomResourceConversion must be configured only once.",
resourceMeta.getName()));
}
}

@Override
public String toString() {
return getClass().getName() + " [name:" + getName() + ", conversionInfo:" + conversionInfo + "]";
}
}
Loading
Loading