Skip to content

Issue #12990 - Introduce static-deploy module #12998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Apr 30, 2025
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d72b287
Issue #12990 - Restore static directory deployment for core-deploy
joakime Apr 11, 2025
a598469
Issue #12990 - Introduce static-deploy module
joakime Apr 15, 2025
c5cb76d
Fixing <dependency> reference
joakime Apr 15, 2025
81c1f51
Move ENVIRONMENT_COMPARATOR to DeploymentScanner and add test cases.
joakime Apr 16, 2025
3c7156b
Make ContextHandler implement Deployable
joakime Apr 16, 2025
cd2a996
Constructor StaticContextHandler with ResourceHandler
joakime Apr 16, 2025
6e9adbb
Merge remote-tracking branch 'origin/jetty-12.1.x' into fix/12.1.x/co…
joakime Apr 16, 2025
fa2e720
The CoreContextHandler cares about default context-path, but not in t…
joakime Apr 16, 2025
17ea7ed
Moving default-context-path from WebAppContext to ContextHandler so t…
joakime Apr 17, 2025
7351898
Using default contextPath from CoreContextHandler and StaticContextHa…
joakime Apr 17, 2025
518ea13
fixing ee11 DeploymentDefaultContextPathTest by ensuring ee11 exists …
joakime Apr 17, 2025
499f5b0
Handle static directory test properly
joakime Apr 17, 2025
5b80a94
Changes from review
joakime Apr 18, 2025
60065cc
More places to use new IO.asFile(Object)
joakime Apr 18, 2025
eb2c5dd
Fix naming initializeDefaultsComplete()
joakime Apr 18, 2025
0ee1529
Don't use Resource for IO.asFile
joakime Apr 18, 2025
3d5c525
Fixing isResourceHandlerAlreadyPresent returns
joakime Apr 18, 2025
0329896
Removing redundant constructors
joakime Apr 18, 2025
1f49310
Revert "Removing redundant constructors"
joakime Apr 18, 2025
cadf0a5
Changes from review
joakime Apr 22, 2025
8149809
Log warning instead of throwing
joakime Apr 23, 2025
532b422
Changes from review
joakime Apr 23, 2025
db8f04d
More work on StaticContextHandler testing
joakime Apr 23, 2025
f0a2f3b
Environment default based only on configured Environments for deploy
joakime Apr 23, 2025
b159daf
Merge remote-tracking branch 'origin/jetty-12.1.x' into fix/12.1.x/co…
joakime Apr 24, 2025
99174e4
Fixing 1 line description in new deploy modules
joakime Apr 24, 2025
660ae3f
Cleaning up javadoc for new handlers
joakime Apr 24, 2025
aaab35f
Rename attributes.
joakime Apr 28, 2025
860415b
Adding check of declared app environment to configured deployed envir…
joakime Apr 28, 2025
e3c7ce4
Merged branch 'jetty-12.1.x' into 'fix/12.1.x/coredeploy-static-direc…
sbordet Apr 29, 2025
65aaca2
Adding testcase for baseResource in property for static deploy
joakime Apr 29, 2025
40f0996
Merge remote-tracking branch 'origin/fix/12.1.x/coredeploy-static-dir…
joakime Apr 29, 2025
6ca1507
Adding two /environments/ examples as test cases
joakime Apr 29, 2025
32c5507
Fix testcase environment behavior
joakime Apr 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -31,8 +31,11 @@
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@@ -128,12 +131,40 @@ public class DeploymentScanner extends ContainerLifeCycle implements Scanner.Bul
// old attributes prefix, now stripped.
private static final String ATTRIBUTE_PREFIX = "jetty.deploy.attribute.";

private static final Pattern EE_ENVIRONMENT_NAME_PATTERN = Pattern.compile("ee(\\d+)");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we use "ee", but just in case, perhaps this should be

Suggested change
private static final Pattern EE_ENVIRONMENT_NAME_PATTERN = Pattern.compile("ee(\\d+)");
private static final Pattern EE_ENVIRONMENT_NAME_PATTERN = Pattern.compile("ee(\\d+)", Pattern.CASE_INSENSITIVE);


