Skip to content

Commit 581f5c8

Browse files
committed
Fix #34940: automatically generate password for Redis
1 parent 1071011 commit 581f5c8

7 files changed

Lines changed: 63 additions & 26 deletions

File tree

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<fabric8.client.version>7.1.0</fabric8.client.version>
3535
<docker.client.version>7.0.8-OA-3</docker.client.version>
3636
<jersey.version>3.1.10</jersey.version>
37+
<apache.commons.lang3>3.17.0</apache.commons.lang3>
3738
<!-- Plugin versions -->
3839
<maven.assembly.plugin.version>3.7.1</maven.assembly.plugin.version>
3940
<maven.compiler.plugin.version>3.14.0</maven.compiler.plugin.version>
@@ -150,6 +151,11 @@
150151
<artifactId>jackson-module-kotlin</artifactId>
151152
<version>${jackson.version}</version>
152153
</dependency>
154+
<dependency>
155+
<groupId>org.apache.commons</groupId>
156+
<artifactId>commons-lang3</artifactId>
157+
<version>${apache.commons.lang3}</version>
158+
</dependency>
153159

154160
<!-- Dependencies for tests -->
155161
<dependency>

src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/DockerOrchestrator.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class DockerOrchestrator(channel: Channel<ShinyProxyEvent>,
9898
caddyConfig = CaddyConfig(dockerClient, dataDir, config)
9999
dockerActions = DockerActions(dockerClient)
100100
shinyProxyReadyChecker = ShinyProxyReadyChecker(channel, dockerActions, dockerClient, dataDir)
101-
redisConfig = RedisConfig(dockerClient, dockerActions, dataDir, config)
101+
redisConfig = RedisConfig(dockerClient, dockerActions, persistentState, dataDir, config)
102102
craneConfig = CraneConfig(dockerClient, dockerActions, dataDir, inputDir, redisConfig, caddyConfig, persistentState)
103103
monitoringConfig = MonitoringConfig(dockerClient, dockerActions, dataDir, caddyConfig, config)
104104
logFilesCleaner = LogFilesCleaner(dataDir.resolve("logs"), fileManager, dockerActions)
@@ -390,6 +390,7 @@ class DockerOrchestrator(channel: Channel<ShinyProxyEvent>,
390390

391391
override suspend fun init(source: IShinyProxySource) {
392392
logger.info { "Initializing DockerOrchestrator" }
393+
redisConfig.init()
393394
val containers = dockerClient.listContainers(
394395
DockerClient.ListContainersParam.withStatusRunning(),
395396
DockerClient.ListContainersParam.withLabel(LabelFactory.APP_LABEL, LabelFactory.APP_LABEL_VALUE)

src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/PersistentState.kt

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,46 +40,56 @@ class PersistentState(dataDir: Path) {
4040
fun readState(): State {
4141
try {
4242
if (!file.exists()) {
43-
return State(mapOf())
43+
return State(mapOf(), null)
4444
}
4545
return objectMapper.readValue(file)
4646
} catch (e: Exception) {
4747
logger.warn(e) { "Could not read store state" }
48-
return State(mapOf())
48+
return State(mapOf(), null)
4949
}
5050
}
5151

5252
fun storeLatest(realmId: String, instance: String) {
53-
try {
54-
val newMap = readState().realms.toMutableMap()
53+
updateState { state ->
54+
val newMap = state.realms.toMutableMap()
5555
if (newMap.containsKey(realmId)) {
5656
newMap[realmId] = newMap.getValue(realmId).copy(latestInstance = instance)
5757
} else {
5858
newMap[realmId] = RealmState(instance)
5959
}
60-
val newState = State(newMap)
61-
objectMapper.writeValue(file, newState)
62-
} catch (e: Exception) {
63-
logger.warn(e) { "Could not store state" }
60+
state.copy(realms = newMap)
6461
}
6562
}
6663

6764
fun storeLatestCrane(realmId: String, instance: String) {
68-
try {
69-
val newMap = readState().realms.toMutableMap()
65+
updateState { state ->
66+
val newMap = state.realms.toMutableMap()
7067
if (newMap.containsKey(realmId)) {
7168
newMap[realmId] = newMap.getValue(realmId).copy(craneLatestInstance = instance)
7269
} else {
7370
newMap[realmId] = RealmState(null, instance)
7471
}
75-
val newState = State(newMap)
72+
state.copy(realms = newMap)
73+
}
74+
}
75+
76+
fun storeRedisPassword(password: String) {
77+
updateState { state ->
78+
state.copy(redisPassword = password)
79+
}
80+
}
81+
82+
private fun updateState(block: (State) -> State) {
83+
try {
84+
val state = readState()
85+
val newState = block(state)
7686
objectMapper.writeValue(file, newState)
7787
} catch (e: Exception) {
7888
logger.warn(e) { "Could not store state" }
7989
}
8090
}
8191

82-
data class State(val realms: Map<String, RealmState>)
92+
data class State(val realms: Map<String, RealmState> = mapOf(), val redisPassword: String? = null)
8393

8494
data class RealmState(val latestInstance: String?, val craneLatestInstance: String? = null)
8595

src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/RedisConfig.kt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,41 @@ import eu.openanalytics.shinyproxyoperator.FileManager
2525
import io.github.oshai.kotlinlogging.KotlinLogging
2626
import kotlinx.coroutines.Dispatchers
2727
import kotlinx.coroutines.withContext
28+
import org.apache.commons.lang3.RandomStringUtils
2829
import org.mandas.docker.client.DockerClient
2930
import org.mandas.docker.client.messages.ContainerConfig
3031
import org.mandas.docker.client.messages.HostConfig
3132
import java.nio.file.Files
3233
import java.nio.file.Path
3334

34-
class RedisConfig(private val dockerClient: DockerClient, private val dockerActions: DockerActions, mainDataDir: Path, config: Config) {
35+
class RedisConfig(private val dockerClient: DockerClient,
36+
private val dockerActions: DockerActions,
37+
private val persistentState: PersistentState,
38+
mainDataDir: Path,
39+
config: Config) {
3540

3641
private val containerName = "sp-redis"
3742
private val dataDir: Path = mainDataDir.resolve(containerName)
38-
private val redisPassword: String
43+
private lateinit var redisPassword: String
3944
private val logger = KotlinLogging.logger {}
4045
private val redisImage: String = config.readConfigValue("redis:7.2.4", "SPO_REDIS_IMAGE") { it }
4146
private val fileManager = FileManager()
4247

43-
init {
44-
redisPassword = readPasswordFile("/run/secrets/redis_password")
45-
?: readPasswordFile("redis_password.txt")
46-
?: config.readConfigValue("", "SPO_REDIS_PASSWORD") { it }
47-
if (redisPassword == "") {
48-
error("Invalid redis password")
48+
fun init() {
49+
val redisPasswordInState = persistentState.readState().redisPassword
50+
if (redisPasswordInState == null) {
51+
val redisPasswordInConfig = readPasswordFile("/run/secrets/redis_password")
52+
?: readPasswordFile("redis_password.txt")
53+
redisPassword = if (redisPasswordInConfig != null) {
54+
redisPasswordInConfig
55+
} else {
56+
logger.info { "Generating password for Redis" }
57+
RandomStringUtils.secureStrong().nextAlphanumeric(32)
58+
}
59+
persistentState.storeRedisPassword(redisPassword)
60+
} else {
61+
logger.info { "Re-using password from Redis state" }
62+
redisPassword = redisPasswordInState
4963
}
5064
}
5165

src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/MainIntegrationTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import org.junit.jupiter.params.ParameterizedTest
3333
import org.junit.jupiter.params.provider.ValueSource
3434
import kotlin.io.path.deleteIfExists
3535
import kotlin.io.path.readText
36+
import kotlin.io.path.writeText
3637
import kotlin.test.assertEquals
3738
import kotlin.test.assertNotNull
3839

@@ -248,7 +249,7 @@ class MainIntegrationTest : IntegrationTestBase() {
248249

249250
// 6. wait for instance to startup (startup will fail)
250251
val error = eventController.waitForInputError()
251-
assertEquals("Failed to read file 'realm1.shinyproxy.yaml', error: No or invalid realm-id", error)
252+
assertEquals("Failed to read file 'realm1.shinyproxy.yaml', error: 'No or invalid realm-id'", error)
252253

253254
// 7. check that caddy still points to original instance
254255
dockerAssertions.assertCaddyContainer("simple_test_caddy.json", mapOf("#CONTAINER_IP#" to shinyProxyContainerA.getSharedNetworkIpAddress()!!))
@@ -409,6 +410,7 @@ class MainIntegrationTest : IntegrationTestBase() {
409410
fun `restart operator during (failing) update without state file`() = setup { dataDir, inputDir, operator, eventController, dockerAssertions, recyclableChecker, config ->
410411
// idea of test: launch instance A, update config to get instance B, however, instance B fails to start up, in the meantime restart the operator and then update config again to A
411412
// the operator will start a new instance, with an increased revision
413+
dataDir.resolve("state.yaml").writeText("redisPassword: 75ZEykd5RYsZS7v9H7PYCaFcrscJHWa4\n")
412414
// 1. create a SP instance
413415
val hashA = createInputFile(inputDir, "simple_config.yaml", "realm1.shinyproxy.yaml")
414416
val shinyProxyInstanceA = ShinyProxyInstance("realm1", "default", "default-realm1", hashA, true, 0)
@@ -439,6 +441,7 @@ class MainIntegrationTest : IntegrationTestBase() {
439441
operator.stop()
440442

441443
dataDir.resolve("state.yaml").deleteIfExists()
444+
dataDir.resolve("state.yaml").writeText("redisPassword: 75ZEykd5RYsZS7v9H7PYCaFcrscJHWa4\n")
442445
delay(10_000)
443446

444447
// 7. restart operator
@@ -627,6 +630,7 @@ class MainIntegrationTest : IntegrationTestBase() {
627630
assertEquals(listOf(
628631
"${dataDir}${craneContainerInfo.name()}/application.yml:/opt/crane/application.yml:ro",
629632
"${dataDir}${craneContainerInfo.name()}/generated.yml:/opt/crane/generated.yml:ro",
633+
"${dataDir}/logs${craneContainerInfo.name()}:/opt/crane/logs",
630634
"/tmp/crane-mount:/mnt",
631635
), craneContainerInfo.hostConfig().binds())
632636
dockerAssertions.assertCaddyContainer("simple_config_crane_caddy.yaml", mapOf(

src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/DockerAssertions.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ class DockerAssertions(private val base: IntegrationTestBase,
5555
assertEquals("redis", redisContainer.config().labels()["app"])
5656
assertEquals("1000", redisContainer.config().user())
5757

58-
val redisConfig = dataDir.resolve("sp-redis").resolve("redis.conf").readText()
59-
assertEquals(readExpectedFile("redis.conf"), redisConfig)
58+
// remove generated password from file
59+
val redisConfig = dataDir.resolve("sp-redis").resolve("redis.conf").readText().dropLast(33)
60+
val expectedRedisconfig = readExpectedFile("redis.conf").dropLast(20)
61+
assertEquals(expectedRedisconfig, redisConfig)
6062
}
6163

6264
fun assertCaddyContainer(expectedName: String, replacements: Map<String, String>, tls: Boolean = false) {
@@ -116,13 +118,14 @@ class DockerAssertions(private val base: IntegrationTestBase,
116118
fun assertShinyProxyContainer(shinyProxyContainer: Container, shinyProxyInstance: ShinyProxyInstance) {
117119
val containerInfo = base.dockerClient.inspectContainer(shinyProxyContainer.id())
118120
assertEquals(true, containerInfo.state().running())
119-
assertEquals("sp-network-${shinyProxyInstance.realmId}", containerInfo.hostConfig().networkMode())
121+
assertEquals("sp-shared-network", containerInfo.hostConfig().networkMode())
120122
assertEquals(listOf("sp-network-${shinyProxyInstance.realmId}", "sp-shared-network"), containerInfo.networkSettings().networks().keys.toList())
121123
assertEquals(listOf(
122124
"/var/run/docker.sock:/var/run/docker.sock:ro",
123125
"${dataDir}${containerInfo.name()}/application.yml:/opt/shinyproxy/application.yml:ro",
124126
"${dataDir}${containerInfo.name()}/generated.yml:/opt/shinyproxy/generated.yml:ro",
125127
"${dataDir}${containerInfo.name()}/templates:/opt/shinyproxy/templates:ro",
128+
"${dataDir}/logs${containerInfo.name()}:/opt/shinyproxy/logs",
126129
"${dataDir}${containerInfo.name()}/termination-log:/dev/termination-log",
127130
), containerInfo.hostConfig().binds())
128131
assertEquals(listOf(dockerGID), containerInfo.hostConfig().groupAdd())

src/test/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/helpers/IntegrationTestBase.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ abstract class IntegrationTestBase {
9898
val mockConfig = MockConfig(config + mapOf(
9999
"SPO_DOCKER_DATA_DIR" to dataDir.toString(),
100100
"SPO_INPUT_DIR" to inputDir.toString(),
101-
"SPO_REDIS_PASSWORD" to "MOCK_REDIS_PASSWORD",
102101
"SPO_FILE_POLL_INTERVAL" to "10"
103102
))
104103
val eventController = AwaitableEvenController()

0 commit comments

Comments
 (0)