Introduce autoconfiguration for Cloud Bindings

Fixes gh-499

Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2024-03-23 21:00:34 +01:00
committed by Mark Pollack
parent ac518df4db
commit 312156e2cb
12 changed files with 482 additions and 64 deletions

View File

@@ -0,0 +1,20 @@
package org.springframework.ai.bindings;
import org.springframework.core.env.Environment;
/**
* From https://github.com/spring-cloud/spring-cloud-bindings to switch on/off the
* bindings.
*/
final class BindingsValidator {
static final String CONFIG_PATH = "spring.ai.cloud.bindings";
/**
* Whether the given binding type should be used to contribute properties.
*/
static boolean isTypeEnabled(Environment environment, String type) {
return environment.getProperty("%s.%s.enabled".formatted(CONFIG_PATH, type), Boolean.class, true);
}
}

View File

@@ -0,0 +1,40 @@
package org.springframework.ai.bindings;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor;
import org.springframework.core.env.Environment;
import java.net.URI;
import java.util.Map;
/**
* An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s
* of type: {@value TYPE}.
*
* @author Thomas Vitale
*/
public class ChromaBindingsPropertiesProcessor implements BindingsPropertiesProcessor {
/**
* The {@link Binding} type that this processor is interested in: {@value}.
**/
public static final String TYPE = "chroma";
@Override
public void process(Environment environment, Bindings bindings, Map<String, Object> properties) {
if (!BindingsValidator.isTypeEnabled(environment, TYPE)) {
return;
}
bindings.filterBindings(TYPE).forEach(binding -> {
var uri = URI.create(binding.getSecret().get("uri"));
properties.put("spring.ai.vectorstore.chroma.client.host",
"%s://%s".formatted(uri.getScheme(), uri.getHost()));
properties.put("spring.ai.vectorstore.chroma.client.port", String.valueOf(uri.getPort()));
properties.put("spring.ai.vectorstore.chroma.client.username", binding.getSecret().get("username"));
properties.put("spring.ai.vectorstore.chroma.client.password", binding.getSecret().get("password"));
});
}
}

View File

@@ -0,0 +1,34 @@
package org.springframework.ai.bindings;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor;
import org.springframework.core.env.Environment;
import java.util.Map;
/**
* An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s
* of type: {@value TYPE}.
*
* @author Thomas Vitale
*/
public class OllamaBindingsPropertiesProcessor implements BindingsPropertiesProcessor {
/**
* The {@link Binding} type that this processor is interested in: {@value}.
**/
public static final String TYPE = "ollama";
@Override
public void process(Environment environment, Bindings bindings, Map<String, Object> properties) {
if (!BindingsValidator.isTypeEnabled(environment, TYPE)) {
return;
}
bindings.filterBindings(TYPE).forEach(binding -> {
properties.put("spring.ai.ollama.base-url", binding.getSecret().get("uri"));
});
}
}

View File

@@ -0,0 +1,35 @@
package org.springframework.ai.bindings;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor;
import org.springframework.core.env.Environment;
import java.util.Map;
/**
* An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s
* of type: {@value TYPE}.
*
* @author Thomas Vitale
*/
public class OpenAiBindingsPropertiesProcessor implements BindingsPropertiesProcessor {
/**
* The {@link Binding} type that this processor is interested in: {@value}.
**/
public static final String TYPE = "openai";
@Override
public void process(Environment environment, Bindings bindings, Map<String, Object> properties) {
if (!BindingsValidator.isTypeEnabled(environment, TYPE)) {
return;
}
bindings.filterBindings(TYPE).forEach(binding -> {
properties.put("spring.ai.openai.api-key", binding.getSecret().get("api-key"));
properties.put("spring.ai.openai.base-url", binding.getSecret().get("uri"));
});
}
}

View File

@@ -0,0 +1,36 @@
package org.springframework.ai.bindings;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor;
import org.springframework.core.env.Environment;
import java.net.URI;
import java.util.Map;
/**
* An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s
* of type: {@value TYPE}.
*/
public class WeaviateBindingsPropertiesProcessor implements BindingsPropertiesProcessor {
/**
* The {@link Binding} type that this processor is interested in: {@value}.
**/
public static final String TYPE = "weaviate";
@Override
public void process(Environment environment, Bindings bindings, Map<String, Object> properties) {
if (!BindingsValidator.isTypeEnabled(environment, TYPE)) {
return;
}
bindings.filterBindings(TYPE).forEach(binding -> {
var uri = URI.create(binding.getSecret().get("uri"));
properties.put("spring.ai.vectorstore.weaviate.scheme", uri.getScheme());
properties.put("spring.ai.vectorstore.weaviate.host", "%s:%s".formatted(uri.getHost(), uri.getPort()));
properties.put("spring.ai.vectorstore.weaviate.api-key", binding.getSecret().get("api-key"));
});
}
}

View File

