Skip to content

Commit 6aeedd0

Browse files
authored
Add a set-properties sub-command (#306)
1 parent 3994d55 commit 6aeedd0

File tree

8 files changed

+617
-1
lines changed

8 files changed

+617
-1
lines changed

.github/dependabot.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
version: 2
22
updates:
3-
# Maintain dependencies for GitHub Actions
43
- package-ecosystem: "github-actions"
54
directory: "/"
65
schedule:
@@ -9,3 +8,9 @@ updates:
98
directory: "/"
109
schedule:
1110
interval: "weekly"
11+
groups:
12+
patches:
13+
patterns:
14+
- "*"
15+
update-types:
16+
- "patch"

src/main/java/me/itzg/helpers/McImageHelper.java

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import me.itzg.helpers.mvn.MavenDownloadCommand;
2626
import me.itzg.helpers.paper.InstallPaperCommand;
2727
import me.itzg.helpers.patch.PatchCommand;
28+
import me.itzg.helpers.properties.SetPropertiesCommand;
2829
import me.itzg.helpers.purpur.InstallPurpurCommand;
2930
import me.itzg.helpers.quilt.InstallQuiltCommand;
3031
import me.itzg.helpers.singles.Asciify;
@@ -74,6 +75,7 @@
7475
NetworkInterfacesCommand.class,
7576
PatchCommand.class,
7677
ResolveMinecraftVersionCommand.class,
78+
SetPropertiesCommand.class,
7779
Sync.class,
7880
SyncAndInterpolate.class,
7981
YamlPathCmd.class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package me.itzg.helpers.properties;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import lombok.Data;
6+
7+
@Data
8+
public class PropertyDefinition {
9+
String env;
10+
List<String> allowed;
11+
Map<String,String> mappings;
12+
boolean remove;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package me.itzg.helpers.properties;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import java.io.InputStream;
5+
import java.io.OutputStream;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.nio.file.StandardOpenOption;
9+
import java.time.Instant;
10+
import java.util.Map;
11+
import java.util.Objects;
12+
import java.util.Properties;
13+
import java.util.concurrent.Callable;
14+
import lombok.Setter;
15+
import lombok.extern.slf4j.Slf4j;
16+
import me.itzg.helpers.env.EnvironmentVariablesProvider;
17+
import me.itzg.helpers.env.StandardEnvironmentVariablesProvider;
18+
import me.itzg.helpers.errors.InvalidParameterException;
19+
import me.itzg.helpers.json.ObjectMappers;
20+
import picocli.CommandLine.Command;
21+
import picocli.CommandLine.ExitCode;
22+
import picocli.CommandLine.Option;
23+
import picocli.CommandLine.Parameters;
24+
25+
@Command(name = "set-properties", description = "Maps environment variables to a properties file")
26+
@Slf4j
27+
public class SetPropertiesCommand implements Callable<Integer> {
28+
29+
public static final TypeReference<Map<String, PropertyDefinition>> PROPERTY_DEFINITIONS_TYPE = new TypeReference<Map<String, PropertyDefinition>>() {
30+
};
31+
32+
@Option(names = "--definitions", required = true, description = "JSON file of property names to PropertyDefinition mappings")
33+
Path propertyDefinitions;
34+
35+
@Parameters(arity = "1")
36+
Path propertiesFile;
37+
38+
@Setter
39+
private EnvironmentVariablesProvider environmentVariablesProvider = new StandardEnvironmentVariablesProvider();
40+
41+
@Override
42+
public Integer call() throws Exception {
43+
44+
if (!Files.exists(propertyDefinitions)) {
45+
throw new InvalidParameterException("Property definitions file does not exist");
46+
}
47+
48+
final Map<String, PropertyDefinition> propertyDefinitions = ObjectMappers.defaultMapper()
49+
.readValue(this.propertyDefinitions.toFile(), PROPERTY_DEFINITIONS_TYPE);
50+
51+
final Properties properties = new Properties();
52+
if (Files.exists(propertiesFile)) {
53+
try (InputStream propsIn = Files.newInputStream(propertiesFile)) {
54+
properties.load(propsIn);
55+
}
56+
}
57+
58+
final long changes = processProperties(propertyDefinitions, properties);
59+
if (changes > 0) {
60+
log.info("Created/updated {} propert{} in {}", changes, changes != 1 ? "ies":"y", propertiesFile);
61+
62+
try (OutputStream propsOut = Files.newOutputStream(propertiesFile, StandardOpenOption.TRUNCATE_EXISTING)) {
63+
properties.store(propsOut, String.format("Updated %s by mc-image-helper", Instant.now()));
64+
}
65+
}
66+
67+
return ExitCode.OK;
68+
}
69+
70+
/**
71+
* @return count of added/modified properties
72+
*/
73+
private long processProperties(Map<String, PropertyDefinition> propertyDefinitions, Properties properties) {
74+
return propertyDefinitions.entrySet().stream()
75+
.map(entry -> {
76+
final String name = entry.getKey();
77+
final PropertyDefinition definition = entry.getValue();
78+
79+
if (definition.isRemove()) {
80+
if (properties.containsKey(name)) {
81+
log.debug("Removing {}, which is marked for removal", name);
82+
properties.remove(name);
83+
return true;
84+
}
85+
else {
86+
return false;
87+
}
88+
}
89+
90+
final String envValue = environmentVariablesProvider.get(definition.getEnv());
91+
if (envValue != null) {
92+
final String expectedValue = mapAndValidateValue(definition, envValue);
93+
94+
final String propValue = properties.getProperty(name);
95+
96+
if (!Objects.equals(expectedValue, propValue)) {
97+
log.debug("Setting property {} to new value '{}'", name, expectedValue);
98+
properties.setProperty(name, expectedValue);
99+
return true;
100+
}
101+
}
102+
103+
return false;
104+
})
105+
.filter(modified -> modified)
106+
.count();
107+
}
108+
109+
private String mapAndValidateValue(PropertyDefinition definition, String value) {
110+
if (definition.getMappings() != null) {
111+
value = definition.getMappings().getOrDefault(value, value);
112+
}
113+
if (definition.getAllowed() != null) {
114+
if (!definition.getAllowed().contains(value)) {
115+
throw new InvalidParameterException(
116+
String.format("The environment variable %s does not contain an allowed value '%s'. Allowed: %s",
117+
definition.getEnv(), value, definition.getAllowed()
118+
)
119+
);
120+
}
121+
}
122+
return value;
123+
}
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package me.itzg.helpers.env;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
public class MappedEnvVarProvider implements EnvironmentVariablesProvider {
9+
10+
final Map<String,String> values = new HashMap<>();
11+
12+
private MappedEnvVarProvider() {
13+
14+
}
15+
16+
public static MappedEnvVarProvider of(String... nameValue) {
17+
assertThat(nameValue.length).isEven();
18+
19+
final MappedEnvVarProvider provider = new MappedEnvVarProvider();
20+
for (int i = 0; i < nameValue.length; i += 2) {
21+
provider.values.put(nameValue[i], nameValue[i + 1]);
22+
}
23+
24+
return provider;
25+
}
26+
27+
@Override
28+
public String get(String name) {
29+
return values.get(name);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package me.itzg.helpers.properties;
2+
3+
import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.net.URISyntaxException;
9+
import java.net.URL;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.Paths;
13+
import java.util.Arrays;
14+
import java.util.Collections;
15+
import java.util.HashSet;
16+
import java.util.Properties;
17+
import me.itzg.helpers.env.MappedEnvVarProvider;
18+
import org.apache.commons.lang3.RandomStringUtils;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.io.TempDir;
23+
import picocli.CommandLine;
24+
import picocli.CommandLine.ExitCode;
25+
26+
class SetPropertiesCommandTest {
27+
28+
@TempDir
29+
Path tempDir;
30+
31+
private Path propertiesFile;
32+
private Path definitionsFile;
33+
private Properties originalProperties;
34+
35+
@BeforeEach
36+
void setUp() throws IOException, URISyntaxException {
37+
propertiesFile = preparePropertiesFile();
38+
39+
final URL definitionsResource = getClass().getResource("/properties/property-definitions.json");
40+
assertThat(definitionsResource).isNotNull();
41+
definitionsFile = Paths.get(definitionsResource.toURI());
42+
43+
originalProperties = loadProperties();
44+
}
45+
46+
@Test
47+
void simpleNeedsChange() throws Exception {
48+
final String name = RandomStringUtils.randomAlphabetic(10);
49+
50+
final int exitCode = new CommandLine(new SetPropertiesCommand()
51+
.setEnvironmentVariablesProvider(MappedEnvVarProvider.of(
52+
"SERVER_NAME", name
53+
))
54+
)
55+
.execute(
56+
"--definitions", definitionsFile.toString(),
57+
propertiesFile.toString()
58+
);
59+
60+
assertThat(exitCode).isEqualTo(ExitCode.OK);
61+
62+
final Properties properties = loadProperties();
63+
64+
assertThat(properties).containsEntry("server-name", name);
65+
assertPropertiesEqualExcept(properties, "server-name");
66+
}
67+
68+
@Test
69+
void hasMapping() throws Exception {
70+
71+
final int exitCode = new CommandLine(new SetPropertiesCommand()
72+
.setEnvironmentVariablesProvider(MappedEnvVarProvider.of(
73+
"GAMEMODE", "1"
74+
))
75+
)
76+
.execute(
77+
"--definitions", definitionsFile.toString(),
78+
propertiesFile.toString()
79+
);
80+
81+
assertThat(exitCode).isEqualTo(ExitCode.OK);
82+
83+
final Properties properties = loadProperties();
84+
85+
assertThat(properties).containsEntry("gamemode", "creative");
86+
assertPropertiesEqualExcept(properties, "gamemode");
87+
}
88+
89+
@Test
90+
void disallowedValue() throws Exception {
91+
final String err = tapSystemErr(() -> {
92+
final int exitCode = new CommandLine(new SetPropertiesCommand()
93+
.setEnvironmentVariablesProvider(MappedEnvVarProvider.of(
94+
"ONLINE_MODE", "invalid"
95+
))
96+
)
97+
.execute(
98+
"--definitions", definitionsFile.toString(),
99+
propertiesFile.toString()
100+
);
101+
102+
assertThat(exitCode).isNotEqualTo(ExitCode.OK);
103+
});
104+
105+
assertThat(err)
106+
.contains("ONLINE_MODE")
107+
.contains("InvalidParameterException");
108+
109+
}
110+
111+
@Test
112+
void removesMarkedForRemoval() throws IOException {
113+
final Path hasWhiteList = Files.write(tempDir.resolve("old.properties"), Collections.singletonList("white-list=true"));
114+
final int exitCode = new CommandLine(new SetPropertiesCommand()
115+
)
116+
.execute(
117+
"--definitions", definitionsFile.toString(),
118+
hasWhiteList.toString()
119+
);
120+
121+
assertThat(exitCode).isEqualTo(ExitCode.OK);
122+
123+
final Properties properties = new Properties();
124+
try (InputStream propsIn = Files.newInputStream(hasWhiteList)) {
125+
properties.load(propsIn);
126+
}
127+
128+
assertThat(properties).doesNotContainKey("white-list");
129+
}
130+
131+
private void assertPropertiesEqualExcept(Properties properties, String... propertiesToIgnore) {
132+
final HashSet<Object> actualKeys = new HashSet<>(properties.keySet());
133+
Arrays.asList(propertiesToIgnore).forEach(actualKeys::remove);
134+
final HashSet<Object> originalKeys = new HashSet<>(originalProperties.keySet());
135+
Arrays.asList(propertiesToIgnore).forEach(originalKeys::remove);
136+
137+
assertThat(actualKeys).isEqualTo(originalKeys);
138+
139+
for (final Object key : originalKeys) {
140+
assertThat(properties.get(key)).withFailMessage(() -> String.format("Property %s does not equal", key))
141+
.isEqualTo(originalProperties.get(key));
142+
}
143+
}
144+
145+
@NotNull
146+
private Properties loadProperties() throws IOException {
147+
final Properties properties = new Properties();
148+
try (InputStream propsIn = Files.newInputStream(propertiesFile)) {
149+
properties.load(propsIn);
150+
}
151+
return properties;
152+
}
153+
154+
private Path preparePropertiesFile() throws IOException {
155+
try (InputStream in = getClass().getClassLoader().getResourceAsStream("properties/server.properties")) {
156+
assertThat(in).isNotNull();
157+
final Path outFile = tempDir.resolve("server.properties");
158+
Files.copy(in, outFile);
159+
return outFile;
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)