Skip to content

Commit bcb7ec8

Browse files
authored
feat: Enable Cloudflare cache purging (tolgee#2461)
1 parent cd198e1 commit bcb7ec8

14 files changed

+233
-22
lines changed

backend/app/src/test/kotlin/io/tolgee/automation/AutomationIntegrationTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class AutomationIntegrationTest : ProjectAuthControllerTest("/v2/projects/") {
9393
fileStorageMock = mock()
9494
doReturn(fileStorageMock).whenever(contentDeliveryFileStorageProvider).getContentStorageWithDefaultClient()
9595
purgingMock = mock()
96-
doReturn(purgingMock).whenever(contentDeliveryCachePurgingProvider).defaultPurging
96+
doReturn(listOf(purgingMock)).whenever(contentDeliveryCachePurgingProvider).purgings
9797

9898
// wait for the first invocation happening because of test data saving, then clear invocations
9999
Thread.sleep(1000)

backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/ContentDeliveryUploader.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ class ContentDeliveryUploader(
4747
) {
4848
val isDefaultStorage = contentDeliveryConfig.contentStorage == null
4949
if (isDefaultStorage) {
50-
contentDeliveryCachePurgingProvider.defaultPurging?.purgeForPaths(contentDeliveryConfig, paths)
50+
contentDeliveryCachePurgingProvider.purgings.forEach {
51+
it.purgeForPaths(contentDeliveryConfig, paths)
52+
}
5153
}
5254
}
5355

backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/ContentDeliveryCachePurgingProvider.kt

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,16 @@ class ContentDeliveryCachePurgingProvider(
99
private val applicationContext: AbstractApplicationContext,
1010
private val configs: List<ContentDeliveryPurgingConfig>,
1111
) {
12-
val defaultPurging by lazy {
12+
val purgings by lazy {
1313
getDefaultFactory()
1414
}
1515

16-
private fun getDefaultFactory(): ContentDeliveryCachePurging? {
17-
val purgings =
18-
configs.mapNotNull {
19-
if (!it.enabled) {
20-
return@mapNotNull null
21-
}
22-
applicationContext.getBean(it.contentDeliveryCachePurgingType.factory.java).create(it)
16+
private fun getDefaultFactory(): List<ContentDeliveryCachePurging> {
17+
return configs.mapNotNull {
18+
if (!it.enabled) {
19+
return@mapNotNull null
2320
}
24-
if (purgings.size > 1) {
25-
throw RuntimeException("Exactly one content delivery purging must be set")
21+
applicationContext.getBean(it.contentDeliveryCachePurgingType.factory.java).create(it)
2622
}
27-
28-
return purgings.firstOrNull()
2923
}
3024
}

backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/AzureContentDeliveryCachePurging.kt renamed to backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/azureFrontDoor/AzureContentDeliveryCachePurging.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
package io.tolgee.component.contentDelivery.cachePurging
1+
package io.tolgee.component.contentDelivery.cachePurging.azureFrontDoor
22

33
import com.azure.core.credential.TokenRequestContext
44
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import io.tolgee.component.contentDelivery.cachePurging.ContentDeliveryCachePurging
56
import io.tolgee.model.contentDelivery.AzureFrontDoorConfig
67
import io.tolgee.model.contentDelivery.ContentDeliveryConfig
78
import org.springframework.http.HttpEntity

backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/AzureContentDeliveryCachePurgingFactory.kt renamed to backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/azureFrontDoor/AzureContentDeliveryCachePurgingFactory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package io.tolgee.component.contentDelivery.cachePurging
1+
package io.tolgee.component.contentDelivery.cachePurging.azureFrontDoor
22

3+
import io.tolgee.component.contentDelivery.cachePurging.ContentDeliveryCachePurgingFactory
34
import io.tolgee.model.contentDelivery.AzureFrontDoorConfig
45
import org.springframework.stereotype.Component
56
import org.springframework.web.client.RestTemplate

backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/AzureCredentialProvider.kt renamed to backend/data/src/main/kotlin/io/tolgee/component/contentDelivery/cachePurging/azureFrontDoor/AzureCredentialProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.tolgee.component.contentDelivery.cachePurging
1+
package io.tolgee.component.contentDelivery.cachePurging.azureFrontDoor
22

33
import com.azure.identity.ClientSecretCredential
44
import com.azure.identity.ClientSecretCredentialBuilder
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.tolgee.component.contentDelivery.cachePurging.cloudflare
2+
3+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
4+
import io.tolgee.component.contentDelivery.cachePurging.ContentDeliveryCachePurging
5+
import io.tolgee.configuration.tolgee.ContentDeliveryCloudflareProperties
6+
import io.tolgee.model.contentDelivery.ContentDeliveryConfig
7+
import org.springframework.http.HttpEntity
8+
import org.springframework.http.HttpHeaders
9+
import org.springframework.http.HttpMethod
10+
import org.springframework.http.MediaType
11+
import org.springframework.web.client.RestTemplate
12+
13+
class CloudflareContentDeliveryCachePurging(
14+
private val config: ContentDeliveryCloudflareProperties,
15+
private val restTemplate: RestTemplate,
16+
) : ContentDeliveryCachePurging {
17+
override fun purgeForPaths(
18+
contentDeliveryConfig: ContentDeliveryConfig,
19+
paths: Set<String>,
20+
) {
21+
val bodies = getChunkedBody(paths, contentDeliveryConfig)
22+
executePurgeRequest(bodies)
23+
}
24+
25+
private fun getChunkedBody(
26+
paths: Set<String>,
27+
contentDeliveryConfig: ContentDeliveryConfig,
28+
): List<Map<String, List<String>>> {
29+
return paths.map {
30+
"$prefix/${contentDeliveryConfig.slug}/$it"
31+
}
32+
.chunked(config.maxFilesPerRequest)
33+
.map { urls ->
34+
mapOf("files" to urls)
35+
}
36+
}
37+
38+
val prefix by lazy {
39+
config.urlPrefix?.removeSuffix("/") ?: ""
40+
}
41+
42+
private fun executePurgeRequest(bodies: List<Map<String, List<String>>>) {
43+
bodies.forEach { body ->
44+
val entity: HttpEntity<String> = getHttpEntity(body)
45+
46+
val url = "https://api.cloudflare.com/client/v4/zones/${config.zoneId}/purge_cache"
47+
48+
val response =
49+
restTemplate.exchange(
50+
url,
51+
HttpMethod.POST,
52+
entity,
53+
String::class.java,
54+
)
55+
56+
if (!response.statusCode.is2xxSuccessful) {
57+
throw IllegalStateException("Purging failed with status code ${response.statusCode}")
58+
}
59+
}
60+
}
61+
62+
private fun getHttpEntity(body: Map<String, List<String>>): HttpEntity<String> {
63+
val headers = getHeaders()
64+
val jsonBody = jacksonObjectMapper().writeValueAsString(body)
65+
return HttpEntity(jsonBody, headers)
66+
}
67+
68+
private fun getHeaders(): HttpHeaders {
69+
val headers = HttpHeaders()
70+
headers.contentType = MediaType.APPLICATION_JSON
71+
headers.set("Authorization", "Bearer ${config.apiKey}")
72+
return headers
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.tolgee.component.contentDelivery.cachePurging.cloudflare
2+
3+
import io.tolgee.component.contentDelivery.cachePurging.ContentDeliveryCachePurgingFactory
4+
import io.tolgee.configuration.tolgee.ContentDeliveryCloudflareProperties
5+
import org.springframework.stereotype.Component
6+
import org.springframework.web.client.RestTemplate
7+
8+
@Component
9+
class CloudflareContentDeliveryCachePurgingFactory(
10+
private val restTemplate: RestTemplate,
11+
) : ContentDeliveryCachePurgingFactory {
12+
override fun create(config: Any): CloudflareContentDeliveryCachePurging {
13+
return CloudflareContentDeliveryCachePurging(
14+
config as ContentDeliveryCloudflareProperties,
15+
restTemplate,
16+
)
17+
}
18+
}

backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/ContentDeliveryCachePurgingProperties.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties
44

55
@ConfigurationProperties(prefix = "tolgee.content-delivery.cache-purging")
66
class ContentDeliveryCachePurgingProperties {
7-
var azureFrontDoor: ContentDeliveryAzureFrontDoorProperties = ContentDeliveryAzureFrontDoorProperties()
7+
var azureFrontDoor = ContentDeliveryAzureFrontDoorProperties()
8+
var cloudflare = ContentDeliveryCloudflareProperties()
89
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.tolgee.configuration.tolgee
2+
3+
import io.tolgee.configuration.annotations.DocProperty
4+
import io.tolgee.model.contentDelivery.ContentDeliveryCachePurgingType
5+
import io.tolgee.model.contentDelivery.ContentDeliveryPurgingConfig
6+
import org.springframework.boot.context.properties.ConfigurationProperties
7+
8+
@ConfigurationProperties(prefix = "tolgee.content-delivery.cache-purging.cloudflare")
9+
class ContentDeliveryCloudflareProperties(
10+
var apiKey: String? = null,
11+
var urlPrefix: String? = null,
12+
var zoneId: String? = null,
13+
@DocProperty(
14+
"Number of paths to purge in one request. " +
15+
"(Cloudflare limit is 30 now, but it might be subject to change)",
16+
)
17+
var maxFilesPerRequest: Int = 30,
18+
) : ContentDeliveryPurgingConfig {
19+
override val enabled: Boolean
20+
get() = !apiKey.isNullOrEmpty() && !urlPrefix.isNullOrEmpty() && !zoneId.isNullOrEmpty()
21+
22+
override val contentDeliveryCachePurgingType: ContentDeliveryCachePurgingType
23+
get() = ContentDeliveryCachePurgingType.CLOUDFLARE
24+
}

0 commit comments

Comments
 (0)