diff --git a/docs-source/content/userguide/managing-domains/configoverrides/_index.md b/docs-source/content/userguide/managing-domains/configoverrides/_index.md
index 1f9fcd57660..c5898489627 100644
--- a/docs-source/content/userguide/managing-domains/configoverrides/_index.md
+++ b/docs-source/content/userguide/managing-domains/configoverrides/_index.md
@@ -451,8 +451,8 @@ when `spec.logHome` is configured and `spec.logHomeEnabled` is true.
* The operator runtime:
* Reads the expanded configuration overrides files or errors from the introspector.
* And, if the introspector reported no errors, it:
- * Puts configuration overrides files in a ConfigMap named `DOMAIN_UID-weblogic-domain-introspect-cm`.
- * Mounts this ConfigMap into the WebLogic Server instance Pods.
+ * Puts configuration overrides files in one or more ConfigMaps whose names start with `DOMAIN_UID-weblogic-domain-introspect-cm`.
+ * Mounts these ConfigMaps into the WebLogic Server instance Pods.
* Otherwise, if the introspector reported errors, it:
* Logs warning, error, or severe messages.
* Will not start WebLogic Server instance Pods; however, any already running Pods are preserved.
diff --git a/docs-source/content/userguide/managing-domains/model-in-image/overview.md b/docs-source/content/userguide/managing-domains/model-in-image/overview.md
index 6b2c995e3f0..ece66c16a0c 100644
--- a/docs-source/content/userguide/managing-domains/model-in-image/overview.md
+++ b/docs-source/content/userguide/managing-domains/model-in-image/overview.md
@@ -47,7 +47,7 @@ When you deploy a Model in Image Domain YAML file:
- Packages the domain home and passes it to the operator.
- After the introspector job completes:
- - The operator creates a ConfigMap named `DOMAIN_UID-weblogic-domain-introspect-cm` and puts the packaged domain home in it.
+ - The operator creates a ConfigMap named `DOMAIN_UID-weblogic-domain-introspect-cm` (possibly with some additional maps distinguished serial names) and puts the packaged domain home in it.
- The operator subsequently boots your domain's WebLogic Server pods.
- The pods will obtain their domain home from the ConfigMap.
diff --git a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/FileSystem.java b/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/FileSystem.java
deleted file mode 100644
index 8a021ffbd7e..00000000000
--- a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/FileSystem.java
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2018, 2020, Oracle Corporation and/or its affiliates.
-// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
-
-package oracle.kubernetes.json.mojo;
-
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.Writer;
-import java.net.MalformedURLException;
-import java.net.URL;
-
-abstract class FileSystem {
-
- static final FileSystem LIVE_FILE_SYSTEM = new LiveFileSystem();
-
- abstract URL toUrl(File file) throws MalformedURLException;
-
- abstract File[] listFiles(File directory);
-
- abstract File[] listFiles(File directory, FilenameFilter filter);
-
- abstract boolean exists(File file);
-
- abstract boolean isDirectory(File file);
-
- abstract boolean isWritable(File directory);
-
- abstract void createDirectory(File directory);
-
- abstract Writer createWriter(File file) throws IOException;
-
- abstract Reader createReader(File file) throws IOException;
-
- abstract long getLastModified(File file);
-
- private static class LiveFileSystem extends FileSystem {
-
- @Override
- URL toUrl(File file) throws MalformedURLException {
- return file.toURI().toURL();
- }
-
- File[] listFiles(File directory) {
- return directory.listFiles();
- }
-
- File[] listFiles(File directory, FilenameFilter filter) {
- return directory.listFiles(filter);
- }
-
- @Override
- boolean exists(File file) {
- return file.exists();
- }
-
- @Override
- boolean isDirectory(File file) {
- return file.isDirectory();
- }
-
- @Override
- boolean isWritable(File directory) {
- return directory.canWrite();
- }
-
- @Override
- void createDirectory(File directory) {
- directory.mkdirs();
- }
-
- @Override
- Writer createWriter(File file) throws IOException {
- return new FileWriter(file);
- }
-
- @Override
- Reader createReader(File file) throws IOException {
- return new FileReader(file);
- }
-
- @Override
- long getLastModified(File file) {
- return file.lastModified();
- }
- }
-}
diff --git a/json-schema-maven-plugin/pom.xml b/operator-build-maven-plugin/pom.xml
similarity index 96%
rename from json-schema-maven-plugin/pom.xml
rename to operator-build-maven-plugin/pom.xml
index 4b4a955e309..44aebec84aa 100644
--- a/json-schema-maven-plugin/pom.xml
+++ b/operator-build-maven-plugin/pom.xml
@@ -10,9 +10,9 @@
3.2.0
- jsonschema-maven-plugin
+ operator-build-maven-plugin
maven-plugin
- jsonschema-maven-plugin Maven Mojo
+ Operator Build Maven Plugin
diff --git a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/ExternalSchema.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/ExternalSchema.java
similarity index 100%
rename from json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/ExternalSchema.java
rename to operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/ExternalSchema.java
diff --git a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/JsonSchemaMojo.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/JsonSchemaMojo.java
similarity index 93%
rename from json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/JsonSchemaMojo.java
rename to operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/JsonSchemaMojo.java
index 0d6838ead94..9c5ff8b618c 100644
--- a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/JsonSchemaMojo.java
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/JsonSchemaMojo.java
@@ -12,6 +12,7 @@
import java.util.Map;
import java.util.Optional;
+import oracle.kubernetes.mojosupport.FileSystem;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
@@ -26,12 +27,21 @@
public class JsonSchemaMojo extends AbstractMojo {
private static final String DOT = "\\.";
+
+ @SuppressWarnings("FieldMayBeFinal") // must be non-final so unit tests can set it
private static Main main = new MainImpl();
+
+ @SuppressWarnings("FieldMayBeFinal") // must be non-final so unit tests can set it
private static FileSystem fileSystem = FileSystem.LIVE_FILE_SYSTEM;
+
+ @SuppressWarnings("unused") // set by Maven
@Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
private List compileClasspathElements;
+
+ @SuppressWarnings("unused") // set by Maven
@Parameter(defaultValue = "${project.build.outputDirectory}/schema")
private String targetDir;
+
@Parameter private String kubernetesVersion;
@Parameter private final List externalSchemas = Collections.emptyList();
@Parameter(required = true)
diff --git a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/Main.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/Main.java
similarity index 100%
rename from json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/Main.java
rename to operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/Main.java
diff --git a/json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/MainImpl.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/MainImpl.java
similarity index 100%
rename from json-schema-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/MainImpl.java
rename to operator-build-maven-plugin/src/main/java/oracle/kubernetes/json/mojo/MainImpl.java
diff --git a/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/AnsiUtils.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/AnsiUtils.java
new file mode 100644
index 00000000000..720192bcd56
--- /dev/null
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/AnsiUtils.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities related to ANSI-formatting of strings sent to a terminal.
+ */
+class AnsiUtils {
+
+ private static final Pattern ANSI_ESCAPE_CHARS = Pattern.compile("(\\x9B|\\x1B\\[)[0-?]*[ -\\/]*[@-~]");
+
+ static String withoutAnsiEscapeChars(String input) {
+ final Matcher matcher = ANSI_ESCAPE_CHARS.matcher(input);
+ return matcher.replaceAll("");
+ }
+
+ public static AnsiFormatter createFormatter(Format... formats) {
+ return new AnsiFormatter(formats);
+ }
+
+ static class AnsiFormatter {
+
+ private final Format[] formats;
+
+ AnsiFormatter(Format... formats) {
+ this.formats = formats;
+ }
+
+ String format(String string) {
+ return startCodes() + string + endCodes();
+ }
+
+ private String startCodes() {
+ return formats.length == 0 ? "" : sequence(Arrays.stream(formats).map(Format::getFormat).toArray(String[]::new));
+ }
+
+ private String endCodes() {
+ return formats.length == 0 ? "" : sequence("0");
+ }
+
+ String sequence(String... formatCodes) {
+ return "\u001B[" + String.join(";", formatCodes) + "m";
+ }
+ }
+
+ static enum Format {
+ BOLD(1), RED_FG(31), BLUE_FG(34), GREEN_FG(32);
+
+ private final String format;
+ Format(int format) {
+ this.format = Integer.toString(format);
+ }
+
+ public String getFormat() {
+ return format;
+ }
+ }
+}
diff --git a/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/BashProcessBuilder.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/BashProcessBuilder.java
new file mode 100644
index 00000000000..22938f4b76e
--- /dev/null
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/BashProcessBuilder.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+import javax.annotation.Nonnull;
+
+/**
+ * A wrapper for ProcessBuilder, in order to allow unit testing.
+ */
+class BashProcessBuilder {
+
+ private final String commands;
+ private final Map environmentVariables = new HashMap<>();
+ private final BiFunction,Process> processBiFunction;
+
+ /**
+ * Constructs a builder.
+ * @param commands the commands to be issued to a new bash process
+ */
+ BashProcessBuilder(String commands) {
+ this(BashProcessBuilder::createProcess, commands);
+ }
+
+ BashProcessBuilder(BiFunction, Process> processBiFunction, String commands) {
+ this.commands = commands;
+ this.processBiFunction = processBiFunction;
+ }
+
+ /**
+ * Updates the builder by adding an environment variable to be set in the process.
+ * @param name the environment variable name
+ * @param value the environment variable value
+ */
+ void addEnvironmentVariable(String name, String value) {
+ environmentVariables.put(name, value);
+ }
+
+ /**
+ * Starts the specified process and returns a Process object to control it.
+ */
+ public Process build() {
+ return processBiFunction.apply(commands, environmentVariables);
+ }
+
+ @Nonnull
+ protected static Process createProcess(String command, Map environmentVariables) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder("bash", command);
+ pb.environment().putAll(environmentVariables);
+ return pb.start();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/ShUnit2Mojo.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/ShUnit2Mojo.java
new file mode 100644
index 00000000000..56fa0f99634
--- /dev/null
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/ShUnit2Mojo.java
@@ -0,0 +1,261 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nonnull;
+
+import oracle.kubernetes.mojosupport.FileSystem;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BLUE_FG;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BOLD;
+
+/*
+ Will run all shunit2 tests in the testSourceDirectory named "test*" or "*test" with environment variables:
+ SHUNIT2_PATH - pointing to the shunit2 script to include
+ SCRIPTPATH - pointing to the sourceDirectory that contains scripts to test
+
+ This mojo will run as part of the build in which it is configured. To run from the command line,
+
+ 1. add the following to ~/.m2/settings.xml:
+
+
+ oracle.kubernetes
+
+
+ 2. execute:
+
+ mvn operator-build:shunit2 -pl
+
+ where is the name of the subdirectory containing the module on which the plugin is to be run.
+ */
+
+
+@Mojo(
+ name = "shunit2",
+ defaultPhase = LifecyclePhase.TEST,
+ requiresDependencyResolution = ResolutionScope.NONE)
+public class ShUnit2Mojo extends AbstractMojo {
+
+ static final String SHUNIT2_PATH = "SHUNIT2_PATH";
+ static final String SCRIPTPATH = "SCRIPTPATH";
+
+ private static final String BASH_TOO_OLD_MESSAGE =
+ "Bash %d.%d is too old to run unit tests."
+ + " Install a later version with Homebrew: "
+ + AnsiUtils.createFormatter(BOLD, BLUE_FG).format("brew install bash")
+ + ".";
+ private static final Pattern DIGITS = Pattern.compile("\\d+");
+ private static final Pattern VERSION_PATTERN = Pattern.compile("GNU bash, version (\\d+)\\.(\\d+)");
+ private static final String SHUNIT2_SCRIPT_ROOT = "shunit2";
+ private static final int MINIMUM_SUPPORTED_BASH_MAJOR_VERSION = 4;
+ private static final int MINIMUM_SUPPORTED_BASH_MINOR_VERSION = 2;
+
+ @SuppressWarnings("FieldMayBeFinal") // not final to allow unit test to change it
+ private static FileSystem fileSystem = FileSystem.LIVE_FILE_SYSTEM;
+
+ @SuppressWarnings("FieldMayBeFinal") // not final to allow unit test to change it
+ private static Function builderFunction = BashProcessBuilder::new;
+
+ /** The directory into which the mojo will copy the shunit2 script. */
+ @SuppressWarnings("unused") // set by Maven
+ @Parameter(defaultValue = "${project.build.testOutputDirectory}", readonly = true, required = true)
+ private File outputDirectory;
+
+ /** The directory containing the scripts to be tested. Test scripts will find SOURCE_DIR set with this value. */
+ @SuppressWarnings("unused") // set by Maven
+ @Parameter(defaultValue = "${project.basedir}/src/main/sh", readonly = true, required = true)
+ private File sourceDirectory;
+
+ /** The director this mojo will search for tests to execute. */
+ @Parameter(defaultValue = "${project.basedir}/src/test/sh", readonly = true, required = true)
+ @SuppressWarnings("unused") // set by Maven
+ private File testSourceDirectory;
+
+ private Map environmentVariables;
+ private List testSuites;
+
+ @Override
+ public void execute() throws MojoFailureException, MojoExecutionException {
+ if (isEnvironmentNotSupported()) {
+ return;
+ }
+
+ environmentVariables = getEnvironmentVariables();
+ testSuites = Arrays.stream(getScriptPaths()).map(this::createTestSuite).collect(Collectors.toList());
+
+ testSuites.forEach(TestSuite::run);
+ if ((totalNumFailures() + totalNumErrors()) != 0) {
+ throw new MojoFailureException(String.format("%d failures, %d errors", totalNumFailures(), totalNumErrors()));
+ }
+ }
+
+ private boolean isEnvironmentNotSupported() throws MojoExecutionException {
+ return isMacOSX() && isBashTooOld(getBashVersion());
+ }
+
+ private boolean isMacOSX() {
+ return System.getProperty("os.name").startsWith("Mac ");
+ }
+
+ private boolean isBashTooOld(int[] bashVersion) {
+ if (isBashVersionTooOld(bashVersion)) {
+ getLog().warn(createVersionTooOldMessage(bashVersion));
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isBashVersionTooOld(int[] version) {
+ return version[0] < MINIMUM_SUPPORTED_BASH_MAJOR_VERSION
+ || (version[0] == MINIMUM_SUPPORTED_BASH_MAJOR_VERSION && version[1] < MINIMUM_SUPPORTED_BASH_MINOR_VERSION);
+ }
+
+ private int[] getBashVersion() throws MojoExecutionException {
+ try {
+ final Process process = builderFunction.apply("-version").build();
+ process.waitFor();
+ return extractVersionFromResponse(process);
+ } catch (InterruptedException | IOException e) {
+ throw new MojoExecutionException("Unable to check bash version", e);
+ }
+ }
+
+ private int[] extractVersionFromResponse(Process process) throws IOException {
+ try (final InputStream inputStream = process.getInputStream()) {
+ return new BufferedReader(new InputStreamReader(inputStream)).lines()
+ .map(this::parseVersionNumber)
+ .filter(Objects::nonNull)
+ .findAny()
+ .orElseThrow(() -> new IOException("No bash version detected"));
+ }
+ }
+
+ private int[] parseVersionNumber(String message) {
+ final Matcher matcher = VERSION_PATTERN.matcher(message);
+ if (!matcher.find()) {
+ return null;
+ } else {
+ return new int[] {Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))};
+ }
+ }
+
+ private String createVersionTooOldMessage(int[] version) {
+ return String.format(BASH_TOO_OLD_MESSAGE, version[0], version[1]);
+ }
+
+
+ private int totalNumFailures() {
+ return testSuites.stream().mapToInt(TestSuite::numFailures).sum();
+ }
+
+ private int totalNumErrors() {
+ return testSuites.stream().mapToInt(TestSuite::getNumErrors).sum();
+ }
+
+ private TestSuite createTestSuite(String scriptPath) {
+ return new TestSuite(builderFunction, scriptPath, getLog(), environmentVariables);
+ }
+
+ List getTestSuites() {
+ return testSuites;
+ }
+
+ private Map getEnvironmentVariables() throws MojoExecutionException {
+ return Map.of(
+ SHUNIT2_PATH, getEffectiveShUnit2Directory() + "/shunit2",
+ SCRIPTPATH, sourceDirectory.getAbsolutePath());
+ }
+
+ private String[] getScriptPaths() {
+ return Arrays.stream(fileSystem.listFiles(testSourceDirectory, this::isTestScript))
+ .map(File::getAbsolutePath)
+ .toArray(String[]::new);
+ }
+
+ private boolean isTestScript(File directory, String fileName) {
+ return isTestName(fileName.toLowerCase().split("\\.")[0]);
+ }
+
+ private boolean isTestName(String baseName) {
+ return baseName.startsWith("test") || baseName.endsWith("test");
+ }
+
+ File getEffectiveShUnit2Directory() throws MojoExecutionException {
+ return lookupShUnit2Install();
+ }
+
+ private File lookupShUnit2Install() throws MojoExecutionException {
+ return Optional.ofNullable(lookupLatestShUnit2Install())
+ .orElseThrow(() -> new MojoExecutionException("Cannot find shunit2 installation."));
+ }
+
+ // It is possible that we have more than one version of shunit2 built into the plugin, which the copy-resources
+ // phase will copy into the classes directory. This method iterates through them, selects the highest version,
+ // looking only at those which actually contain a 'shunit2' script, and returns the selected install directory.
+ private File lookupLatestShUnit2Install() {
+ return Optional.of(getShUnitRootDirectory())
+ .filter(this::exists)
+ .map(this::getVersionSubdirectories).orElse(Stream.empty())
+ .max(this::compare)
+ .orElse(null);
+ }
+
+ private File getShUnitRootDirectory() {
+ return new File(outputDirectory, SHUNIT2_SCRIPT_ROOT);
+ }
+
+ private boolean exists(File file) {
+ return fileSystem.exists(file);
+ }
+
+ @Nonnull
+ private Stream getVersionSubdirectories(File rootDirectory) {
+ return Arrays.stream(fileSystem.listFiles(rootDirectory, this::hasShUnit2Install));
+ }
+
+ boolean hasShUnit2Install(File directory, String fileName) {
+ return fileSystem.exists(new File(directory, String.join(File.separator, fileName, SHUNIT2_SCRIPT_ROOT)));
+ }
+
+ // The last element of each file is expected to be a version in the form ... This comparator
+ // sorts the lowest version first, so that "1.2.4" compared with "1.3.1" will return -1.
+ int compare(File first, File second) {
+ return Long.compare(toLong(first), toLong(second));
+ }
+
+ // Given a File representing a path to a version directory (consisting of numbers and periods), converts it to a long.
+ private long toLong(File versionFile) {
+ long result = 0;
+ final Matcher m = DIGITS.matcher(versionFile.getName());
+ while (m.find()) {
+ result = (result * 100) + Long.parseLong(m.group());
+ }
+ return result;
+ }
+
+
+}
diff --git a/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/TestSuite.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/TestSuite.java
new file mode 100644
index 00000000000..6663c2cbc2e
--- /dev/null
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/TestSuite.java
@@ -0,0 +1,191 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import oracle.kubernetes.mojo.shunit2.AnsiUtils.AnsiFormatter;
+import org.apache.maven.plugin.logging.Log;
+
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BOLD;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.GREEN_FG;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.RED_FG;
+
+class TestSuite {
+ private static final Pattern TESTS_RUN_PATTERN = Pattern.compile("Ran (\\d+) tests.");
+ private static final Pattern TEST_FAILED_PATTERN = Pattern.compile("ASSERT:(.*)");
+
+ private static final AnsiFormatter SUCCESS_PREFIX_STYLE = AnsiUtils.createFormatter(BOLD, GREEN_FG);
+ private static final AnsiFormatter RUN_PROBLEM_STYLE = AnsiUtils.createFormatter(BOLD);
+ private static final AnsiFormatter PLAIN_STYLE = AnsiUtils.createFormatter();
+ private static final AnsiFormatter FAILURE_STYLE = AnsiUtils.createFormatter(BOLD, RED_FG);
+
+
+ @SuppressWarnings("FieldMayBeFinal") // not final to allow unit test to change it
+ private Function builderFunction;
+ private final String scriptPath;
+ private final Log log;
+ private final Map environmentVariables;
+
+ private int numTestsRun;
+ private int numFailures;
+ private int numErrors;
+
+ TestSuite(Function builderFunction,
+ String scriptPath, Log log, Map environmentVariables) {
+ this.builderFunction = builderFunction;
+ this.scriptPath = scriptPath;
+ this.log = log;
+ this.environmentVariables = environmentVariables;
+ }
+
+ void run() {
+ try {
+ final Process process = createProcess();
+ process.waitFor();
+ processResults(process);
+ } catch (IOException | InterruptedException e) {
+ throw new TestSuiteFailedException("Unable to run test script " + scriptPath, e);
+ }
+ }
+
+ int numTestsRun() {
+ return numTestsRun;
+ }
+
+ int numFailures() {
+ return numFailures;
+ }
+
+ public int getNumErrors() {
+ return numErrors;
+ }
+
+ private Process createProcess() {
+ final BashProcessBuilder builder = builderFunction.apply(scriptPath);
+ for (Map.Entry variable : environmentVariables.entrySet()) {
+ builder.addEnvironmentVariable(variable.getKey(), variable.getValue());
+ }
+ return builder.build();
+ }
+
+ private void processResults(Process process) throws IOException {
+ processOutputMessages(process);
+ processErrorMessages(process);
+
+ logSummaryLine();
+ }
+
+ private void logSummaryLine() {
+ if (wasSuccess()) {
+ log.info(createSummaryLine());
+ } else {
+ log.error(createSummaryLine());
+ }
+ }
+
+ private boolean wasSuccess() {
+ return numErrors == 0 && numFailures == 0;
+ }
+
+ String createSummaryLine() {
+ return getLineStart() + getTestsRun() + getFailures() + getErrors() + getLineEnd();
+ }
+
+ private String getLineStart() {
+ return getTestsFormat().format("Tests ");
+ }
+
+ private AnsiFormatter getTestsFormat() {
+ return wasSuccess() ? SUCCESS_PREFIX_STYLE : FAILURE_STYLE;
+ }
+
+ private String getTestsRun() {
+ return getRunFormat().format("run: " + numTestsRun);
+ }
+
+ private AnsiFormatter getRunFormat() {
+ return wasSuccess() ? SUCCESS_PREFIX_STYLE : RUN_PROBLEM_STYLE;
+ }
+
+ private String getFailures() {
+ return ", " + getFailureFormat().format("Failures: " + numFailures);
+ }
+
+ private AnsiFormatter getFailureFormat() {
+ return numFailures == 0 ? PLAIN_STYLE : FAILURE_STYLE;
+ }
+
+ private String getErrors() {
+ return ", " + getErrorFormat().format("Errors: " + numErrors);
+ }
+
+ private AnsiFormatter getErrorFormat() {
+ return numErrors == 0 ? PLAIN_STYLE : FAILURE_STYLE;
+ }
+
+ private String getLineEnd() {
+ return wasSuccess() ? getLineEndText() : FAILURE_STYLE.format(" <<< FAILURE!" + getLineEndText());
+ }
+
+ private String getLineEndText() {
+ return " - in " + scriptPath;
+ }
+
+ private void processOutputMessages(Process process) throws IOException {
+ try (final InputStream inputStream = process.getInputStream()) {
+ new BufferedReader(new InputStreamReader(inputStream)).lines()
+ .map(AnsiUtils::withoutAnsiEscapeChars)
+ .filter(this::isAllowedOutput)
+ .forEach(this::processOutputLine);
+ }
+ }
+
+ // Returns true if the line should be logged as output.
+ private boolean isAllowedOutput(String outputLine) {
+ final Matcher testsRunMatcher = TESTS_RUN_PATTERN.matcher(outputLine);
+ if (testsRunMatcher.find()) {
+ numTestsRun = Integer.parseInt(testsRunMatcher.group(1));
+ return false;
+ } else {
+ return !outputLine.contains("FAILED ");
+ }
+ }
+
+ private void processOutputLine(String line) {
+ final Matcher testFailedMatcher = TEST_FAILED_PATTERN.matcher(line);
+ if (testFailedMatcher.find()) {
+ numFailures++;
+ line = AnsiUtils.createFormatter(RED_FG, BOLD).format("FAILED: ") + testFailedMatcher.group(1);
+ }
+
+ log.info(line);
+ }
+
+ private void processErrorMessages(Process process) throws IOException {
+ try (final InputStream errorStream = process.getErrorStream()) {
+ new BufferedReader(new InputStreamReader(errorStream)).lines()
+ .map(AnsiUtils::withoutAnsiEscapeChars)
+ .filter(this::allowedError)
+ .forEach(this::logError);
+ }
+ }
+
+ private void logError(String line) {
+ numErrors++;
+ log.error(line);
+ }
+
+ private boolean allowedError(String errorLine) {
+ return !errorLine.contains("returned non-zero return code");
+ }
+
+}
diff --git a/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/TestSuiteFailedException.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/TestSuiteFailedException.java
new file mode 100644
index 00000000000..845ac8f568a
--- /dev/null
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojo/shunit2/TestSuiteFailedException.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+class TestSuiteFailedException extends RuntimeException {
+
+ public TestSuiteFailedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojosupport/FileSystem.java b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojosupport/FileSystem.java
new file mode 100644
index 00000000000..19d1f882327
--- /dev/null
+++ b/operator-build-maven-plugin/src/main/java/oracle/kubernetes/mojosupport/FileSystem.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2018, 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojosupport;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public abstract class FileSystem {
+
+ public static final FileSystem LIVE_FILE_SYSTEM = new LiveFileSystem();
+
+ public abstract URL toUrl(File file) throws MalformedURLException;
+
+ public abstract File[] listFiles(File directory);
+
+ public abstract File[] listFiles(File directory, FilenameFilter filter);
+
+ public abstract boolean exists(File file);
+
+ public abstract boolean isDirectory(File file);
+
+ public abstract boolean isWritable(File directory);
+
+ public abstract void createDirectory(File directory);
+
+ public abstract Writer createWriter(File file) throws IOException;
+
+ public abstract Reader createReader(File file) throws IOException;
+
+ public abstract long getLastModified(File file);
+
+ private static class LiveFileSystem extends FileSystem {
+
+ @Override
+ public URL toUrl(File file) throws MalformedURLException {
+ return file.toURI().toURL();
+ }
+
+ public File[] listFiles(File directory) {
+ return directory.listFiles();
+ }
+
+ public File[] listFiles(File directory, FilenameFilter filter) {
+ return directory.listFiles(filter);
+ }
+
+ @Override
+ public boolean exists(File file) {
+ return file.exists();
+ }
+
+ @Override
+ public boolean isDirectory(File file) {
+ return file.isDirectory();
+ }
+
+ @Override
+ public boolean isWritable(File directory) {
+ return directory.canWrite();
+ }
+
+ @Override
+ public void createDirectory(File directory) {
+ directory.mkdirs();
+ }
+
+ @Override
+ public Writer createWriter(File file) throws IOException {
+ return new FileWriter(file);
+ }
+
+ @Override
+ public Reader createReader(File file) throws IOException {
+ return new FileReader(file);
+ }
+
+ @Override
+ public long getLastModified(File file) {
+ return file.lastModified();
+ }
+ }
+}
diff --git a/json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/JsonSchemaMojoTest.java b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/JsonSchemaMojoTest.java
similarity index 54%
rename from json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/JsonSchemaMojoTest.java
rename to operator-build-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/JsonSchemaMojoTest.java
index b041b6b7a8d..5ea39a6b145 100644
--- a/json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/JsonSchemaMojoTest.java
+++ b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/JsonSchemaMojoTest.java
@@ -4,97 +4,53 @@
package oracle.kubernetes.json.mojo;
import java.io.File;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
import java.net.URL;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import com.google.common.collect.ImmutableMap;
-import com.meterware.simplestub.Memento;
import com.meterware.simplestub.StaticStubSupport;
-import org.apache.maven.plugin.AbstractMojo;
+import oracle.kubernetes.mojosupport.MojoTestBase;
+import oracle.kubernetes.mojosupport.TestFileSystem;
import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.FieldVisitor;
-import org.objectweb.asm.TypePath;
-import static com.meterware.simplestub.Stub.createNiceStub;
import static java.util.Collections.singletonList;
import static org.apache.maven.plugins.annotations.LifecyclePhase.PROCESS_CLASSES;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;
-import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
-import static org.objectweb.asm.Opcodes.ASM7;
@SuppressWarnings("SameParameterValue")
-public class JsonSchemaMojoTest {
+public class JsonSchemaMojoTest extends MojoTestBase {
private static final List EMPTY_CLASSPATH = new ArrayList<>();
private static final String TARGET_DIR = "/target/dir";
private static final String TEST_ROOT_CLASS = "a.b.c.D";
- private static final String DOT = "\\.";
private static final File SCHEMA_FILE = createFile(TARGET_DIR, TEST_ROOT_CLASS, ".json");
private static final File MARKDOWN_FILE = createFile(TARGET_DIR, TEST_ROOT_CLASS, ".md");
private static final File CLASS_FILE = createFile("/classes", TEST_ROOT_CLASS, ".class");
private static final String SPECIFIED_FILE_NAME = "specifiedFile.json";
private static final File SPECIFIED_FILE = new File(TARGET_DIR + "/" + SPECIFIED_FILE_NAME);
- private JsonSchemaMojo mojo = new JsonSchemaMojo();
- private Map classAnnotations = new HashMap<>();
+ private final TestFileSystem fileSystem = new TestFileSystem();
- // a map of fields to their annotations
- private Map> fieldAnnotations = new HashMap<>();
- private List mementos = new ArrayList<>();
private TestMain main;
- private TestFileSystem fileSystem = new TestFileSystem();
- @SuppressWarnings("SameParameterValue")
- private static File createFile(String dir, String className, String extension) {
- return new File(dir + File.separator + classNameToPath(className) + extension);
+ public JsonSchemaMojoTest() {
+ super(new JsonSchemaMojo());
}
- private static String classNameToPath(String className) {
- return className.replaceAll(DOT, File.separator);
- }
-
- private static File getTargetDir(Class> aaClass) throws URISyntaxException {
- File dir = getPackageDir(aaClass);
- while (dir.getParent() != null && !dir.getName().equals("target")) {
- dir = dir.getParentFile();
- }
- return dir;
- }
-
- private static File getPackageDir(Class> aaClass) throws URISyntaxException {
- URL url = aaClass.getResource(aaClass.getSimpleName() + ".class");
- return Paths.get(url.toURI()).toFile().getParentFile();
- }
-
- /**
- * Setup test.
- * @throws Exception on failure
- */
@Before
public void setUp() throws Exception {
ClassReader classReader = new ClassReader(JsonSchemaMojo.class.getName());
@@ -112,50 +68,14 @@ public void setUp() throws Exception {
mementos.add(StaticStubSupport.install(JsonSchemaMojo.class, "fileSystem", fileSystem));
}
- private void silenceMojoLog() {
- mojo.setLog(createNiceStub(Log.class));
- }
-
- private void setMojoParameter(String fieldName, Object value) throws Exception {
- Field field = mojo.getClass().getDeclaredField(fieldName);
- field.setAccessible(true);
- field.set(mojo, value);
- }
-
- private Object getMojoParameter(String fieldName) throws Exception {
- Field field = mojo.getClass().getDeclaredField(fieldName);
- field.setAccessible(true);
- return field.get(mojo);
- }
-
- /**
- * Tear down test.
- */
- @After
- public void tearDown() {
- for (Memento memento : mementos) {
- memento.revert();
- }
- }
-
- @Test
- public void mojoExtendsBaseClass() {
- assertThat(mojo, instanceOf(AbstractMojo.class));
- }
-
- @Test
- public void mojoHasGoalAnnotation() {
- assertThat(getClassAnnotation(Mojo.class), notNullValue());
- }
-
@Test
public void mojoAnnotatedWithName() {
- assertThat(getClassAnnotation(Mojo.class).fields.get("name"), equalTo("generate"));
+ assertThat(getClassAnnotation(Mojo.class).getField("name"), equalTo("generate"));
}
@Test
public void mojoAnnotatedWithDefaultPhase() {
- assertThat(getClassAnnotation(Mojo.class).fields.get("defaultPhase"), equalTo(PROCESS_CLASSES));
+ assertThat(getClassAnnotation(Mojo.class).getField("defaultPhase"), equalTo(PROCESS_CLASSES));
}
@Test
@@ -163,7 +83,7 @@ public void hasClasspathElementsField_withAnnotation() throws NoSuchFieldExcepti
Field classPathField = JsonSchemaMojo.class.getDeclaredField("compileClasspathElements");
assertThat(classPathField.getType(), equalTo(List.class));
assertThat(
- getFieldAnnotation(classPathField, Parameter.class).fields.get("defaultValue"),
+ getFieldAnnotation(classPathField, Parameter.class).getField("defaultValue"),
equalTo("${project.compileClasspathElements}"));
}
@@ -172,7 +92,7 @@ public void hasTargetDirField_withAnnotation() throws NoSuchFieldException {
Field targetDirField = JsonSchemaMojo.class.getDeclaredField("targetDir");
assertThat(targetDirField.getType(), equalTo(String.class));
assertThat(
- getFieldAnnotation(targetDirField, Parameter.class).fields.get("defaultValue"),
+ getFieldAnnotation(targetDirField, Parameter.class).getField("defaultValue"),
equalTo("${project.build.outputDirectory}/schema"));
}
@@ -188,7 +108,7 @@ public void hasRootClassNameField_withAnnotation() throws NoSuchFieldException {
Field rootClassField = JsonSchemaMojo.class.getDeclaredField("rootClass");
assertThat(rootClassField.getType(), equalTo(String.class));
assertThat(
- getFieldAnnotation(rootClassField, Parameter.class).fields.get("required"), is(true));
+ getFieldAnnotation(rootClassField, Parameter.class).getField("required"), is(true));
}
@Test
@@ -241,7 +161,7 @@ public void hasOutputFileField_withAnnotation() throws Exception {
public void whenKubernetesVersionSpecified_passToGenerator() throws Exception {
setMojoParameter("kubernetesVersion", "1.9.0");
- mojo.execute();
+ executeMojo();
assertThat(main.getKubernetesVersion(), equalTo("1.9.0"));
}
@@ -250,7 +170,7 @@ public void whenKubernetesVersionSpecified_passToGenerator() throws Exception {
public void whenKubernetesVersionNotSpecified_passToGenerator() throws Exception {
setMojoParameter("kubernetesVersion", null);
- mojo.execute();
+ executeMojo();
assertThat(main.getKubernetesVersion(), nullValue());
}
@@ -261,7 +181,7 @@ public void whenExternalSchemaSpecified_passToGenerator() throws Exception {
"externalSchemas",
singletonList(new ExternalSchema("http://schema.json", "src/cache/schema.json")));
- mojo.execute();
+ executeMojo();
assertThat(
main.getCacheFor(new URL("http://schema.json")),
@@ -274,18 +194,18 @@ public void whenUnableToUseDefineSchema_haltTheBuild() throws Exception {
"externalSchemas",
singletonList(new ExternalSchema("abcd://schema.json", "src/cache/schema.json")));
- mojo.execute();
+ executeMojo();
}
@Test(expected = MojoExecutionException.class)
public void whenNoClassSpecified_haltTheBuild() throws Exception {
setMojoParameter("rootClass", null);
- mojo.execute();
+ executeMojo();
}
@Test
public void whenLookingForClassFile_specifyRelativeFilePath() throws Exception {
- mojo.execute();
+ executeMojo();
assertThat(main.getResourceName(), equalTo(classNameToPath(TEST_ROOT_CLASS) + ".class"));
}
@@ -293,7 +213,7 @@ public void whenLookingForClassFile_specifyRelativeFilePath() throws Exception {
@Test(expected = MojoExecutionException.class)
public void whenRootClassNotFound_haltTheBuild() throws Exception {
main.setClasspathResource(null);
- mojo.execute();
+ executeMojo();
}
@Test
@@ -305,21 +225,21 @@ public void useSpecifiedClasspath() throws Exception {
fileSystem.defineUrl(new File(classpathElements[i]), classPathUrls[i]);
}
- mojo.execute();
+ executeMojo();
assertThat(main.getClasspath(), arrayContaining(classPathUrls));
}
@Test
public void generateToExpectedLocation() throws Exception {
- mojo.execute();
+ executeMojo();
assertThat(main.getSchemaFile(), equalTo(SCHEMA_FILE));
}
@Test
public void whenGenerateMarkdownNotSpecified_dontGenerateMarkdown() throws Exception {
- mojo.execute();
+ executeMojo();
assertThat(main.getMarkdownFile(), nullValue());
}
@@ -328,7 +248,7 @@ public void whenGenerateMarkdownNotSpecified_dontGenerateMarkdown() throws Excep
public void whenGenerateMarkdownSpecified_generateMarkdown() throws Exception {
setMojoParameter("generateMarkdown", true);
- mojo.execute();
+ executeMojo();
assertThat(main.getMarkdownFile(), equalTo(MARKDOWN_FILE));
}
@@ -339,7 +259,7 @@ public void whenGenerateMarkdownSpecified_useGeneratedSchemaForMarkdown() throws
main.setGeneratedSchema(generatedSchema);
setMojoParameter("generateMarkdown", true);
- mojo.execute();
+ executeMojo();
assertThat(main.getMarkdownSchema(), sameInstance(generatedSchema));
}
@@ -350,7 +270,7 @@ public void whenSchemaMoreRecentThanClassFile_dontGenerateNewSchema() throws Exc
fileSystem.defineFileContents(SCHEMA_FILE, "");
fileSystem.touch(SCHEMA_FILE);
- mojo.execute();
+ executeMojo();
assertThat(main.getSchemaFile(), nullValue());
}
@@ -361,7 +281,7 @@ public void whenClassFileMoreRecentThanSchema_generateNewSchema() throws Excepti
fileSystem.defineFileContents(SCHEMA_FILE, "");
fileSystem.touch(CLASS_FILE);
- mojo.execute();
+ executeMojo();
assertThat(main.getSchemaFile(), equalTo(SCHEMA_FILE));
}
@@ -369,7 +289,7 @@ public void whenClassFileMoreRecentThanSchema_generateNewSchema() throws Excepti
@Test
public void whenOutputFileSpecified_generateToIt() throws Exception {
setMojoParameter("outputFile", SPECIFIED_FILE_NAME);
- mojo.execute();
+ executeMojo();
assertThat(main.getSchemaFile(), equalTo(SPECIFIED_FILE));
}
@@ -378,7 +298,7 @@ public void whenOutputFileSpecified_generateToIt() throws Exception {
public void whenIncludeAdditionalPropertiesSet_setOnMain() throws Exception {
setMojoParameter("includeAdditionalProperties", true);
- mojo.execute();
+ executeMojo();
assertThat(main.isIncludeAdditionalProperties(), is(true));
}
@@ -387,159 +307,9 @@ public void whenIncludeAdditionalPropertiesSet_setOnMain() throws Exception {
public void whenSupportObjectReferencesSet_setOnMain() throws Exception {
setMojoParameter("supportObjectReferences", true);
- mojo.execute();
+ executeMojo();
assertThat(main.isSupportObjectReferences(), is(true));
}
- @SuppressWarnings("SameParameterValue")
- private AnnotationInfo getClassAnnotation(Class extends Annotation> annotationClass) {
- return classAnnotations.get(toDescription(annotationClass));
- }
-
- @SuppressWarnings("SameParameterValue")
- private AnnotationInfo getFieldAnnotation(Field field, Class extends Annotation> annotation) {
- return fieldAnnotations.get(field).get(toDescription(annotation));
- }
-
- private Map getOrCreateAnnotationMap(Field field) {
- Map map = fieldAnnotations.get(field);
- return map != null ? map : createAnnotationMap(field);
- }
-
- private Map createAnnotationMap(Field field) {
- Map map = new HashMap<>();
- fieldAnnotations.put(field, map);
- return map;
- }
-
- private String toDescription(Class extends Annotation> aaClass) {
- return "L" + aaClass.getName().replaceAll(DOT, "/") + ';';
- }
-
- private AnnotationInfo getOrCreateAnnotationInfo(
- String description, Map map) {
- AnnotationInfo info = map.get(description);
- return info != null ? info : createAnnotationInfo(map, description);
- }
-
- private AnnotationInfo createAnnotationInfo(Map map, String description) {
- AnnotationInfo info = new AnnotationInfo();
- map.put(description, info);
- return info;
- }
-
- private URL toModuleUrl(String path) throws URISyntaxException, MalformedURLException {
- return new File(getModuleDir(), path).toURI().toURL();
- }
-
- private File getModuleDir() throws URISyntaxException {
- return getTargetDir(getClass()).getParentFile();
- }
-
- private class Visitor extends ClassVisitor {
- private Class> theClass;
-
- Visitor(Class> theClass) {
- super(ASM7);
- this.theClass = theClass;
- }
-
- @Override
- public AnnotationVisitor visitTypeAnnotation(
- int typeRef, TypePath typePath, String desc, boolean visible) {
- return super.visitTypeAnnotation(typeRef, typePath, desc, visible);
- }
-
- @Override
- public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
- return new ClassAnnotationVisitor(desc);
- }
-
- @Override
- public FieldVisitor visitField(int flags, String fieldName, String desc, String s, Object v) {
- try {
- return new MojoFieldVisitor(getField(fieldName));
- } catch (NoSuchFieldException e) {
- return super.visitField(flags, fieldName, desc, s, v);
- }
- }
-
- private Field getField(String fieldName) throws NoSuchFieldException {
- return theClass.getDeclaredField(fieldName);
- }
- }
-
- private abstract class MojoAnnotationVisitor extends AnnotationVisitor {
- private String annotationClassDesc;
- private Map annotations;
-
- MojoAnnotationVisitor(Map annotations, String desc) {
- super(ASM7);
- this.annotations = annotations;
- annotationClassDesc = desc;
- annotations.put(desc, new AnnotationInfo());
- }
-
- @Override
- public void visit(String name, Object value) {
- getOrCreateAnnotationInfo(annotationClassDesc, annotations).fields.put(name, value);
- }
-
- @Override
- public void visitEnum(String name, String enumDesc, String value) {
- getOrCreateAnnotationInfo(annotationClassDesc, annotations)
- .fields
- .put(name, getEnumConstant(getEnumClass(enumDesc), value));
- }
-
- Class> getEnumClass(String desc) {
- try {
- String className = desc.substring(1, desc.length() - 1).replaceAll("/", DOT);
- return Class.forName(className);
- } catch (ClassNotFoundException e) {
- throw new RuntimeException(e.toString());
- }
- }
-
- private Object getEnumConstant(Class> enumClass, String value) {
- for (Object constant : enumClass.getEnumConstants()) {
- if (value.equalsIgnoreCase(constant.toString())) {
- return constant;
- }
- }
- throw new RuntimeException("No enum constant " + value + " in " + enumClass);
- }
- }
-
- private class ClassAnnotationVisitor extends MojoAnnotationVisitor {
-
- ClassAnnotationVisitor(String annotationDescriptor) {
- super(classAnnotations, annotationDescriptor);
- }
- }
-
- private class FieldAnnotationVisitor extends MojoAnnotationVisitor {
- FieldAnnotationVisitor(Map annotationMap, String desc) {
- super(annotationMap, desc);
- }
- }
-
- private class MojoFieldVisitor extends FieldVisitor {
- private final Map annotationMap;
-
- MojoFieldVisitor(Field field) {
- super(ASM7);
- this.annotationMap = getOrCreateAnnotationMap(field);
- }
-
- @Override
- public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
- return new FieldAnnotationVisitor(annotationMap, desc);
- }
- }
-
- private class AnnotationInfo {
- private Map fields = new HashMap<>();
- }
}
diff --git a/json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestMain.java b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestMain.java
similarity index 100%
rename from json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestMain.java
rename to operator-build-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestMain.java
diff --git a/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojo/shunit2/AnsiUtilsTest.java b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojo/shunit2/AnsiUtilsTest.java
new file mode 100644
index 00000000000..e608d2c664b
--- /dev/null
+++ b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojo/shunit2/AnsiUtilsTest.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+import org.junit.Test;
+
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BLUE_FG;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BOLD;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.GREEN_FG;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.RED_FG;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+public class AnsiUtilsTest {
+
+
+ @Test
+ public void removeAnsiEscapeCharacters() {
+ assertThat(AnsiUtils.withoutAnsiEscapeChars("\u001B[1;31mASSERT:\u001B[0mIt didn't work"),
+ equalTo("ASSERT:It didn't work"));
+ assertThat(AnsiUtils.withoutAnsiEscapeChars("Ran \u001B[1;36m2\u001B[0m tests."),
+ equalTo("Ran 2 tests."));
+ assertThat(AnsiUtils.withoutAnsiEscapeChars("\u001B[1;31mFAILED\u001B[0m (\u001B[1;31mfailures=1\u001B[0m)"),
+ equalTo("FAILED (failures=1)"));
+ }
+
+ @Test
+ public void formatBoldTexts() {
+ assertThat(AnsiUtils.createFormatter(BOLD).format("sample"),
+ equalTo("\u001B[1msample\u001B[0m"));
+ }
+
+ @Test
+ public void formatBoldText() {
+ assertThat(AnsiUtils.createFormatter(BOLD).format("sample"), equalTo("\u001B[1msample\u001B[0m"));
+ }
+
+ @Test
+ public void formatBoldRedText() {
+ assertThat(AnsiUtils.createFormatter(BOLD, RED_FG).format("sample"),
+ equalTo("\u001B[1;31msample\u001B[0m"));
+ }
+
+ @Test
+ public void formatBlueText() {
+ assertThat(AnsiUtils.createFormatter(BLUE_FG).format("sample"),
+ equalTo("\u001B[34msample\u001B[0m"));
+ }
+
+ @Test
+ public void formatGreenText() {
+ assertThat(AnsiUtils.createFormatter(GREEN_FG).format("sample"),
+ equalTo("\u001B[32msample\u001B[0m"));
+ }
+}
\ No newline at end of file
diff --git a/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojo/shunit2/ShUnit2MojoTest.java b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojo/shunit2/ShUnit2MojoTest.java
new file mode 100644
index 00000000000..a14c162b90a
--- /dev/null
+++ b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojo/shunit2/ShUnit2MojoTest.java
@@ -0,0 +1,450 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojo.shunit2;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+
+import com.meterware.simplestub.StaticStubSupport;
+import com.meterware.simplestub.SystemPropertySupport;
+import oracle.kubernetes.mojosupport.MojoTestBase;
+import oracle.kubernetes.mojosupport.TestFileSystem;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+
+import static com.meterware.simplestub.Stub.createNiceStub;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BLUE_FG;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.BOLD;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.GREEN_FG;
+import static oracle.kubernetes.mojo.shunit2.AnsiUtils.Format.RED_FG;
+import static org.apache.maven.plugins.annotations.LifecyclePhase.TEST;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
+import static org.hamcrest.Matchers.both;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
+import static org.junit.Assert.fail;
+
+@SuppressWarnings("UnconstructableJUnitTestCase") // mistaken warning due to private constructor
+public class ShUnit2MojoTest extends MojoTestBase {
+
+ private static final String TEST_SCRIPT = "shunit2";
+ private static final String OS_NAME_PROPERTY = "os.name";
+ private static final String[] INSTALLED_OSX_BASH_VERSION = {
+ "GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)",
+ "Copyright (C) 2007 Free Software Foundation, Inc."
+ };
+
+ private static final String[] HOMEBREW_BASH_VERSION = {
+ "GNU bash, version 5.0.18(1)-release (x86_64-apple-darwin19.6.0)",
+ "Copyright (C) 2019 Free Software Foundation, Inc.",
+ "License GPLv3+: GNU GPL version 3 or later "
+ };
+
+ private final ShUnit2Mojo mojo;
+ private final Function builderFunction = this::createProcessBuilder;
+ private final TestDelegate delegate = new TestDelegate();
+ private final TestFileSystem fileSystem = new TestFileSystem();
+ private static final File TEST_CLASSES_DIRECTORY = new File("/test-classes");
+ private static final File LATEST_SHUNIT2_DIRECTORY = new File(TEST_CLASSES_DIRECTORY, "shunit2/2.1.8");
+ private static final File EARLIER_SHUNIT2_DIRECTORY = new File(TEST_CLASSES_DIRECTORY, "shunit2/2.1.6");
+ private static final File SOURCE_DIRECTORY = new File("/sources");
+ private static final File TEST_SOURCE_DIRECTORY = new File("/tests");
+
+ public ShUnit2MojoTest() {
+ this(new ShUnit2Mojo());
+ }
+
+ private ShUnit2MojoTest(ShUnit2Mojo mojo) {
+ super(mojo);
+ this.mojo = mojo;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ ClassReader classReader = new ClassReader(ShUnit2Mojo.class.getName());
+ classReader.accept(new Visitor(ShUnit2Mojo.class), 0);
+
+ mementos.add(StaticStubSupport.install(ShUnit2Mojo.class, "fileSystem", fileSystem));
+ mementos.add(StaticStubSupport.install(ShUnit2Mojo.class, "builderFunction", builderFunction));
+ mementos.add(SystemPropertySupport.install(OS_NAME_PROPERTY, "Linux"));
+
+ setMojoParameter("outputDirectory", TEST_CLASSES_DIRECTORY);
+ setMojoParameter("sourceDirectory", SOURCE_DIRECTORY);
+ setMojoParameter("testSourceDirectory", TEST_SOURCE_DIRECTORY);
+ silenceMojoLog();
+
+ fileSystem.defineFileContents(new File(LATEST_SHUNIT2_DIRECTORY, TEST_SCRIPT), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test1.sh"), "");
+ }
+
+ BashProcessBuilder createProcessBuilder(String command) {
+ return new BashProcessBuilder(delegate, command);
+ }
+
+ @Test
+ public void mojoAnnotatedWithName() {
+ assertThat(getClassAnnotation(Mojo.class).getField("name"), equalTo("shunit2"));
+ }
+
+ @Test
+ public void mojoAnnotatedWithDefaultPhase() {
+ assertThat(getClassAnnotation(Mojo.class).getField("defaultPhase"), equalTo(TEST));
+ }
+
+ @Test
+ public void hasOutputDirectoryField_withAnnotation() throws NoSuchFieldException {
+ Field classPathField = ShUnit2Mojo.class.getDeclaredField("outputDirectory");
+ assertThat(classPathField.getType(), equalTo(File.class));
+ assertThat(
+ getFieldAnnotation(classPathField, Parameter.class).getField("defaultValue"),
+ equalTo("${project.build.testOutputDirectory}"));
+ }
+
+ @Test
+ public void hasTestSourceDirectoryField_withAnnotation() throws NoSuchFieldException {
+ Field classPathField = ShUnit2Mojo.class.getDeclaredField("testSourceDirectory");
+ assertThat(classPathField.getType(), equalTo(File.class));
+ assertThat(
+ getFieldAnnotation(classPathField, Parameter.class).getField("defaultValue"),
+ equalTo("${project.basedir}/src/test/sh"));
+ }
+
+ @Test
+ public void hasSourceDirectoryField_withAnnotation() throws NoSuchFieldException {
+ Field classPathField = ShUnit2Mojo.class.getDeclaredField("sourceDirectory");
+ assertThat(classPathField.getType(), equalTo(File.class));
+ assertThat(
+ getFieldAnnotation(classPathField, Parameter.class).getField("defaultValue"),
+ equalTo("${project.basedir}/src/main/sh"));
+ }
+
+ @Test
+ public void useCopiedShUnit2Directory() throws MojoExecutionException {
+ fileSystem.defineFileContents(new File(LATEST_SHUNIT2_DIRECTORY, TEST_SCRIPT), "");
+
+ assertThat(mojo.getEffectiveShUnit2Directory(), equalTo(LATEST_SHUNIT2_DIRECTORY));
+ }
+
+ @Test
+ public void whenMultipleShUnit2VersionsInstalled_selectLatest() throws MojoExecutionException {
+ fileSystem.defineFileContents(new File(EARLIER_SHUNIT2_DIRECTORY, TEST_SCRIPT), "");
+ fileSystem.defineFileContents(new File(LATEST_SHUNIT2_DIRECTORY, TEST_SCRIPT), "");
+
+ assertThat(mojo.getEffectiveShUnit2Directory(), equalTo(LATEST_SHUNIT2_DIRECTORY));
+ }
+
+ @Test
+ public void whenLatestShUnit2VersionsMissing_selectPrior() throws MojoExecutionException {
+ fileSystem.clear();
+ fileSystem.defineFileContents(new File(EARLIER_SHUNIT2_DIRECTORY, TEST_SCRIPT), "");
+ fileSystem.defineFileContents(LATEST_SHUNIT2_DIRECTORY, "");
+
+ assertThat(mojo.getEffectiveShUnit2Directory(), equalTo(EARLIER_SHUNIT2_DIRECTORY));
+ }
+
+ @Test(expected = MojoExecutionException.class)
+ public void whenShUnit2NotInstalled_reportFailure() throws MojoFailureException, MojoExecutionException {
+ fileSystem.clear();
+ fileSystem.defineFileContents(TEST_CLASSES_DIRECTORY, "");
+
+ executeMojo();
+ }
+
+ @Test
+ public void onMacOS_warnIfBashIsOld() throws MojoFailureException, MojoExecutionException {
+ System.setProperty(OS_NAME_PROPERTY, "Mac OS X");
+ defineExecution().withOutputs(INSTALLED_OSX_BASH_VERSION);
+
+ executeMojo();
+
+ assertThat(getWarningLines(),
+ contains("Bash 3.2 is too old to run unit tests. Install a later version with Homebrew: "
+ + AnsiUtils.createFormatter(BOLD, BLUE_FG).format("brew install bash") + "."));
+ }
+
+ @Test
+ public void onMacOS_dontRunTestsIfBashIsOld() throws MojoFailureException, MojoExecutionException {
+ System.setProperty(OS_NAME_PROPERTY, "Mac OS X");
+ defineExecution().withOutputs(INSTALLED_OSX_BASH_VERSION);
+ defineExecution().withOutputs("This is an example", "and here is another", "Ran 2 tests.");
+
+ executeMojo();
+
+ assertThat(getInfoLines(), empty());
+ }
+
+ @Test
+ public void onMacOS_dontWarnIfBashVersionIsSupported() throws MojoFailureException, MojoExecutionException {
+ System.setProperty(OS_NAME_PROPERTY, "Mac OS X");
+ defineExecution().withOutputs(HOMEBREW_BASH_VERSION);
+
+ executeMojo();
+
+ assertThat(getWarningLines(), empty());
+ }
+
+ @Test
+ public void onMacOS_runTestsIfBashVersionIsSupported() throws MojoFailureException, MojoExecutionException {
+ System.setProperty(OS_NAME_PROPERTY, "Mac OS X");
+ defineExecution().withOutputs(HOMEBREW_BASH_VERSION);
+ defineExecution().withOutputs("This is an example", "and here is another", "Ran 2 tests.");
+
+ executeMojo();
+
+ assertThat(getInfoLines(), contains("This is an example", "and here is another",
+ createExpectedSuccessSummary(2, "test1.sh")));
+ }
+
+ @Test
+ public void onExecution_specifyTheSelectedShUnit2ScriptPath() throws MojoFailureException, MojoExecutionException {
+ executeMojo();
+
+ assertThat(delegate.getShUnit2ScriptPath(), equalTo(LATEST_SHUNIT2_DIRECTORY + "/shunit2"));
+ }
+
+ @Test
+ public void onExecution_specifyPathToSourceScripts() throws MojoFailureException, MojoExecutionException {
+ executeMojo();
+
+ assertThat(delegate.getSourceScriptDir(), equalTo(SOURCE_DIRECTORY.getAbsolutePath()));
+ }
+
+ @Test
+ public void onExecution_specifyTestScripts() throws MojoFailureException, MojoExecutionException {
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test1.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test3.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "nothing.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "2ndtest.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "4thtest"), "");
+
+ executeMojo();
+
+ assertThat(delegate.getScriptPaths(),
+ arrayContainingInAnyOrder("/tests/test1.sh", "/tests/2ndtest.sh", "/tests/test3.sh", "/tests/4thtest"));
+ }
+
+ @Test
+ public void onExecution_logOutputs() throws MojoFailureException, MojoExecutionException {
+ defineExecution().withOutputs("This is an example", "and here is another", "Ran 2 tests.");
+
+ executeMojo();
+
+ assertThat(getInfoLines(), contains("This is an example", "and here is another",
+ createExpectedSuccessSummary(2, "test1.sh")));
+ }
+
+ protected ProcessStub defineExecution() {
+ return delegate.defineScriptExecution();
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private String createExpectedSuccessSummary(int numTestsRun, String testScript) {
+ return AnsiUtils.createFormatter(BOLD, GREEN_FG).format("Tests ")
+ + AnsiUtils.createFormatter(BOLD, GREEN_FG).format("run: " + numTestsRun)
+ + String.format(", Failures: 0, Errors: 0 - in /tests/%s", testScript);
+ }
+
+ @Test
+ public void whenErrorDetected_reportInSummary() throws MojoExecutionException {
+ defineExecution().withErrors("This is an example", "and here is another").withOutputs("Ran 3 tests.");
+
+ try {
+ executeMojo();
+ fail("Should have thrown an exception");
+ } catch (MojoFailureException ignored) {
+ assertThat(getErrorLines(),
+ contains("This is an example", "and here is another",
+ createExpectedErrorSummary(3, 2, "test1.sh")));
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private String createExpectedErrorSummary(int numTestsRun, int numErrors, String testScript) {
+ return AnsiUtils.createFormatter(BOLD, RED_FG).format("Tests ")
+ + AnsiUtils.createFormatter(BOLD).format("run: " + numTestsRun)
+ + ", Failures: 0, "
+ + AnsiUtils.createFormatter(BOLD, RED_FG).format("Errors: " + numErrors)
+ + AnsiUtils.createFormatter(BOLD, RED_FG).format(" <<< FAILURE! - in /tests/" + testScript);
+ }
+
+ @Test
+ public void onExecution_logErrors() throws MojoExecutionException {
+ defineExecution().withErrors("This is an example", "and here is another");
+
+ try {
+ executeMojo();
+ fail("Should have thrown an exception");
+ } catch (MojoFailureException ignored) {
+ assertThat(getErrorLines(), both(hasItem("This is an example")).and(hasItem("and here is another")));
+ }
+ }
+
+ @Test
+ public void onExecution_ignoreNonZeroReturnCodeErrors() throws MojoExecutionException {
+ defineExecution().withErrors("This is an example",
+ "\u001B[1;31mERROR:\u001B[0m testPartyLikeItIs1999() returned non-zero return code.");
+
+ try {
+ executeMojo();
+ fail("Should have thrown an exception");
+ } catch (MojoFailureException ignored) {
+ assertThat(getErrorLines(), hasItem("This is an example"));
+ }
+ }
+
+ @Test
+ public void onExecution_ignoreFailureMessage() throws MojoFailureException, MojoExecutionException {
+ defineExecution().withOutputs("This is an example", "FAILED (failures=2)");
+
+ executeMojo();
+
+ assertThat(getErrorLines(), empty());
+ }
+
+ @Test
+ public void onExecution_recordReportedNumberOfTests() throws MojoFailureException, MojoExecutionException {
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test1.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test2.sh"), "");
+ final AnsiUtils.AnsiFormatter shUnit2RunCountFormat = AnsiUtils.createFormatter(BOLD, BLUE_FG);
+ defineExecution().withOutputs(String.format("Ran %s tests.", shUnit2RunCountFormat.format("3")));
+ defineExecution().withOutputs(String.format("Ran %s tests.", shUnit2RunCountFormat.format("2")));
+
+ executeMojo();
+
+ assertThat(mojo.getTestSuites().stream().map(TestSuite::numTestsRun).collect(Collectors.toList()), contains(3, 2));
+ }
+
+ @Test
+ public void onExecution_recordReportedNumberOfFailures() throws MojoExecutionException {
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test1.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test2.sh"), "");
+ defineExecution()
+ .withOutputs("test1", "test2", createExpectedTestFailure("expected up but was down"))
+ .withErrors("test2 returned non-zero return code.");
+ defineExecution()
+ .withOutputs("test3", createExpectedTestFailure("expected blue but was red"),
+ "test4", createExpectedTestFailure("expected left but was right"));
+
+ try {
+ executeMojo();
+ } catch (MojoFailureException e) {
+ assertThat(getFailuresByTestSuite(), contains(1, 2));
+ }
+ }
+
+ @Nonnull
+ protected List getFailuresByTestSuite() {
+ return mojo.getTestSuites().stream().map(TestSuite::numFailures).collect(Collectors.toList());
+ }
+
+ private String createExpectedTestFailure(String explanation) {
+ return AnsiUtils.createFormatter(BOLD, RED_FG).format("ASSERT:") + explanation;
+ }
+
+ @Test(expected = MojoFailureException.class)
+ public void whenAnyTestsFail_mojoThrowsException() throws MojoFailureException, MojoExecutionException {
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test1.sh"), "");
+ fileSystem.defineFileContents(new File(TEST_SOURCE_DIRECTORY, "test2.sh"), "");
+ defineExecution()
+ .withOutputs("test1", "test2", createExpectedTestFailure("expected up but was down"))
+ .withErrors("test2 returned non-zero return code.");
+ defineExecution()
+ .withOutputs("test3", createExpectedTestFailure("expected blue but was red"),
+ "test4", createExpectedTestFailure("expected left but was right"));
+
+ executeMojo();
+ }
+
+ // todo print tests run, failures at end of each testsuite
+ // todo print total tests run, total failures across multiple tests
+
+ static class TestDelegate implements BiFunction, Process> {
+ private final ArrayDeque processStubs = new ArrayDeque<>();
+ private final List commands = new ArrayList<>();
+ private Map environmentVariables;
+
+ ProcessStub defineScriptExecution() {
+ final ProcessStub processStub = createNiceStub(ProcessStub.class);
+ processStubs.add(processStub);
+ return processStub;
+ }
+
+ String getShUnit2ScriptPath() {
+ return environmentVariables.get(ShUnit2Mojo.SHUNIT2_PATH);
+ }
+
+ String getSourceScriptDir() {
+ return environmentVariables.get(ShUnit2Mojo.SCRIPTPATH);
+ }
+
+ String[] getScriptPaths() {
+ return commands.toArray(new String[0]);
+ }
+
+ @Override
+ public Process apply(String command, Map environmentVariables) {
+ this.commands.add(command);
+ this.environmentVariables = environmentVariables;
+ return Optional.ofNullable(processStubs.pollFirst()).orElseGet(this::defineScriptExecution);
+ }
+ }
+
+
+ abstract static class ProcessStub extends Process {
+ private final List outputLines = new ArrayList<>();
+ private final List errorLines = new ArrayList<>();
+
+ ProcessStub withOutputs(String... lines) {
+ outputLines.addAll(Arrays.asList(lines));
+ return this;
+ }
+
+ ProcessStub withErrors(String... lines) {
+ errorLines.addAll(Arrays.asList(lines));
+ return this;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new StringListInputStream(outputLines);
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return new StringListInputStream(errorLines);
+ }
+ }
+
+ static class StringListInputStream extends ByteArrayInputStream {
+
+ public StringListInputStream(List inputs) {
+ super(toByteArray(inputs));
+ }
+
+ private static byte[] toByteArray(List inputs) {
+ return String.join(System.lineSeparator(), inputs).getBytes(StandardCharsets.UTF_8);
+ }
+ }
+}
diff --git a/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojosupport/MojoTestBase.java b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojosupport/MojoTestBase.java
new file mode 100644
index 00000000000..71ca3047b3f
--- /dev/null
+++ b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojosupport/MojoTestBase.java
@@ -0,0 +1,349 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.mojosupport;
+
+import java.io.File;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+
+import com.meterware.simplestub.Memento;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.junit.After;
+import org.junit.Test;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.TypePath;
+
+import static com.meterware.simplestub.Stub.createNiceStub;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.objectweb.asm.Opcodes.ASM7;
+
+public abstract class MojoTestBase {
+
+ private final AbstractMojo mojo;
+ private static final String DOT = "\\.";
+ // a map of fields to their annotations
+ protected Map> fieldAnnotations = new HashMap<>();
+ protected List mementos = new ArrayList<>();
+ private final Map classAnnotations = new HashMap<>();
+ private final LogStub logStub = createNiceStub(LogStub.class);
+
+ @SuppressWarnings("SameParameterValue")
+ protected static File createFile(String dir, String className, String extension) {
+ return new File(dir + File.separator + MojoTestBase.classNameToPath(className) + extension);
+ }
+
+ protected static String classNameToPath(String className) {
+ return className.replaceAll(DOT, File.separator);
+ }
+
+ private static File getTargetDir(Class> aaClass) throws URISyntaxException {
+ File dir = MojoTestBase.getPackageDir(aaClass);
+ while (dir.getParent() != null && !dir.getName().equals("target")) {
+ dir = dir.getParentFile();
+ }
+ return dir;
+ }
+
+ private static File getPackageDir(Class> aaClass) throws URISyntaxException {
+ URL url = aaClass.getResource(aaClass.getSimpleName() + ".class");
+ return Paths.get(url.toURI()).toFile().getParentFile();
+ }
+
+ public MojoTestBase(AbstractMojo mojo) {
+ this.mojo = mojo;
+ }
+
+ public void executeMojo() throws MojoFailureException, MojoExecutionException {
+ mojo.execute();
+ }
+
+ protected List getInfoLines() {
+ return logStub.infoMessages;
+ }
+
+ protected List getWarningLines() {
+ return logStub.warningMessages;
+ }
+
+ protected List getErrorLines() {
+ return logStub.errorMessages;
+ }
+
+ protected void silenceMojoLog() {
+ mojo.setLog(logStub);
+ }
+
+ protected void setMojoParameter(String fieldName, Object value) throws Exception {
+ Field field = mojo.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(mojo, value);
+ }
+
+ protected Object getMojoParameter(String fieldName) throws Exception {
+ Field field = mojo.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(mojo);
+ }
+
+ /**
+ * Tear down test.
+ */
+ @After
+ public void tearDown() {
+ for (Memento memento : mementos) {
+ memento.revert();
+ }
+ }
+
+ @Test
+ public void mojoExtendsBaseClass() {
+ assertThat(mojo, instanceOf(AbstractMojo.class));
+ }
+
+ @Test
+ public void mojoHasGoalAnnotation() {
+ assertThat(getClassAnnotation(Mojo.class), notNullValue());
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ protected AnnotationInfo getClassAnnotation(Class extends Annotation> annotationClass) {
+ return classAnnotations.get(toDescription(annotationClass));
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ protected AnnotationInfo getFieldAnnotation(Field field, Class extends Annotation> annotation) {
+ return fieldAnnotations.get(field).get(toDescription(annotation));
+ }
+
+ private Map getOrCreateAnnotationMap(Field field) {
+ Map map = fieldAnnotations.get(field);
+ return map != null ? map : createAnnotationMap(field);
+ }
+
+ private Map createAnnotationMap(Field field) {
+ Map map = new HashMap<>();
+ fieldAnnotations.put(field, map);
+ return map;
+ }
+
+ protected String toDescription(Class extends Annotation> aaClass) {
+ return "L" + aaClass.getName().replaceAll(DOT, "/") + ';';
+ }
+
+ private AnnotationInfo getOrCreateAnnotationInfo(
+ String description, Map map) {
+ AnnotationInfo info = map.get(description);
+ return info != null ? info : createAnnotationInfo(map, description);
+ }
+
+ private AnnotationInfo createAnnotationInfo(Map map, String description) {
+ AnnotationInfo info = new AnnotationInfo();
+ map.put(description, info);
+ return info;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ protected URL toModuleUrl(String path) throws URISyntaxException, MalformedURLException {
+ return new File(getModuleDir(), path).toURI().toURL();
+ }
+
+ protected File getModuleDir() throws URISyntaxException {
+ return MojoTestBase.getTargetDir(getClass()).getParentFile();
+ }
+
+ protected class Visitor extends ClassVisitor {
+ private final Class> theClass;
+
+ public Visitor(Class> theClass) {
+ super(ASM7);
+ this.theClass = theClass;
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ return super.visitTypeAnnotation(typeRef, typePath, desc, visible);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ return new ClassAnnotationVisitor(desc);
+ }
+
+ @Override
+ public FieldVisitor visitField(int flags, String fieldName, String desc, String s, Object v) {
+ try {
+ return new MojoFieldVisitor(getField(fieldName));
+ } catch (NoSuchFieldException e) {
+ return super.visitField(flags, fieldName, desc, s, v);
+ }
+ }
+
+ private Field getField(String fieldName) throws NoSuchFieldException {
+ return theClass.getDeclaredField(fieldName);
+ }
+ }
+
+ private abstract class MojoAnnotationVisitor extends AnnotationVisitor {
+
+ private final String annotationClassDesc;
+ private final Map annotations;
+
+ MojoAnnotationVisitor(Map annotations, String desc) {
+ super(ASM7);
+ this.annotations = annotations;
+ annotationClassDesc = desc;
+ annotations.put(desc, new AnnotationInfo());
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ getOrCreateAnnotationInfo(annotationClassDesc, annotations).fields.put(name, value);
+ }
+
+ @Override
+ public void visitEnum(String name, String enumDesc, String value) {
+ getOrCreateAnnotationInfo(annotationClassDesc, annotations)
+ .fields
+ .put(name, getEnumConstant(getEnumClass(enumDesc), value));
+ }
+
+ Class> getEnumClass(String desc) {
+ try {
+ String className = desc.substring(1, desc.length() - 1).replaceAll("/", DOT);
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e.toString());
+ }
+ }
+
+ private Object getEnumConstant(Class> enumClass, String value) {
+ for (Object constant : enumClass.getEnumConstants()) {
+ if (value.equalsIgnoreCase(constant.toString())) {
+ return constant;
+ }
+ }
+ throw new RuntimeException("No enum constant " + value + " in " + enumClass);
+ }
+ }
+
+ private class ClassAnnotationVisitor extends MojoAnnotationVisitor {
+
+ ClassAnnotationVisitor(String annotationDescriptor) {
+ super(classAnnotations, annotationDescriptor);
+ }
+ }
+
+ private class FieldAnnotationVisitor extends MojoAnnotationVisitor {
+
+ FieldAnnotationVisitor(Map annotationMap, String desc) {
+ super(annotationMap, desc);
+ }
+ }
+
+ private class MojoFieldVisitor extends FieldVisitor {
+
+ private final Map annotationMap;
+
+ MojoFieldVisitor(Field field) {
+ super(ASM7);
+ this.annotationMap = getOrCreateAnnotationMap(field);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ return new FieldAnnotationVisitor(annotationMap, desc);
+ }
+ }
+
+ public static class AnnotationInfo {
+ private final Map fields = new HashMap<>();
+
+ public Object getField(String name) {
+ return fields.get(name);
+ }
+ }
+
+ abstract static class LogStub implements Log {
+ private final List infoMessages = new ArrayList<>();
+ private final List warningMessages = new ArrayList<>();
+ private final List errorMessages = new ArrayList<>();
+
+ @Override
+ public boolean isInfoEnabled() {
+ return true;
+ }
+
+ @Override
+ public void info(CharSequence content) {
+ info(content, null);
+ }
+
+ @Override
+ public void info(CharSequence content, Throwable error) {
+ infoMessages.add(createLogMessage(content, error));
+ }
+
+ @Override
+ public void info(Throwable error) {
+ info(null, error);
+ }
+
+ @Nonnull
+ public static String createLogMessage(CharSequence content, Throwable error) {
+ final List builder = new ArrayList<>();
+ Optional.ofNullable(content).map(CharSequence::toString).ifPresent(builder::add);
+ Optional.ofNullable(error).map(Throwable::toString).ifPresent(builder::add);
+ return String.join(System.lineSeparator(), builder);
+ }
+
+ @Override
+ public boolean isWarnEnabled() {
+ return true;
+ }
+
+ @Override
+ public void warn(CharSequence content) {
+ warningMessages.add(createLogMessage(content, null));
+ }
+
+ @Override
+ public boolean isErrorEnabled() {
+ return true;
+ }
+
+ @Override
+ public void error(CharSequence content) {
+ error(content, null);
+ }
+
+ @Override
+ public void error(CharSequence content, Throwable error) {
+ errorMessages.add(createLogMessage(content, error));
+ }
+
+ @Override
+ public void error(Throwable error) {
+ error(null, error);
+ }
+ }
+}
diff --git a/json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestFileSystem.java b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojosupport/TestFileSystem.java
similarity index 62%
rename from json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestFileSystem.java
rename to operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojosupport/TestFileSystem.java
index 9cf3056321f..c14edf9ad11 100644
--- a/json-schema-maven-plugin/src/test/java/oracle/kubernetes/json/mojo/TestFileSystem.java
+++ b/operator-build-maven-plugin/src/test/java/oracle/kubernetes/mojosupport/TestFileSystem.java
@@ -1,7 +1,7 @@
// Copyright (c) 2018, 2020, Oracle Corporation and/or its affiliates.
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
-package oracle.kubernetes.json.mojo;
+package oracle.kubernetes.mojosupport;
import java.io.File;
import java.io.FilenameFilter;
@@ -9,7 +9,6 @@
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
-import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
@@ -17,15 +16,29 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nonnull;
+@SuppressWarnings("unused")
public class TestFileSystem extends FileSystem {
+ protected static final File[] NO_FILES = new File[0];
long lastModificationTime = 0;
- private Map> directories = new HashMap>();
- private Map contents = new HashMap();
- private Set writeOnlyFiles = new HashSet();
- private Map lastModified = new HashMap();
- private Map urls = new HashMap<>();
+ private final Map> directories = new HashMap<>();
+ private final Map contents = new HashMap<>();
+ private final Set writeOnlyFiles = new HashSet<>();
+ private final Map lastModified = new HashMap<>();
+ private final Map urls = new HashMap<>();
+
+ /**
+ * Clear all defined test files.
+ */
+ public void clear() {
+ directories.clear();
+ contents.clear();
+ writeOnlyFiles.clear();
+ lastModified.clear();
+ urls.clear();
+ }
public void touch(File file) {
setLastModified(file, ++lastModificationTime);
@@ -57,7 +70,7 @@ private void addDirectoryIfNotDefined(File dir) {
return;
}
addToParent(dir);
- directories.put(dir, new ArrayList());
+ directories.put(dir, new ArrayList<>());
}
public void defineFileContents(File file, String data) {
@@ -74,62 +87,66 @@ public void makeWriteOnly(File file) {
writeOnlyFiles.add(file);
}
- void defineUrl(File file, URL url) {
+ public void defineUrl(File file, URL url) {
urls.put(file, url);
}
@Override
- URL toUrl(File file) throws MalformedURLException {
+ public URL toUrl(File file) {
return urls.get(file);
}
- File[] listFiles(File directory) {
- return isDirectory(directory) ? getDirectoryContents(directory, null) : null;
+ public File[] listFiles(File directory) {
+ return isDirectory(directory) ? getDirectoryContents(directory, null) : NO_FILES;
}
- File[] listFiles(File directory, FilenameFilter filter) {
- return isDirectory(directory) ? getDirectoryContents(directory, filter) : null;
+ public File[] listFiles(File directory, FilenameFilter filter) {
+ return isDirectory(directory) ? getDirectoryContents(directory, filter) : NO_FILES;
}
private File[] getDirectoryContents(File directory, FilenameFilter filter) {
- List files = new ArrayList();
+ List files = new ArrayList<>();
for (File file : directories.get(directory)) {
if (filter == null || filter.accept(file.getParentFile(), file.getName())) {
files.add(file);
}
}
- return files.toArray(new File[files.size()]);
+ return toArray(files);
}
private File[] toArray(List files) {
- return files.toArray(new File[files.size()]);
+ return files.toArray(NO_FILES);
}
- boolean exists(File file) {
+ public boolean exists(File file) {
return contents.containsKey(file) || isDirectory(file);
}
- boolean isDirectory(File file) {
+ public boolean isDirectory(File file) {
return directories.containsKey(file);
}
- boolean isWritable(File file) {
+ public boolean isWritable(File file) {
return !writeOnlyFiles.contains(file);
}
- void createDirectory(File directory) {
+ /**
+ * Creates the specified directory.
+ * @param directory a file which describes a directory
+ */
+ public void createDirectory(File directory) {
if (!isDirectory(directory)) {
- directories.put(directory, new ArrayList());
+ directories.put(directory, new ArrayList<>());
}
}
@Override
- long getLastModified(File file) {
+ public long getLastModified(File file) {
return lastModified.containsKey(file) ? lastModified.get(file) : 0;
}
@Override
- Writer createWriter(File file) throws IOException {
+ public Writer createWriter(File file) throws IOException {
if (!exists(file.getParentFile())) {
throw new IOException("Parent directory " + file.getParentFile() + " does not exist");
}
@@ -137,46 +154,46 @@ Writer createWriter(File file) throws IOException {
}
@Override
- Reader createReader(File file) throws IOException {
+ public Reader createReader(File file) {
return new TestFileReader(file);
}
class TestFileWriter extends Writer {
- private StringBuilder sb = new StringBuilder();
- private File file;
+ private final StringBuilder sb = new StringBuilder();
+ private final File file;
TestFileWriter(File file) {
this.file = file;
}
@Override
- public void write(char[] cbuf, int off, int len) throws IOException {
+ public void write(@Nonnull char[] cbuf, int off, int len) {
sb.append(cbuf, off, len);
}
@Override
- public void flush() throws IOException {
+ public void flush() {
}
@Override
- public void close() throws IOException {
+ public void close() {
contents.put(file, sb.toString());
}
}
class TestFileReader extends Reader {
- private StringReader reader;
+ private final StringReader reader;
TestFileReader(File file) {
reader = new StringReader(contents.get(file));
}
@Override
- public void close() throws IOException {
+ public void close() {
}
@Override
- public int read(char[] cbuf, int off, int len) throws IOException {
+ public int read(@Nonnull char[] cbuf, int off, int len) throws IOException {
return reader.read(cbuf, off, len);
}
}
diff --git a/operator/pom.xml b/operator/pom.xml
index 8e8438a5a4e..ddc3e635505 100644
--- a/operator/pom.xml
+++ b/operator/pom.xml
@@ -158,10 +158,11 @@
${project.groupId}
- jsonschema-maven-plugin
+ operator-build-maven-plugin
${project.version}
+ generate-schema
process-classes
generate
@@ -172,6 +173,16 @@
true
+
+ default-cli
+ test
+
+ shunit2
+
+
+ src/main/resources/scripts
+
+
diff --git a/operator/src/main/java/oracle/kubernetes/operator/IntrospectorConfigMapConstants.java b/operator/src/main/java/oracle/kubernetes/operator/IntrospectorConfigMapConstants.java
new file mode 100644
index 00000000000..3a55d22fd12
--- /dev/null
+++ b/operator/src/main/java/oracle/kubernetes/operator/IntrospectorConfigMapConstants.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.operator;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Constants for generating introspector config maps.
+ */
+public interface IntrospectorConfigMapConstants {
+
+ /** The topology generated from the WebLogic domain. */
+ String TOPOLOGY_YAML = "topology.yaml";
+
+ /** An MD5 has of the Model-in-Image secrets. */
+ String SECRETS_MD_5 = "secrets.md5";
+
+ /** A hash computed from the WebLogic domain. */
+ String DOMAINZIP_HASH = "domainzip_hash";
+
+ /** The last value of the restartVersion field from the domain resource. */
+ String DOMAIN_RESTART_VERSION = "weblogic.domainRestartVersion";
+
+ /** A hash of the Model-in-Image inputs. */
+ String DOMAIN_INPUTS_HASH = "weblogic.domainInputsHash";
+
+ /** The number of config maps required to hold the encoded domains. */
+ String NUM_CONFIG_MAPS = "numConfigMaps";
+
+ /**
+ * The prefix for a number of keys which may appear in the introspector config map.
+ * They are not preserved from one update to another.
+ */
+ String SIT_CONFIG_FILE_PREFIX = "Sit-Cfg";
+
+ /** The suffix for naming introspector config maps. */
+ String INTROSPECTOR_CONFIG_MAP_NAME_SUFFIX = "-weblogic-domain-introspect-cm";
+
+ /**
+ * Returns the name of an introspector config map.
+ * @param domainUid the unique UID for the domain containing the map
+ * @param index the index of the config map
+ */
+ static String getIntrospectorConfigMapName(String domainUid, int index) {
+ return getIntrospectorConfigMapNamePrefix(domainUid) + suffix(index);
+ }
+
+ static String getIntrospectorConfigMapNamePrefix(String uid) {
+ return uid + INTROSPECTOR_CONFIG_MAP_NAME_SUFFIX;
+ }
+
+ /** A (possibly empty) suffix for introspector config maps. */
+ static String suffix(int index) {
+ return index == 0 ? "" : "_" + index;
+ }
+
+ /**
+ * Returns the mount path for an introspector volume mount.
+ * @param index the index of the mount / config map
+ */
+ @Nonnull
+ static String getIntrospectorVolumePath(int index) {
+ return "/weblogic-operator/introspector" + suffix(index);
+ }
+}
diff --git a/operator/src/main/java/oracle/kubernetes/operator/IntrospectorConfigMapKeys.java b/operator/src/main/java/oracle/kubernetes/operator/IntrospectorConfigMapKeys.java
deleted file mode 100644
index 3a55d9df5dd..00000000000
--- a/operator/src/main/java/oracle/kubernetes/operator/IntrospectorConfigMapKeys.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
-// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
-
-package oracle.kubernetes.operator;
-
-/**
- * Keys in the generated introspector config map.
- */
-public interface IntrospectorConfigMapKeys {
-
- /** The topology generated from the WebLogic domain. */
- String TOPOLOGY_YAML = "topology.yaml";
-
- /** An MD5 has of the Model-in-Image secrets. */
- String SECRETS_MD_5 = "secrets.md5";
-
- /** A hash computed from the WebLogic domain. */
- String DOMAINZIP_HASH = "domainzip_hash";
-
- /** The last value of the restartVersion field from the domain resource. */
- String DOMAIN_RESTART_VERSION = "weblogic.domainRestartVersion";
-
- /** A hash of the Model-in-Image inputs. */
- String DOMAIN_INPUTS_HASH = "weblogic.domainInputsHash";
-
- /**
- * The prefix for a number of keys which may appear in the introspector config map.
- * They are not preserved from one update to another.
- */
- String SIT_CONFIG_FILE_PREFIX = "Sit-Cfg";
-}
diff --git a/operator/src/main/java/oracle/kubernetes/operator/KubernetesConstants.java b/operator/src/main/java/oracle/kubernetes/operator/KubernetesConstants.java
index 496c89db6a2..8640dfc13ef 100644
--- a/operator/src/main/java/oracle/kubernetes/operator/KubernetesConstants.java
+++ b/operator/src/main/java/oracle/kubernetes/operator/KubernetesConstants.java
@@ -35,7 +35,6 @@ public interface KubernetesConstants {
String SCRIPT_CONFIG_MAP_NAME = "weblogic-scripts-cm";
String DOMAIN_DEBUG_CONFIG_MAP_SUFFIX = "-weblogic-domain-debug-cm";
- String INTROSPECTOR_CONFIG_MAP_NAME_SUFFIX = "-weblogic-domain-introspect-cm";
String GRACEFUL_SHUTDOWNTYPE = ShutdownType.Graceful.name();
diff --git a/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapHelper.java b/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapHelper.java
index 8b8232ce65f..a3f51639365 100644
--- a/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapHelper.java
+++ b/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapHelper.java
@@ -6,12 +6,17 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
import javax.json.Json;
import javax.json.JsonPatchBuilder;
import javax.json.JsonValue;
@@ -19,11 +24,11 @@
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.models.V1ConfigMap;
+import io.kubernetes.client.openapi.models.V1ConfigMapList;
import io.kubernetes.client.openapi.models.V1DeleteOptions;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import oracle.kubernetes.operator.DomainStatusUpdater;
-import oracle.kubernetes.operator.IntrospectorConfigMapKeys;
-import oracle.kubernetes.operator.KubernetesConstants;
+import oracle.kubernetes.operator.IntrospectorConfigMapConstants;
import oracle.kubernetes.operator.LabelConstants;
import oracle.kubernetes.operator.ProcessingConstants;
import oracle.kubernetes.operator.calls.CallResponse;
@@ -43,11 +48,12 @@
import static java.lang.System.lineSeparator;
import static oracle.kubernetes.operator.DomainStatusUpdater.BAD_TOPOLOGY;
-import static oracle.kubernetes.operator.IntrospectorConfigMapKeys.DOMAINZIP_HASH;
-import static oracle.kubernetes.operator.IntrospectorConfigMapKeys.DOMAIN_INPUTS_HASH;
-import static oracle.kubernetes.operator.IntrospectorConfigMapKeys.DOMAIN_RESTART_VERSION;
-import static oracle.kubernetes.operator.IntrospectorConfigMapKeys.SECRETS_MD_5;
-import static oracle.kubernetes.operator.IntrospectorConfigMapKeys.SIT_CONFIG_FILE_PREFIX;
+import static oracle.kubernetes.operator.IntrospectorConfigMapConstants.DOMAINZIP_HASH;
+import static oracle.kubernetes.operator.IntrospectorConfigMapConstants.DOMAIN_INPUTS_HASH;
+import static oracle.kubernetes.operator.IntrospectorConfigMapConstants.DOMAIN_RESTART_VERSION;
+import static oracle.kubernetes.operator.IntrospectorConfigMapConstants.NUM_CONFIG_MAPS;
+import static oracle.kubernetes.operator.IntrospectorConfigMapConstants.SECRETS_MD_5;
+import static oracle.kubernetes.operator.IntrospectorConfigMapConstants.SIT_CONFIG_FILE_PREFIX;
import static oracle.kubernetes.operator.KubernetesConstants.SCRIPT_CONFIG_MAP_NAME;
import static oracle.kubernetes.operator.LabelConstants.INTROSPECTION_STATE_LABEL;
import static oracle.kubernetes.operator.ProcessingConstants.DOMAIN_VALIDATION_ERRORS;
@@ -144,12 +150,12 @@ public static int getModelInImageSpecHash(String imageName) {
}
/**
- * Returns the standard name for the generated domain config map.
+ * Returns the standard name for the introspector config map.
* @param domainUid the unique ID of the domain
* @return map name
*/
public static String getIntrospectorConfigMapName(String domainUid) {
- return domainUid + KubernetesConstants.INTROSPECTOR_CONFIG_MAP_NAME_SUFFIX;
+ return IntrospectorConfigMapConstants.getIntrospectorConfigMapName(domainUid, 0);
}
abstract static class ConfigMapComparator {
@@ -227,6 +233,10 @@ abstract static class ConfigMapContext extends StepContextBase {
void recordCurrentMap(Packet packet, V1ConfigMap configMap) {
}
+ void setContentValue(String key, String value) {
+ contents.put(key, value);
+ }
+
protected String getName() {
return name;
}
@@ -352,7 +362,7 @@ private Step patchCurrentMap(V1ConfigMap currentMap, Step next) {
return new CallBuilder()
.patchConfigMapAsync(name, namespace,
- getDomainUidLabel(Optional.ofNullable(currentMap).map(V1ConfigMap::getMetadata).orElse(null)),
+ getDomainUidLabel(Optional.of(currentMap).map(V1ConfigMap::getMetadata).orElse(null)),
new V1Patch(patchBuilder.build().toString()), createPatchResponseStep(next));
}
@@ -488,6 +498,7 @@ private long timeSinceJobStart(Packet packet) {
}
static class IntrospectionLoader {
+
private final Packet packet;
private final Step conflictStep;
private final DomainPresenceInfo info;
@@ -509,7 +520,7 @@ private void parseIntrospectorResult() {
LOGGER.fine(data.toString());
LOGGER.fine("================");
- wlsDomainConfig = Optional.ofNullable(data.get(IntrospectorConfigMapKeys.TOPOLOGY_YAML))
+ wlsDomainConfig = Optional.ofNullable(data.get(IntrospectorConfigMapConstants.TOPOLOGY_YAML))
.map(this::getDomainTopology)
.map(DomainTopology::getDomain)
.orElse(null);
@@ -532,16 +543,42 @@ private void updatePacket() {
private Step createIntrospectionVersionUpdateStep() {
return DomainValidationSteps.createValidateDomainTopologyStep(
- createIntrospectorConfigMapContext(conflictStep).patchOnly().verifyConfigMap(conflictStep.getNext()));
+ createIntrospectorConfigMapContext().patchOnly().verifyConfigMap(conflictStep.getNext()));
}
private Step createValidationStep() {
- return DomainValidationSteps.createValidateDomainTopologyStep(
- createIntrospectorConfigMapContext(conflictStep).verifyConfigMap(conflictStep.getNext()));
+ return Step.chain(
+ DomainValidationSteps.createValidateDomainTopologyStep(null),
+ new IntrospectionConfigMapStep(data, conflictStep.getNext()));
}
- private IntrospectorConfigMapContext createIntrospectorConfigMapContext(Step conflictStep) {
- return new IntrospectorConfigMapContext(conflictStep, info.getDomain(), data, info);
+ private class IntrospectionConfigMapStep extends Step {
+ private final Map data;
+ private final ConfigMapSplitter splitter;
+
+ IntrospectionConfigMapStep(Map data, Step next) {
+ super(next);
+ this.splitter = new ConfigMapSplitter<>(IntrospectionLoader.this::createIntrospectorConfigMapContext);
+ this.data = data;
+ }
+
+ @Override
+ public NextAction apply(Packet packet) {
+ Collection startDetails = splitter.split(data).stream()
+ .map(c -> c.createStepAndPacket(packet))
+ .collect(Collectors.toList());
+ packet.put(NUM_CONFIG_MAPS, Integer.toString(startDetails.size()));
+ return doForkJoin(getNext(), packet, startDetails);
+ }
+
+ }
+
+ private IntrospectorConfigMapContext createIntrospectorConfigMapContext() {
+ return createIntrospectorConfigMapContext(data, 0);
+ }
+
+ private IntrospectorConfigMapContext createIntrospectorConfigMapContext(Map data, int index) {
+ return new IntrospectorConfigMapContext(conflictStep, info, data, index);
}
private String getModelInImageSpecHash() {
@@ -604,19 +641,33 @@ private String perLine(List errors) {
}
}
- public static class IntrospectorConfigMapContext extends ConfigMapContext {
- final String domainUid;
+ public static class IntrospectorConfigMapContext extends ConfigMapContext implements SplitterTarget {
+
+ private static final Pattern ENCODED_ZIP_PATTERN = Pattern.compile("([A-Za-z_]+)\\.secure");
+
private boolean patchOnly;
- IntrospectorConfigMapContext(
- Step conflictStep,
- Domain domain,
- Map data,
- DomainPresenceInfo info) {
- super(conflictStep, getIntrospectorConfigMapName(domain.getDomainUid()), domain.getNamespace(), data, info);
+ IntrospectorConfigMapContext(Step conflictStep, DomainPresenceInfo info, Map data, int index) {
+ super(conflictStep, getConfigMapName(info, index), info.getNamespace(), data, info);
- this.domainUid = domain.getDomainUid();
- addLabel(LabelConstants.DOMAINUID_LABEL, domainUid);
+ addLabel(LabelConstants.DOMAINUID_LABEL, info.getDomainUid());
+ }
+
+ private static String getConfigMapName(DomainPresenceInfo info, int index) {
+ return IntrospectorConfigMapConstants.getIntrospectorConfigMapName(info.getDomainUid(), index);
+ }
+
+ @Override
+ public void recordNumTargets(int numTargets) {
+ setContentValue(NUM_CONFIG_MAPS, Integer.toString(numTargets));
+ }
+
+ private boolean isEncodedZip(String key) {
+ return ENCODED_ZIP_PATTERN.matcher(key).matches();
+ }
+
+ private String createRangeName(String key) {
+ return key + ".range";
}
IntrospectorConfigMapContext patchOnly() {
@@ -643,6 +694,9 @@ private boolean isRemovableKey(String key) {
return key.startsWith(SIT_CONFIG_FILE_PREFIX);
}
+ public Step.StepAndPacket createStepAndPacket(Packet packet) {
+ return new Step.StepAndPacket(verifyConfigMap(null), packet.clone());
+ }
}
/**
@@ -654,19 +708,84 @@ private boolean isRemovableKey(String key) {
* @return the created step
*/
public static Step deleteIntrospectorConfigMapStep(String domainUid, String namespace, Step next) {
- return new DeleteIntrospectorConfigMapStep(domainUid, namespace, next);
+ return new DeleteIntrospectorConfigMapsStep(domainUid, namespace, next);
}
- private static class DeleteIntrospectorConfigMapStep extends Step {
+ private static class DeleteIntrospectorConfigMapsStep extends Step {
private final String domainUid;
private final String namespace;
- DeleteIntrospectorConfigMapStep(String domainUid, String namespace, Step next) {
+ private DeleteIntrospectorConfigMapsStep(String domainUid, String namespace, Step next) {
super(next);
this.domainUid = domainUid;
this.namespace = namespace;
}
+ @Override
+ public NextAction apply(Packet packet) {
+ Step step = new CallBuilder()
+ .withLabelSelectors(LabelConstants.getCreatedbyOperatorSelector())
+ .listConfigMapsAsync(namespace, new SelectConfigMapsToDeleteStep(domainUid, namespace));
+
+ return doNext(step, packet);
+ }
+ }
+
+ private static class SelectConfigMapsToDeleteStep extends DefaultResponseStep {
+ private final String domainUid;
+ private final String namespace;
+
+ public SelectConfigMapsToDeleteStep(String domainUid, String namespace) {
+ this.domainUid = domainUid;
+ this.namespace = namespace;
+ }
+
+ @Override
+ public NextAction onSuccess(Packet packet, CallResponse callResponse) {
+ final List configMapNames = getIntrospectorConfigMapNames(callResponse.getResult());
+ if (configMapNames.isEmpty()) {
+ return doNext(packet);
+ } else {
+ Collection startDetails = new ArrayList<>();
+ for (String configMapName : configMapNames) {
+ startDetails.add(new StepAndPacket(
+ new DeleteIntrospectorConfigMapStep(domainUid, namespace, configMapName), packet));
+ }
+ return doForkJoin(getNext(), packet, startDetails);
+ }
+ }
+
+ @Nonnull
+ protected List getIntrospectorConfigMapNames(V1ConfigMapList list) {
+ return list.getItems().stream()
+ .map(this::getName)
+ .filter(this::isIntrospectorConfigMapName)
+ .collect(Collectors.toList());
+ }
+
+ private boolean isIntrospectorConfigMapName(String name) {
+ return name.startsWith(IntrospectorConfigMapConstants.getIntrospectorConfigMapNamePrefix(domainUid));
+ }
+
+ @Nonnull
+ private String getName(V1ConfigMap configMap) {
+ return Optional.ofNullable(configMap.getMetadata())
+ .map(V1ObjectMeta::getName).orElse("");
+ }
+ }
+
+ private static class DeleteIntrospectorConfigMapStep extends Step {
+ private final String domainUid;
+ private final String namespace;
+ private final String configMapName;
+
+
+ DeleteIntrospectorConfigMapStep(String domainUid, String namespace, String configMapName) {
+ this.domainUid = domainUid;
+ this.namespace = namespace;
+ this.configMapName = configMapName;
+ }
+
@Override
public NextAction apply(Packet packet) {
return doNext(deleteIntrospectorConfigMap(getNext()), packet);
@@ -682,9 +801,8 @@ protected void logConfigMapDeleted() {
private Step deleteIntrospectorConfigMap(Step next) {
logConfigMapDeleted();
- String configMapName = getIntrospectorConfigMapName(this.domainUid);
return new CallBuilder()
- .deleteConfigMapAsync(configMapName, namespace, this.domainUid,
+ .deleteConfigMapAsync(configMapName, namespace, domainUid,
new V1DeleteOptions(), new DefaultResponseStep<>(next));
}
}
@@ -718,6 +836,7 @@ public NextAction onSuccess(Packet packet, CallResponse callRespons
copyMapEntryToPacket(result, packet, DOMAINZIP_HASH);
copyMapEntryToPacket(result, packet, DOMAIN_RESTART_VERSION);
copyMapEntryToPacket(result, packet, DOMAIN_INPUTS_HASH);
+ copyMapEntryToPacket(result, packet, NUM_CONFIG_MAPS);
DomainTopology domainTopology =
Optional.ofNullable(result)
@@ -735,7 +854,7 @@ public NextAction onSuccess(Packet packet, CallResponse callRespons
}
private String getTopologyYaml(Map data) {
- return data.get(IntrospectorConfigMapKeys.TOPOLOGY_YAML);
+ return data.get(IntrospectorConfigMapConstants.TOPOLOGY_YAML);
}
private void recordTopology(Packet packet, DomainPresenceInfo info, DomainTopology domainTopology) {
diff --git a/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapSplitter.java b/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapSplitter.java
new file mode 100644
index 00000000000..ef12ee1848c
--- /dev/null
+++ b/operator/src/main/java/oracle/kubernetes/operator/helpers/ConfigMapSplitter.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
+// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+
+package oracle.kubernetes.operator.helpers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+
+/**
+ * A Kubernetes ConfigMap has a hard size limit; attempts to create one larger will fail. This is a
+ * problem when we need to store more data in a config map. Our solution is to split the data among multiple maps.
+ *
+ * @param the kind of target object to create, which will ultimately be used to create config maps
+ */
+public class ConfigMapSplitter {
+
+ // The limit for a Kubernetes Config Map is 1MB, including all components of the map. We use a data limit a bit
+ // below that to ensure that the map structures, including the keys, metadata and the results of JSON encoding, don't
+ // accidentally put us over the limit.
+ @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) // not private or local so that unit tests can set it.
+ private static int DATA_LIMIT = 900_000;
+
+ private final BiFunction