diff --git a/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProvider.java b/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProvider.java index 2719973c28..c76dd0dd51 100644 --- a/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProvider.java @@ -177,4 +177,47 @@ final class ZatarCertificateProvider extends CertificateProvider { checkAndReloadCertificates(); } } + + abstract static class Factory { + private static final Factory DEFAULT_INSTANCE = + new Factory() { + @Override + ZatarCertificateProvider create( + DistributorWatcher watcher, + boolean notifyCertUpdates, + String directory, + String certFile, + String privateKeyFile, + String trustFile, + long refreshIntervalInSeconds, + ScheduledExecutorService scheduledExecutorService, + TimeProvider timeProvider) { + return new ZatarCertificateProvider( + watcher, + notifyCertUpdates, + directory, + certFile, + privateKeyFile, + trustFile, + refreshIntervalInSeconds, + scheduledExecutorService, + timeProvider); + } + }; + + static Factory getInstance() { + return DEFAULT_INSTANCE; + } + + abstract ZatarCertificateProvider create( + DistributorWatcher watcher, + boolean notifyCertUpdates, + String directory, + String certFile, + String privateKeyFile, + String trustFile, + long refreshIntervalInSeconds, + ScheduledExecutorService scheduledExecutorService, + TimeProvider timeProvider); + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProviderProvider.java b/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProviderProvider.java new file mode 100644 index 0000000000..13dfeed9c3 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/certprovider/ZatarCertificateProviderProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.certprovider; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.grpc.internal.JsonUtil; +import io.grpc.internal.TimeProvider; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Provider of {@link ZatarCertificateProvider}s. + */ +final class ZatarCertificateProviderProvider implements CertificateProviderProvider { + + private static final String DIRECTORY_KEY = "directory"; + private static final String CERT_FILE_KEY = "certificate-file"; + private static final String KEY_FILE_KEY = "private-key-file"; + private static final String ROOT_FILE_KEY = "ca-certificate-file"; + private static final String REFRESH_INTERVAL_KEY = "refresh-interval"; + + @VisibleForTesting static final long REFRESH_INTERVAL_DEFAULT = 600L; + + + static final String ZATAR_PROVIDER_NAME = "gke-cas-certs"; + + static { + CertificateProviderRegistry.getInstance() + .register( + new ZatarCertificateProviderProvider( + ZatarCertificateProvider.Factory.getInstance(), + ScheduledExecutorServiceFactory.DEFAULT_INSTANCE, + TimeProvider.SYSTEM_TIME_PROVIDER)); + } + + final ZatarCertificateProvider.Factory zatarCertificateProviderFactory; + private final ScheduledExecutorServiceFactory scheduledExecutorServiceFactory; + private final TimeProvider timeProvider; + + @VisibleForTesting + ZatarCertificateProviderProvider( + ZatarCertificateProvider.Factory zatarCertificateProviderFactory, + ScheduledExecutorServiceFactory scheduledExecutorServiceFactory, + TimeProvider timeProvider) { + this.zatarCertificateProviderFactory = zatarCertificateProviderFactory; + this.scheduledExecutorServiceFactory = scheduledExecutorServiceFactory; + this.timeProvider = timeProvider; + } + + @Override + public String getName() { + return ZATAR_PROVIDER_NAME; + } + + @Override + public CertificateProvider createCertificateProvider( + Object config, CertificateProvider.DistributorWatcher watcher, boolean notifyCertUpdates) { + + Config configObj = validateAndTranslateConfig(config); + return zatarCertificateProviderFactory.create( + watcher, + notifyCertUpdates, + configObj.directory, + configObj.certFile, + configObj.keyFile, + configObj.rootFile, + configObj.refrehInterval, + scheduledExecutorServiceFactory.create(), + timeProvider); + } + + private static String checkForNullAndGet(Map map, String key) { + return checkNotNull(JsonUtil.getString(map, key), "'" + key + "' is required in the config"); + } + + private static Config validateAndTranslateConfig(Object config) { + checkArgument(config instanceof Map, "Only Map supported for config"); + @SuppressWarnings("unchecked") Map map = (Map)config; + + Config configObj = new Config(); + configObj.directory = checkForNullAndGet(map, DIRECTORY_KEY); + configObj.certFile = checkForNullAndGet(map, CERT_FILE_KEY); + configObj.keyFile = checkForNullAndGet(map, KEY_FILE_KEY); + configObj.rootFile = checkForNullAndGet(map, ROOT_FILE_KEY); + configObj.refrehInterval = JsonUtil.getNumberAsLong(map, REFRESH_INTERVAL_KEY); + if (configObj.refrehInterval == null) { + configObj.refrehInterval = REFRESH_INTERVAL_DEFAULT; + } + return configObj; + } + + abstract static class ScheduledExecutorServiceFactory { + + private static final ScheduledExecutorServiceFactory DEFAULT_INSTANCE = + new ScheduledExecutorServiceFactory() { + + @Override + ScheduledExecutorService create() { + return Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder() + .setNameFormat("zatar" + "-%d") + .setDaemon(true) + .build()); + } + }; + + abstract ScheduledExecutorService create(); + } + + /** POJO class for storing various config values. */ + @VisibleForTesting + static class Config { + String directory; + String certFile; + String keyFile; + String rootFile; + Long refrehInterval; + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/certprovider/ZatarCertificateProviderProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/certprovider/ZatarCertificateProviderProviderTest.java new file mode 100644 index 0000000000..ac577bc829 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/certprovider/ZatarCertificateProviderProviderTest.java @@ -0,0 +1,214 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.certprovider; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.grpc.internal.JsonParser; +import io.grpc.internal.TimeProvider; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link ZatarCertificateProviderProvider}. */ +@RunWith(JUnit4.class) +public class ZatarCertificateProviderProviderTest { + + @Mock ZatarCertificateProvider.Factory zatarCertificateProviderFactory; + @Mock private ZatarCertificateProviderProvider.ScheduledExecutorServiceFactory + scheduledExecutorServiceFactory; + @Mock private TimeProvider timeProvider; + + private ZatarCertificateProviderProvider provider; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + provider = + new ZatarCertificateProviderProvider( + zatarCertificateProviderFactory, scheduledExecutorServiceFactory, timeProvider); + } + + @Test + public void providerRegisteredName() { + CertificateProviderProvider certProviderProvider = + CertificateProviderRegistry.getInstance() + .getProvider(ZatarCertificateProviderProvider.ZATAR_PROVIDER_NAME); + assertThat(certProviderProvider).isInstanceOf(ZatarCertificateProviderProvider.class); + ZatarCertificateProviderProvider zatarCertificateProviderProvider = + (ZatarCertificateProviderProvider) certProviderProvider; + assertThat(zatarCertificateProviderProvider.zatarCertificateProviderFactory) + .isSameInstanceAs(ZatarCertificateProvider.Factory.getInstance()); + } + + @Test + public void createProvider_minimalConfig() throws IOException { + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(MINIMAL_ZATAR_CONFIG); + ScheduledExecutorService mockService = mock(ScheduledExecutorService.class); + when(scheduledExecutorServiceFactory.create()).thenReturn(mockService); + provider.createCertificateProvider(map, distWatcher, true); + verify(zatarCertificateProviderFactory, times(1)) + .create( + eq(distWatcher), + eq(true), + eq("/var/run/gke-spiffe/certs/..data"), + eq("certificates.pem"), + eq("private_key.pem"), + eq("ca_certificates.pem"), + eq(600L), + eq(mockService), + eq(timeProvider)); + } + + @Test + public void createProvider_fullConfig() throws IOException { + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(FULL_ZATAR_CONFIG); + ScheduledExecutorService mockService = mock(ScheduledExecutorService.class); + when(scheduledExecutorServiceFactory.create()).thenReturn(mockService); + provider.createCertificateProvider(map, distWatcher, true); + verify(zatarCertificateProviderFactory, times(1)) + .create( + eq(distWatcher), + eq(true), + eq("/var/run/gke-spiffe/certs/..data1"), + eq("certificates2.pem"), + eq("private_key3.pem"), + eq("ca_certificates4.pem"), + eq(7890L), + eq(mockService), + eq(timeProvider)); + } + + @Test + public void createProvider_missingDir_expectException() throws IOException { + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(MISSING_DIR_CONFIG); + try { + provider.createCertificateProvider(map, distWatcher, true); + fail("exception expected"); + } catch (NullPointerException npe) { + assertThat(npe).hasMessageThat().isEqualTo("'directory' is required in the config"); + } + } + + @Test + public void createProvider_missingCert_expectException() throws IOException { + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(MISSING_CERT_CONFIG); + try { + provider.createCertificateProvider(map, distWatcher, true); + fail("exception expected"); + } catch (NullPointerException npe) { + assertThat(npe).hasMessageThat().isEqualTo("'certificate-file' is required in the config"); + } + } + + @Test + public void createProvider_missingKey_expectException() throws IOException { + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(MISSING_KEY_CONFIG); + try { + provider.createCertificateProvider(map, distWatcher, true); + fail("exception expected"); + } catch (NullPointerException npe) { + assertThat(npe).hasMessageThat().isEqualTo("'private-key-file' is required in the config"); + } + } + + @Test + public void createProvider_missingRoot_expectException() throws IOException { + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(MISSING_ROOT_CONFIG); + try { + provider.createCertificateProvider(map, distWatcher, true); + fail("exception expected"); + } catch (NullPointerException npe) { + assertThat(npe).hasMessageThat().isEqualTo("'ca-certificate-file' is required in the config"); + } + } + + private static final String MINIMAL_ZATAR_CONFIG = + "{\n" + + " \"directory\": \"/var/run/gke-spiffe/certs/..data\"," + + " \"certificate-file\": \"certificates.pem\"," + + " \"private-key-file\": \"private_key.pem\"," + + " \"ca-certificate-file\": \"ca_certificates.pem\"" + + " }"; + + private static final String FULL_ZATAR_CONFIG = + "{\n" + + " \"directory\": \"/var/run/gke-spiffe/certs/..data1\"," + + " \"certificate-file\": \"certificates2.pem\"," + + " \"private-key-file\": \"private_key3.pem\"," + + " \"ca-certificate-file\": \"ca_certificates4.pem\"," + + " \"refresh-interval\": 7890" + + " }"; + + private static final String MISSING_DIR_CONFIG = + "{\n" + + " \"certificate-file\": \"certificates.pem\"," + + " \"private-key-file\": \"private_key.pem\"," + + " \"ca-certificate-file\": \"ca_certificates.pem\"" + + " }"; + + private static final String MISSING_CERT_CONFIG = + "{\n" + + " \"directory\": \"/var/run/gke-spiffe/certs/..data\"," + + " \"private-key-file\": \"private_key.pem\"," + + " \"ca-certificate-file\": \"ca_certificates.pem\"" + + " }"; + + private static final String MISSING_KEY_CONFIG = + "{\n" + + " \"directory\": \"/var/run/gke-spiffe/certs/..data\"," + + " \"certificate-file\": \"certificates.pem\"," + + " \"ca-certificate-file\": \"ca_certificates.pem\"" + + " }"; + + private static final String MISSING_ROOT_CONFIG = + "{\n" + + " \"directory\": \"/var/run/gke-spiffe/certs/..data\"," + + " \"certificate-file\": \"certificates.pem\"," + + " \"private-key-file\": \"private_key.pem\"" + + " }"; +}