feat: add production capability foundations
This commit is contained in:
@@ -4,12 +4,35 @@ dependencies {
|
||||
implementation project(':adapter:outbound:support')
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-autoconfigure'
|
||||
implementation 'org.springframework.session:spring-session-core'
|
||||
implementation 'org.springframework.session:spring-session-data-redis'
|
||||
implementation 'org.springframework.data:spring-data-redis'
|
||||
implementation 'io.lettuce:lettuce-core'
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
}
|
||||
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
|
||||
|
||||
sourceSets {
|
||||
redisTest {
|
||||
java.srcDir 'src/redisTest/java'
|
||||
resources.srcDir 'src/redisTest/resources'
|
||||
compileClasspath += sourceSets.main.output
|
||||
runtimeClasspath += sourceSets.main.output
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
redisTestImplementation.extendsFrom testImplementation
|
||||
redisTestCompileOnly.extendsFrom testCompileOnly
|
||||
redisTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
}
|
||||
|
||||
dependencies {
|
||||
redisTestImplementation 'org.testcontainers:testcontainers'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform {
|
||||
excludeTags 'redis-service'
|
||||
@@ -32,3 +55,562 @@ tasks.register('redisServiceTest', Test) {
|
||||
}
|
||||
shouldRunAfter tasks.named('test')
|
||||
}
|
||||
|
||||
def verifyRedisEvidenceSourcesPresent = tasks.register('verifyRedisEvidenceSourcesPresent') {
|
||||
group = 'redis verification'
|
||||
description = 'Fails readiness lanes when the redisTest evidence source set is empty.'
|
||||
inputs.files(sourceSets.redisTest.allSource)
|
||||
doLast {
|
||||
Set<File> javaSources = sourceSets.redisTest.java.files.findAll {
|
||||
it.isFile() && it.name.endsWith('.java')
|
||||
}
|
||||
if (javaSources.isEmpty()) {
|
||||
throw new GradleException(
|
||||
'Redis evidence source set is empty; readiness tasks must not pass as NO-SOURCE.')
|
||||
}
|
||||
File imageRegistry = rootProject.file('gradle/redis-test-images.properties')
|
||||
if (!imageRegistry.isFile() || imageRegistry.length() == 0) {
|
||||
throw new GradleException(
|
||||
"Redis evidence image registry is missing or empty: ${imageRegistry}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def redisCapabilityMetadata = rootProject.ext.redisCapabilityMetadata
|
||||
|
||||
def redisSanitizedEvidenceFileNames = [
|
||||
'manifest.json',
|
||||
'capability-card.json',
|
||||
'topology-fault-timeline.json'
|
||||
] as Set<String>
|
||||
def redisSanitizedBundleSha256 = { File directory ->
|
||||
java.security.MessageDigest digest = java.security.MessageDigest.getInstance('SHA-256')
|
||||
redisSanitizedEvidenceFileNames.toList().sort().each { String name ->
|
||||
File file = new File(directory, name)
|
||||
if (!file.isFile()) {
|
||||
throw new GradleException(
|
||||
"Redis sanitized bundle is missing ${name}: ${directory}")
|
||||
}
|
||||
byte[] nameBytes = name.getBytes('UTF-8')
|
||||
byte[] contentBytes = file.bytes
|
||||
digest.update(java.nio.ByteBuffer.allocate(Long.BYTES).putLong(nameBytes.length).array())
|
||||
digest.update(nameBytes)
|
||||
digest.update(java.nio.ByteBuffer.allocate(Long.BYTES).putLong(contentBytes.length).array())
|
||||
digest.update(contentBytes)
|
||||
}
|
||||
digest.digest().encodeHex().toString()
|
||||
}
|
||||
|
||||
def registerRedisEvidenceTask = { String taskName, String tagExpression, String descriptionText ->
|
||||
def evidenceTask = tasks.register(taskName, Test) {
|
||||
group = 'redis verification'
|
||||
description = descriptionText
|
||||
dependsOn verifyRedisEvidenceSourcesPresent
|
||||
testClassesDirs = sourceSets.redisTest.output.classesDirs
|
||||
classpath = sourceSets.redisTest.runtimeClasspath
|
||||
useJUnitPlatform {
|
||||
includeTags tagExpression
|
||||
}
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
jvmArgs '-Duser.timezone=UTC'
|
||||
systemProperty 'redis.image.registry',
|
||||
rootProject.file('gradle/redis-test-images.properties').absolutePath
|
||||
List<Map<String, Object>> sanitizedTimeline = []
|
||||
List<String> declaredTags = tagExpression.split(/\s*&\s*/).toList()
|
||||
String cardTag = declaredTags.find { it.startsWith('card-') }
|
||||
String cardId = cardTag == null ? null : cardTag.substring('card-'.length())
|
||||
Set<String> evidenceCategories = [
|
||||
'standalone',
|
||||
'security',
|
||||
'sentinel',
|
||||
'cluster',
|
||||
'fault',
|
||||
'compatibility'
|
||||
] as Set<String>
|
||||
String evidenceCategory = declaredTags.find {
|
||||
it.startsWith('redis-') && evidenceCategories.contains(it.substring('redis-'.length()))
|
||||
}
|
||||
if (evidenceCategory != null) {
|
||||
evidenceCategory = evidenceCategory.substring('redis-'.length())
|
||||
}
|
||||
File evidenceDirectory = layout.buildDirectory.dir(
|
||||
"redis-evidence/${taskName}").get().asFile
|
||||
outputs.dir evidenceDirectory
|
||||
afterTest { descriptor, result ->
|
||||
String identity = "${descriptor.className ?: ''}#${descriptor.name ?: ''}"
|
||||
String identityDigest = java.security.MessageDigest.getInstance('SHA-256')
|
||||
.digest(identity.getBytes('UTF-8')).encodeHex().toString()
|
||||
sanitizedTimeline << [
|
||||
sequence : sanitizedTimeline.size() + 1,
|
||||
testCaseIdSha256: identityDigest,
|
||||
outcome : result.resultType.name(),
|
||||
durationMillis : Math.max(0L, result.endTime - result.startTime)
|
||||
]
|
||||
}
|
||||
afterSuite { descriptor, result ->
|
||||
if (descriptor.parent != null) {
|
||||
return
|
||||
}
|
||||
evidenceDirectory.mkdirs()
|
||||
Map<String, Object> card = cardId == null
|
||||
? null
|
||||
: rootProject.ext.redisReadinessCards[cardId] as Map<String, Object>
|
||||
Map<String, String> digests = rootProject.ext.redisEvidenceDigests()
|
||||
Map<String, Object> metadata = cardId == null
|
||||
? [
|
||||
providerIds : [],
|
||||
roles : [],
|
||||
programs : [],
|
||||
keyVersions : [],
|
||||
codecVersions : [],
|
||||
guarantees : ['cross-cutting Redis evidence lane'],
|
||||
nonGuarantees : ['does not qualify a capability card by itself'],
|
||||
requiredSettings: []
|
||||
]
|
||||
: redisCapabilityMetadata[cardId] as Map<String, Object>
|
||||
Map<String, Object> capabilityCard = [
|
||||
schemaVersion : 1,
|
||||
cardId : cardId,
|
||||
readiness : card?.state,
|
||||
releaseQualification: 'NOT_CLAIMED',
|
||||
promotionTopology : card?.selectedTopology,
|
||||
sourceRevision : rootProject.ext.redisEvidenceSourceRevision,
|
||||
sourceTreeState : rootProject.ext.redisEvidenceSourceTreeState,
|
||||
digests : digests,
|
||||
minimumRedisVersion: '7.2',
|
||||
providerIds : metadata.providerIds,
|
||||
roles : metadata.roles,
|
||||
programIds : metadata.programs,
|
||||
keyVersions : metadata.keyVersions,
|
||||
codecVersions : metadata.codecVersions,
|
||||
guarantees : metadata.guarantees,
|
||||
nonGuarantees : metadata.nonGuarantees,
|
||||
requiredSettings : metadata.requiredSettings,
|
||||
evidenceProfile : card?.requiredEvidence ?: []
|
||||
]
|
||||
File capabilityCardFile = new File(evidenceDirectory, 'capability-card.json')
|
||||
capabilityCardFile.setText(
|
||||
groovy.json.JsonOutput.prettyPrint(
|
||||
groovy.json.JsonOutput.toJson(capabilityCard)) + '\n',
|
||||
'UTF-8')
|
||||
Map<String, Object> timeline = [
|
||||
schemaVersion: 1,
|
||||
taskName : taskName,
|
||||
cardId : cardId,
|
||||
topology : card?.selectedTopology,
|
||||
evidence : evidenceCategory,
|
||||
timelineKind : 'SANITIZED_TEST_RESULT',
|
||||
actualEventTimeline: 'NOT_CAPTURED',
|
||||
sourceRevision: rootProject.ext.redisEvidenceSourceRevision,
|
||||
sourceTreeState: rootProject.ext.redisEvidenceSourceTreeState,
|
||||
digests : digests,
|
||||
events : sanitizedTimeline
|
||||
]
|
||||
File timelineFile = new File(evidenceDirectory, 'topology-fault-timeline.json')
|
||||
timelineFile.setText(
|
||||
groovy.json.JsonOutput.prettyPrint(
|
||||
groovy.json.JsonOutput.toJson(timeline)) + '\n',
|
||||
'UTF-8')
|
||||
Closure<String> sha256 = { File file ->
|
||||
java.security.MessageDigest.getInstance('SHA-256')
|
||||
.digest(file.bytes).encodeHex().toString()
|
||||
}
|
||||
String outcome = result.resultType.name() == 'FAILURE'
|
||||
? 'failed'
|
||||
: (result.testCount == 0 || result.skippedTestCount > 0
|
||||
? 'skipped-with-reason'
|
||||
: 'executed')
|
||||
Map<String, Object> manifest = [
|
||||
schemaVersion : 1,
|
||||
taskPath : path,
|
||||
tagExpression : tagExpression,
|
||||
cardId : cardId,
|
||||
cardState : card?.state,
|
||||
selectedTopology : card?.selectedTopology,
|
||||
evidenceCategory : evidenceCategory,
|
||||
outcome : outcome,
|
||||
tests : [
|
||||
discovered: result.testCount,
|
||||
executed : result.testCount - result.skippedTestCount,
|
||||
passed : result.successfulTestCount,
|
||||
failed : result.failedTestCount,
|
||||
errors : 0,
|
||||
skipped : result.skippedTestCount
|
||||
],
|
||||
runtimeImageAttestation: 'NOT_CAPTURED',
|
||||
actualEventTimeline: 'NOT_CAPTURED',
|
||||
releaseQualification: 'NOT_CLAIMED',
|
||||
sourceRevision : rootProject.ext.redisEvidenceSourceRevision,
|
||||
sourceTreeState : rootProject.ext.redisEvidenceSourceTreeState,
|
||||
digests : digests,
|
||||
companionSha256 : [
|
||||
capabilityCardSha256: sha256(capabilityCardFile),
|
||||
timelineSha256 : sha256(timelineFile)
|
||||
]
|
||||
]
|
||||
new File(evidenceDirectory, 'manifest.json').setText(
|
||||
groovy.json.JsonOutput.prettyPrint(
|
||||
groovy.json.JsonOutput.toJson(manifest)) + '\n',
|
||||
'UTF-8')
|
||||
}
|
||||
doFirst {
|
||||
[
|
||||
'manifest.json',
|
||||
'capability-card.json',
|
||||
'topology-fault-timeline.json'
|
||||
].each { String generatedFile ->
|
||||
new File(evidenceDirectory, generatedFile).delete()
|
||||
}
|
||||
layout.buildDirectory.file(
|
||||
"redis-evidence-sanitizer/${taskName}.sha256").get().asFile.delete()
|
||||
Set<File> matchingSources = sourceSets.redisTest.java.files.findAll { File source ->
|
||||
if (!source.isFile() || !source.name.endsWith('.java')) {
|
||||
return false
|
||||
}
|
||||
String content = source.getText('UTF-8')
|
||||
declaredTags.every { String tag -> content.contains("@Tag(\"${tag}\")") }
|
||||
}
|
||||
if (matchingSources.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"${taskName}: no redisTest source declares every required tag " +
|
||||
"${declaredTags}; zero-evidence readiness must not pass.")
|
||||
}
|
||||
}
|
||||
}
|
||||
def sanitizerTask = tasks.register("${taskName}SanitizeEvidence") {
|
||||
group = 'redis verification'
|
||||
description = "Validates the bounded sanitized artifact for ${taskName} before upload."
|
||||
mustRunAfter evidenceTask
|
||||
File sanitizerMarker = layout.buildDirectory.file(
|
||||
"redis-evidence-sanitizer/${taskName}.sha256").get().asFile
|
||||
doFirst {
|
||||
sanitizerMarker.delete()
|
||||
}
|
||||
doLast {
|
||||
File evidenceDirectory = layout.buildDirectory.dir(
|
||||
"redis-evidence/${taskName}").get().asFile
|
||||
if (!evidenceDirectory.isDirectory()) {
|
||||
throw new GradleException(
|
||||
"${taskName}: sanitized evidence directory was not generated")
|
||||
}
|
||||
Set<String> allowedNames = redisSanitizedEvidenceFileNames
|
||||
List<File> files = evidenceDirectory.listFiles()?.findAll { it.isFile() } ?: []
|
||||
if (files.collect { it.name } as Set<String> != allowedNames ||
|
||||
evidenceDirectory.listFiles()?.any { it.isDirectory() }) {
|
||||
throw new GradleException(
|
||||
"${taskName}: sanitized evidence must contain exactly ${allowedNames}")
|
||||
}
|
||||
files.each { File file ->
|
||||
if (file.length() > 1_048_576L ||
|
||||
java.nio.file.Files.isSymbolicLink(file.toPath()) ||
|
||||
!file.toPath().toRealPath().startsWith(
|
||||
evidenceDirectory.toPath().toRealPath())) {
|
||||
throw new GradleException(
|
||||
"${taskName}: oversized, symlinked, or path-escaping artifact ${file}")
|
||||
}
|
||||
String text = file.getText('UTF-8')
|
||||
Map<String, java.util.regex.Pattern> forbidden = [
|
||||
pem : java.util.regex.Pattern.compile(
|
||||
'(?i)-----BEGIN [^-]*(?:PRIVATE KEY|CERTIFICATE)-----'),
|
||||
aclMaterial : java.util.regex.Pattern.compile(
|
||||
"(?i)(?:users\\.acl|--pass|[\"']password[\"']\\s*:)"),
|
||||
uriUserInfo : java.util.regex.Pattern.compile(
|
||||
'(?i)rediss?://[^\\s/@:]+:[^\\s/@]+@'),
|
||||
secretReference : java.util.regex.Pattern.compile('(?i)secret://'),
|
||||
rawMessageFields : java.util.regex.Pattern.compile(
|
||||
'(?i)"(?:stackTrace|systemOut|systemErr|exception|containerId|host|ip|port|endpoint|rawKey|physicalKey|value|sessionId|csrf|idempotencyToken|ownerToken|operationToken)"\\s*:')
|
||||
]
|
||||
forbidden.each { String marker, java.util.regex.Pattern pattern ->
|
||||
if (pattern.matcher(text).find()) {
|
||||
throw new GradleException(
|
||||
"${taskName}: sanitized artifact ${file.name} contains forbidden ${marker} material")
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, Object> manifest = new groovy.json.JsonSlurper().parse(
|
||||
new File(evidenceDirectory, 'manifest.json')) as Map<String, Object>
|
||||
Map<String, Object> capability = new groovy.json.JsonSlurper().parse(
|
||||
new File(evidenceDirectory, 'capability-card.json')) as Map<String, Object>
|
||||
Map<String, Object> timeline = new groovy.json.JsonSlurper().parse(
|
||||
new File(evidenceDirectory, 'topology-fault-timeline.json')) as Map<String, Object>
|
||||
Set<String> manifestFields = [
|
||||
'schemaVersion',
|
||||
'taskPath',
|
||||
'tagExpression',
|
||||
'cardId',
|
||||
'cardState',
|
||||
'selectedTopology',
|
||||
'evidenceCategory',
|
||||
'outcome',
|
||||
'tests',
|
||||
'runtimeImageAttestation',
|
||||
'actualEventTimeline',
|
||||
'releaseQualification',
|
||||
'sourceRevision',
|
||||
'sourceTreeState',
|
||||
'digests',
|
||||
'companionSha256'
|
||||
] as Set<String>
|
||||
Set<String> capabilityFields = [
|
||||
'schemaVersion',
|
||||
'cardId',
|
||||
'readiness',
|
||||
'releaseQualification',
|
||||
'promotionTopology',
|
||||
'sourceRevision',
|
||||
'sourceTreeState',
|
||||
'digests',
|
||||
'minimumRedisVersion',
|
||||
'providerIds',
|
||||
'roles',
|
||||
'programIds',
|
||||
'keyVersions',
|
||||
'codecVersions',
|
||||
'guarantees',
|
||||
'nonGuarantees',
|
||||
'requiredSettings',
|
||||
'evidenceProfile'
|
||||
] as Set<String>
|
||||
Set<String> timelineFields = [
|
||||
'schemaVersion',
|
||||
'taskName',
|
||||
'cardId',
|
||||
'topology',
|
||||
'evidence',
|
||||
'timelineKind',
|
||||
'actualEventTimeline',
|
||||
'sourceRevision',
|
||||
'sourceTreeState',
|
||||
'digests',
|
||||
'events'
|
||||
] as Set<String>
|
||||
if (manifest.keySet() != manifestFields ||
|
||||
capability.keySet() != capabilityFields ||
|
||||
timeline.keySet() != timelineFields ||
|
||||
(manifest.tests as Map).keySet() != [
|
||||
'discovered',
|
||||
'executed',
|
||||
'passed',
|
||||
'failed',
|
||||
'errors',
|
||||
'skipped'
|
||||
] as Set<String> ||
|
||||
(manifest.digests as Map).keySet() != [
|
||||
'registrySha256',
|
||||
'imageRegistrySha256',
|
||||
'programSetSha256',
|
||||
'configurationSha256'
|
||||
] as Set<String> ||
|
||||
(manifest.companionSha256 as Map).keySet() != [
|
||||
'capabilityCardSha256',
|
||||
'timelineSha256'
|
||||
] as Set<String>) {
|
||||
throw new GradleException(
|
||||
"${taskName}: sanitized evidence contains unknown or missing schema fields")
|
||||
}
|
||||
List<Map<String, Object>> events = timeline.events as List<Map<String, Object>>
|
||||
if (events.size() > 10_000 ||
|
||||
events.withIndex().any { Map<String, Object> event, int index ->
|
||||
event.keySet() != [
|
||||
'sequence',
|
||||
'testCaseIdSha256',
|
||||
'outcome',
|
||||
'durationMillis'
|
||||
] as Set<String> ||
|
||||
event.sequence != index + 1 ||
|
||||
!(event.testCaseIdSha256 ==~ /[0-9a-f]{64}/) ||
|
||||
!(event.outcome in ['SUCCESS', 'FAILURE', 'SKIPPED']) ||
|
||||
!(event.durationMillis instanceof Number) ||
|
||||
(event.durationMillis as Number).longValue() < 0L
|
||||
}) {
|
||||
throw new GradleException(
|
||||
"${taskName}: sanitized test summary contains malformed events")
|
||||
}
|
||||
if ((capability.requiredSettings as List).any {
|
||||
!(it instanceof Map) ||
|
||||
(it as Map).keySet() != ['name', 'type', 'constraint'] as Set<String>
|
||||
}) {
|
||||
throw new GradleException(
|
||||
"${taskName}: capability card required settings are not a safe name/type/constraint projection")
|
||||
}
|
||||
Map<String, Object> tests = manifest.tests as Map<String, Object>
|
||||
if (!(manifest.outcome in ['executed', 'failed', 'skipped-with-reason']) ||
|
||||
events.size() != (tests.discovered as Number).intValue() ||
|
||||
events.count { it.outcome == 'SUCCESS' } !=
|
||||
(tests.passed as Number).intValue() ||
|
||||
events.count { it.outcome == 'FAILURE' } !=
|
||||
(tests.failed as Number).intValue() ||
|
||||
events.count { it.outcome == 'SKIPPED' } !=
|
||||
(tests.skipped as Number).intValue()) {
|
||||
throw new GradleException(
|
||||
"${taskName}: manifest outcome/counts do not match the sanitized test summary")
|
||||
}
|
||||
if (manifest.outcome == 'executed' &&
|
||||
((tests.discovered as Number).longValue() <= 0L ||
|
||||
(tests.executed as Number).longValue() <= 0L ||
|
||||
(tests.passed as Number).longValue() <= 0L ||
|
||||
(tests.failed as Number).longValue() != 0L ||
|
||||
(tests.errors as Number).longValue() != 0L ||
|
||||
(tests.skipped as Number).longValue() != 0L)) {
|
||||
throw new GradleException(
|
||||
"${taskName}: executed evidence must be positive with zero failure/error/skip")
|
||||
}
|
||||
if (manifest.outcome == 'failed' &&
|
||||
(tests.failed as Number).longValue() <= 0L) {
|
||||
throw new GradleException(
|
||||
"${taskName}: failed evidence must retain a positive bounded failure count")
|
||||
}
|
||||
sanitizerMarker.parentFile.mkdirs()
|
||||
String bundleSha = redisSanitizedBundleSha256(evidenceDirectory)
|
||||
sanitizerMarker.setText("${bundleSha}\n", 'UTF-8')
|
||||
if (manifest.outcome == 'skipped-with-reason') {
|
||||
throw new GradleException(
|
||||
"${taskName}: skipped or zero-executed evidence is not a passing readiness lane")
|
||||
}
|
||||
}
|
||||
}
|
||||
evidenceTask.configure {
|
||||
finalizedBy sanitizerTask
|
||||
}
|
||||
evidenceTask
|
||||
}
|
||||
|
||||
tasks.register('verifyRedisEvidenceArtifactsForUpload') {
|
||||
group = 'redis verification'
|
||||
description = 'Allows CI upload only when every generated Redis evidence directory was sanitized.'
|
||||
doLast {
|
||||
File evidenceRoot = layout.buildDirectory.dir('redis-evidence').get().asFile
|
||||
File markerRoot = layout.buildDirectory.dir('redis-evidence-sanitizer').get().asFile
|
||||
List<File> evidenceDirectories = evidenceRoot.isDirectory()
|
||||
? evidenceRoot.listFiles().findAll { it.isDirectory() }
|
||||
: []
|
||||
if (evidenceDirectories.isEmpty()) {
|
||||
throw new GradleException(
|
||||
'No sanitized Redis evidence directory exists for upload')
|
||||
}
|
||||
Set<String> evidenceTasks = evidenceDirectories.collect { it.name } as Set<String>
|
||||
Set<String> markerTasks = markerRoot.isDirectory()
|
||||
? markerRoot.listFiles().findAll {
|
||||
it.isFile() && it.name.endsWith('.sha256')
|
||||
}.collect {
|
||||
it.name.substring(0, it.name.length() - '.sha256'.length())
|
||||
} as Set<String>
|
||||
: [] as Set<String>
|
||||
if (evidenceTasks != markerTasks) {
|
||||
throw new GradleException(
|
||||
"Redis evidence upload sanitizer coverage mismatch; evidence=${evidenceTasks}, markers=${markerTasks}")
|
||||
}
|
||||
evidenceDirectories.each { File directory ->
|
||||
Set<String> files = directory.listFiles().findAll { it.isFile() }
|
||||
.collect { it.name } as Set<String>
|
||||
if (files != redisSanitizedEvidenceFileNames) {
|
||||
throw new GradleException(
|
||||
"Redis upload directory ${directory.name} is outside the sanitized allowlist")
|
||||
}
|
||||
String bundleSha = redisSanitizedBundleSha256(directory)
|
||||
String recordedSha = new File(
|
||||
markerRoot, "${directory.name}.sha256").getText('UTF-8').trim()
|
||||
if (recordedSha != bundleSha) {
|
||||
throw new GradleException(
|
||||
"Redis upload sanitizer bundle marker is stale for ${directory.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerRedisEvidenceTask(
|
||||
'redisStandaloneTest',
|
||||
'redis-standalone',
|
||||
'Runs real standalone Redis evidence. Docker/service absence and zero tests fail.')
|
||||
registerRedisEvidenceTask(
|
||||
'redisSecurityTest',
|
||||
'redis-security',
|
||||
'Runs Redis TLS, ACL, secret-redaction, and fail-closed security evidence.')
|
||||
registerRedisEvidenceTask(
|
||||
'redisSentinelTest',
|
||||
'redis-sentinel',
|
||||
'Runs the explicit Redis Sentinel topology evidence lane.')
|
||||
registerRedisEvidenceTask(
|
||||
'redisClusterTest',
|
||||
'redis-cluster',
|
||||
'Runs the explicit Redis Cluster topology evidence lane.')
|
||||
registerRedisEvidenceTask(
|
||||
'redisFaultTest',
|
||||
'redis-fault',
|
||||
'Runs bounded Redis outage, response-loss, memory, and recovery evidence.')
|
||||
registerRedisEvidenceTask(
|
||||
'redisCompatibilityTest',
|
||||
'redis-compatibility',
|
||||
'Runs pinned minimum/next/approved Redis compatibility evidence.')
|
||||
registerRedisEvidenceTask(
|
||||
'redisEfficiencyLeaseTest',
|
||||
'redis-efficiency-lease',
|
||||
'Runs non-fenced EFFICIENCY_ONLY lease standalone, security, fault, and compatibility qualification.')
|
||||
|
||||
def redisCardTags = [
|
||||
redisCacheCapabilityTest : 'card-redis-cache',
|
||||
redisRateLimitCapabilityTest : 'card-redis-edge-rate-limit',
|
||||
redisIdempotencyCapabilityTest : 'card-redis-request-replay-idempotency',
|
||||
redisSoftLeaseCapabilityTest : 'card-redis-cache-refresh-soft-lease',
|
||||
redisFencedCoordinationCapabilityTest: 'card-redis-fenced-coordination',
|
||||
redisSessionCapabilityTest : 'card-redis-session'
|
||||
]
|
||||
redisCardTags.each { String taskName, String cardTag ->
|
||||
registerRedisEvidenceTask(
|
||||
taskName,
|
||||
cardTag,
|
||||
"Runs all real-service evidence owned by Redis capability card ${cardTag}.")
|
||||
}
|
||||
|
||||
def redisEvidenceTags = [
|
||||
Standalone : 'redis-standalone',
|
||||
Security : 'redis-security',
|
||||
Sentinel : 'redis-sentinel',
|
||||
Cluster : 'redis-cluster',
|
||||
Fault : 'redis-fault',
|
||||
Compatibility: 'redis-compatibility'
|
||||
]
|
||||
def redisCardTaskStems = [
|
||||
Cache : 'card-redis-cache',
|
||||
RateLimit : 'card-redis-edge-rate-limit',
|
||||
Idempotency : 'card-redis-request-replay-idempotency',
|
||||
SoftLease : 'card-redis-cache-refresh-soft-lease',
|
||||
FencedCoordination: 'card-redis-fenced-coordination',
|
||||
Session : 'card-redis-session'
|
||||
]
|
||||
redisCardTaskStems.each { String cardStem, String cardTag ->
|
||||
redisEvidenceTags.each { String evidenceStem, String evidenceTag ->
|
||||
registerRedisEvidenceTask(
|
||||
"redis${cardStem}${evidenceStem}EvidenceTest",
|
||||
"${cardTag} & ${evidenceTag}",
|
||||
"Runs ${evidenceTag} evidence owned only by ${cardTag}.")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('check') {
|
||||
dependsOn tasks.named('redisStandaloneTest')
|
||||
}
|
||||
|
||||
def redisLabContractDirectory = rootProject.file('../infra/redis-lab')
|
||||
def redisLabContractTest = tasks.register('redisLabContractTest', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Runs the VM-free Redis lab lifecycle and host-isolation contract with fake commands.'
|
||||
workingDir rootProject.projectDir
|
||||
executable 'bash'
|
||||
args new File(redisLabContractDirectory, 'test/redis-lab-contract.sh').absolutePath
|
||||
inputs.files(
|
||||
new File(redisLabContractDirectory, 'versions.env'),
|
||||
new File(redisLabContractDirectory, 'bin/redis-lab'),
|
||||
new File(redisLabContractDirectory, 'cloud-init/node.yaml'),
|
||||
new File(redisLabContractDirectory, 'lib/render-kubeconfig.awk'),
|
||||
fileTree(new File(redisLabContractDirectory, 'test/fixtures')) {
|
||||
include '**/*'
|
||||
},
|
||||
new File(redisLabContractDirectory, 'test/redis-lab-contract.sh'))
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.named('check') {
|
||||
dependsOn redisLabContractTest
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user