/**
* A comparator that ranks names matching EE_ENVIRONMENT_NAME_PATTERN higher than other names,
* EE names are compared by EE number, otherwise simple name comparison is used.
*/
protected static final Comparator<String> ENVIRONMENT_COMPARATOR = (e1, e2) ->
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparator will generate a lot of matchers, so perhaps take the returns where you can:

Suggested change
{
{
if (Objects.equals(e1, e2))
return 0;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better yet, why not just create an Index that is case insensitive and maps all the known environment names to an integer.

    Index<Integer> environmentOrdinals = new Index.Builder<Integer>()
        .caseSensitive(false)
        .with("static", 1)
        .with("core", 2)
        .with("ee9", 9)
        .with("ee10", 10)
        .with("ee11", 11)
        .build();

Copy link
Contributor Author

@joakime joakime Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a conversation with @sbordet and @lorban this will be fixed in a followup PR.

I have an approach that will push the weight of the tracked environments (the ones allowed to be deployed to) via a change to the DeploymentScanner.configureEnvironment(String name, int weight), putting the weights into the XMLs instead.

That way the weights are not hardcoded, and can be adjusted by any user, and even add new environments (with their own weights) without the need to modify code.

Matcher m1 = EE_ENVIRONMENT_NAME_PATTERN.matcher(e1);
Matcher m2 = EE_ENVIRONMENT_NAME_PATTERN.matcher(e2);

if (m1.matches())
{
if (m2.matches())
{
int n1 = Integer.parseInt(m1.group(1));
int n2 = Integer.parseInt(m2.group(1));
return Integer.compare(n2, n1);
}
return -1;
}
if (m2.matches())
return 1;

return e1.compareTo(e2);
};

private final Server server;
private final FilenameFilter filenameFilter;
private final List<Path> monitoredDirs = new CopyOnWriteArrayList<>();
private final ContextHandlerFactory contextHandlerFactory;
private final Map<String, PathsApp> trackedApps = new HashMap<>();
private final Map<String, Attributes> environmentAttributesMap = new HashMap<>();
private Set<String> trackedEnvironments = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

private Deployer deployer;
private Comparator<DeployAction> actionComparator = new DeployActionComparator();
@@ -293,7 +324,13 @@ void addScannerListener(Scanner.Listener listener)
*/
public EnvironmentConfig configureEnvironment(String name)
{
return new EnvironmentConfig(Environment.get(name));
Environment environment = Environment.get(name);
// Check to make sure that the Environment was created before jetty-deploy is involved.
// This is to ensure that the Environment ClassLoader is setup properly.
if (environment == null)
throw new IllegalStateException("Environment [" + name + "] does not exist.");
trackedEnvironments.add(name);
return new EnvironmentConfig(environment);
}

/**
@@ -314,8 +351,7 @@ public void setActionComparator(Comparator<DeployAction> actionComparator)
* do not declare the {@link Environment} that they belong to.
*
* <p>
* Falls back to {@link Environment#getAll()} list, and returns
* the first name returned after sorting with {@link Deployable#ENVIRONMENT_COMPARATOR}
* Only uses environments that have been previously configured with {@link #configureEnvironment(String)}
* </p>
*
* @return the default environment name.
@@ -324,9 +360,8 @@ public String getDefaultEnvironmentName()
{
if (defaultEnvironmentName == null)
{
return Environment.getAll().stream()
.map(Environment::getName)
.max(Deployable.ENVIRONMENT_COMPARATOR)
return trackedEnvironments.stream()
.min(ENVIRONMENT_COMPARATOR)
.orElse(null);
}
return defaultEnvironmentName;
@@ -813,21 +848,26 @@ protected void performActions(List<DeployAction> actions)
app.loadProperties();

// Ensure Environment name is set
String appEnvironment = app.getEnvironmentName();
if (StringUtil.isBlank(appEnvironment))
appEnvironment = getDefaultEnvironmentName();
app.setEnvironment(Environment.get(appEnvironment));
String envName = app.getEnvironmentName();
if (StringUtil.isBlank(envName))
envName = getDefaultEnvironmentName();
Environment env = Environment.get(envName);

if (env == null || !trackedEnvironments.contains(envName))
throw new IllegalArgumentException("Unable to deploy %s to environment %s. Available Environments to deploy to %s"
.formatted(app.name, envName, trackedEnvironments.stream().sorted(ENVIRONMENT_COMPARATOR)
.collect(Collectors.joining(", ", "[", "]"))));

// Create a new Attributes layer for the app deployment, which is the
// combination of layered Environment Attributes with app Attributes overlaying them.
Attributes envAttributes = environmentAttributesMap.get(appEnvironment);
Attributes envAttributes = environmentAttributesMap.get(envName);
Attributes deployAttributes = envAttributes == null ? app.getAttributes() : new Attributes.Layer(envAttributes, app.getAttributes());

// Create the Context Handler
Path mainPath = app.getMainPath();
if (mainPath == null)
throw new IllegalStateException("Unable to create ContextHandler for app with no main path defined: " + app);
ContextHandler contextHandler = contextHandlerFactory.newContextHandler(server, app.getEnvironment(), mainPath, app.getPaths().keySet(), deployAttributes);
ContextHandler contextHandler = contextHandlerFactory.newContextHandler(server, env, mainPath, app.getPaths().keySet(), deployAttributes);
app.setContextHandler(contextHandler);

// Introduce the ContextHandler to the Deployer
@@ -844,21 +884,26 @@ protected void performActions(List<DeployAction> actions)
app.loadProperties();

// Ensure Environment name is set
String appEnvironment = app.getEnvironmentName();
if (StringUtil.isBlank(appEnvironment))
appEnvironment = getDefaultEnvironmentName();
app.setEnvironment(Environment.get(appEnvironment));
String envName = app.getEnvironmentName();
if (StringUtil.isBlank(envName))
envName = getDefaultEnvironmentName();
Environment env = Environment.get(envName);

if (env == null || !trackedEnvironments.contains(envName))
throw new IllegalArgumentException("Unable to deploy app [%s] to environment [%s]. Available Environments to deploy to %s"
.formatted(app.name, envName, trackedEnvironments.stream().sorted(ENVIRONMENT_COMPARATOR)
.collect(Collectors.joining(", ", "[", "]"))));

// Create a new Attributes layer for the app deployment, which is the
// combination of layered Environment Attributes with app Attributes overlaying them.
Attributes envAttributes = environmentAttributesMap.get(appEnvironment);
Attributes envAttributes = environmentAttributesMap.get(envName);
Attributes deployAttributes = envAttributes == null ? app.getAttributes() : new Attributes.Layer(envAttributes, app.getAttributes());

// Create the Context Handler
Path mainPath = app.getMainPath();
if (mainPath == null)
throw new IllegalStateException("Unable to create ContextHandler for app with no main path defined: " + app);
ContextHandler contextHandler = contextHandlerFactory.newContextHandler(server, app.getEnvironment(), mainPath, app.getPaths().keySet(), deployAttributes);
ContextHandler contextHandler = contextHandlerFactory.newContextHandler(server, env, mainPath, app.getPaths().keySet(), deployAttributes);
app.setContextHandler(contextHandler);

// Introduce the ContextHandler to the Deployer
@@ -1341,23 +1386,14 @@ public void setContextHandler(ContextHandler contextHandler)
this.contextHandler = contextHandler;
}

public Environment getEnvironment()
{
return (Environment)getAttributes().getAttribute(ContextHandlerFactory.ENVIRONMENT_ATTRIBUTE);
}

public void setEnvironment(Environment env)
{
getAttributes().setAttribute(ContextHandlerFactory.ENVIRONMENT_ATTRIBUTE, env);
}

public String getEnvironmentName()
{
Environment env = getEnvironment();
if (env == null)
return "";
else
Object obj = this.attributes.getAttribute(ContextHandlerFactory.ENVIRONMENT_ATTRIBUTE);
if (obj instanceof String str)
return str;
if (obj instanceof Environment env)
return env.getName();
return null;
}

/**
@@ -1389,6 +1425,19 @@ public void setMainPath(Path mainPath)
this.mainPath = mainPath;
}

private Path filterPath(List<Path> paths, String type, Predicate<Path> predicate)
{
List<Path> hits = paths.stream()
.filter(predicate)
.toList();
if (hits.size() == 1)
return hits.get(0);
else if (hits.size() > 1)
throw new IllegalStateException("More than 1 " + type + " for deployable " + asStringList(hits));

return null;
}

private Path calcMainPath()
{
List<Path> livePaths = paths
@@ -1403,37 +1452,52 @@ private Path calcMainPath()
return null;

// XML always win.
List<Path> xmls = livePaths.stream()
.filter(FileID::isXml)
.toList();
if (xmls.size() == 1)
return xmls.get(0);
else if (xmls.size() > 1)
throw new IllegalStateException("More than 1 XML for deployable " + asStringList(xmls));
Path xml = filterPath(livePaths, "XML", FileID::isXml);
if (xml != null)
return xml;

// WAR files are next.
List<Path> wars = livePaths.stream()
.filter(FileID::isWebArchive)
.toList();
if (wars.size() == 1)
return wars.get(0);
else if (wars.size() > 1)
throw new IllegalStateException("More than 1 WAR for deployable " + asStringList(wars));
Path war = filterPath(livePaths, "WAR", FileID::isWebArchive);
if (war != null)
return war;

// JAR files are next.
Path jar = filterPath(livePaths, "JAR", FileID::isJavaArchive);
if (jar != null)
return jar;

// ZIP files are next.
Path zip = filterPath(livePaths, "ZIP", (p -> FileID.isExtension(p, "zip")));
if (zip != null)
return zip;

// Directories next.
List<Path> dirs = livePaths.stream()
.filter(Files::isDirectory)
.toList();
if (dirs.size() == 1)
return dirs.get(0);
if (dirs.size() > 1)
throw new IllegalStateException("More than 1 Directory for deployable " + asStringList(dirs));
Path dir = filterPath(livePaths, "Directory", PathsApp::isDeployableDirectory);
if (dir != null)
return dir;

// Finally properties files
Path propertyFile = filterPath(livePaths, "Property File", (p -> FileID.isExtension(p, "properties")));
if (propertyFile != null)
return propertyFile;

LOG.warn("Unable to determine main deployable for {}", this);
if (LOG.isDebugEnabled())
LOG.debug("Unable to determine main deployable for {}", this);

return null;
}

private static boolean isDeployableDirectory(Path p)
{
if (p == null)
return false;
if (Files.isDirectory(p))
{
return !FileID.isExtension(p, "d"); // ignore nominated dirs
}
return false;
}

public String getName()
{
return name;
@@ -1508,21 +1572,23 @@ public void loadProperties()
}
}

// Look for environment attribute.
// Verify that environment exists
Object envObj = getAttributes().getAttribute(ContextHandlerFactory.ENVIRONMENT_ATTRIBUTE);
if (envObj instanceof Environment environment)
if (envObj != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Using defaulted environment of {} to {}", name, environment);
}
else if (envObj instanceof String environmentName)
{
if (StringUtil.isNotBlank(environmentName))
if (envObj instanceof String environmentName)
{
if (StringUtil.isNotBlank(environmentName))
{
Environment env = Environment.get(environmentName);
if (env == null)
LOG.warn("Environment not found {}", environmentName);
}
}
else
{
Environment env = Environment.get(environmentName);
if (LOG.isDebugEnabled())
LOG.debug("Setting environment of {} to {}", name, env);
setEnvironment(env);
LOG.debug("Unable to use attribute {} as type {}", ContextHandlerFactory.ENVIRONMENT_ATTRIBUTE, envObj.getClass().getName());
}
}
}
Original file line number Diff line number Diff line change
@@ -32,7 +32,9 @@
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.Environment;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.resource.Resources;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -217,11 +219,15 @@ protected void initializeContextHandler(ContextHandler contextHandler, Path path

if (contextHandler.getBaseResource() == null)
{
if (Files.isDirectory(path))
{
ResourceFactory resourceFactory = ResourceFactory.of(contextHandler);
contextHandler.setBaseResource(resourceFactory.newResource(path));
}
ResourceFactory resourceFactory = ResourceFactory.of(contextHandler);
Resource resource = resourceFactory.newResource(path);

// Only set base resource on safe directory paths.
// Deployables that want to do other things, can use the Deployable.BASE_RESOURCE to figure things out.
if (Resources.isDirectory(resource))
contextHandler.setBaseResource(resource);
else if (attributes.getAttribute(Deployable.BASE_RESOURCE) == null)
attributes.setAttribute(Deployable.BASE_RESOURCE, resource);
}

// copy attributes into context
Loading