diff --git a/avaje-aws-appconfig/pom.xml b/avaje-aws-appconfig/pom.xml new file mode 100644 index 0000000..9744e7c --- /dev/null +++ b/avaje-aws-appconfig/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.avaje + java11-oss + 4.0 + + + + avaje-aws-appconfig + 0.1-SNAPSHOT + + + false + + + + + io.avaje + avaje-config + 3.11-SNAPSHOT + provided + + + + io.avaje + junit + 1.3 + test + + + + diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java new file mode 100644 index 0000000..1b2d447 --- /dev/null +++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java @@ -0,0 +1,40 @@ +package io.avaje.aws.appconfig; + +public interface AppConfigFetcher { + + static AppConfigFetcher.Builder builder() { + return new DAppConfigFetcher.Builder(); + } + + Result fetch() throws FetchException; + + class FetchException extends Exception { + + public FetchException(Exception e) { + super(e); + } + } + + interface Result { + + String version(); + + String contentType(); + + String body(); + } + + interface Builder { + + AppConfigFetcher.Builder application(String application); + + AppConfigFetcher.Builder environment(String environment); + + AppConfigFetcher.Builder configuration(String configuration); + + AppConfigFetcher.Builder port(int port); + + AppConfigFetcher build(); + } + +} diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java new file mode 100644 index 0000000..5096053 --- /dev/null +++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java @@ -0,0 +1,103 @@ +package io.avaje.aws.appconfig; + +import io.avaje.config.ConfigParser; +import io.avaje.config.Configuration; +import io.avaje.config.ConfigurationSource; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; +import java.util.Properties; + +import static java.lang.System.Logger.Level.*; + +public final class AppConfigPlugin implements ConfigurationSource { + + private static final System.Logger log = System.getLogger("io.avaje.config.AppConfigPlugin"); + + @Override + public void load(Configuration configuration) { + if (!configuration.getBool("aws.appconfig.enabled", true)) { + log.log(INFO, "AppConfigPlugin is not enabled"); + } + + var loader = new Loader(configuration); + loader.schedule(); + loader.reload(); + } + + + static final class Loader { + + private final Configuration configuration; + private final AppConfigFetcher fetcher; + private final ConfigParser yamlParser; + private final long frequency; + + private String currentVersion = ""; + + Loader(Configuration configuration) { + this.configuration = configuration; + this.frequency = configuration.getLong("aws.appconfig.frequency", 60L); + this.yamlParser = configuration.parser("yaml").orElse(null); + + var app = configuration.get("aws.appconfig.application"); + var env = configuration.get("aws.appconfig.environment"); + var con = configuration.get("aws.appconfig.configuration"); + + this.fetcher = AppConfigFetcher.builder() + .application(app) + .environment(env) + .configuration(con) + .build(); + } + + void schedule() { + configuration.schedule(frequency * 1000L, frequency * 1000L, this::reload); + } + + void reload() { + try { + AppConfigFetcher.Result result = fetcher.fetch(); + if (currentVersion.equals(result.version())) { + log.log(TRACE, "AwsAppConfig unchanged, version", currentVersion); + } else { + String contentType = result.contentType(); + if (log.isLoggable(TRACE)) { + log.log(TRACE, "AwsAppConfig fetched version:{0} contentType:{1} body:{2}", result.version(), contentType, result.body()); + } + if (contentType.endsWith("yaml")) { + if (yamlParser == null) { + log.log(ERROR, "No Yaml Parser registered to parse AWS AppConfig"); + } else { + Map keyValues = yamlParser.load(new StringReader(result.body())); + configuration.eventBuilder("AwsAppConfig") + .putAll(keyValues) + .publish(); + currentVersion = result.version(); + debugLog(result, keyValues.size()); + } + } else { + // assuming properties content + Properties properties = new Properties(); + properties.load(new StringReader(result.body())); + configuration.eventBuilder("AwsAppConfig") + .putAll(properties) + .publish(); + currentVersion = result.version(); + debugLog(result, properties.size()); + } + } + + } catch (AppConfigFetcher.FetchException | IOException e) { + log.log(ERROR, "Error fetching or processing AppConfig", e); + } + } + + private static void debugLog(AppConfigFetcher.Result result, int size) { + if (log.isLoggable(DEBUG)) { + log.log(DEBUG, "AwsAppConfig loaded version {0} with {1} properties", result.version(), size); + } + } + } +} diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java new file mode 100644 index 0000000..215ffb2 --- /dev/null +++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java @@ -0,0 +1,85 @@ +package io.avaje.aws.appconfig; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +final class DAppConfigFetcher implements AppConfigFetcher { + + private final URI uri; + private final HttpClient httpClient; + + DAppConfigFetcher(String uri) { + this.uri = URI.create(uri); + this.httpClient = HttpClient.newBuilder() + .build(); + } + + @Override + public AppConfigFetcher.Result fetch() throws FetchException { + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .GET() + .build(); + + try { + HttpResponse res = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + String version = res.headers().firstValue("Configuration-Version").orElse(null); + String contentType = res.headers().firstValue("Content-Type").orElse("unknown"); + String body = res.body(); + return new DResult(version, contentType, body); + + } catch (IOException | InterruptedException e) { + throw new FetchException(e); + } + } + + static class Builder implements AppConfigFetcher.Builder { + + private int port = 2772; + private String application; + private String environment; + private String configuration; + + @Override + public Builder application(String application) { + this.application = application; + return this; + } + + @Override + public Builder environment(String environment) { + this.environment = environment; + return this; + } + + @Override + public Builder configuration(String configuration) { + this.configuration = configuration; + return this; + } + + @Override + public Builder port(int port) { + this.port = port; + return this; + } + + @Override + public AppConfigFetcher build() { + return new DAppConfigFetcher(uri()); + } + + private String uri() { + if (configuration == null) { + configuration = environment + "-" + application; + } + return "http://localhost:" + port + "/applications/" + + application + "/environments/" + + environment + "/configurations/" + + configuration; + } + } +} diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java new file mode 100644 index 0000000..5b85817 --- /dev/null +++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java @@ -0,0 +1,28 @@ +package io.avaje.aws.appconfig; + +final class DResult implements AppConfigFetcher.Result { + + private final String version; + private final String contentType; + private final String body; + public DResult(String version, String contentType, String body) { + this.version = version; + this.contentType = contentType; + this.body = body; + } + + @Override + public String version() { + return version; + } + + @Override + public String contentType() { + return contentType; + } + + @Override + public String body() { + return body; + } +} diff --git a/avaje-aws-appconfig/src/main/java/module-info.java b/avaje-aws-appconfig/src/main/java/module-info.java new file mode 100644 index 0000000..89a56e9 --- /dev/null +++ b/avaje-aws-appconfig/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import io.avaje.aws.appconfig.AppConfigPlugin; + +module io.avaje.aws.appconfig { + + exports io.avaje.aws.appconfig; + + requires io.avaje.config; + requires java.net.http; + provides io.avaje.config.ConfigurationSource with AppConfigPlugin; +} diff --git a/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource b/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource new file mode 100644 index 0000000..fd4b517 --- /dev/null +++ b/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource @@ -0,0 +1 @@ +io.avaje.aws.appconfig.AppConfigPlugin diff --git a/pom.xml b/pom.xml index 76070ea..8f80dcd 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ avaje-config + avaje-aws-appconfig