Skip to content

Commit

Permalink
fix #3144: making the crud mock logic more crd aware
Browse files Browse the repository at this point in the history
also working around making the namespace and kind available from the
path
  • Loading branch information
shawkins committed May 21, 2021
1 parent cc4945f commit 4fc4e52
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 83 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 5.5-SNAPSHOT

#### Bugs
* Fix #3144 walking back the assumption that /status should be a subresource, now it will be only if a registered crd indicates that it should be

#### Improvements
* Fix #3135 added mock crud support for patch status, and will return exceptions for unsupported patch types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class CustomResourceDefinitionContext {
private String plural;
private String version;
private String kind;
private boolean statusSubresource;

public String getName() { return name; }

Expand All @@ -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) {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down
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
Expand Up @@ -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> {

Expand Down Expand Up @@ -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) {
Expand All @@ -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()) {
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, String>();
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.
Expand All @@ -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);
}
Expand Down Expand Up @@ -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

0 comments on commit 4fc4e52

Please sign in to comment.