Skip to content
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

Added check for last modified time to settings CLI #3403

Merged
merged 5 commits into from
Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
2 changes: 1 addition & 1 deletion prime-router/docs/getting-started/faster-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ a one time procedure and only needs to be run at workstation startup or when you

```bash
docker-compose -f docker-compose.build.yml up --detach
docker-compose up --scale prime_dev=0 --scale settings=0 --scale web_receiver=0 --detach
docker-compose up --scale prime_dev=0 --detach
```

### Running the Azure functions
Expand Down
11 changes: 8 additions & 3 deletions prime-router/src/main/kotlin/azure/HttpUtilities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class HttpUtilities {
const val watersApi = "/api/waters"
const val tokenApi = "/api/token"

/**
* Last modified time header value formatter.
*/
val lastModifiedFormatter: DateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME

fun okResponse(
request: HttpRequestMessage<String?>,
responseBody: String,
Expand Down Expand Up @@ -151,10 +156,10 @@ class HttpUtilities {
) {
if (lastModified == null) return
// https://datatracker.ietf.org/doc/html/rfc7232#section-2.2 defines this header format
val lastModifiedFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss")
// Convert to GMT timezone

// Convert to UTC timezone
val lastModifiedGMT = OffsetDateTime.ofInstant(lastModified.toInstant(), ZoneOffset.UTC)
val lastModifiedFormatted = "${lastModifiedGMT.format(lastModifiedFormatter)} GMT"
val lastModifiedFormatted = lastModifiedGMT.format(lastModifiedFormatter)
builder.header(HttpHeaders.LAST_MODIFIED, lastModifiedFormatted)
}

Expand Down
84 changes: 73 additions & 11 deletions prime-router/src/main/kotlin/cli/SettingCommands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.file
import com.github.ajalt.clikt.parameters.types.inputStream
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.outputStream
Expand All @@ -27,16 +28,21 @@ import com.github.kittinunf.fuel.core.extensions.authentication
import com.github.kittinunf.fuel.json.FuelJson
import com.github.kittinunf.fuel.json.responseJson
import com.github.kittinunf.result.Result
import com.google.common.net.HttpHeaders
import gov.cdc.prime.router.DeepOrganization
import gov.cdc.prime.router.Organization
import gov.cdc.prime.router.Receiver
import gov.cdc.prime.router.Sender
import gov.cdc.prime.router.azure.HttpUtilities
import gov.cdc.prime.router.azure.OrganizationAPI
import gov.cdc.prime.router.azure.ReceiverAPI
import gov.cdc.prime.router.azure.SenderAPI
import gov.cdc.prime.router.common.Environment
import org.apache.http.HttpStatus
import java.io.File
import java.io.InputStream
import java.time.OffsetDateTime
import java.time.format.DateTimeParseException

private const val apiPath = "/api/settings"
private const val dummyAccessToken = "dummy"
Expand Down Expand Up @@ -192,13 +198,25 @@ abstract class SettingCommand(
}
}

fun readInput(): String {
if (inStream == null) abort("Missing input file")
val input = String(inStream!!.readAllBytes())
/**
* Read the contents of an [inputStream].
* @return the file contents
*/
fun readInput(inputStream: InputStream? = inStream): String {
if (inputStream == null) abort("Missing input file")
val input = String(inputStream.readAllBytes())
if (input.isBlank()) abort("Blank input")
return input
}

/**
* Read the contents [file].
* @return the file contents
*/
fun readInput(file: File): String {
return readInput(file.inputStream())
}

fun writeOutput(output: String) {
outStream.write(output.toByteArray())
}
Expand Down Expand Up @@ -518,15 +536,24 @@ class PutMultipleSettings : SettingCommand(
help = "set all settings from a 'organizations.yml' file"
) {

override val inStream by option("-i", "--input", help = "Input from file", metavar = "<file>")
.inputStream()
private val inputFile by option("-i", "--input", help = "Input from file", metavar = "<file>")
.file(true, mustBeReadable = true).required()

/**
* Number of connection retries.
*/
private val connRetries by option("-r", "--retries", help = "Number of seconds to retry waiting for the API")
.int().default(30)

/**
* Number of connection retries.
*/
private val checkLastModified by option(
"--check-last-modified",
help = "Update settings only if input file is newer"
)
.flag(default = false)

override fun run() {
val environment = Environment.get(env)
val accessToken = getAccessToken(environment)
Expand All @@ -535,13 +562,44 @@ class PutMultipleSettings : SettingCommand(
TermUi.echo("Waiting for the API at ${environment.url} to be available...")
CommandUtilities.waitForApi(environment, connRetries)

val results = putAll(environment, accessToken)
val output = "${results.joinToString("\n")}\n"
writeOutput(output)
if (!checkLastModified || (checkLastModified && isFileUpdated(environment))) {
val results = putAll(environment, accessToken)
val output = "${results.joinToString("\n")}\n"
writeOutput(output)
} else {
TermUi.echo("No new updates found for settings.")
}
}

/**
* Check if the settings from a file are newer than the data stored in the database for the
* given [environment].
* @return true if the file settings are newer or there is nothing in the database, false otherwise
*/
private fun isFileUpdated(environment: Environment): Boolean {
val url = formPath(environment, Operation.LIST, SettingType.ORG, "")
val (_, response, result) = Fuel.head(url).authentication()
.bearer(getAccessToken(environment)).response()
return when (result) {
is Result.Success -> {
if (response[HttpHeaders.LAST_MODIFIED].isNotEmpty()) {
try {
val apiModifiedTime = OffsetDateTime.parse(
response[HttpHeaders.LAST_MODIFIED].first(),
HttpUtilities.lastModifiedFormatter
)
apiModifiedTime.toInstant().toEpochMilli() < inputFile.lastModified()
} catch (e: DateTimeParseException) {
false
}
} else false
}
else -> error("Unable to fetch settings last update time from API. $result")
}
}

private fun putAll(environment: Environment, accessToken: String): List<String> {
val deepOrgs = readYaml()
val deepOrgs = readYaml(inputFile)
val results = mutableListOf<String>()
// Put orgs
deepOrgs.forEach { deepOrg ->
Expand All @@ -562,8 +620,12 @@ class PutMultipleSettings : SettingCommand(
return results
}

private fun readYaml(): List<DeepOrganization> {
val input = readInput()
/**
* Read the settings from a YAML file.
* @return the settings
*/
private fun readYaml(file: File): List<DeepOrganization> {
val input = readInput(file.inputStream())
return yamlMapper.readValue(input)
}
}
Expand Down
8 changes: 5 additions & 3 deletions prime-router/start_func.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ function load_config() {
top_dir=$function_folder/../..
test_config_dir=$top_dir/resources/test
fatjar=$top_dir/libs/prime-router-0.1-SNAPSHOT-all.jar
java -jar $fatjar lookuptables loadall -d $test_config_dir/metadata/tables -r 60 | awk '{print "[LOAD TABLES] " $0}'
echo "Loading lookup tables..."
java -jar $fatjar lookuptables loadall -d $test_config_dir/metadata/tables -r 60
# Note the settings require the full metadata catalog to be in place, so run last
java -jar $fatjar multiple-settings set -i $function_folder/settings/organizations.yml -r 60 | awk '{print "[LOAD SETTINGS] " $0}'
echo "Loading organization settings..."
java -jar $fatjar multiple-settings set -i $function_folder/settings/organizations.yml -r 60 --check-last-modified
}

# Load the configuration in the background. It will wait for the API to start the loading.
load_config &
load_config | awk '{print "[LOAD CONFIG] " $0}' &

# Run the functions
func host start --cors http://localhost:8090,http://localhost:3000 --language-worker -- "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"