diff --git a/apps/k8s-networking/BUILD b/apps/k8s-networking/BUILD index 0794152a2b..2cf9004066 100644 --- a/apps/k8s-networking/BUILD +++ b/apps/k8s-networking/BUILD @@ -2,6 +2,8 @@ BUNDLES = [ "//apps/k8s-networking/api:onos-apps-k8s-networking-api", "//apps/k8s-networking/app:onos-apps-k8s-networking-app", "@commons_net//jar", + "@k8s_client//jar", + "@k8s_model//jar", ] onos_app( diff --git a/apps/k8s-networking/app/BUILD b/apps/k8s-networking/app/BUILD index eb29430d5d..515cf0d4e8 100644 --- a/apps/k8s-networking/app/BUILD +++ b/apps/k8s-networking/app/BUILD @@ -5,6 +5,8 @@ COMPILE_DEPS = CORE_DEPS + JACKSON + KRYO + CLI + REST + [ "//apps/k8s-node/api:onos-apps-k8s-node-api", "//apps/k8s-networking/api:onos-apps-k8s-networking-api", "@commons_net//jar", + "@k8s_client//jar", + "@k8s_model//jar", ] TEST_DEPS = TEST_ADAPTERS + TEST_REST + [ diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sApiConfig.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sApiConfig.java new file mode 100644 index 0000000000..157763f732 --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sApiConfig.java @@ -0,0 +1,202 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import com.google.common.base.MoreObjects; +import org.onlab.packet.IpAddress; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.onosproject.k8snode.api.K8sApiConfig.Scheme.HTTPS; + +/** + * Default implementation of kubernetes API configuration. + */ +public final class DefaultK8sApiConfig implements K8sApiConfig { + + private static final String NOT_NULL_MSG = "API Config % cannot be null"; + + private final Scheme scheme; + private final IpAddress ipAddress; + private final int port; + private final String token; + private final String caCertData; + private final String clientCertData; + private final String clientKeyData; + + private DefaultK8sApiConfig(Scheme scheme, IpAddress ipAddress, int port, + String token, String caCertData, + String clientCertData, String clientKeyData) { + this.scheme = scheme; + this.ipAddress = ipAddress; + this.port = port; + this.token = token; + this.caCertData = caCertData; + this.clientCertData = clientCertData; + this.clientKeyData = clientKeyData; + } + + @Override + public Scheme scheme() { + return scheme; + } + + @Override + public IpAddress ipAddress() { + return ipAddress; + } + + @Override + public int port() { + return port; + } + + @Override + public String token() { + return token; + } + + @Override + public String caCertData() { + return caCertData; + } + + @Override + public String clientCertData() { + return clientCertData; + } + + @Override + public String clientKeyData() { + return clientKeyData; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultK8sApiConfig that = (DefaultK8sApiConfig) o; + return port == that.port && + scheme == that.scheme && + ipAddress.equals(that.ipAddress) && + token.equals(that.token) && + caCertData.equals(that.caCertData) && + clientCertData.equals(that.clientCertData) && + clientKeyData.equals(that.clientKeyData); + } + + @Override + public int hashCode() { + return Objects.hash(scheme, ipAddress, port, token, caCertData, + clientCertData, clientKeyData); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("scheme", scheme) + .add("ipAddress", ipAddress) + .add("port", port) + .add("token", token) + .add("caCertData", caCertData) + .add("clientCertData", clientCertData) + .add("clientKeyData", clientKeyData) + .toString(); + } + + /** + * Returns new builder instance. + * + * @return kubernetes API server config builder + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder implements K8sApiConfig.Builder { + + private Scheme scheme; + private IpAddress ipAddress; + private int port; + private String token; + private String caCertData; + private String clientCertData; + private String clientKeyData; + + @Override + public K8sApiConfig build() { + checkArgument(scheme != null, NOT_NULL_MSG, "scheme"); + checkArgument(ipAddress != null, NOT_NULL_MSG, "ipAddress"); + + if (scheme == HTTPS) { + checkArgument(token != null, NOT_NULL_MSG, "token"); + checkArgument(caCertData != null, NOT_NULL_MSG, "caCertData"); + checkArgument(clientCertData != null, NOT_NULL_MSG, "clientCertData"); + checkArgument(clientKeyData != null, NOT_NULL_MSG, "clientKeyData"); + } + + return new DefaultK8sApiConfig(scheme, ipAddress, port, token, + caCertData, clientCertData, clientKeyData); + } + + @Override + public Builder scheme(Scheme scheme) { + this.scheme = scheme; + return this; + } + + @Override + public Builder ipAddress(IpAddress ipAddress) { + this.ipAddress = ipAddress; + return this; + } + + @Override + public Builder port(int port) { + this.port = port; + return this; + } + + @Override + public Builder token(String token) { + this.token = token; + return this; + } + + @Override + public Builder caCertData(String caCertData) { + this.caCertData = caCertData; + return this; + } + + @Override + public Builder clientCertData(String clientCertData) { + this.clientCertData = clientCertData; + return this; + } + + @Override + public Builder clientKeyData(String clientKeyData) { + this.clientKeyData = clientKeyData; + return this; + } + } +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sNode.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sNode.java index 5f22f70a08..76b845cd56 100644 --- a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sNode.java +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/DefaultK8sNode.java @@ -59,7 +59,8 @@ public class DefaultK8sNode implements K8sNode { * @param state node state */ protected DefaultK8sNode(String hostname, Type type, DeviceId intgBridge, - IpAddress managementIp, IpAddress dataIp, K8sNodeState state) { + IpAddress managementIp, IpAddress dataIp, + K8sNodeState state) { this.hostname = hostname; this.type = type; this.intgBridge = intgBridge; @@ -222,6 +223,7 @@ public class DefaultK8sNode implements K8sNode { private IpAddress managementIp; private IpAddress dataIp; private K8sNodeState state; + private K8sApiConfig apiConfig; // private constructor not intended to use from external private Builder() { diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfig.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfig.java new file mode 100644 index 0000000000..0c637339ee --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfig.java @@ -0,0 +1,157 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onlab.packet.IpAddress; + +/** + * Representation of configuration used in kubernetes API server. + */ +public interface K8sApiConfig { + + /** + * Lists of authentication schemes. + */ + enum Scheme { + /** + * Signifies that this is a HTTP authentication scheme. + */ + HTTP, + + /** + * Signifies that this is a HTTPS authentication scheme. + */ + HTTPS, + } + + /** + * Returns the authentication scheme. + * + * @return authentication scheme + */ + Scheme scheme(); + + /** + * Returns the IP address of kubernetes API server. + * + * @return IP address of kubernetes API server + */ + IpAddress ipAddress(); + + /** + * Returns the port number of kubernetes API server. + * + * @return port number of kubernetes API server + */ + int port(); + + /** + * Returns the token used for authenticating to API server. + * + * @return token value + */ + String token(); + + /** + * Returns the CA certificate data. + * + * @return CA certificate data + */ + String caCertData(); + + /** + * Returns the client certificate data. + * + * @return client certificate data + */ + String clientCertData(); + + /** + * Returns the client key data. + * + * @return client key data + */ + String clientKeyData(); + + /** + * Builder of new API config entity. + */ + interface Builder { + + /** + * Builds an immutable kubernetes API config instance. + * + * @return kubernetes API config instance + */ + K8sApiConfig build(); + + /** + * Returns kubernetes API server config builder with supplied scheme. + * + * @param scheme scheme of authentication + * @return kubernetes API config builder + */ + Builder scheme(Scheme scheme); + + /** + * Returns kubernetes API server config builder with supplied IP address. + * + * @param ipAddress IP address of kubernetes API server + * @return kubernetes API config builder + */ + Builder ipAddress(IpAddress ipAddress); + + /** + * Returns kubernetes API server config builder with supplied port number. + * + * @param port port number of kubernetes API server + * @return kubernetes API config builder + */ + Builder port(int port); + + /** + * Returns kubernetes API server config builder with supplied token. + * + * @param token token for authentication + * @return kubernetes API config builder + */ + Builder token(String token); + + /** + * Returns kubernetes API server config builder with supplied CA certificate data. + * + * @param caCertData CA certificate data + * @return kubernetes API config builder + */ + Builder caCertData(String caCertData); + + /** + * Returns kubernetes API server config builder with supplied client certificate data. + * + * @param clientCertData client certificate data + * @return kubernetes API config builder + */ + Builder clientCertData(String clientCertData); + + /** + * Returns kubernetes API server config builder with supplied client key data. + * + * @param clientKeyData client key data + * @return kubernetes API config builder + */ + Builder clientKeyData(String clientKeyData); + } +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigAdminService.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigAdminService.java new file mode 100644 index 0000000000..4f96f788bc --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigAdminService.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onlab.packet.IpAddress; + +/** + * Service for administering inventory of kubernetes API configs. + */ +public interface K8sApiConfigAdminService extends K8sApiConfigService { + + /** + * Creates an API config. + * + * @param config kubernetes API server config + */ + void createApiConfig(K8sApiConfig config); + + /** + * Updates the API config. + * + * @param config kubernetes API server config + */ + void updateApiConfig(K8sApiConfig config); + + /** + * Removes the API config. + * + * @param endpoint kubernetes API endpoint + * @return removed kubernetes API server config; null if no config + * associated with the endpoint + */ + K8sApiConfig removeApiConfig(String endpoint); + + /** + * Removes the kubernetes API config with the given scheme, IP and port. + * + * @param scheme scheme (HTTP/HTTPS) + * @param ipAddress IP address of API server + * @param port port number of API server + * @return removed kubernetes API server config; null if no config + * associated with the scheme, IP and port + */ + K8sApiConfig removeApiConfig(K8sApiConfig.Scheme scheme, IpAddress ipAddress, int port); +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigEvent.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigEvent.java new file mode 100644 index 0000000000..1b85dc9bcc --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onosproject.event.AbstractEvent; + +/** + * Describes kubernetes API server config event. + */ +public class K8sApiConfigEvent extends AbstractEvent { + + /** + * Lists of kubernetes API server config event types. + */ + public enum Type { + /** + * Signifies that API config is created. + */ + K8S_API_CONFIG_CREATED, + + /** + * Signifies that API config is updated. + */ + K8S_API_CONFIG_UPDATED, + + /** + * Signifies that API config is removed. + */ + K8S_API_CONFIG_REMOVED, + } + + /** + * Creates an event with the given type and node. + * + * @param type event type + * @param subject kubernetes API config + */ + public K8sApiConfigEvent(Type type, K8sApiConfig subject) { + super(type, subject); + } +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigListener.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigListener.java new file mode 100644 index 0000000000..4901b802ce --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigListener.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onosproject.event.EventListener; + +/** + * Listener for kubernetes API config event. + */ +public interface K8sApiConfigListener extends EventListener { +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigService.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigService.java new file mode 100644 index 0000000000..42ef15e4e9 --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigService.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onlab.packet.IpAddress; +import org.onosproject.event.ListenerService; +import org.onosproject.k8snode.api.K8sApiConfig.Scheme; + +import java.util.Set; + +/** + * Service for interfacing with the inventory of kubernetes API server config. + */ +public interface K8sApiConfigService + extends ListenerService { + String APP_ID = "org.onosproject.k8snode"; + + /** + * Returns all registered API configs. + * + * @return set of kubernetes API configs + */ + Set apiConfigs(); + + /** + * Returns the API config with the specified endpoint. + * + * @param endpoint endpoint + * @return kubernetes API config + */ + K8sApiConfig apiConfig(String endpoint); + + /** + * Returns the API config with the specified scheme, IP and port. + * + * @param scheme scheme (HTTP/HTTPS) + * @param ipAddress IP address of API server + * @param port port number of API server + * @return kubernetes API config + */ + K8sApiConfig apiConfig(Scheme scheme, IpAddress ipAddress, int port); +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigStore.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigStore.java new file mode 100644 index 0000000000..0f8f221367 --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigStore.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onlab.packet.IpAddress; +import org.onosproject.k8snode.api.K8sApiConfig.Scheme; +import org.onosproject.store.Store; + +import java.util.Set; + +/** + * Manages inventory of kubernetes API config; not intended for direct use. + */ +public interface K8sApiConfigStore + extends Store { + + /** + * Creates a API config. + * + * @param config kubernetes API server config + */ + void createApiConfig(K8sApiConfig config); + + /** + * Updates the API config. + * + * @param config kubernetes API server config + */ + void updateApiConfig(K8sApiConfig config); + + /** + * Removes the kubernetes API config with the given endpoint. + * Endpoint comprises of scheme (HTTP), IP address and port + * + * @param endpoint kubernetes API endpoint + * @return removed kubernetes API server config; null if no config + * associated with the endpoint + */ + K8sApiConfig removeApiConfig(String endpoint); + + /** + * Removes the kubernetes API config with the given scheme, IP and port. + * + * @param scheme scheme (HTTP/HTTPS) + * @param ipAddress IP address of API server + * @param port port number of API server + * @return removed kubernetes API server config; null if no config + * associated with the scheme, IP and port + */ + K8sApiConfig removeApiConfig(Scheme scheme, IpAddress ipAddress, int port); + + /** + * Returns all registered kubernetes API configs. + * + * @return set of kubernetes API configs + */ + Set apiConfigs(); + + /** + * Returns the API config with the specified endpoint. + * + * @param endpoint endpoint + * @return kubernetes API config + */ + K8sApiConfig apiConfig(String endpoint); + + /** + * Returns the API config with the specified scheme, IP and port. + * + * @param scheme scheme (HTTP/HTTPS) + * @param ipAddress IP address of API server + * @param port port number of API server + * @return kubernetes API config + */ + K8sApiConfig apiConfig(Scheme scheme, IpAddress ipAddress, int port); +} diff --git a/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigStoreDelegate.java b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigStoreDelegate.java new file mode 100644 index 0000000000..11ee5f9e79 --- /dev/null +++ b/apps/k8s-node/api/src/main/java/org/onosproject/k8snode/api/K8sApiConfigStoreDelegate.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import org.onosproject.store.StoreDelegate; + +/** + * Kubernetes API server config store delegate. + */ +public interface K8sApiConfigStoreDelegate extends StoreDelegate { +} diff --git a/apps/k8s-node/api/src/test/java/org/onosproject/k8snode/api/DefaultK8sApiConfigTest.java b/apps/k8s-node/api/src/test/java/org/onosproject/k8snode/api/DefaultK8sApiConfigTest.java new file mode 100644 index 0000000000..55e56f14e1 --- /dev/null +++ b/apps/k8s-node/api/src/test/java/org/onosproject/k8snode/api/DefaultK8sApiConfigTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.api; + +import com.google.common.testing.EqualsTester; +import org.junit.Before; +import org.junit.Test; +import org.onlab.packet.IpAddress; +import org.onosproject.k8snode.api.K8sApiConfig.Scheme; + +import static junit.framework.TestCase.assertEquals; +import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable; +import static org.onosproject.k8snode.api.K8sApiConfig.Scheme.HTTP; +import static org.onosproject.k8snode.api.K8sApiConfig.Scheme.HTTPS; + +/** + * Unit tests for DefaultK8sApiConfig. + */ +public final class DefaultK8sApiConfigTest { + + private static final Scheme SCHEME_1 = HTTP; + private static final Scheme SCHEME_2 = HTTPS; + + private static final IpAddress IP_ADDRESS_1 = IpAddress.valueOf("192.168.0.200"); + private static final IpAddress IP_ADDRESS_2 = IpAddress.valueOf("192.168.0.201"); + + private static final int PORT_1 = 6443; + private static final int PORT_2 = 443; + + private static final String TOKEN_1 = "token1"; + private static final String TOKEN_2 = "token2"; + + private static final String CA_CERT_DATA_1 = "caCertData1"; + private static final String CA_CERT_DATA_2 = "caCertData2"; + + private static final String CLIENT_CERT_DATA_1 = "clientCertData1"; + private static final String CLIENT_CERT_DATA_2 = "clientCertData2"; + + private static final String CLIENT_KEY_DATA_1 = "clientKeyData1"; + private static final String CLIENT_KEY_DATA_2 = "clientKeyData2"; + + private K8sApiConfig config1; + private K8sApiConfig sameAsConfig1; + private K8sApiConfig config2; + + /** + * Tests class immutability. + */ + @Test + public void testImmutability() { + assertThatClassIsImmutable(DefaultK8sApiConfig.class); + } + + /** + * Initial setup for this unit test. + */ + @Before + public void setUp() { + config1 = DefaultK8sApiConfig.builder() + .scheme(SCHEME_1) + .ipAddress(IP_ADDRESS_1) + .port(PORT_1) + .token(TOKEN_1) + .caCertData(CA_CERT_DATA_1) + .clientCertData(CLIENT_CERT_DATA_1) + .clientKeyData(CLIENT_KEY_DATA_1) + .build(); + + sameAsConfig1 = DefaultK8sApiConfig.builder() + .scheme(SCHEME_1) + .ipAddress(IP_ADDRESS_1) + .port(PORT_1) + .token(TOKEN_1) + .caCertData(CA_CERT_DATA_1) + .clientCertData(CLIENT_CERT_DATA_1) + .clientKeyData(CLIENT_KEY_DATA_1) + .build(); + + config2 = DefaultK8sApiConfig.builder() + .scheme(SCHEME_2) + .ipAddress(IP_ADDRESS_2) + .port(PORT_2) + .token(TOKEN_2) + .caCertData(CA_CERT_DATA_2) + .clientCertData(CLIENT_CERT_DATA_2) + .clientKeyData(CLIENT_KEY_DATA_2) + .build(); + } + + /** + * Tests object equality. + */ + @Test + public void testEquality() { + new EqualsTester().addEqualityGroup(config1, sameAsConfig1) + .addEqualityGroup(config2) + .testEquals(); + } + + /** + * Test object construction. + */ + @Test + public void testConstruction() { + K8sApiConfig config = config1; + + assertEquals(SCHEME_1, config.scheme()); + assertEquals(IP_ADDRESS_1, config.ipAddress()); + assertEquals(PORT_1, config.port()); + assertEquals(TOKEN_1, config.token()); + assertEquals(CA_CERT_DATA_1, config.caCertData()); + assertEquals(CLIENT_CERT_DATA_1, config.clientCertData()); + assertEquals(CLIENT_KEY_DATA_1, config.clientKeyData()); + } +} diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/cli/K8sApiConfigListCommand.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/cli/K8sApiConfigListCommand.java new file mode 100644 index 0000000000..6ede6dab9d --- /dev/null +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/cli/K8sApiConfigListCommand.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.cli; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.collect.Lists; +import org.apache.karaf.shell.api.action.Command; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.onosproject.cli.AbstractShellCommand; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfigService; + +import java.util.Comparator; +import java.util.List; + +import static org.onosproject.k8snode.util.K8sNodeUtil.prettyJson; + +/** + * Lists all kubernetes API server configs registered to the service. + */ +@Service +@Command(scope = "onos", name = "k8s-api-configs", + description = "Lists all kubernetes API server configs registered to the service") +public class K8sApiConfigListCommand extends AbstractShellCommand { + + private static final String FORMAT = "%-10s%-25s%-10s"; + + @Override + protected void doExecute() { + K8sApiConfigService configService = get(K8sApiConfigService.class); + List configs = Lists.newArrayList(configService.apiConfigs()); + configs.sort(Comparator.comparing(K8sApiConfig::ipAddress)); + + if (outputJson()) { + print("%s", json(configs)); + } else { + print(FORMAT, "Scheme", "IpAddress", "Port"); + for (K8sApiConfig config : configs) { + print(FORMAT, config.scheme().name(), + config.ipAddress().toString(), config.port()); + } + print("Total %s API configs", configService.apiConfigs().size()); + } + } + + private String json(List configs) { + ObjectMapper mapper = new ObjectMapper(); + ArrayNode result = mapper.createArrayNode(); + for (K8sApiConfig config : configs) { + result.add(jsonForEntity(config, K8sApiConfig.class)); + } + return prettyJson(mapper, result.toString()); + } +} diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/codec/K8sApiConfigCodec.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/codec/K8sApiConfigCodec.java new file mode 100644 index 0000000000..5e0cb27e90 --- /dev/null +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/codec/K8sApiConfigCodec.java @@ -0,0 +1,140 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.codec; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.onlab.packet.IpAddress; +import org.onosproject.codec.CodecContext; +import org.onosproject.codec.JsonCodec; +import org.onosproject.k8snode.api.DefaultK8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig.Scheme; + +import static org.onlab.util.Tools.nullIsIllegal; +import static org.onosproject.k8snode.api.K8sApiConfig.Scheme.HTTPS; + +/** + * Kubernetes API server config codec used for serializing and de-serializing JSON string. + */ +public final class K8sApiConfigCodec extends JsonCodec { + + private static final String SCHEME = "scheme"; + private static final String IP_ADDRESS = "ipAddress"; + private static final String PORT = "port"; + private static final String TOKEN = "token"; + private static final String CA_CERT_DATA = "caCertData"; + private static final String CLIENT_CERT_DATA = "clientCertData"; + private static final String CLIENT_KEY_DATA = "clientKeyData"; + + private static final String MISSING_MESSAGE = " is required in K8sApiConfig"; + + @Override + public ObjectNode encode(K8sApiConfig entity, CodecContext context) { + ObjectNode node = context.mapper().createObjectNode() + .put(SCHEME, entity.scheme().name()) + .put(IP_ADDRESS, entity.ipAddress().toString()) + .put(PORT, entity.port()); + + if (entity.scheme() == HTTPS) { + node.put(TOKEN, entity.token()) + .put(CA_CERT_DATA, entity.caCertData()) + .put(CLIENT_CERT_DATA, entity.clientCertData()) + .put(CLIENT_KEY_DATA, entity.clientKeyData()); + } else { + if (entity.token() != null) { + node.put(TOKEN, entity.token()); + } + + if (entity.caCertData() != null) { + node.put(CA_CERT_DATA, entity.caCertData()); + } + + if (entity.clientCertData() != null) { + node.put(CLIENT_CERT_DATA, entity.clientCertData()); + } + + if (entity.clientKeyData() != null) { + node.put(CLIENT_KEY_DATA, entity.clientKeyData()); + } + } + + return node; + } + + @Override + public K8sApiConfig decode(ObjectNode json, CodecContext context) { + if (json == null || !json.isObject()) { + return null; + } + + Scheme scheme = Scheme.valueOf(nullIsIllegal( + json.get(SCHEME).asText(), SCHEME + MISSING_MESSAGE)); + IpAddress ipAddress = IpAddress.valueOf(nullIsIllegal( + json.get(IP_ADDRESS).asText(), IP_ADDRESS + MISSING_MESSAGE)); + int port = json.get(PORT).asInt(); + + K8sApiConfig.Builder builder = DefaultK8sApiConfig.builder() + .scheme(scheme) + .ipAddress(ipAddress) + .port(port); + + JsonNode tokenJson = json.get(TOKEN); + JsonNode caCertDataJson = json.get(CA_CERT_DATA); + JsonNode clientCertDataJson = json.get(CLIENT_CERT_DATA); + JsonNode clientKeyDataJson = json.get(CLIENT_KEY_DATA); + + String token = ""; + String caCertData = ""; + String clientCertData = ""; + String clientKeyData = ""; + + if (scheme == HTTPS) { + token = nullIsIllegal(tokenJson.asText(), + TOKEN + MISSING_MESSAGE); + caCertData = nullIsIllegal(caCertDataJson.asText(), + CA_CERT_DATA + MISSING_MESSAGE); + clientCertData = nullIsIllegal(clientCertDataJson.asText(), + CLIENT_CERT_DATA + MISSING_MESSAGE); + clientKeyData = nullIsIllegal(clientKeyDataJson.asText(), + CLIENT_KEY_DATA + MISSING_MESSAGE); + + + } else { + if (tokenJson != null) { + token = tokenJson.asText(); + } + + if (caCertDataJson != null) { + caCertData = caCertDataJson.asText(); + } + + if (clientCertDataJson != null) { + clientCertData = clientCertDataJson.asText(); + } + + if (clientKeyDataJson != null) { + clientKeyData = clientKeyDataJson.asText(); + } + } + + return builder.token(token) + .caCertData(caCertData) + .clientCertData(clientCertData) + .clientKeyData(clientKeyData) + .build(); + } +} diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/impl/DistributedK8sApiConfigStore.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/impl/DistributedK8sApiConfigStore.java new file mode 100644 index 0000000000..321aac0ec6 --- /dev/null +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/impl/DistributedK8sApiConfigStore.java @@ -0,0 +1,198 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.impl; + +import com.google.common.collect.ImmutableSet; +import org.onlab.packet.IpAddress; +import org.onlab.util.KryoNamespace; +import org.onosproject.core.ApplicationId; +import org.onosproject.core.CoreService; +import org.onosproject.k8snode.api.DefaultK8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfigEvent; +import org.onosproject.k8snode.api.K8sApiConfigStore; +import org.onosproject.k8snode.api.K8sApiConfigStoreDelegate; +import org.onosproject.store.AbstractStore; +import org.onosproject.store.serializers.KryoNamespaces; +import org.onosproject.store.service.ConsistentMap; +import org.onosproject.store.service.MapEvent; +import org.onosproject.store.service.MapEventListener; +import org.onosproject.store.service.Serializer; +import org.onosproject.store.service.StorageService; +import org.onosproject.store.service.Versioned; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.slf4j.Logger; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.onlab.util.Tools.groupedThreads; +import static org.onosproject.k8snode.api.K8sApiConfigEvent.Type.K8S_API_CONFIG_CREATED; +import static org.onosproject.k8snode.api.K8sApiConfigEvent.Type.K8S_API_CONFIG_REMOVED; +import static org.onosproject.k8snode.api.K8sApiConfigEvent.Type.K8S_API_CONFIG_UPDATED; +import static org.onosproject.k8snode.util.K8sNodeUtil.endpoint; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Implementation of kubernetes API config store using consistent map. + */ +@Component(immediate = true, service = K8sApiConfigStore.class) +public class DistributedK8sApiConfigStore + extends AbstractStore + implements K8sApiConfigStore { + + private final Logger log = getLogger(getClass()); + + private static final String ERR_NOT_FOUND = " does not exist"; + private static final String ERR_DUPLICATE = " already exists"; + private static final String APP_ID = "org.onosproject.k8snode"; + + private static final KryoNamespace + SERIALIZER_K8S_API_CONFIG = KryoNamespace.newBuilder() + .register(KryoNamespaces.API) + .register(K8sApiConfig.class) + .register(DefaultK8sApiConfig.class) + .register(K8sApiConfig.Scheme.class) + .register(Collection.class) + .build(); + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected CoreService coreService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected StorageService storageService; + + private final ExecutorService eventExecutor = newSingleThreadExecutor( + groupedThreads(this.getClass().getSimpleName(), "event-handler", log)); + + private final MapEventListener apiConfigMapListener = + new K8sApiConfigMapListener(); + private ConsistentMap apiConfigStore; + + @Activate + protected void activate() { + ApplicationId appId = coreService.registerApplication(APP_ID); + apiConfigStore = storageService.consistentMapBuilder() + .withSerializer(Serializer.using(SERIALIZER_K8S_API_CONFIG)) + .withName("k8s-apiconfig-store") + .withApplicationId(appId) + .build(); + apiConfigStore.addListener(apiConfigMapListener); + log.info("Started"); + } + + @Deactivate + protected void deactivate() { + apiConfigStore.removeListener(apiConfigMapListener); + eventExecutor.shutdown(); + log.info("Stopped"); + } + + @Override + public void createApiConfig(K8sApiConfig config) { + String key = endpoint(config); + apiConfigStore.compute(key, (endpoint, existing) -> { + final String error = key + ERR_DUPLICATE; + checkArgument(existing == null, error); + return config; + }); + } + + @Override + public void updateApiConfig(K8sApiConfig config) { + String key = endpoint(config); + apiConfigStore.compute(key, (endpoint, existing) -> { + final String error = key + ERR_NOT_FOUND; + checkArgument(existing != null, error); + return config; + }); + } + + @Override + public K8sApiConfig removeApiConfig(String endpoint) { + Versioned apiConfig = apiConfigStore.remove(endpoint); + if (apiConfig == null) { + final String error = endpoint + ERR_NOT_FOUND; + throw new IllegalArgumentException(error); + } + return apiConfig.value(); + } + + @Override + public K8sApiConfig removeApiConfig(K8sApiConfig.Scheme scheme, + IpAddress ipAddress, int port) { + String key = endpoint(scheme, ipAddress, port); + return removeApiConfig(key); + } + + @Override + public Set apiConfigs() { + return ImmutableSet.copyOf(apiConfigStore.asJavaMap().values()); + } + + @Override + public K8sApiConfig apiConfig(String endpoint) { + return apiConfigStore.asJavaMap().get(endpoint); + } + + @Override + public K8sApiConfig apiConfig(K8sApiConfig.Scheme scheme, + IpAddress ipAddress, int port) { + String key = endpoint(scheme, ipAddress, port); + return apiConfig(key); + } + + private class K8sApiConfigMapListener + implements MapEventListener { + + @Override + public void event(MapEvent event) { + switch (event.type()) { + case INSERT: + log.debug("Kubernetes API config created {}", event.newValue()); + eventExecutor.execute(() -> + notifyDelegate(new K8sApiConfigEvent( + K8S_API_CONFIG_CREATED, event.newValue().value() + ))); + break; + case UPDATE: + log.debug("Kubernetes API config updated {}", event.newValue()); + eventExecutor.execute(() -> + notifyDelegate(new K8sApiConfigEvent( + K8S_API_CONFIG_UPDATED, event.newValue().value() + ))); + break; + case REMOVE: + log.debug("Kubernetes API config removed {}", event.oldValue()); + eventExecutor.execute(() -> + notifyDelegate(new K8sApiConfigEvent( + K8S_API_CONFIG_REMOVED, event.oldValue().value() + ))); + break; + default: + // do nothing + break; + } + } + } +} diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/impl/K8sApiConfigManager.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/impl/K8sApiConfigManager.java new file mode 100644 index 0000000000..c7a541488c --- /dev/null +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/impl/K8sApiConfigManager.java @@ -0,0 +1,167 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.impl; + +import com.google.common.base.Strings; +import org.onlab.packet.IpAddress; +import org.onosproject.cluster.ClusterService; +import org.onosproject.cluster.LeadershipService; +import org.onosproject.core.ApplicationId; +import org.onosproject.core.CoreService; +import org.onosproject.event.ListenerRegistry; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig.Scheme; +import org.onosproject.k8snode.api.K8sApiConfigAdminService; +import org.onosproject.k8snode.api.K8sApiConfigEvent; +import org.onosproject.k8snode.api.K8sApiConfigListener; +import org.onosproject.k8snode.api.K8sApiConfigService; +import org.onosproject.k8snode.api.K8sApiConfigStore; +import org.onosproject.k8snode.api.K8sApiConfigStoreDelegate; +import org.onosproject.store.service.StorageService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.slf4j.Logger; + +import java.util.Set; +import java.util.concurrent.ExecutorService; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.onlab.util.Tools.groupedThreads; +import static org.onosproject.k8snode.util.K8sNodeUtil.endpoint; +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Service administering the inventory of kubernetes API configs. + */ +@Component( + immediate = true, + service = { K8sApiConfigService.class, K8sApiConfigAdminService.class } +) +public class K8sApiConfigManager + extends ListenerRegistry + implements K8sApiConfigService, K8sApiConfigAdminService { + + private final Logger log = getLogger(getClass()); + + private static final String MSG_CONFIG = "Kubernetes API config %s %s"; + private static final String MSG_CREATED = "created"; + private static final String MSG_UPDATED = "updated"; + private static final String MSG_REMOVED = "removed"; + + private static final String ERR_NULL_CONFIG = "Kubernetes API config cannot be null"; + private static final String ERR_NULL_ENDPOINT = "Kubernetes API endpoint cannot be null"; + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected K8sApiConfigStore configStore; + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected CoreService coreService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected ClusterService clusterService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected LeadershipService leadershipService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY) + protected StorageService storageService; + + private final ExecutorService eventExecutor = newSingleThreadExecutor( + groupedThreads(this.getClass().getSimpleName(), "event-handler", log)); + + private final K8sApiConfigStoreDelegate delegate = new InternalApiConfigStoreDelegate(); + + private ApplicationId appId; + + @Activate + protected void activate() { + appId = coreService.registerApplication(APP_ID); + configStore.setDelegate(delegate); + + leadershipService.runForLeadership(appId.name()); + + log.info("Started"); + } + + @Deactivate + protected void deactivate() { + configStore.unsetDelegate(delegate); + + leadershipService.withdraw(appId.name()); + eventExecutor.shutdown(); + + log.info("Stopped"); + } + + @Override + public void createApiConfig(K8sApiConfig config) { + checkNotNull(config, ERR_NULL_CONFIG); + configStore.createApiConfig(config); + log.info(String.format(MSG_CONFIG, endpoint(config), MSG_CREATED)); + } + + @Override + public void updateApiConfig(K8sApiConfig config) { + checkNotNull(config, ERR_NULL_CONFIG); + configStore.updateApiConfig(config); + log.info(String.format(MSG_CONFIG, endpoint(config), MSG_UPDATED)); + } + + @Override + public K8sApiConfig removeApiConfig(String endpoint) { + checkArgument(!Strings.isNullOrEmpty(endpoint), ERR_NULL_ENDPOINT); + K8sApiConfig config = configStore.removeApiConfig(endpoint); + log.info(String.format(MSG_CONFIG, endpoint, MSG_REMOVED)); + return config; + } + + @Override + public K8sApiConfig removeApiConfig(Scheme scheme, + IpAddress ipAddress, int port) { + return removeApiConfig(endpoint(scheme, ipAddress, port)); + } + + @Override + public Set apiConfigs() { + return configStore.apiConfigs(); + } + + @Override + public K8sApiConfig apiConfig(String endpoint) { + return configStore.apiConfig(endpoint); + } + + @Override + public K8sApiConfig apiConfig(Scheme scheme, IpAddress ipAddress, int port) { + return apiConfig(endpoint(scheme, ipAddress, port)); + } + + private class InternalApiConfigStoreDelegate implements K8sApiConfigStoreDelegate { + + @Override + public void notify(K8sApiConfigEvent event) { + if (event != null) { + log.trace("send kubernetes API config event {}", event); + process(event); + } + } + } +} diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/util/K8sNodeUtil.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/util/K8sNodeUtil.java index c0a158ed26..7fea9cff7c 100644 --- a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/util/K8sNodeUtil.java +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/util/K8sNodeUtil.java @@ -17,6 +17,10 @@ package org.onosproject.k8snode.util; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Strings; +import org.apache.commons.lang.StringUtils; +import org.onlab.packet.IpAddress; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig.Scheme; import org.onosproject.k8snode.api.K8sNode; import org.onosproject.net.Device; import org.onosproject.net.behaviour.BridgeConfig; @@ -39,6 +43,9 @@ import static org.onlab.util.Tools.get; public final class K8sNodeUtil { private static final Logger log = LoggerFactory.getLogger(K8sNodeUtil.class); + private static final String COLON_SLASH = "://"; + private static final String COLON = ":"; + /** * Prevents object installation from external. */ @@ -146,4 +153,35 @@ public final class K8sNodeUtil { } return null; } + + /** + * Generates endpoint URL by referring to scheme, ipAddress and port. + * + * @param scheme scheme + * @param ipAddress IP address + * @param port port number + * @return generated endpoint URL + */ + public static String endpoint(Scheme scheme, IpAddress ipAddress, int port) { + StringBuilder endpoint = new StringBuilder(); + String protocol = StringUtils.lowerCase(scheme.name()); + + endpoint.append(protocol); + endpoint.append(COLON_SLASH); + endpoint.append(ipAddress.toString()); + endpoint.append(COLON); + endpoint.append(port); + + return endpoint.toString(); + } + + /** + * Generates endpoint URL by referring to scheme, ipAddress and port. + * + * @param apiConfig kubernetes API config + * @return generated endpoint URL + */ + public static String endpoint(K8sApiConfig apiConfig) { + return endpoint(apiConfig.scheme(), apiConfig.ipAddress(), apiConfig.port()); + } } diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeCodecRegister.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeCodecRegister.java index cb283f943e..9138539b0d 100644 --- a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeCodecRegister.java +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeCodecRegister.java @@ -16,7 +16,9 @@ package org.onosproject.k8snode.web; import org.onosproject.codec.CodecService; +import org.onosproject.k8snode.api.K8sApiConfig; import org.onosproject.k8snode.api.K8sNode; +import org.onosproject.k8snode.codec.K8sApiConfigCodec; import org.onosproject.k8snode.codec.K8sNodeCodec; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -42,6 +44,7 @@ public class K8sNodeCodecRegister { protected void activate() { codecService.registerCodec(K8sNode.class, new K8sNodeCodec()); + codecService.registerCodec(K8sApiConfig.class, new K8sApiConfigCodec()); log.info("Started"); } @@ -50,6 +53,7 @@ public class K8sNodeCodecRegister { protected void deactivate() { codecService.unregisterCodec(K8sNode.class); + codecService.unregisterCodec(K8sApiConfig.class); log.info("Stopped"); } diff --git a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeWebResource.java b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeWebResource.java index 0367e0f4ea..590dc5693f 100644 --- a/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeWebResource.java +++ b/apps/k8s-node/app/src/main/java/org/onosproject/k8snode/web/K8sNodeWebResource.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Sets; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfigAdminService; import org.onosproject.k8snode.api.K8sNode; import org.onosproject.k8snode.api.K8sNodeAdminService; import org.onosproject.rest.AbstractWebResource; @@ -26,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; @@ -43,6 +46,7 @@ import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; import static javax.ws.rs.core.Response.created; import static org.onlab.util.Tools.nullIsIllegal; import static org.onlab.util.Tools.readTreeFromStream; +import static org.onosproject.k8snode.util.K8sNodeUtil.endpoint; /** * Handles REST API call of kubernetes node config. @@ -55,15 +59,18 @@ public class K8sNodeWebResource extends AbstractWebResource { private static final String MESSAGE_NODE = "Received node %s request"; private static final String NODES = "nodes"; + private static final String API_CONFIGS = "apiConfigs"; private static final String CREATE = "CREATE"; private static final String UPDATE = "UPDATE"; private static final String NODE_ID = "NODE_ID"; - private static final String DELETE = "DELETE"; + private static final String REMOVE = "REMOVE"; private static final String HOST_NAME = "hostname"; + private static final String ENDPOINT = "endpoint"; private static final String ERROR_MESSAGE = " cannot be null"; - private final K8sNodeAdminService adminService = get(K8sNodeAdminService.class); + private final K8sNodeAdminService nodeAdminService = get(K8sNodeAdminService.class); + private final K8sApiConfigAdminService configAdminService = get(K8sApiConfigAdminService.class); @Context private UriInfo uriInfo; @@ -77,15 +84,16 @@ public class K8sNodeWebResource extends AbstractWebResource { * @onos.rsModel K8sNode */ @POST + @Path("node") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response createNodes(InputStream input) { log.trace(String.format(MESSAGE_NODE, CREATE)); readNodeConfiguration(input).forEach(node -> { - K8sNode existing = adminService.node(node.hostname()); + K8sNode existing = nodeAdminService.node(node.hostname()); if (existing == null) { - adminService.createNode(node); + nodeAdminService.createNode(node); } }); @@ -105,6 +113,7 @@ public class K8sNodeWebResource extends AbstractWebResource { * @onos.rsModel K8sNode */ @PUT + @Path("node") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response updateNodes(InputStream input) { @@ -112,12 +121,12 @@ public class K8sNodeWebResource extends AbstractWebResource { Set nodes = readNodeConfiguration(input); for (K8sNode node: nodes) { - K8sNode existing = adminService.node(node.hostname()); + K8sNode existing = nodeAdminService.node(node.hostname()); if (existing == null) { log.warn("There is no node configuration to update : {}", node.hostname()); return Response.notModified().build(); } else if (!existing.equals(node)) { - adminService.updateNode(node); + nodeAdminService.updateNode(node); } } @@ -130,23 +139,22 @@ public class K8sNodeWebResource extends AbstractWebResource { * @param hostname host name contained in kubernetes nodes configuration * @return 204 NO_CONTENT, 400 BAD_REQUEST if the JSON is malformed, and * 304 NOT_MODIFIED without the updated config - * @onos.rsModel K8sNode */ - @javax.ws.rs.DELETE + @DELETE + @Path("node/{hostname}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Path("{hostname}") public Response deleteNodes(@PathParam("hostname") String hostname) { - log.trace(String.format(MESSAGE_NODE, DELETE)); + log.trace(String.format(MESSAGE_NODE, REMOVE)); K8sNode existing = - adminService.node(nullIsIllegal(hostname, HOST_NAME + ERROR_MESSAGE)); + nodeAdminService.node(nullIsIllegal(hostname, HOST_NAME + ERROR_MESSAGE)); if (existing == null) { log.warn("There is no node configuration to delete : {}", hostname); return Response.notModified().build(); } else { - adminService.removeNode(hostname); + nodeAdminService.removeNode(hostname); } return Response.noContent().build(); @@ -175,4 +183,111 @@ public class K8sNodeWebResource extends AbstractWebResource { return nodeSet; } + + /** + * Creates a set of kubernetes API config from the JSON input stream. + * + * @param input kubernetes API configs JSON input stream + * @return 201 CREATED if the JSON is correct, 400 BAD_REQUEST if the JSON + * is malformed + * @onos.rsModel K8sApiConfig + */ + @POST + @Path("api") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createApiConfigs(InputStream input) { + log.trace(String.format(MESSAGE_NODE, CREATE)); + + readApiConfigConfiguration(input).forEach(config -> { + K8sApiConfig existing = configAdminService.apiConfig(endpoint(config)); + if (existing == null) { + configAdminService.createApiConfig(config); + } + }); + + UriBuilder locationBuilder = uriInfo.getBaseUriBuilder() + .path(API_CONFIGS); + + return created(locationBuilder.build()).build(); + } + + /** + * Updates a set of kubernetes API config from the JSON input stream. + * + * @param input kubernetes API configs JSON input stream + * @return 200 OK with the updated kubernetes API config, 400 BAD_REQUEST + * if the JSON is malformed, and 304 NOT_MODIFIED without the updated config + * @onos.rsModel K8sApiConfig + */ + @PUT + @Path("api") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updateApiConfigs(InputStream input) { + log.trace(String.format(MESSAGE_NODE, UPDATE)); + + Set configs = readApiConfigConfiguration(input); + for (K8sApiConfig config: configs) { + K8sApiConfig existing = configAdminService.apiConfig(endpoint(config)); + if (existing == null) { + log.warn("There is no API configuration to update : {}", endpoint(config)); + return Response.notModified().build(); + } else if (!existing.equals(config)) { + configAdminService.updateApiConfig(config); + } + } + + return Response.ok().build(); + } + + /** + * Removes a kubernetes API config. + * + * @param endpoint kubernetes API endpoint + * @return 204 NO_CONTENT, 400 BAD_REQUEST if the JSON is malformed + */ + @DELETE + @Path("api/{endpoint : .+}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response deleteApiConfig(@PathParam("endpoint") String endpoint) { + log.trace(String.format(MESSAGE_NODE, REMOVE)); + + K8sApiConfig existing = + configAdminService.apiConfig(nullIsIllegal(endpoint, ENDPOINT + ERROR_MESSAGE)); + + if (existing == null) { + log.warn("There is no API configuration to delete : {}", endpoint); + return Response.notModified().build(); + } else { + configAdminService.removeApiConfig(endpoint); + } + + return Response.noContent().build(); + } + + private Set readApiConfigConfiguration(InputStream input) { + Set configSet = Sets.newHashSet(); + try { + JsonNode jsonTree = readTreeFromStream(mapper().enable(INDENT_OUTPUT), input); + ArrayNode configs = (ArrayNode) jsonTree.path(API_CONFIGS); + configs.forEach(config -> { + try { + ObjectNode objectNode = config.deepCopy(); + K8sApiConfig k8sApiConfig = + codec(K8sApiConfig.class).decode(objectNode, this); + + configSet.add(k8sApiConfig); + } catch (Exception e) { + log.error("Exception occurred due to {}", e); + throw new IllegalArgumentException(); + } + }); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + + return configSet; + } } diff --git a/apps/k8s-node/app/src/main/resources/definitions/K8sApiConfig.json b/apps/k8s-node/app/src/main/resources/definitions/K8sApiConfig.json new file mode 100644 index 0000000000..b82447f22e --- /dev/null +++ b/apps/k8s-node/app/src/main/resources/definitions/K8sApiConfig.json @@ -0,0 +1,33 @@ +{ + "type": "object", + "required": [ + "apiConfigs" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "scheme", + "ipAddress", + "port" + ], + "properties": { + "scheme": { + "type": "string", + "example": "HTTP" + }, + "ipAddress": { + "type": "string", + "example": "10.10.10.2" + }, + "port": { + "type": "integer", + "example": 6443 + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java new file mode 100644 index 0000000000..ece2ef8a89 --- /dev/null +++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigCodecTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.codec; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Before; +import org.junit.Test; +import org.onlab.packet.IpAddress; +import org.onosproject.codec.CodecContext; +import org.onosproject.codec.JsonCodec; +import org.onosproject.codec.impl.CodecManager; +import org.onosproject.core.CoreService; +import org.onosproject.k8snode.api.DefaultK8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static junit.framework.TestCase.assertEquals; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.onosproject.k8snode.codec.K8sApiConfigJsonMatcher.matchesK8sApiConfig; +import static org.onosproject.net.NetTestTools.APP_ID; + +/** + * Unit tests for kubernetes API config codec. + */ +public class K8sApiConfigCodecTest { + + MockCodecContext context; + + JsonCodec k8sApiConfigCodec; + + final CoreService mockCoreService = createMock(CoreService.class); + private static final String REST_APP_ID = "org.onosproject.rest"; + + /** + * Initial setup for this unit test. + */ + @Before + public void setUp() { + context = new MockCodecContext(); + k8sApiConfigCodec = new K8sApiConfigCodec(); + + assertThat(k8sApiConfigCodec, notNullValue()); + + expect(mockCoreService.registerApplication(REST_APP_ID)) + .andReturn(APP_ID).anyTimes(); + replay(mockCoreService); + context.registerService(CoreService.class, mockCoreService); + } + + /** + * Tests the kubernetes API config encoding. + */ + @Test + public void testK8sApiConfigEncode() { + K8sApiConfig config = DefaultK8sApiConfig.builder() + .scheme(K8sApiConfig.Scheme.HTTPS) + .ipAddress(IpAddress.valueOf("10.10.10.23")) + .port(6443) + .token("token") + .caCertData("caCertData") + .clientCertData("clientCertData") + .clientKeyData("clientKeyData") + .build(); + + ObjectNode configJson = k8sApiConfigCodec.encode(config, context); + assertThat(configJson, matchesK8sApiConfig(config)); + } + + /** + * Tests the kubernetes API config decoding. + * + * @throws IOException IO exception + */ + @Test + public void testK8sApiConfigDecode() throws IOException { + K8sApiConfig config = getK8sApiConfig("K8sApiConfig.json"); + + assertEquals("HTTPS", config.scheme().name()); + assertEquals("10.134.34.223", config.ipAddress().toString()); + assertEquals(6443, config.port()); + assertEquals("token", config.token()); + assertEquals("caCertData", config.caCertData()); + assertEquals("clientCertData", config.clientCertData()); + assertEquals("clientKeyData", config.clientKeyData()); + } + + private K8sApiConfig getK8sApiConfig(String resourceName) throws IOException { + InputStream jsonStream = K8sNodeCodecTest.class.getResourceAsStream(resourceName); + JsonNode json = context.mapper().readTree(jsonStream); + assertThat(json, notNullValue()); + K8sApiConfig config = k8sApiConfigCodec.decode((ObjectNode) json, context); + assertThat(config, notNullValue()); + return config; + } + + private class MockCodecContext implements CodecContext { + private final ObjectMapper mapper = new ObjectMapper(); + private final CodecManager manager = new CodecManager(); + private final Map, Object> services = new HashMap<>(); + + /** + * Constructs a new mock codec context. + */ + public MockCodecContext() { + manager.activate(); + } + + @Override + public ObjectMapper mapper() { + return mapper; + } + + @Override + @SuppressWarnings("unchecked") + public JsonCodec codec(Class entityClass) { + if (entityClass == K8sApiConfig.class) { + return (JsonCodec) k8sApiConfigCodec; + } + return manager.getCodec(entityClass); + } + + @SuppressWarnings("unchecked") + @Override + public T getService(Class serviceClass) { + return (T) services.get(serviceClass); + } + + // for registering mock services + public void registerService(Class serviceClass, T impl) { + services.put(serviceClass, impl); + } + } +} diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java new file mode 100644 index 0000000000..df1ac2f3ee --- /dev/null +++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/codec/K8sApiConfigJsonMatcher.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.codec; + +import com.fasterxml.jackson.databind.JsonNode; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.onosproject.k8snode.api.K8sApiConfig; + +/** + * Hamcrest matcher for kubernetes API config. + */ +public final class K8sApiConfigJsonMatcher extends TypeSafeDiagnosingMatcher { + + private final K8sApiConfig k8sApiConfig; + + private static final String SCHEME = "scheme"; + private static final String IP_ADDRESS = "ipAddress"; + private static final String PORT = "port"; + private static final String TOKEN = "token"; + private static final String CA_CERT_DATA = "caCertData"; + private static final String CLIENT_CERT_DATA = "clientCertData"; + private static final String CLIENT_KEY_DATA = "clientKeyData"; + + private K8sApiConfigJsonMatcher(K8sApiConfig k8sApiConfig) { + this.k8sApiConfig = k8sApiConfig; + } + + @Override + protected boolean matchesSafely(JsonNode jsonNode, Description description) { + + // check scheme + String jsonScheme = jsonNode.get(SCHEME).asText(); + String scheme = k8sApiConfig.scheme().name(); + if (!jsonScheme.equals(scheme)) { + description.appendText("scheme was " + jsonScheme); + return false; + } + + // check IP address + String jsonIpAddress = jsonNode.get(IP_ADDRESS).asText(); + String ipAddress = k8sApiConfig.ipAddress().toString(); + if (!jsonIpAddress.equals(ipAddress)) { + description.appendText("ipAddress was " + jsonIpAddress); + return false; + } + + // check port + int jsonPort = jsonNode.get(PORT).asInt(); + int port = k8sApiConfig.port(); + if (jsonPort != port) { + description.appendText("port was " + jsonPort); + return false; + } + + // check token + JsonNode jsonToken = jsonNode.get(TOKEN); + String token = k8sApiConfig.token(); + if (jsonToken != null) { + if (!jsonToken.asText().equals(token)) { + description.appendText("token was " + jsonToken); + return false; + } + } + + // check caCertData + JsonNode jsonCaCertData = jsonNode.get(CA_CERT_DATA); + String caCertData = k8sApiConfig.caCertData(); + if (jsonCaCertData != null) { + if (!jsonCaCertData.asText().equals(caCertData)) { + description.appendText("caCertData was " + jsonCaCertData); + return false; + } + } + + // check clientCertData + JsonNode jsonClientCertData = jsonNode.get(CLIENT_CERT_DATA); + String clientCertData = k8sApiConfig.clientCertData(); + + if (jsonClientCertData != null) { + if (!jsonClientCertData.asText().equals(clientCertData)) { + description.appendText("clientCertData was " + jsonClientCertData); + return false; + } + } + + // check clientKeyData + JsonNode jsonClientKeyData = jsonNode.get(CLIENT_KEY_DATA); + String clientKeyData = k8sApiConfig.clientKeyData(); + + if (jsonClientKeyData != null) { + if (!jsonClientKeyData.asText().equals(clientKeyData)) { + description.appendText("clientKeyData was " + jsonClientKeyData); + return false; + } + } + + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText(k8sApiConfig.toString()); + } + + /** + * Factory to allocate an k8sApiConfig matcher. + * + * @param config k8sApiConfig object we are looking for + * @return matcher + */ + public static K8sApiConfigJsonMatcher matchesK8sApiConfig(K8sApiConfig config) { + return new K8sApiConfigJsonMatcher(config); + } +} diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java new file mode 100644 index 0000000000..90f56c3816 --- /dev/null +++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/impl/K8sApiConfigManagerTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2019-present Open Networking Foundation + * + * 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 org.onosproject.k8snode.impl; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.onlab.junit.TestUtils; +import org.onlab.packet.IpAddress; +import org.onosproject.cluster.ClusterServiceAdapter; +import org.onosproject.cluster.LeadershipServiceAdapter; +import org.onosproject.core.ApplicationId; +import org.onosproject.core.CoreServiceAdapter; +import org.onosproject.core.DefaultApplicationId; +import org.onosproject.event.Event; +import org.onosproject.k8snode.api.DefaultK8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfigEvent; +import org.onosproject.k8snode.api.K8sApiConfigListener; +import org.onosproject.store.service.TestStorageService; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.onosproject.k8snode.api.K8sApiConfigEvent.Type.K8S_API_CONFIG_CREATED; +import static org.onosproject.k8snode.api.K8sApiConfigEvent.Type.K8S_API_CONFIG_REMOVED; +import static org.onosproject.k8snode.util.K8sNodeUtil.endpoint; + +/** + * Unit tests for kubernetes API config manager. + */ +public class K8sApiConfigManagerTest { + + private static final ApplicationId TEST_APP_ID = new DefaultApplicationId(1, "test"); + + private static final String ERR_SIZE = "Number of configs did not match"; + private static final String ERR_NOT_MATCH = "Config did not match"; + private static final String ERR_NOT_FOUND = "Config did not exist"; + + private K8sApiConfig apiConfig1; + private K8sApiConfig apiConfig2; + private K8sApiConfig apiConfig3; + + private final TestK8sApiConfigListener testListener = new TestK8sApiConfigListener(); + + private K8sApiConfigManager target; + private DistributedK8sApiConfigStore configStore; + + /** + * Initial setup for this unit test. + */ + @Before + public void setUp() { + + apiConfig1 = DefaultK8sApiConfig.builder() + .scheme(K8sApiConfig.Scheme.HTTP) + .ipAddress(IpAddress.valueOf("10.10.10.2")) + .port(6443) + .build(); + apiConfig2 = DefaultK8sApiConfig.builder() + .scheme(K8sApiConfig.Scheme.HTTPS) + .ipAddress(IpAddress.valueOf("10.10.10.3")) + .port(6443) + .token("token") + .caCertData("caCertData") + .clientCertData("clientCertData") + .clientKeyData("clientKeyData") + .build(); + apiConfig3 = DefaultK8sApiConfig.builder() + .scheme(K8sApiConfig.Scheme.HTTP) + .ipAddress(IpAddress.valueOf("10.10.10.4")) + .port(8080) + .build(); + + configStore = new DistributedK8sApiConfigStore(); + TestUtils.setField(configStore, "coreService", new TestCoreService()); + TestUtils.setField(configStore, "storageService", new TestStorageService()); + TestUtils.setField(configStore, "eventExecutor", MoreExecutors.newDirectExecutorService()); + configStore.activate(); + + configStore.createApiConfig(apiConfig2); + configStore.createApiConfig(apiConfig3); + + target = new K8sApiConfigManager(); + target.storageService = new TestStorageService(); + target.coreService = new TestCoreService(); + target.clusterService = new TestClusterService(); + target.leadershipService = new TestLeadershipService(); + target.configStore = configStore; + target.addListener(testListener); + target.activate(); + testListener.events.clear(); + } + + /** + * Clean up unit test. + */ + @After + public void tearDown() { + target.removeListener(testListener); + target.deactivate(); + configStore.deactivate(); + configStore = null; + target = null; + } + + /** + * Checks if creating and removing a config work well with proper events. + */ + @Test + public void testCreateAndRemoveConfig() { + target.createApiConfig(apiConfig1); + assertEquals(ERR_SIZE, 3, target.apiConfigs().size()); + assertNotNull(target.apiConfig(endpoint(apiConfig1))); + + target.removeApiConfig(endpoint(apiConfig1)); + assertEquals(ERR_SIZE, 2, target.apiConfigs().size()); + assertNull(target.apiConfig(endpoint(apiConfig1))); + + validateEvents(K8S_API_CONFIG_CREATED, K8S_API_CONFIG_REMOVED); + } + + /** + * Checks if creating null config fails with proper exception. + */ + @Test(expected = NullPointerException.class) + public void testCreateNullConfig() { + target.createApiConfig(null); + } + + private static class TestK8sApiConfigListener implements K8sApiConfigListener { + private List events = Lists.newArrayList(); + + @Override + public void event(K8sApiConfigEvent event) { + events.add(event); + } + } + + /** + * Checks if creating a duplicated config fails with proper exception. + */ + @Test(expected = IllegalArgumentException.class) + public void testCreateDuplicateConfig() { + target.createApiConfig(apiConfig1); + target.createApiConfig(apiConfig1); + } + + /** + * Checks if removing null config fails with proper exception. + */ + @Test(expected = IllegalArgumentException.class) + public void testRemoveNullConfig() { + target.removeApiConfig(null); + } + + private void validateEvents(Enum... types) { + int i = 0; + assertEquals("Number of events did not match", types.length, testListener.events.size()); + for (Event event : testListener.events) { + assertEquals("Incorrect event received", types[i], event.type()); + i++; + } + testListener.events.clear(); + } + + /** + * Checks if updating a null config fails with proper exception. + */ + @Test(expected = NullPointerException.class) + public void testUpdateNullConfig() { + target.updateApiConfig(null); + } + + /** + * Checks if updating not existing config fails with proper exception. + */ + @Test(expected = IllegalArgumentException.class) + public void testUpdateNotExistingConfig() { + target.updateApiConfig(apiConfig1); + } + + /** + * Checks if getting all nodes method returns correct set of nodes. + */ + @Test + public void testGetAllNodes() { + assertEquals(ERR_SIZE, 2, target.apiConfigs().size()); + assertTrue(ERR_NOT_FOUND, target.apiConfigs().contains(apiConfig2)); + assertTrue(ERR_NOT_FOUND, target.apiConfigs().contains(apiConfig3)); + } + + private static class TestCoreService extends CoreServiceAdapter { + @Override + public ApplicationId registerApplication(String name) { + return TEST_APP_ID; + } + } + + private class TestClusterService extends ClusterServiceAdapter { + + } + + private static class TestLeadershipService extends LeadershipServiceAdapter { + + } +} diff --git a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java index 61c2e2de92..75d4433df5 100644 --- a/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java +++ b/apps/k8s-node/app/src/test/java/org/onosproject/k8snode/web/K8sNodeWebResourceTest.java @@ -23,10 +23,14 @@ import org.onlab.osgi.TestServiceDirectory; import org.onlab.packet.IpAddress; import org.onosproject.codec.CodecService; import org.onosproject.codec.impl.CodecManager; +import org.onosproject.k8snode.api.DefaultK8sApiConfig; import org.onosproject.k8snode.api.DefaultK8sNode; +import org.onosproject.k8snode.api.K8sApiConfig; +import org.onosproject.k8snode.api.K8sApiConfigAdminService; import org.onosproject.k8snode.api.K8sNode; import org.onosproject.k8snode.api.K8sNodeAdminService; import org.onosproject.k8snode.api.K8sNodeState; +import org.onosproject.k8snode.codec.K8sApiConfigCodec; import org.onosproject.k8snode.codec.K8sNodeCodec; import org.onosproject.net.DeviceId; import org.onosproject.rest.resources.ResourceTest; @@ -52,9 +56,13 @@ import static org.junit.Assert.assertThat; public class K8sNodeWebResourceTest extends ResourceTest { final K8sNodeAdminService mockK8sNodeAdminService = createMock(K8sNodeAdminService.class); - private static final String PATH = "configure"; + final K8sApiConfigAdminService mockK8sApiConfigAdminService = + createMock(K8sApiConfigAdminService.class); + private static final String NODE_PATH = "configure/node"; + private static final String API_PATH = "configure/api"; private K8sNode k8sNode; + private K8sApiConfig k8sApiConfig; /** * Constructs a kubernetes node resource test instance. @@ -71,9 +79,11 @@ public class K8sNodeWebResourceTest extends ResourceTest { final CodecManager codecService = new CodecManager(); codecService.activate(); codecService.registerCodec(K8sNode.class, new K8sNodeCodec()); + codecService.registerCodec(K8sApiConfig.class, new K8sApiConfigCodec()); ServiceDirectory testDirectory = new TestServiceDirectory() .add(K8sNodeAdminService.class, mockK8sNodeAdminService) + .add(K8sApiConfigAdminService.class, mockK8sApiConfigAdminService) .add(CodecService.class, codecService); setServiceDirectory(testDirectory); @@ -85,6 +95,16 @@ public class K8sNodeWebResourceTest extends ResourceTest { .intgBridge(DeviceId.deviceId("of:00000000000000a1")) .state(K8sNodeState.INIT) .build(); + + k8sApiConfig = DefaultK8sApiConfig.builder() + .scheme(K8sApiConfig.Scheme.HTTPS) + .ipAddress(IpAddress.valueOf("10.134.34.223")) + .port(6443) + .token("tokenMod") + .caCertData("caCertData") + .clientCertData("clientCertData") + .clientKeyData("clientKeyData") + .build(); } /** @@ -99,7 +119,7 @@ public class K8sNodeWebResourceTest extends ResourceTest { final WebTarget wt = target(); InputStream jsonStream = K8sNodeWebResourceTest.class .getResourceAsStream("k8s-node-minion-config.json"); - Response response = wt.path(PATH).request(MediaType.APPLICATION_JSON_TYPE) + Response response = wt.path(NODE_PATH).request(MediaType.APPLICATION_JSON_TYPE) .post(Entity.json(jsonStream)); final int status = response.getStatus(); @@ -119,7 +139,7 @@ public class K8sNodeWebResourceTest extends ResourceTest { final WebTarget wt = target(); InputStream jsonStream = K8sNodeWebResourceTest.class .getResourceAsStream("k8s-node-minion-config.json"); - Response response = wt.path(PATH).request(MediaType.APPLICATION_JSON_TYPE) + Response response = wt.path(NODE_PATH).request(MediaType.APPLICATION_JSON_TYPE) .post(Entity.json(jsonStream)); final int status = response.getStatus(); @@ -140,7 +160,7 @@ public class K8sNodeWebResourceTest extends ResourceTest { final WebTarget wt = target(); InputStream jsonStream = K8sNodeWebResourceTest.class .getResourceAsStream("k8s-node-minion-config.json"); - Response response = wt.path(PATH).request(MediaType.APPLICATION_JSON_TYPE) + Response response = wt.path(NODE_PATH).request(MediaType.APPLICATION_JSON_TYPE) .put(Entity.json(jsonStream)); final int status = response.getStatus(); @@ -160,7 +180,7 @@ public class K8sNodeWebResourceTest extends ResourceTest { final WebTarget wt = target(); InputStream jsonStream = K8sNodeWebResourceTest.class .getResourceAsStream("k8s-node-minion-config.json"); - Response response = wt.path(PATH).request(MediaType.APPLICATION_JSON_TYPE) + Response response = wt.path(NODE_PATH).request(MediaType.APPLICATION_JSON_TYPE) .put(Entity.json(jsonStream)); final int status = response.getStatus(); @@ -178,7 +198,7 @@ public class K8sNodeWebResourceTest extends ResourceTest { expect(mockK8sNodeAdminService.removeNode(anyString())).andReturn(k8sNode).once(); replay(mockK8sNodeAdminService); - String location = PATH + "/minion-node"; + String location = NODE_PATH + "/minion-node"; final WebTarget wt = target(); Response response = wt.path(location).request( @@ -199,7 +219,7 @@ public class K8sNodeWebResourceTest extends ResourceTest { expect(mockK8sNodeAdminService.node(anyString())).andReturn(null).once(); replay(mockK8sNodeAdminService); - String location = PATH + "/minion-node"; + String location = NODE_PATH + "/minion-node"; final WebTarget wt = target(); Response response = wt.path(location).request( @@ -211,4 +231,132 @@ public class K8sNodeWebResourceTest extends ResourceTest { verify(mockK8sNodeAdminService); } + + /** + * Tests the results of the REST API POST method with creating new configs operation. + */ + @Test + public void testCreateConfigsWithCreateOperation() { + expect(mockK8sApiConfigAdminService.apiConfig(anyString())).andReturn(null).once(); + mockK8sApiConfigAdminService.createApiConfig(anyObject()); + replay(mockK8sApiConfigAdminService); + + final WebTarget wt = target(); + InputStream jsonStream = K8sNodeWebResourceTest.class + .getResourceAsStream("k8s-api-config.json"); + Response response = wt.path(API_PATH).request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.json(jsonStream)); + final int status = response.getStatus(); + + assertThat(status, is(201)); + + verify(mockK8sApiConfigAdminService); + } + + /** + * Tests the results of the REST API POST method without creating new configs operation. + */ + @Test + public void testCreateConfigsWithoutCreateOperation() { + expect(mockK8sApiConfigAdminService.apiConfig(anyString())).andReturn(k8sApiConfig).once(); + replay(mockK8sApiConfigAdminService); + + final WebTarget wt = target(); + InputStream jsonStream = K8sNodeWebResourceTest.class + .getResourceAsStream("k8s-api-config.json"); + Response response = wt.path(API_PATH).request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.json(jsonStream)); + final int status = response.getStatus(); + + assertThat(status, is(201)); + + verify(mockK8sApiConfigAdminService); + } + + /** + * Tests the results of the REST API PUT method with modifying the configs. + */ + @Test + public void testUpdateConfigsWithModifyOperation() { + expect(mockK8sApiConfigAdminService.apiConfig(anyString())) + .andReturn(k8sApiConfig).once(); + mockK8sApiConfigAdminService.updateApiConfig(anyObject()); + replay(mockK8sApiConfigAdminService); + + final WebTarget wt = target(); + InputStream jsonStream = K8sNodeWebResourceTest.class + .getResourceAsStream("k8s-api-config.json"); + Response response = wt.path(API_PATH).request(MediaType.APPLICATION_JSON_TYPE) + .put(Entity.json(jsonStream)); + final int status = response.getStatus(); + + assertThat(status, is(200)); + + verify(mockK8sApiConfigAdminService); + } + + /** + * Tests the results of the REST API PUT method without modifying the configs. + */ + @Test + public void testUpdateConfigsWithoutModifyOperation() { + expect(mockK8sApiConfigAdminService.apiConfig(anyString())).andReturn(null).once(); + replay(mockK8sApiConfigAdminService); + + final WebTarget wt = target(); + InputStream jsonStream = K8sNodeWebResourceTest.class + .getResourceAsStream("k8s-api-config.json"); + Response response = wt.path(API_PATH).request(MediaType.APPLICATION_JSON_TYPE) + .put(Entity.json(jsonStream)); + final int status = response.getStatus(); + + assertThat(status, is(304)); + + verify(mockK8sApiConfigAdminService); + } + + /** + * Tests the results of the REST API DELETE method with deleting the configs. + */ + @Test + public void testDeleteConfigsWithDeletionOperation() { + expect(mockK8sApiConfigAdminService.apiConfig(anyString())) + .andReturn(k8sApiConfig).once(); + expect(mockK8sApiConfigAdminService.removeApiConfig(anyString())) + .andReturn(k8sApiConfig).once(); + replay(mockK8sApiConfigAdminService); + + String location = API_PATH + "/https://test:8663"; + + final WebTarget wt = target(); + Response response = wt.path(location).request( + MediaType.APPLICATION_JSON_TYPE).delete(); + + final int status = response.getStatus(); + + assertThat(status, is(204)); + + verify(mockK8sApiConfigAdminService); + } + + /** + * Tests the results of the REST API DELETE method without deleting the configs. + */ + @Test + public void testDeleteConfigsWithoutDeletionOperation() { + expect(mockK8sApiConfigAdminService.apiConfig(anyString())).andReturn(null).once(); + replay(mockK8sApiConfigAdminService); + + String location = API_PATH + "/https://test:8663"; + + final WebTarget wt = target(); + Response response = wt.path(location).request( + MediaType.APPLICATION_JSON_TYPE).delete(); + + final int status = response.getStatus(); + + assertThat(status, is(304)); + + verify(mockK8sApiConfigAdminService); + } } diff --git a/apps/k8s-node/app/src/test/resources/org/onosproject/k8snode/codec/K8sApiConfig.json b/apps/k8s-node/app/src/test/resources/org/onosproject/k8snode/codec/K8sApiConfig.json new file mode 100644 index 0000000000..f706cc3934 --- /dev/null +++ b/apps/k8s-node/app/src/test/resources/org/onosproject/k8snode/codec/K8sApiConfig.json @@ -0,0 +1,9 @@ +{ + "scheme" : "HTTPS", + "ipAddress" : "10.134.34.223", + "port" : 6443, + "token": "token", + "caCertData": "caCertData", + "clientCertData": "clientCertData", + "clientKeyData": "clientKeyData" +} \ No newline at end of file diff --git a/apps/k8s-node/app/src/test/resources/org/onosproject/k8snode/web/k8s-api-config.json b/apps/k8s-node/app/src/test/resources/org/onosproject/k8snode/web/k8s-api-config.json new file mode 100644 index 0000000000..958d4348df --- /dev/null +++ b/apps/k8s-node/app/src/test/resources/org/onosproject/k8snode/web/k8s-api-config.json @@ -0,0 +1,13 @@ +{ + "apiConfigs" : [ + { + "scheme" : "HTTPS", + "ipAddress" : "10.134.34.223", + "port" : 6443, + "token": "token", + "caCertData": "caCertData", + "clientCertData": "clientCertData", + "clientKeyData": "clientKeyData" + } + ] +} \ No newline at end of file diff --git a/lib/deps.json b/lib/deps.json index 1535dc7898..bf7c4eadaf 100644 --- a/lib/deps.json +++ b/lib/deps.json @@ -304,6 +304,13 @@ "msg-simple": "mvn:com.github.fge:msg-simple:1.1", "snakeyaml": "mvn:org.yaml:snakeyaml:1.15", + // Kubernetes related jars + "k8s-client": "mvn:io.fabric8:kubernetes-client:4.1.1", + "k8s-model": "mvn:io.fabric8:kubernetes-model:4.1.1", + "zjsonpatch": "mvn:io.fabric8:zjsonpatch:0.3.0", + "generex": "mvn:com.github.mifmif:generex:1.0.2", + "automaton": "mvn:dk.brics.automaton:automaton:1.11-8", + "onos-yang-model":"mvn:org.onosproject:onos-yang-model:2.6.1", "onos-yang-compiler-api":"mvn:org.onosproject:onos-yang-compiler-api:2.6.1", "onos-yang-compiler-main":"mvn:org.onosproject:onos-yang-compiler-main:2.6.1", diff --git a/tools/build/bazel/generate_workspace.bzl b/tools/build/bazel/generate_workspace.bzl index edd8a310ea..791259f030 100644 --- a/tools/build/bazel/generate_workspace.bzl +++ b/tools/build/bazel/generate_workspace.bzl @@ -1156,6 +1156,36 @@ def generated_maven_jars(): jar_sha256 = "79ea8aac6590f49ee8390c2f17ed9343079e85b44158a097b301dfee42af86ec", licenses = ["notice"], jar_urls = ["http://repo1.maven.org/maven2/org/yaml/snakeyaml/1.15/snakeyaml-1.15.jar"], ) + if "k8s_client" not in native.existing_rules(): + java_import_external( + name = "k8s_client", + jar_sha256 = "b3dd82d2fb2d566a4ba49c64456649a78fa1035135b3ac9b73b4f9d6ee5b2a86", + licenses = ["notice"], + jar_urls = ["http://repo1.maven.org/maven2/io/fabric8/kubernetes-client/4.1.1/kubernetes-client-4.1.1.jar"], ) + if "k8s_model" not in native.existing_rules(): + java_import_external( + name = "k8s_model", + jar_sha256 = "ef53a674d73d518b5897cbd38032aa4e9e2b083ef236fd32cc0e4bedd837c38b", + licenses = ["notice"], + jar_urls = ["http://repo1.maven.org/maven2/io/fabric8/kubernetes-model/4.1.1/kubernetes-model-4.1.1.jar"], ) + if "zjsonpatch" not in native.existing_rules(): + java_import_external( + name = "zjsonpatch", + jar_sha256 = "ae4e5e931646a25cb09b55186de4f3346e358e01130bef279ddf495a719c71d5", + licenses = ["notice"], + jar_urls = ["http://repo1.maven.org/maven2/io/fabric8/zjsonpatch/0.3.0/zjsonpatch-0.3.0.jar"], ) + if "generex" not in native.existing_rules(): + java_import_external( + name = "generex", + jar_sha256 = "8f8ce233c335e08e113a3f9579de1046fb19927e82468b1bbebcd6cba8760b81", + licenses = ["notice"], + jar_urls = ["http://repo1.maven.org/maven2/com/github/mifmif/generex/1.0.2/generex-1.0.2.jar"], ) + if "automaton" not in native.existing_rules(): + java_import_external( + name = "automaton", + jar_sha256 = "a24475f6ccfe1cc7a4fe9e34e05ce687b0ce0c6e8cb781e0eced3b186482c61e", + licenses = ["notice"], + jar_urls = ["http://repo1.maven.org/maven2/dk/brics/automaton/automaton/1.11-8/automaton-1.11-8.jar"], ) if "onos_yang_model" not in native.existing_rules(): java_import_external( name = "onos_yang_model", @@ -1455,6 +1485,11 @@ artifact_map["@jackson_coreutils//:jackson_coreutils"] = "mvn:com.github.fge:jac artifact_map["@btf//:btf"] = "mvn:com.github.fge:btf:jar:1.2" artifact_map["@msg_simple//:msg_simple"] = "mvn:com.github.fge:msg-simple:jar:1.1" artifact_map["@snakeyaml//:snakeyaml"] = "mvn:org.yaml:snakeyaml:jar:1.15" +artifact_map["@k8s_client//:k8s_client"] = "mvn:io.fabric8:kubernetes-client:jar:NON-OSGI:4.1.1" +artifact_map["@k8s_model//:k8s_model"] = "mvn:io.fabric8:kubernetes-model:jar:4.1.1" +artifact_map["@zjsonpatch//:zjsonpatch"] = "mvn:io.fabric8:zjsonpatch:jar:0.3.0" +artifact_map["@generex//:generex"] = "mvn:com.github.mifmif:generex:jar:NON-OSGI:1.0.2" +artifact_map["@automaton//:automaton"] = "mvn:dk.brics.automaton:automaton:jar:NON-OSGI:1.11-8" artifact_map["@onos_yang_model//:onos_yang_model"] = "mvn:org.onosproject:onos-yang-model:jar:2.6.1" artifact_map["@onos_yang_compiler_api//:onos_yang_compiler_api"] = "mvn:org.onosproject:onos-yang-compiler-api:jar:2.6.1" artifact_map["@onos_yang_compiler_main//:onos_yang_compiler_main"] = "mvn:org.onosproject:onos-yang-compiler-main:jar:2.6.1"