@@ -0,0 +1,6 @@
# Binding Properties Factories
org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor=\
org.springframework.ai.bindings.ChromaBindingsPropertiesProcessor,\
org.springframework.ai.bindings.OllamaBindingsPropertiesProcessor,\
org.springframework.ai.bindings.OpenAiBindingsPropertiesProcessor,\
org.springframework.ai.bindings.WeaviateBindingsPropertiesProcessor

View File

@@ -0,0 +1,53 @@
package org.springframework.ai.bindings;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.mock.env.MockEnvironment;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH;
/**
* Unit tests for {@link ChromaBindingsPropertiesProcessor}.
*
* @author Thomas Vitale
*/
class ChromaBindingsPropertiesProcessorTests {
private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"),
// @formatter:off
Map.of(
Binding.TYPE, ChromaBindingsPropertiesProcessor.TYPE,
"uri", "https://example.net:8000",
"username", "itsme",
"password", "youknowit"
)));
// @formatter:on
private final MockEnvironment environment = new MockEnvironment();
private final Map<String, Object> properties = new HashMap<>();
@Test
void propertiesAreContributed() {
new ChromaBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.host", "https://example.net");
assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.port", "8000");
assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.username", "itsme");
assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.password", "youknowit");
}
@Test
void whenDisabledThenPropertiesAreNotContributed() {
environment.setProperty("%s.chroma.enabled".formatted(CONFIG_PATH), "false");
new ChromaBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).isEmpty();
}
}

View File

@@ -0,0 +1,48 @@
package org.springframework.ai.bindings;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.mock.env.MockEnvironment;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH;
/**
* Unit tests for {@link OllamaBindingsPropertiesProcessor}.
*
* @author Thomas Vitale
*/
class OllamaBindingsPropertiesProcessorTests {
private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"),
// @formatter:off
Map.of(
Binding.TYPE, OllamaBindingsPropertiesProcessor.TYPE,
"uri", "https://example.net/ollama:11434"
)));
// @formatter:on
private final MockEnvironment environment = new MockEnvironment();
private final Map<String, Object> properties = new HashMap<>();
@Test
void propertiesAreContributed() {
new OllamaBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).containsEntry("spring.ai.ollama.base-url", "https://example.net/ollama:11434");
}
@Test
void whenDisabledThenPropertiesAreNotContributed() {
environment.setProperty("%s.ollama.enabled".formatted(CONFIG_PATH), "false");
new OllamaBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).isEmpty();
}
}

View File

@@ -0,0 +1,50 @@
package org.springframework.ai.bindings;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.mock.env.MockEnvironment;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH;
/**
* Unit tests for {@link OpenAiBindingsPropertiesProcessor}.
*
* @author Thomas Vitale
*/
class OpenAiBindingsPropertiesProcessorTests {
private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"),
// @formatter:off
Map.of(
Binding.TYPE, OpenAiBindingsPropertiesProcessor.TYPE,
"api-key", "demo",
"uri", "https://my.openai.example.net"
)));
// @formatter:on
private final MockEnvironment environment = new MockEnvironment();
private final Map<String, Object> properties = new HashMap<>();
@Test
void propertiesAreContributed() {
new OpenAiBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).containsEntry("spring.ai.openai.api-key", "demo");
assertThat(properties).containsEntry("spring.ai.openai.base-url", "https://my.openai.example.net");
}
@Test
void whenDisabledThenPropertiesAreNotContributed() {
environment.setProperty("%s.openai.enabled".formatted(CONFIG_PATH), "false");
new OpenAiBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).isEmpty();
}
}

View File

@@ -0,0 +1,51 @@
package org.springframework.ai.bindings;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.bindings.Binding;
import org.springframework.cloud.bindings.Bindings;
import org.springframework.mock.env.MockEnvironment;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH;
/**
* Unit tests for {@link WeaviateBindingsPropertiesProcessor}.
*
* @author Thomas Vitale
*/
class WeaviateBindingsPropertiesProcessorTests {
private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"),
// @formatter:off
Map.of(
Binding.TYPE, WeaviateBindingsPropertiesProcessor.TYPE,
"uri", "https://example.net:8000",
"api-key", "demo"
)));
// @formatter:on
private final MockEnvironment environment = new MockEnvironment();
private final Map<String, Object> properties = new HashMap<>();
@Test
void propertiesAreContributed() {
new WeaviateBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).containsEntry("spring.ai.vectorstore.weaviate.scheme", "https");
assertThat(properties).containsEntry("spring.ai.vectorstore.weaviate.host", "example.net:8000");
assertThat(properties).containsEntry("spring.ai.vectorstore.weaviate.api-key", "demo");
}
@Test
void whenDisabledThenPropertiesAreNotContributed() {
environment.setProperty("%s.weaviate.enabled".formatted(CONFIG_PATH), "false");
new WeaviateBindingsPropertiesProcessor().process(environment, bindings, properties);
assertThat(properties).isEmpty();
}
}