Skip to content

Commit f0a21c4

Browse files
authored
Merge pull request #1180 from yue9944882/model-mapper-refactor
Enhances and refactors model mapper
2 parents 9907ff5 + 41acb43 commit f0a21c4

File tree

2 files changed

+280
-47
lines changed

2 files changed

+280
-47
lines changed

util/src/main/java/io/kubernetes/client/util/ModelMapper.java

Lines changed: 176 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212
*/
1313
package io.kubernetes.client.util;
1414

15+
import com.google.common.base.Objects;
1516
import com.google.common.collect.ImmutableMap;
1617
import com.google.common.reflect.ClassPath;
18+
import io.kubernetes.client.Discovery;
19+
import io.kubernetes.client.common.KubernetesListObject;
20+
import io.kubernetes.client.common.KubernetesObject;
21+
import io.kubernetes.client.openapi.ApiException;
1722
import java.io.IOException;
1823
import java.util.ArrayList;
1924
import java.util.HashMap;
@@ -31,12 +36,14 @@
3136
*/
3237
public class ModelMapper {
3338

34-
private static Map<String, Class<?>> classes = new HashMap<>();
39+
private static final Logger logger = LoggerFactory.getLogger(ModelMapper.class);
40+
41+
private static Map<GroupVersionKind, Class<?>> classes = new HashMap<>();
42+
// Model's api-group prefix to kubernetes api-group
3543
private static Map<String, String> apiGroups = new HashMap<>();
44+
// Model's api-version midfix to kubernetes api-version
3645
private static List<String> apiVersions = new ArrayList<>();
3746

38-
private static final Logger logger = LoggerFactory.getLogger(ModelMapper.class);
39-
4047
static {
4148
try {
4249
initModelMap();
@@ -46,30 +53,114 @@ public class ModelMapper {
4653
}
4754

4855
/**
49-
* Add a mapping from API Group/version/kind to a Class to use when calling <code>load(...)
50-
* </code> .
56+
* Registering concrete model classes by its apiGroupVersion (e.g. "apps/v1") and kind (e.g.
57+
* "Deployment").
5158
*
52-
* <p>Shouldn't really be needed as most API Group/Version/Kind are loaded dynamically at startup.
59+
* <p>Note that the the so-called apiGroupVersion equals to the "apiVersion" in the kubenretes
60+
* resource yamls.
5361
*/
5462
public static void addModelMap(String apiGroupVersion, String kind, Class<?> clazz) {
55-
classes.put(apiGroupVersion + "/" + kind, clazz);
63+
String[] parts = apiGroupVersion.split("/");
64+
if (parts.length == 1) {
65+
String version = apiGroupVersion;
66+
addModelMap("", version, kind, clazz);
67+
}
68+
addModelMap(parts[0], parts[1], kind, clazz);
5669
}
5770

58-
public static Class<?> getApiTypeClass(String apiVersion, String kind) {
59-
Class<?> clazz = (Class<?>) classes.get(apiVersion + "/" + kind);
60-
if (clazz == null) {
61-
// Attempt to detect class from version and kind alone
62-
if (apiVersion.contains("/")) {
63-
clazz = (Class<?>) classes.get(apiVersion.split("/")[1] + "/" + kind);
64-
}
71+
/**
72+
* Registering concrete model classes by its group, version and kind (e.g. "apps", "v1",
73+
* "Deployment")
74+
*
75+
* @param group the group
76+
* @param version the version
77+
* @param kind the kind
78+
* @param clazz the clazz
79+
*/
80+
public static void addModelMap(String group, String version, String kind, Class<?> clazz) {
81+
classes.put(new GroupVersionKind(group, version, kind), clazz);
82+
}
83+
84+
/**
85+
* Gets the model classes by given apiGroupVersion (e.g. "apps/v1") and kind (e.g. "Deployment").
86+
*
87+
* @param apiGroupVersion the api version
88+
* @param kind the kind
89+
* @return the api type class
90+
*/
91+
public static Class<?> getApiTypeClass(String apiGroupVersion, String kind) {
92+
String[] parts = apiGroupVersion.split("/");
93+
if (parts.length == 1) {
94+
// legacy group
95+
return getApiTypeClass("", apiGroupVersion, kind);
6596
}
66-
return clazz;
97+
return getApiTypeClass(parts[0], parts[1], kind);
6798
}
6899

69-
public static Map<String, Class<?>> getAllKnownClasses() {
100+
/**
101+
* Gets the model classes by given group, version and kind (e.g. "apps", ""v1", "Deployment").
102+
*
103+
* @param group the group
104+
* @param version the version
105+
* @param kind the kind
106+
* @return the api type class
107+
*/
108+
public static Class<?> getApiTypeClass(String group, String version, String kind) {
109+
Class<?> clazz = classes.get(new GroupVersionKind(group, version, kind));
110+
if (clazz != null) {
111+
return clazz;
112+
}
113+
return classes.get(new GroupVersionKind(null, version, kind));
114+
}
115+
116+
/**
117+
* Returns all the registered model classes.
118+
*
119+
* @return the all known classes
120+
*/
121+
public static Map<GroupVersionKind, Class<?>> getAllKnownClasses() {
70122
return ImmutableMap.copyOf(classes);
71123
}
72124

125+
/**
126+
* Gets the GVK by the given model class.
127+
*
128+
* <p>Note: This method will run a loop over the registered models, consider use {@link
129+
* ModelMapper#getAllKnownClasses} and build your own index for faster queries.
130+
*
131+
* @param clazz the clazz
132+
* @return the group version kind by class
133+
*/
134+
public static GroupVersionKind getGroupVersionKindByClass(Class<?> clazz) {
135+
return classes.entrySet().stream()
136+
.filter(e -> clazz.equals(e.getValue()))
137+
.map(e -> e.getKey())
138+
.findFirst()
139+
.get();
140+
}
141+
142+
/**
143+
* Refreshes the model mapping by syncing up w/the api discovery info from the kubernetes
144+
* apiserver.
145+
*
146+
* @param discovery the discovery
147+
* @throws ApiException the api exception
148+
*/
149+
public static void refresh(Discovery discovery) throws ApiException {
150+
// TODO(yue9944882): integration test it
151+
for (Discovery.APIResource apiResource : discovery.findAll()) {
152+
for (String version : apiResource.getVersions()) {
153+
Class<?> clazz = getApiTypeClass(apiResource.getGroup(), version, apiResource.getKind());
154+
if (clazz == null) {
155+
continue;
156+
}
157+
// sync up w/ the latest api discovery
158+
classes.remove(getGroupVersionKindByClass(clazz));
159+
addModelMap(apiResource.getGroup(), version, apiResource.getKind(), clazz);
160+
}
161+
}
162+
}
163+
73164
private static void initApiGroupMap() {
74165
apiGroups.put("Admissionregistration", "admissionregistration.k8s.io");
75166
apiGroups.put("Apiextensions", "apiextensions.k8s.io");
@@ -87,6 +178,7 @@ private static void initApiGroupMap() {
87178
apiGroups.put("Scheduling", "scheduling.k8s.io");
88179
apiGroups.put("Settings", "settings.k8s.io");
89180
apiGroups.put("Storage", "storage.k8s.io");
181+
apiGroups.put("FlowControl", "flowcontrol.apiserver.k8s.io");
90182
}
91183

92184
private static void initApiVersionList() {
@@ -100,20 +192,6 @@ private static void initApiVersionList() {
100192
apiVersions.add("V1");
101193
}
102194

103-
private static Pair<String, String> getApiVersion(String name) {
104-
MutablePair<String, String> parts = new MutablePair<>();
105-
for (String version : apiVersions) {
106-
if (name.startsWith(version)) {
107-
parts.left = version.toLowerCase();
108-
parts.right = name.substring(version.length());
109-
break;
110-
}
111-
}
112-
if (parts.left == null) parts.right = name;
113-
114-
return parts;
115-
}
116-
117195
private static void initModelMap() throws IOException {
118196
initApiGroupMap();
119197
initApiVersionList();
@@ -122,29 +200,80 @@ private static void initModelMap() throws IOException {
122200
Set<ClassPath.ClassInfo> allClasses =
123201
cp.getTopLevelClasses("io.kubernetes.client.openapi.models");
124202

125-
for (ClassPath.ClassInfo clazz : allClasses) {
126-
String modelName = "";
127-
Pair<String, String> nameParts = getApiGroup(clazz.getSimpleName());
128-
modelName += nameParts.getLeft() == null ? "" : nameParts.getLeft() + "/";
203+
for (ClassPath.ClassInfo classInfo : allClasses) {
204+
Class clazz = classInfo.load();
205+
if (!KubernetesObject.class.isAssignableFrom(clazz)
206+
&& !KubernetesListObject.class.isAssignableFrom(clazz)) {
207+
continue;
208+
}
209+
210+
Pair<String, String> groupAndOther = getApiGroup(clazz.getSimpleName());
211+
Pair<String, String> versionAndOther = getApiVersion(groupAndOther.getRight());
129212

130-
nameParts = getApiVersion(nameParts.getRight());
131-
modelName += nameParts.getLeft() == null ? "" : nameParts.getLeft() + "/";
132-
modelName += nameParts.getRight();
213+
String group = groupAndOther.getLeft();
214+
String version = versionAndOther.getLeft();
215+
String kind = versionAndOther.getRight();
133216

134-
classes.put(modelName, clazz.load());
217+
classes.put(new GroupVersionKind(group, version, kind), clazz);
135218
}
136219
}
137220

138221
private static Pair<String, String> getApiGroup(String name) {
139-
MutablePair<String, String> parts = new MutablePair<>();
140-
for (Map.Entry<String, String> entry : apiGroups.entrySet()) {
141-
if (name.startsWith(entry.getKey())) {
142-
parts.left = entry.getValue();
143-
parts.right = name.substring(entry.getKey().length());
144-
break;
145-
}
222+
return apiGroups.entrySet().stream()
223+
.filter(e -> name.startsWith(e.getKey()))
224+
.map(e -> new MutablePair(e.getValue(), name.substring(e.getKey().length())))
225+
.findFirst()
226+
.orElse(new MutablePair(null, name));
227+
}
228+
229+
private static Pair<String, String> getApiVersion(String name) {
230+
return apiVersions.stream()
231+
.filter(v -> name.startsWith(v))
232+
.map(v -> new MutablePair(v.toLowerCase(), name.substring(v.length())))
233+
.findFirst()
234+
.orElse(new MutablePair(null, name));
235+
}
236+
237+
public static class GroupVersionKind {
238+
239+
public GroupVersionKind(String group, String version, String kind) {
240+
this.group = group;
241+
this.version = version;
242+
this.kind = kind;
243+
}
244+
245+
private String group;
246+
private String version;
247+
private String kind;
248+
249+
@Override
250+
public boolean equals(Object o) {
251+
if (this == o) return true;
252+
if (o == null || getClass() != o.getClass()) return false;
253+
GroupVersionKind that = (GroupVersionKind) o;
254+
return Objects.equal(group, that.group)
255+
&& Objects.equal(version, that.version)
256+
&& Objects.equal(kind, that.kind);
257+
}
258+
259+
@Override
260+
public int hashCode() {
261+
return Objects.hashCode(group, version, kind);
262+
}
263+
264+
@Override
265+
public String toString() {
266+
return "GroupVersionKind{"
267+
+ "group='"
268+
+ group
269+
+ '\''
270+
+ ", version='"
271+
+ version
272+
+ '\''
273+
+ ", kind='"
274+
+ kind
275+
+ '\''
276+
+ '}';
146277
}
147-
if (parts.left == null) parts.right = name;
148-
return parts;
149278
}
150279
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.util;
14+
15+
import static org.junit.Assert.*;
16+
17+
import io.kubernetes.client.Discovery;
18+
import io.kubernetes.client.openapi.ApiException;
19+
import io.kubernetes.client.openapi.models.V1Deployment;
20+
import io.kubernetes.client.openapi.models.V1Pod;
21+
import io.kubernetes.client.openapi.models.V1beta1CustomResourceDefinition;
22+
import java.io.IOException;
23+
import java.util.Map;
24+
import org.junit.Ignore;
25+
import org.junit.Test;
26+
27+
public class ModelMapperTest {
28+
29+
@Test
30+
public void testPrebuiltModelMapping() {
31+
32+
Map<ModelMapper.GroupVersionKind, Class<?>> maps = ModelMapper.getAllKnownClasses();
33+
assertTrue(maps.size() > 0);
34+
35+
assertEquals(V1Pod.class, ModelMapper.getApiTypeClass("", "v1", "Pod"));
36+
assertEquals(V1Pod.class, ModelMapper.getApiTypeClass("v1", "Pod"));
37+
38+
assertEquals(V1Deployment.class, ModelMapper.getApiTypeClass("", "v1", "Deployment"));
39+
assertEquals(V1Deployment.class, ModelMapper.getApiTypeClass("apps/v1", "Deployment"));
40+
41+
assertEquals(
42+
V1beta1CustomResourceDefinition.class,
43+
ModelMapper.getApiTypeClass("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition"));
44+
assertEquals(
45+
V1beta1CustomResourceDefinition.class,
46+
ModelMapper.getApiTypeClass("apiextensions.k8s.io", "v1beta1", "CustomResourceDefinition"));
47+
}
48+
49+
@Test
50+
public void testAddingModel() {
51+
Class objClass =
52+
new Object() {
53+
{
54+
}
55+
}.getClass();
56+
57+
ModelMapper.addModelMap("example.io", "v1", "Toss", objClass);
58+
59+
assertEquals(objClass, ModelMapper.getApiTypeClass("example.io/v1", "Toss"));
60+
assertEquals(objClass, ModelMapper.getApiTypeClass("example.io", "v1", "Toss"));
61+
62+
assertNull(ModelMapper.getApiTypeClass("example.io/V1", "Toss"));
63+
assertNull(ModelMapper.getApiTypeClass("example.io", "V1", "Toss"));
64+
65+
assertNull(ModelMapper.getApiTypeClass("example.io/v1", "Tofu"));
66+
assertNull(ModelMapper.getApiTypeClass("example.io", "v1", "Tofu"));
67+
}
68+
69+
@Test
70+
public void testGetClassByKind() {
71+
assertEquals(
72+
new ModelMapper.GroupVersionKind(null, "v1", "Pod"),
73+
ModelMapper.getGroupVersionKindByClass(V1Pod.class));
74+
75+
assertEquals(
76+
new ModelMapper.GroupVersionKind(null, "v1", "Pod"),
77+
ModelMapper.getGroupVersionKindByClass(V1Pod.class));
78+
assertEquals(
79+
new ModelMapper.GroupVersionKind(null, "v1", "Deployment"),
80+
ModelMapper.getGroupVersionKindByClass(V1Deployment.class));
81+
assertEquals(
82+
new ModelMapper.GroupVersionKind(null, "v1beta1", "CustomResourceDefinition"),
83+
ModelMapper.getGroupVersionKindByClass(V1beta1CustomResourceDefinition.class));
84+
}
85+
86+
@Test
87+
@Ignore
88+
public void test() throws IOException, ApiException {
89+
// TODO(yue9944882): move this to an integration test
90+
Discovery discovery = new Discovery(ClientBuilder.defaultClient());
91+
ModelMapper.refresh(discovery);
92+
93+
assertEquals(
94+
new ModelMapper.GroupVersionKind("", "v1", "Pod"),
95+
ModelMapper.getGroupVersionKindByClass(V1Pod.class));
96+
assertEquals(
97+
new ModelMapper.GroupVersionKind("apps", "v1", "Deployment"),
98+
ModelMapper.getGroupVersionKindByClass(V1Deployment.class));
99+
assertEquals(
100+
new ModelMapper.GroupVersionKind(
101+
"apiextensions.k8s.io", "v1beta1", "CustomResourceDefinition"),
102+
ModelMapper.getGroupVersionKindByClass(V1beta1CustomResourceDefinition.class));
103+
}
104+
}

0 commit comments

Comments
 (0)