diff --git a/CHANGES.md b/CHANGES.md index a933392d24bf..5a40b60b5ad8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,7 @@ Release Notes. * Support collection type in dynamic configuration core. * Support zookeeper grouped dynamic configurations. * Fix NPE when OAP nodes synchronize events with each other in cluster mode. +* Support k8s configmap grouped dynamic configurations. #### UI diff --git a/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigurationWatcherRegister.java b/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigurationWatcherRegister.java index df2c2d0088e5..7f34e7b6dc23 100644 --- a/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigurationWatcherRegister.java +++ b/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigurationWatcherRegister.java @@ -18,7 +18,7 @@ package org.apache.skywalking.oap.server.configuration.configmap; -import io.kubernetes.client.openapi.models.V1ConfigMap; +import java.util.Map; import java.util.Optional; import java.util.Set; import lombok.extern.slf4j.Slf4j; @@ -40,9 +40,9 @@ public ConfigmapConfigurationWatcherRegister(ConfigmapConfigurationSettings sett @Override public Optional readConfig(Set keys) { final ConfigTable configTable = new ConfigTable(); - Optional v1ConfigMap = informer.configMap(); + Map configMapData = informer.configMapData(); for (final String name : keys) { - final String value = v1ConfigMap.map(V1ConfigMap::getData).map(data -> data.get(name)).orElse(null); + final String value = configMapData.get(name); if (log.isDebugEnabled()) { log.debug("read config: name:{} ,value:{}", name, value); } @@ -53,8 +53,19 @@ public Optional readConfig(Set keys) { @Override public Optional readGroupConfig(final Set keys) { - // TODO: implement readGroupConfig - return Optional.empty(); - } + GroupConfigTable groupConfigTable = new GroupConfigTable(); + Map configMapData = informer.configMapData(); + keys.forEach(key -> { + GroupConfigTable.GroupConfigItems groupConfigItems = new GroupConfigTable.GroupConfigItems(key); + groupConfigTable.addGroupConfigItems(groupConfigItems); + configMapData.forEach((groupItemKey, itemValue) -> { + if (groupItemKey.startsWith(key)) { + String itemName = groupItemKey.replaceFirst(key + ".", ""); + groupConfigItems.add(new ConfigTable.ConfigItem(itemName, itemValue)); + } + }); + }); + return Optional.of(groupConfigTable); + } } diff --git a/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigurationConfigmapInformer.java b/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigurationConfigmapInformer.java index 84daad34aa4b..15061d6719f1 100644 --- a/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigurationConfigmapInformer.java +++ b/oap-server/server-configuration/configuration-k8s-configmap/src/main/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigurationConfigmapInformer.java @@ -28,8 +28,9 @@ import io.kubernetes.client.openapi.models.V1ConfigMapList; import io.kubernetes.client.util.Config; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -37,7 +38,6 @@ @Slf4j public class ConfigurationConfigmapInformer { - private Lister configMapLister; private SharedInformerFactory factory; @@ -84,8 +84,18 @@ private void doStartConfigMapInformer(final ConfigmapConfigurationSettings setti configMapLister = new Lister<>(configMapSharedIndexInformer.getIndexer()); } - public Optional configMap() { - return Optional.ofNullable(configMapLister.list().size() == 1 ? configMapLister.list().get(0) : null); - } + public Map configMapData() { + Map configMapData = new HashMap<>(); + if (configMapLister != null && configMapLister.list() != null) { + configMapLister.list().forEach(cf -> { + Map data = cf.getData(); + if (data == null) { + return; + } + configMapData.putAll(data); + }); + } + return configMapData; + } } diff --git a/oap-server/server-configuration/configuration-k8s-configmap/src/test/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigWatcherRegisterTest.java b/oap-server/server-configuration/configuration-k8s-configmap/src/test/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigWatcherRegisterTest.java index 46125479de64..dc8c5b6d8f64 100644 --- a/oap-server/server-configuration/configuration-k8s-configmap/src/test/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigWatcherRegisterTest.java +++ b/oap-server/server-configuration/configuration-k8s-configmap/src/test/java/org/apache/skywalking/oap/server/configuration/configmap/ConfigmapConfigWatcherRegisterTest.java @@ -18,8 +18,9 @@ package org.apache.skywalking.oap.server.configuration.configmap; -import io.kubernetes.client.openapi.models.V1ConfigMap; +import java.io.FileNotFoundException; import java.io.Reader; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import java.util.Optional; import java.util.stream.Collectors; import org.apache.skywalking.oap.server.configuration.api.ConfigTable; +import org.apache.skywalking.oap.server.configuration.api.GroupConfigTable; import org.apache.skywalking.oap.server.library.util.ResourceUtils; import org.junit.Assert; import org.junit.Before; @@ -59,8 +61,8 @@ public void prepare() throws IllegalAccessException { @Test public void readConfigWhenConfigMapDataIsNull() throws Exception { - V1ConfigMap v1ConfigMap = new V1ConfigMap(); - PowerMockito.doReturn(Optional.of(v1ConfigMap)).when(informer).configMap(); + Map configMapData = new HashMap<>(); + PowerMockito.doReturn(configMapData).when(informer).configMapData(); Optional optionalConfigTable = register.readConfig(new HashSet() {{ add("key1"); }}); @@ -74,7 +76,7 @@ public void readConfigWhenConfigMapDataIsNull() throws Exception { @Test public void readConfigWhenInformerNotwork() throws Exception { - PowerMockito.doReturn(Optional.empty()).when(informer).configMap(); + PowerMockito.doReturn(new HashMap<>()).when(informer).configMapData(); Optional optionalConfigTable = register.readConfig(new HashSet() {{ add("key1"); }}); @@ -88,17 +90,15 @@ public void readConfigWhenInformerNotwork() throws Exception { @Test public void readConfigWhenInformerWork() throws Exception { - Reader configmapReader = ResourceUtils.read("skywalking-dynamic-configmap.example.yaml"); - Map> configmapMap = yaml.loadAs(configmapReader, Map.class); - V1ConfigMap v1ConfigMap = new V1ConfigMap(); - v1ConfigMap.data(configmapMap.get("data")); - PowerMockito.doReturn(Optional.of(v1ConfigMap)).when(informer).configMap(); + Map configMapData = this.readMockConfigMapData(); + PowerMockito.doReturn(configMapData).when(informer).configMapData(); Optional optionalConfigTable = register.readConfig(new HashSet() {{ add("receiver-trace.default.slowDBAccessThreshold"); add("alarm.default.alarm-settings"); add("core.default.apdexThreshold"); add("receiver-trace.default.uninstrumentedGateways"); }}); + Assert.assertTrue(optionalConfigTable.isPresent()); ConfigTable configTable = optionalConfigTable.get(); @@ -108,4 +108,65 @@ public void readConfigWhenInformerWork() throws Exception { .collect(Collectors.toList()); Assert.assertEquals(list.size(), 4); } + + @Test + public void readGroupConfigWhenConfigMapDataIsNull() throws Exception { + Map configMapData = new HashMap<>(); + PowerMockito.doReturn(configMapData).when(informer).configMapData(); + Optional optionalGroupConfigTable = register.readGroupConfig(new HashSet() {{ + add("key1"); + }}); + + Assert.assertTrue(optionalGroupConfigTable.isPresent()); + GroupConfigTable groupConfigTable = optionalGroupConfigTable.get(); + Assert.assertEquals(groupConfigTable.getGroupItems().size(), 1); + Assert.assertEquals(groupConfigTable.getGroupItems().get(0).getName(), "key1"); + Assert.assertEquals(groupConfigTable.getGroupItems().get(0).getItems().size(), 0); + } + + @Test + public void readGroupConfigWhenInformerNotwork() throws Exception { + PowerMockito.doReturn(new HashMap<>()).when(informer).configMapData(); + Optional optionalGroupConfigTable = register.readGroupConfig(new HashSet() {{ + add("key1"); + }}); + + Assert.assertTrue(optionalGroupConfigTable.isPresent()); + GroupConfigTable groupConfigTable = optionalGroupConfigTable.get(); + Assert.assertEquals(groupConfigTable.getGroupItems().size(), 1); + Assert.assertEquals(groupConfigTable.getGroupItems().get(0).getName(), "key1"); + Assert.assertEquals(groupConfigTable.getGroupItems().get(0).getItems().size(), 0); + } + + @Test + public void readGroupConfigWhenInformerWork() throws Exception { + Map configMapData = this.readMockConfigMapData(); + PowerMockito.doReturn(configMapData).when(informer).configMapData(); + Optional optionalGroupConfigTable = register.readGroupConfig(new HashSet() {{ + add("core.default.endpoint-name-grouping-openapi"); + }}); + + Assert.assertTrue(optionalGroupConfigTable.isPresent()); + GroupConfigTable groupConfigTable = optionalGroupConfigTable.get(); + + Assert.assertEquals(groupConfigTable.getGroupItems().size(), 1); + Assert.assertEquals(groupConfigTable.getGroupItems().get(0).getName(), "core.default.endpoint-name-grouping-openapi"); + Assert.assertEquals(groupConfigTable.getGroupItems().get(0).getItems().size(), 3); + } + + private Map readMockConfigMapData() throws FileNotFoundException { + Reader configmapReader1 = ResourceUtils.read("skywalking-dynamic-configmap.example.yaml"); + Reader configmapReader2 = ResourceUtils.read("skywalking-group-dynamic-configmap.example-serviceA.yaml"); + Reader configmapReader3 = ResourceUtils.read("skywalking-group-dynamic-configmap.example-serviceB.yaml"); + Map> configmapMap1 = yaml.loadAs(configmapReader1, Map.class); + Map> configmapMap2 = yaml.loadAs(configmapReader2, Map.class); + Map> configmapMap3 = yaml.loadAs(configmapReader3, Map.class); + + Map configMapData = new HashMap<>(); + configMapData.putAll(configmapMap1.get("data")); + configMapData.putAll(configmapMap2.get("data")); + configMapData.putAll(configmapMap3.get("data")); + + return configMapData; + } } diff --git a/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-dynamic-configmap.example.yaml b/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-dynamic-configmap.example.yaml index 18a07de2b93d..d715988ebe44 100644 --- a/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-dynamic-configmap.example.yaml +++ b/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-dynamic-configmap.example.yaml @@ -21,7 +21,7 @@ kind: ConfigMap metadata: name: skywalking-dynamic-config labels: - app: skywalking-alarm + app: collector release: skywalking data: receiver-trace.default.slowDBAccessThreshold: default:200,mongodb:50 diff --git a/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-group-dynamic-configmap.example-serviceA.yaml b/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-group-dynamic-configmap.example-serviceA.yaml new file mode 100644 index 000000000000..81ac5ed3e219 --- /dev/null +++ b/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-group-dynamic-configmap.example-serviceA.yaml @@ -0,0 +1,348 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: skywalking-dynamic-config2 + labels: + app: collector + release: skywalking +data: + core.default.endpoint-name-grouping-openapi.customerAPI-v1.yaml: |- + openapi: 3.0.0 + x-sw-service-name: serviceA + info: + description: OpenAPI definition for SkyWalking test. + version: v1 + title: Customer API + + tags: + - name: customer + description: customer + + paths: + /customers: + get: + tags: + - customer + summary: Get all customers list + description: Get all customers list. + operationId: getCustomers + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Customer" + /customers/{id}: + get: + tags: + - customer + summary: Get customer details + description: Get customer details with the given id. + operationId: getCustomer + parameters: + - name: id + in: path + description: Customer id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/CustomerDetails" + "400": + description: Invalid customer id + post: + tags: + - customer + summary: Update customer details + description: Update customer details with the given id. + operationId: updateCustomer + parameters: + - name: id + in: path + description: Customer id + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Customer name + required: true + schema: + type: string + responses: + "200": + description: successful operation + delete: + tags: + - customer + summary: Delete customer details + description: Delete customer details with the given id. + operationId: deleteCustomer + parameters: + - name: id + in: path + description: Customer id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + + /customer/{region}/{country}: + get: + tags: + - customer + summary: Get customers regional + description: Get customers regional with the given id. + operationId: getCustomersRegional + parameters: + - name: region + in: path + description: Customers region + required: true + schema: + type: string + - name: country + in: path + description: Customers country + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Customer" + "400": + description: Invalid parameters supplied + components: + schemas: + Customer: + type: object + description: Customer id and name + properties: + id: + type: integer + format: int64 + description: Customer id + name: + type: string + description: Customer name + required: + - id + - name + CustomerDetails: + type: object + description: Customer details + properties: + id: + type: integer + format: int64 + description: Customer id + name: + type: string + description: Customer name + description: + type: string + description: Customer description + required: + - id + - name + core.default.endpoint-name-grouping-openapi.productAPI-v1.yaml: |- + openapi: 3.0.0 + x-sw-service-name: serviceA + info: + description: OpenAPI definition for SkyWalking test. + version: v1 + title: Product API + + tags: + - name: product + description: product + - name: relatedProducts + description: Related Products + + paths: + /products: + get: + tags: + - product + summary: Get all products list + description: Get all products list. + operationId: getProducts + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Product" + /products/{id}: + get: + tags: + - product + summary: Get product details + description: Get product details with the given id. + operationId: getProduct + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ProductDetails" + "400": + description: Invalid product id + post: + tags: + - product + summary: Update product details + description: Update product details with the given id. + operationId: updateProduct + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Product name + required: true + schema: + type: string + responses: + "200": + description: successful operation + delete: + tags: + - product + summary: Delete product details + description: Delete product details with the given id. + operationId: deleteProduct + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + /products/{id}/relatedProducts: + get: + tags: + - relatedProducts + summary: Get related products + description: Get related products with the given product id. + operationId: getRelatedProducts + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/RelatedProducts" + "400": + description: Invalid product id + + components: + schemas: + Product: + type: object + description: Product id and name + properties: + id: + type: integer + format: int64 + description: Product id + name: + type: string + description: Product name + required: + - id + - name + ProductDetails: + type: object + description: Product details + properties: + id: + type: integer + format: int64 + description: Product id + name: + type: string + description: Product name + description: + type: string + description: Product description + required: + - id + - name + RelatedProducts: + type: object + description: Related Products + properties: + id: + type: integer + format: int32 + description: Product id + relatedProducts: + type: array + description: List of related products + items: + $ref: "#/components/schemas/Product" diff --git a/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-group-dynamic-configmap.example-serviceB.yaml b/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-group-dynamic-configmap.example-serviceB.yaml new file mode 100644 index 000000000000..0db85f1aa462 --- /dev/null +++ b/oap-server/server-configuration/configuration-k8s-configmap/src/test/resources/skywalking-group-dynamic-configmap.example-serviceB.yaml @@ -0,0 +1,221 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +# + +apiVersion: v1 +kind: ConfigMap +metadata: + name: skywalking-dynamic-config3 + labels: + app: collector + release: skywalking +data: + core.default.endpoint-name-grouping-openapi.productAPI-v2.yaml: |- + openapi: 3.0.0 + x-sw-service-name: serviceB + info: + description: OpenAPI definition for SkyWalking test. + version: v2 + title: Product API + + tags: + - name: product + description: product + - name: relatedProducts + description: Related Products + + paths: + /products: + get: + tags: + - product + summary: Get all products list + description: Get all products list. + operationId: getProducts + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Product" + /products/{region}/{country}: + get: + tags: + - product + summary: Get products regional + description: Get products regional with the given id. + operationId: getProductRegional + parameters: + - name: region + in: path + description: Products region + required: true + schema: + type: string + - name: country + in: path + description: Products country + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + "400": + description: Invalid parameters supplied + /products/{id}: + get: + tags: + - product + summary: Get product details + description: Get product details with the given id. + operationId: getProduct + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ProductDetails" + "400": + description: Invalid product id + post: + tags: + - product + summary: Update product details + description: Update product details with the given id. + operationId: updateProduct + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Product name + required: true + schema: + type: string + responses: + "200": + description: successful operation + delete: + tags: + - product + summary: Delete product details + description: Delete product details with the given id. + operationId: deleteProduct + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + /products/{id}/relatedProducts: + get: + tags: + - relatedProducts + summary: Get related products + description: Get related products with the given product id. + operationId: getRelatedProducts + parameters: + - name: id + in: path + description: Product id + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/RelatedProducts" + "400": + description: Invalid product id + + components: + schemas: + Product: + type: object + description: Product id and name + properties: + id: + type: integer + format: int64 + description: Product id + name: + type: string + description: Product name + required: + - id + - name + ProductDetails: + type: object + description: Product details + properties: + id: + type: integer + format: int64 + description: Product id + name: + type: string + description: Product name + description: + type: string + description: Product description + required: + - id + - name + RelatedProducts: + type: object + description: Related Products + properties: + id: + type: integer + format: int32 + description: Product id + relatedProducts: + type: array + description: List of related products + items: + $ref: "#/components/schemas/Product"