* Simplify name from CnbBindings (a convention that makes my eyes bleed) so
that you aren't stuttering.  I'm still a bit undecided on the naming of the
modules themselves as well.

* Documentation, documentation, documentation!

* Lots of the final keyword.  Prevents unanticipated modification via extension.

* Constructor/Method overloading allows you to isolate the behaviors that are
really hard to test.  Note that I've isolated everything that needs to read
from the environment and feel confident that the methods that include it are
so trivial they're not worth testing.  Some overloads are non-public to enable
testing but not later usage.

* Since this is a public API, I'm being really verbose with the nullability
annotations.  Most of the time you wouldn't use them, by they do make IDE
users' lives better.

Signed-off-by: Ben Hale <bhale@vmware.com>
This commit is contained in:
Ben Hale
2020-05-08 17:12:56 -07:00
parent 80941c6fe8
commit 71bfec3084
28 changed files with 257 additions and 214 deletions

View File

@@ -26,7 +26,7 @@ import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.cloud.cnb.core.CNBBindingsSingleton;
import org.springframework.cloud.cnb.core.CnbBinding;
import org.springframework.cloud.cnb.core.CnbBindings;
import org.springframework.cloud.cnb.core.Bindings;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
@@ -77,7 +77,7 @@ public class CnbBindingsPostProcessor implements EnvironmentPostProcessor, Order
// TODO: allow users to disable processing of a given binding by name, kind, or processor
increaseInvocationCount();
CnbBindings bindings = CNBBindingsSingleton.getCnbBindingsInstance();
Bindings bindings = CNBBindingsSingleton.getCnbBindingsInstance();
if (bindings.hasBindings()) {
List<CnbBinding> allBindings = bindings.findAllBindings();

View File

@@ -21,7 +21,7 @@ import java.util.Map;
import mockit.MockUp;
import org.springframework.cloud.cnb.core.CnbBindings;
import org.springframework.cloud.cnb.core.Bindings;
/**
@@ -36,7 +36,7 @@ public class EnvMock {
this.mockUp = new MockUp<System>() {
@mockit.Mock
public String getenv(String name) {
if (name.equalsIgnoreCase(CnbBindings.CNB_BINDINGS)) {
if (name.equalsIgnoreCase(Bindings.CNB_BINDINGS)) {
return cnbBindingsPath;
}
return env.get(name);

View File

@@ -16,12 +16,9 @@
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2020 the original author or 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 org.springframework.cloud.cnb.core;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A representation of a collection of bindings as defined by the
* <a href="https://github.com/buildpacks/spec/blob/master/extensions/bindings.md">Cloud Native Buildpacks Specification</a>.
*/
public final class Bindings {
/**
* The name of the environment variable to read to determine the bindings file system root. Specified by the Cloud
* Native Buildpacks Specification.
*/
public static final String CNB_BINDINGS = "CNB_BINDINGS";
private final List<CnbBinding> bindings;
/**
* Creates a new {@code Bindings} instance, using the {@code $CNB_BINDINGS} environment variable to determine the
* file system root. If the {@code $CNB_BINDINGS} environment variable is not set, an empty {@code Bindings} is
* returned. If the directory does not exist, an empty {@code Bindings} is returned.
*/
public Bindings() {
this(System.getenv(CNB_BINDINGS));
}
/**
* Creates a new {@code Bindings} instance, using the specified {@code path}. If the directory does not exist, an
* empty {@code Bindings} is returned.
*
* @param path the path to populate the {@code Bindings} from.
*/
public Bindings(@NotNull String path) {
Path p = Paths.get(path);
if (!Files.exists(p)) {
this.bindings = Collections.emptyList();
} else if (!Files.isDirectory(p)) {
throw new IllegalArgumentException(String.format("%s is not a directory", p));
} else {
try {
this.bindings = Files.list(p)
.map(c -> new CnbBinding(c))
.collect(Collectors.toList());
} catch (IOException e) {
throw new UndeclaredThrowableException(e);
}
}
}
/**
* Indicates whether the {@code $CNB_BINDINGS} is and zero or more bindings will be available.
*
* @return {@code true} if {@code $CNB_BINDINGS} is set, {@code false} otherwise.
*/
public static boolean hasBindings() {
return hasBindings(System.getenv());
}
static boolean hasBindings(@NotNull Map<String, String> environment) {
return environment.containsKey(CNB_BINDINGS);
}
/**
* Returns a {@link Binding} with a given name.
*
* @param name the name of the {@code Binding} to find.
* @return the {@code Binding} with a given name if it exists, {@code null} otherwise.
*/
public @Nullable CnbBinding findBinding(@NotNull String name) {
for (CnbBinding binding : this.bindings) {
if (binding.getName().equals(name)) {
return binding;
}
}
return null;
}
/**
* Returns all the {@link Binding}s that were found during construction.
*/
public @NotNull List<CnbBinding> findBindings() {
return this.bindings;
}
/**
* Returns zero or more {@link Binding}s with a given kind. Equivalent to {@link #findBindings(String, String)}.
*
* @param kind the kind of the {@code Binding} to find.
* @return the collection of {@code Binding}s with a given kind.
*/
public @NotNull List<CnbBinding> findBindings(@Nullable String kind) {
return findBindings(kind, null);
}
/**
* Return zero or more {@link Binding}s with a given kind and provider. If {@code kind} or {@code provider} are
* {@code null}, the result is not filtered on that argument.
*
* @param kind the kind of {@code Binding} to find.
* @param provider the provider of {@code Binding} to find
* @return the collection of {@code Binding}s with a given kind and provider.
*/
public @NotNull List<CnbBinding> findBindings(@Nullable String kind, @Nullable String provider) {
List<CnbBinding> bindings = new ArrayList<>();
for (CnbBinding binding : this.bindings) {
if ((kind == null || binding.getKind().equals(kind)) &&
(provider == null) || binding.getProvider().equals(provider)) {
bindings.add(binding);
}
}
return bindings;
}
}

View File

@@ -21,15 +21,15 @@ package org.springframework.cloud.cnb.core;
*/
public final class CNBBindingsSingleton {
private static CnbBindings INSTANCE;
private static Bindings INSTANCE;
private CNBBindingsSingleton() {
}
public synchronized static CnbBindings getCnbBindingsInstance() {
public synchronized static Bindings getCnbBindingsInstance() {
if (INSTANCE == null) {
INSTANCE = new CnbBindings();
INSTANCE = new Bindings();
}
return INSTANCE;
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2019 the original author or 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 org.springframework.cloud.cnb.core;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Provides access to CNB Bindings.
*
* @author Emily Casey
*/
public class CnbBindings {
public static final String CNB_BINDINGS = "CNB_BINDINGS";
private List<CnbBinding> bindings = new ArrayList<>();
public CnbBindings() {
String cnbBindingsDir = System.getenv(CNB_BINDINGS);
if (cnbBindingsDir == null) {
return;
}
File bindingsDir = new File(cnbBindingsDir);
if (!bindingsDir.exists()) {
return;
}
if (!bindingsDir.isDirectory()) {
throw new IllegalStateException(String.format("CNB_BINDINGS '%s' is not a directory", bindingsDir.toString()));
}
for (File file : bindingsDir.listFiles()) {
if (!file.isDirectory()) {
continue;
}
this.bindings.add(new CnbBinding(file));
}
}
public List<CnbBinding> findAllBindings() {
return this.bindings;
}
public boolean hasBindings() {
return System.getenv(CNB_BINDINGS) != null;
}
public CnbBinding findBindingByName(String name) {
for (CnbBinding binding : this.bindings) {
if (binding.getName().equals(name)) {
return binding;
}
}
return null;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2020 the original author or 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 org.springframework.cloud.cnb.core;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
public class BindingsTests {
@Test
void constructFromNonExistentDirectory() {
String path = "src/test/resources/non-existent";
Bindings b = new Bindings(path);
assertThat(b.findBindings()).isEmpty();
}
@Test
void constructFromNonDirectory() throws IOException {
String path = File.createTempFile("bindings", "").getPath();
Bindings b = new Bindings(path);
assertThat(b.findBindings()).isEmpty();
}
@Test
void construct() {
String path = "src/test/resources";
Bindings b = new Bindings(path);
assertThat(b.findBindings()).hasSize(2);
}
@Test
void hasBindings() {
assertThat(Bindings.hasBindings(Collections.emptyMap())).isFalse();
assertThat(Bindings.hasBindings(Collections.singletonMap("CNB_BINDINGS", ""))).isTrue();
}
@Test
void getAllBindings() {
String path = "src/test/resources";
Bindings b = new Bindings(path);
assertThat(b.findBindings()).hasSize(2);
}
@Test
void findBindingsByKind() {
String path = "src/test/resources";
Bindings b = new Bindings(path);
assertThat(b.findBindings("test-kind-1", null)).containsExactly(new Binding());
}
@Test
void findBindingsByProvider() {
String path = "src/test/resources";
Bindings b = new Bindings(path);
assertThat(b.findBindings(null, "test-provider-1")).containsExactly(new Binding());
}
}

View File

@@ -29,14 +29,14 @@ public class CnbBindingsHasBindingsTests {
@Test
public void testHasBindings() {
new EnvMock("some/path");
CnbBindings cnbBindings = new CnbBindings();
assertThat(cnbBindings.hasBindings()).isTrue();
Bindings bindings = new Bindings();
assertThat(bindings.hasBindings()).isTrue();
}
@Test
@Ignore
public void testHasBindingsFalse() {
CnbBindings cnbBindings = new CnbBindings();
assertThat(cnbBindings.hasBindings()).isFalse();
Bindings bindings = new Bindings();
assertThat(bindings.hasBindings()).isFalse();
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2019 the original author or 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 org.springframework.cloud.cnb.core;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.cloud.cnb.core.test.EnvMock;
import static org.assertj.core.api.Assertions.assertThat;
public class CnbBindingsTests {
@Test
public void testBindings() {
Path resourceDirectory = Paths.get("src", "test", "resources", "test-bindings");
String bindingsDir = resourceDirectory.toFile().getAbsolutePath();
new EnvMock(bindingsDir);
CnbBindings cnbBindings = new CnbBindings();
List<CnbBinding> bindings = cnbBindings.findAllBindings();
assertThat(bindings.size()).isEqualTo(1);
CnbBinding mysqlBinding = cnbBindings.findBindingByName("p-mysql");
Map<String, String> mysqlSecret = mysqlBinding.getSecret();
assertThat(mysqlSecret).containsEntry("hostname", "10.0.4.35")
.containsEntry("port", "3306")
.containsEntry("name", "mysql_name")
.containsEntry("username", "mysql_username")
.containsEntry("password", "mysql_password")
.containsEntry("uri",
"mysql://mysql_username:mysql_password@10.0.4.35:3306/cf_2e23d10a_8738_8c3c_66cf_13e44422698c?reconnect=true")
.containsEntry("jdbcUrl",
"jdbc:mysql://10.0.4.35:3306/cf_2e23d10a_8738_8c3c_66cf_13e44422698c?user=mysql_username&password=mysql_password");
Map<String, String> mysqlMetadata = mysqlBinding.getAllMetadata();
assertThat(mysqlMetadata).containsEntry("kind", "mysql")
.containsEntry("provider", "p-mysql")
.containsEntry("tags", "mysql,relational");
assertThat(mysqlBinding.getKind()).isEqualTo("mysql");
assertThat(mysqlBinding.getProvider()).isEqualTo("p-mysql");
assertThat(mysqlBinding.getTags()).contains("mysql", "relational");
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2019 the original author or 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 org.springframework.cloud.cnb.core.test;
import java.util.HashMap;
import java.util.Map;
import mockit.MockUp;
import org.springframework.cloud.cnb.core.CnbBindings;
/**
* @author Emily Casey
**/
public class EnvMock {
private MockUp<?> mockUp;
public EnvMock(String cnbBindingsPath) {
Map<String, String> env = System.getenv();
this.mockUp = new MockUp<System>() {
@mockit.Mock
public String getenv(String name) {
if (name.equalsIgnoreCase(CnbBindings.CNB_BINDINGS)) {
return cnbBindingsPath;
}
return env.get(name);
}
@mockit.Mock
public Map getenv() {
Map<String, String> finalMap = new HashMap<>();
finalMap.putAll(env);
finalMap.put("CNB_BINDINGS", cnbBindingsPath);
return finalMap;
}
};
}
}

View File

@@ -1 +0,0 @@
mysql,relational

View File

@@ -1 +0,0 @@
jdbc:mysql://10.0.4.35:3306/cf_2e23d10a_8738_8c3c_66cf_13e44422698c?user=mysql_username&password=mysql_password

View File

@@ -1 +0,0 @@
mysql://mysql_username:mysql_password@10.0.4.35:3306/cf_2e23d10a_8738_8c3c_66cf_13e44422698c?reconnect=true

View File

@@ -0,0 +1 @@
test-kind-1

View File

@@ -0,0 +1 @@
test-provider-1

View File

@@ -0,0 +1 @@
test-value

View File

@@ -0,0 +1 @@
test-kind-2

View File

@@ -0,0 +1 @@
test-provider-2

View File

@@ -0,0 +1 @@
test-value

View File

@@ -28,6 +28,7 @@
<properties>
<java.version>1.8</java.version>
<jetbrains-annotations.version>19.0.0</jetbrains-annotations.version>
<spring-boot.version>2.2.7.RELEASE</spring-boot.version>
<!-- Plugins -->
@@ -63,6 +64,12 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>${jetbrains-annotations.version}</version>
</dependency>
</dependencies>
</dependencyManagement>