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

fix fabric8io#3144: making the crud mock logic more crd aware #3156

Merged
merged 1 commit into from
Jun 8, 2021
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@
* Fix #3121: ServiceOperationImpl replace will throw a KubernetesClientException rather than a NPE if the item doesn't exist
* Fix #3189: VersionInfo contains null data in OpenShift 4.6
* Fix #3190: Ignore fields with name "-" when using the Go to JSON schema generator
* Fix #3144: walking back the assumption that resource/status should be a subresource for the crud mock server, now it will be only if a registered crd indicates that it should be
* Fix #3194: the mock server will now infer the namespace from the path

#### Improvements
* Fix #3078: adding javadocs to further clarify patch, edit, replace, etc. and note the possibility of items being modified.
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ public class CustomResourceDefinitionContext {
private String plural;
private String version;
private String kind;
private boolean statusSubresource;

public String getName() { return name; }

@@ -56,6 +57,10 @@ public String getVersion() {
public String getKind() {
return kind;
}

public boolean isStatusSubresource() {
return statusSubresource;
}

@SuppressWarnings("rawtypes")
public static CustomResourceDefinitionBuilder v1beta1CRDFromCustomResourceType(Class<? extends CustomResource> customResource) {
@@ -136,19 +141,26 @@ public static CustomResourceDefinitionContext fromCrd(CustomResourceDefinition c
.withName(crd.getMetadata().getName())
.withPlural(spec.getNames().getPlural())
.withKind(spec.getNames().getKind())
.withStatusSubresource(spec.getSubresources() != null && spec.getSubresources().getStatus() != null)
.build();
}

public static CustomResourceDefinitionContext fromCrd(
io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition crd
) {
String version = getVersion(crd.getSpec());
return new CustomResourceDefinitionContext.Builder()
.withGroup(crd.getSpec().getGroup())
.withVersion(getVersion(crd.getSpec()))
.withVersion(version)
.withScope(crd.getSpec().getScope())
.withName(crd.getMetadata().getName())
.withPlural(crd.getSpec().getNames().getPlural())
.withKind(crd.getSpec().getNames().getKind())
.withStatusSubresource(crd.getSpec()
.getVersions()
.stream()
.filter(v -> version.equals(v.getName()))
.anyMatch(v -> v.getSubresources() != null && v.getSubresources().getStatus() != null))
.build();
}

@@ -214,6 +226,11 @@ public Builder withKind(String kind) {
this.customResourceDefinitionContext.kind = kind;
return this;
}

public Builder withStatusSubresource(boolean statusSubresource) {
this.customResourceDefinitionContext.statusSubresource = statusSubresource;
return this;
}

public CustomResourceDefinitionContext build() {
return this.customResourceDefinitionContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* 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.kubernetes.client.server.mock;

import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext;
import io.fabric8.kubernetes.client.utils.Serialization;

import java.util.Optional;

/**
* Holds state related to crds by manipulating the crds known to the attributes extractor
*/
public class CustomResourceDefinitionProcessor {

private static final String V1BETA1_PATH = "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions";
private static final String V1_PATH = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";

private KubernetesAttributesExtractor extractor;

public CustomResourceDefinitionProcessor(KubernetesAttributesExtractor extractor) {
this.extractor = extractor;
}

public void process(String path, String crdString, boolean delete) {
CustomResourceDefinitionContext context = null;
if (path.startsWith(V1BETA1_PATH)) {
io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition crd = Serialization
.unmarshal(crdString, io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition.class);
context = CustomResourceDefinitionContext.fromCrd(crd);
} else if (path.startsWith(V1_PATH)) {
CustomResourceDefinition crd = Serialization.unmarshal(crdString, CustomResourceDefinition.class);
context = CustomResourceDefinitionContext.fromCrd(crd);
} else {
return;
}
if (delete) {
extractor.getCrdContexts().remove(context.getPlural());
} else {
extractor.getCrdContexts().put(context.getPlural(), context);
}
}

public boolean isStatusSubresource(String kind) {
if (kind == null) {
return false;
}
// we are only holding by plural, lookup now by kind
Optional<CustomResourceDefinitionContext> context =
extractor.getCrdContexts().values().stream().filter(c -> kind.equalsIgnoreCase(c.getKind())).findFirst();
if (!context.isPresent()) {
return false;
}
return context.get().isStatusSubresource();
}

}
Original file line number Diff line number Diff line change
@@ -15,31 +15,33 @@
*/
package io.fabric8.kubernetes.client.server.mock;

import static io.fabric8.mockwebserver.crud.AttributeType.EXISTS;
import static io.fabric8.mockwebserver.crud.AttributeType.NOT_EXISTS;
import static io.fabric8.mockwebserver.crud.AttributeType.WITHOUT;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.kubernetes.client.utils.Utils;
import io.fabric8.mockwebserver.crud.Attribute;
import io.fabric8.mockwebserver.crud.AttributeExtractor;
import io.fabric8.mockwebserver.crud.AttributeSet;
import okhttp3.HttpUrl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.kubernetes.client.utils.Utils;
import io.fabric8.mockwebserver.crud.Attribute;
import io.fabric8.mockwebserver.crud.AttributeExtractor;
import io.fabric8.mockwebserver.crud.AttributeSet;
import okhttp3.HttpUrl;
import static io.fabric8.mockwebserver.crud.AttributeType.EXISTS;
import static io.fabric8.mockwebserver.crud.AttributeType.NOT_EXISTS;
import static io.fabric8.mockwebserver.crud.AttributeType.WITHOUT;

public class KubernetesAttributesExtractor implements AttributeExtractor<HasMetadata> {

@@ -77,14 +79,14 @@ public class KubernetesAttributesExtractor implements AttributeExtractor<HasMeta
// values are not important, HttpUrl expects the scheme to be http or https.
private static final String SCHEME = "http";
private static final String HOST = "localhost";
private List<CustomResourceDefinitionContext> crdContexts;
private Map<String, CustomResourceDefinitionContext> crdContexts;

public KubernetesAttributesExtractor() {
this.crdContexts = Collections.emptyList();
this(Collections.emptyList());
}

public KubernetesAttributesExtractor(List<CustomResourceDefinitionContext> crdContexts) {
this.crdContexts = crdContexts;
this.crdContexts = crdContexts.stream().collect(Collectors.toMap(CustomResourceDefinitionContext::getPlural, Function.identity()));
}

private HttpUrl parseUrlFromPathAndQuery(String s) {
@@ -94,6 +96,23 @@ private HttpUrl parseUrlFromPathAndQuery(String s) {
return HttpUrl.parse(String.format("%s://%s%s", SCHEME, HOST, s));
}

/**
* Get the name, namespace, and kind from the path
*/
public Map<String, String> fromKubernetesPath(String s) {
if (s == null || s.isEmpty()) {
return Collections.emptyMap();
}

//Get paths
HttpUrl url = parseUrlFromPathAndQuery(s);
Matcher m = PATTERN.matcher(url.encodedPath());
if (m.matches()) {
return extract(m);
}
return Collections.emptyMap();
}

@Override
public AttributeSet fromPath(String s) {
if (s == null || s.isEmpty()) {
@@ -104,7 +123,10 @@ public AttributeSet fromPath(String s) {
HttpUrl url = parseUrlFromPathAndQuery(s);
Matcher m = PATTERN.matcher(url.encodedPath());
if (m.matches()) {
AttributeSet set = extract(m, crdContexts);
AttributeSet set = new AttributeSet(extract(m).entrySet()
.stream()
.map(e -> new Attribute(e.getKey(), e.getValue()))
.collect(Collectors.toList()));
set = AttributeSet.merge(set, extractQueryParameters(url));
LOGGER.debug("fromPath {} : {}", s, set);
return set;
@@ -167,24 +189,24 @@ protected AttributeSet extractMetadataAttributes(HasMetadata hasMetadata) {
return metadataAttributes;
}

private static AttributeSet extract(Matcher m, List<CustomResourceDefinitionContext> crdContexts) {
AttributeSet attributes = new AttributeSet();
private Map<String, String> extract(Matcher m) {
Map<String, String> attributes = new HashMap<>();
if (m.matches()) {
String kind = m.group(KIND);
if (!Utils.isNullOrEmpty(kind)) {
kind = resolveKindFromPlural(crdContexts, kind);
attributes = attributes.add(new Attribute(KIND, kind));
kind = resolveKindFromPlural(kind);
attributes.put(KIND, kind);
}

String namespace = m.group(NAMESPACE);
if (!Utils.isNullOrEmpty(namespace)) {
attributes = attributes.add(new Attribute(NAMESPACE, namespace));
attributes.put(NAMESPACE, namespace);
}

try {
String name = m.group(NAME);
if (!Utils.isNullOrEmpty(name)) {
attributes = attributes.add(new Attribute(NAME, name));
attributes.put(NAME, name);
}
} catch (IllegalArgumentException e) {
//group is missing, which is perfectly valid for create, update etc requests.
@@ -193,9 +215,10 @@ private static AttributeSet extract(Matcher m, List<CustomResourceDefinitionCont
return attributes;
}

private static String resolveKindFromPlural(List<CustomResourceDefinitionContext> crdContexts, String kind) {
if (isCustomResourceKind(crdContexts, kind)) {
return getCustomResourceKindFromPlural(crdContexts, kind);
private String resolveKindFromPlural(String kind) {
String result = getCustomResourceKindFromPlural(kind);
if (result != null) {
return result;
}
return getKindFromPluralForKubernetesTypes(kind);
}
@@ -285,16 +308,15 @@ private static HasMetadata toRawHasMetadata(String s) {
}
}

private static boolean isCustomResourceKind(List<CustomResourceDefinitionContext> crdContexts, String kind) {
return crdContexts.stream()
.anyMatch(c -> c.getPlural().equals(kind));
private String getCustomResourceKindFromPlural(String plural) {
CustomResourceDefinitionContext crdContext = crdContexts.get(plural);
return crdContext != null && crdContext.getKind() != null ? crdContext.getKind().toLowerCase() : null;
}

private static String getCustomResourceKindFromPlural(List<CustomResourceDefinitionContext> crdContexts, String kind) {
CustomResourceDefinitionContext crdContext = crdContexts.stream()
.filter(c -> c.getPlural().equals(kind))
.findAny()
.orElse(null);
return crdContext != null && crdContext.getKind() != null ? crdContext.getKind().toLowerCase() : null;
/**
* A mapping of plural name to context
*/
public Map<String, CustomResourceDefinitionContext> getCrdContexts() {
return crdContexts;
}
}
Loading