This repository was archived by the owner on Apr 3, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
wip: add an endpoint for writing configuration data #258
Open
philbooth
wants to merge
1
commit into
master
Choose a base branch
from
pb/185
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| //! Route handler | ||
| //! for the `POST /config` endpoint. | ||
|
|
||
| #[cfg(test)] | ||
| mod test; | ||
|
|
||
| use rocket::{ | ||
| data::{self, FromData}, | ||
| http::Status, | ||
| Data, Request, State, | ||
| }; | ||
| use rocket_contrib::Json; | ||
|
|
||
| use db::config::{Config as ConfigDb, Data as ConfigData}; | ||
| use logging::MozlogLogger; | ||
| use types::error::{AppError, AppErrorKind, AppResult}; | ||
|
|
||
| /// Payload for `POST /config`. | ||
| #[derive(Debug, Deserialize)] | ||
| struct Payload { | ||
| /// Flag indicating whether this payload clobbers existing config or should be merged with it. | ||
| clobber: Option<bool>, | ||
|
|
||
| /// The configuration data. | ||
| config: ConfigData, | ||
| } | ||
|
|
||
| impl FromData for Payload { | ||
| type Error = AppError; | ||
|
|
||
| fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> { | ||
| Json::<Payload>::from_data(request, data) | ||
| .map_failure(|(_status, error)| { | ||
| ( | ||
| Status::BadRequest, | ||
| AppErrorKind::InvalidPayload(error.to_string()).into(), | ||
| ) | ||
| }) | ||
| .map(|json| json.into_inner()) | ||
| } | ||
| } | ||
|
|
||
| #[post("/config", format = "application/json", data = "<payload>")] | ||
| fn handler( | ||
| payload: AppResult<Payload>, | ||
| config_db: State<ConfigDb>, | ||
| // TODO: do we need the logger? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it isn't too hard, I think it might be useful. |
||
| _logger: State<MozlogLogger>, | ||
| ) -> AppResult<Json> { | ||
| let mut payload = payload?; | ||
| if payload.clobber == Some(true) { | ||
| config_db.set("fxa", &payload.config)?; | ||
| } else { | ||
| config_db.merge("fxa", &mut payload.config)?; | ||
| }; | ||
| Ok(Json(json!({}))) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| use rocket::{ | ||
| http::{ContentType, Status}, | ||
| local::Client, | ||
| }; | ||
|
|
||
| use db::config::Config; | ||
| use logging::MozlogLogger; | ||
| use settings::Settings; | ||
| use types::error::{AppError, AppErrorKind}; | ||
|
|
||
| fn setup() -> Client { | ||
| let mut settings = Settings::new().unwrap(); | ||
| settings.provider.forcedefault = false; | ||
| let config = Config::new(&settings); | ||
| let logger = MozlogLogger::new(&settings); | ||
| let server = rocket::ignite() | ||
| .manage(settings) | ||
| .manage(config) | ||
| .manage(logger) | ||
| .mount("/", routes![super::handler]); | ||
|
|
||
| Client::new(server).unwrap() | ||
| } | ||
|
|
||
| #[test] | ||
| fn request_with_all_the_things() { | ||
| let client = setup(); | ||
|
|
||
| let mut response = client | ||
| .post("/config") | ||
| .header(ContentType::JSON) | ||
| .body( | ||
| r#"{ | ||
| "clobber": true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these values documented somewhere? |
||
| "config": { | ||
| "default_provider": "sendgrid", | ||
| "limits": { | ||
| "enabled": true, | ||
| "complaint": [ | ||
| { "period": "1 day", "limit": 0 }, | ||
| { "period": "1 year", "limit": 1 } | ||
| ], | ||
| "hard": [ | ||
| { "period": "2 days", "limit": 2 }, | ||
| { "period": "2 years", "limit": 10 } | ||
| ], | ||
| "soft": [ | ||
| { "period": "3 minutes", "limit": 1 } | ||
| ] | ||
| }, | ||
| "queue": "https://sqs.us-east-1.amazonaws.com/1234567890/blee", | ||
| "rules": [ | ||
| { "percentage": 50, "precedence": -127, "provider": "sendgrid" }, | ||
| { "percentage": 100, "precedence": 0, "provider": "socketlabs", "regex": "^socketlabs@mozilla\\.com$" }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the highest precedence be 0? It does seem strange that the highest precedence would be negative in this case. |
||
| { "percentage": 100, "precedence": 127, "provider": "ses" } | ||
| ], | ||
| "sender": { | ||
| "name": "Firefox Accounts", | ||
| "address": "[email protected]" | ||
| } | ||
| } | ||
| }"#, | ||
| ) | ||
| .dispatch(); | ||
|
|
||
| assert_eq!(response.status(), Status::Ok); | ||
|
|
||
| let body = response.body().unwrap().into_string().unwrap(); | ||
| assert_eq!(body, json!({}).to_string()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn request_without_optional_fields() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can add more tests for clobber. |
||
| let client = setup(); | ||
|
|
||
| let mut response = client | ||
| .post("/config") | ||
| .header(ContentType::JSON) | ||
| .body( | ||
| r#"{ | ||
| "config": {} | ||
| }"#, | ||
| ) | ||
| .dispatch(); | ||
|
|
||
| assert_eq!(response.status(), Status::Ok); | ||
|
|
||
| let body = response.body().unwrap().into_string().unwrap(); | ||
| assert_eq!(body, json!({}).to_string()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn empty_request() { | ||
| let client = setup(); | ||
|
|
||
| let mut response = client | ||
| .post("/config") | ||
| .header(ContentType::JSON) | ||
| .body("{}") | ||
| .dispatch(); | ||
|
|
||
| assert_eq!(response.status(), Status::BadRequest); | ||
|
|
||
| let body = response.body().unwrap().into_string().unwrap(); | ||
| let error: AppError = | ||
| AppErrorKind::InvalidPayload(String::from("missing field `config` at line 1 column 2")) | ||
| .into(); | ||
| let expected = serde_json::to_string(&error).unwrap(); | ||
| assert_eq!(body, expected); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,5 +6,6 @@ | |
| //! for modules pertaining to | ||
| //! the API layer. | ||
|
|
||
| pub mod config; | ||
| pub mod healthcheck; | ||
| pub mod send; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| //! Storage for configuration data. | ||
|
|
||
| use super::core::{Client as DbClient, DataType, Merge}; | ||
| use settings::{DeliveryProblemLimits, Sender, Settings, SqsUrl}; | ||
| use types::{error::AppResult, provider::Provider, regex::SerializableRegex}; | ||
|
|
||
| #[cfg(test)] | ||
| mod test; | ||
|
|
||
| /// Configuration data store. | ||
| /// | ||
| /// Data is keyed by client id. | ||
| #[derive(Debug)] | ||
| pub struct Config { | ||
| client: DbClient, | ||
| } | ||
|
|
||
| impl Config { | ||
| /// Instantiate a storage client. | ||
| pub fn new(settings: &Settings) -> Self { | ||
| Self { | ||
| client: DbClient::new(settings), | ||
| } | ||
| } | ||
|
|
||
| /// Read configuration data. | ||
| pub fn get(&self, client_id: &str) -> AppResult<Option<Data>> { | ||
| self.client.get(client_id, DataType::Configuration) | ||
| } | ||
|
|
||
| /// Store configuration data. | ||
| /// | ||
| /// Any data previously stored for the client id | ||
| /// will be replaced. | ||
| pub fn set(&self, client_id: &str, config: &Data) -> AppResult<()> { | ||
| self.client.set(client_id, config, DataType::Configuration) | ||
| } | ||
|
|
||
| /// Merge configuration data with existing. | ||
| pub fn merge(&self, client_id: &str, config: &mut Data) -> AppResult<()> { | ||
| self.client | ||
| .merge(client_id, config, DataType::Configuration) | ||
| } | ||
| } | ||
|
|
||
| /// Configuration data. | ||
| #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] | ||
| pub struct Data { | ||
| /// The fallback provider if no rules match and it's not specified in the call to `/send`. | ||
| default_provider: Option<Provider>, | ||
|
|
||
| /// Thresholds for bounce and complaint reports. | ||
| limits: Option<DeliveryProblemLimits>, | ||
|
|
||
| /// The queue for outgoing bounce, complaint and delivery notifications. | ||
| queue: Option<SqsUrl>, | ||
|
|
||
| /// Rules for selectively diverting subsets of email traffic via different providers. | ||
| rules: Option<Vec<ProviderRules>>, | ||
|
|
||
| /// The name and email address to use in the `From` and `Sender` headers. | ||
| sender: Option<Sender>, | ||
| } | ||
|
|
||
| impl Merge<Data> for Data { | ||
| fn merge(&self, with: &Self) -> Self { | ||
| Self { | ||
| default_provider: self | ||
| .default_provider | ||
| .as_ref() | ||
| .map(|default_provider| default_provider.clone()) | ||
| .or_else(|| with.default_provider.clone()), | ||
| limits: self | ||
| .limits | ||
| .as_ref() | ||
| .map(|limits| limits.clone()) | ||
| .or_else(|| with.limits.clone()), | ||
| queue: self | ||
| .queue | ||
| .as_ref() | ||
| .map(|queue| queue.clone()) | ||
| .or_else(|| with.queue.clone()), | ||
| rules: self | ||
| .rules | ||
| .as_ref() | ||
| .map(|rules| { | ||
| with.rules.as_ref().map_or_else( | ||
| || rules.clone(), | ||
| |with_rules| { | ||
| let mut with_rules = with_rules.clone(); | ||
| with_rules.extend(rules.clone()); | ||
| with_rules | ||
| }, | ||
| ) | ||
| }) | ||
| .or_else(|| with.rules.clone()), | ||
| sender: self | ||
| .sender | ||
| .as_ref() | ||
| .map(|sender| sender.clone()) | ||
| .or_else(|| with.sender.clone()), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Rules for selectively diverting subsets of email traffic via different providers. | ||
| #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] | ||
| pub struct ProviderRules { | ||
| /// The percentage of prospective addresses to divert via this provider. | ||
| percentage: Option<u8>, | ||
|
|
||
| /// Relative precedence of this ruleset against its peers. | ||
| /// Default is `0` (lower number equals higher precedence). | ||
| precedence: Option<i8>, | ||
|
|
||
| /// The email provider for this ruleset. | ||
| provider: Provider, | ||
|
|
||
| /// Filter for matching email addresses. | ||
| regex: Option<SerializableRegex>, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, you can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| use std::time::SystemTime; | ||
|
|
||
| use regex::Regex; | ||
|
|
||
| use super::*; | ||
| use db::core::test::TestFixture; | ||
| use settings::SenderName; | ||
|
|
||
| #[test] | ||
| fn set() { | ||
| let settings = Settings::new().unwrap(); | ||
| let config = Config::new(&settings); | ||
| let key = create_key("set"); | ||
| let test = TestFixture::setup(&settings, &key, DataType::Configuration); | ||
| let expected = Data { | ||
| default_provider: Some(Provider::SocketLabs), | ||
| limits: Some(DeliveryProblemLimits { | ||
| enabled: true, | ||
| complaint: vec![], | ||
| hard: vec![], | ||
| soft: vec![], | ||
| }), | ||
| queue: Some(SqsUrl( | ||
| "https://sqs.us-east-1.amazonaws.com/1234567890/wibble".to_owned(), | ||
| )), | ||
| rules: Some(vec![ProviderRules { | ||
| percentage: Some(50), | ||
| precedence: Some(-127), | ||
| provider: Provider::Sendgrid, | ||
| regex: Some(SerializableRegex(Regex::new("wibble").unwrap())), | ||
| }]), | ||
| sender: Some(Sender { | ||
| name: SenderName("Firefox Accounts".to_owned()), | ||
| address: "[email protected]".parse().unwrap(), | ||
| }), | ||
| }; | ||
|
|
||
| if let Err(error) = config.set(&key, &expected) { | ||
| assert!(false, format!("{:?}", error)); | ||
| } else { | ||
| test.assert_data(&expected); | ||
| } | ||
|
|
||
| let data = config.get(&key).unwrap(); | ||
| assert_eq!(data, Some(expected)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn merge() { | ||
| // TODO | ||
| } | ||
|
|
||
| fn create_key(test: &str) -> String { | ||
| format!("fxa-email-service.test.config.{}.{}", test, now()) | ||
| } | ||
|
|
||
| fn now() -> u64 { | ||
| let now = SystemTime::now() | ||
| .duration_since(SystemTime::UNIX_EPOCH) | ||
| .expect("system time error"); | ||
| now.as_secs() * 1000 + u64::from(now.subsec_millis()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be a little clearer if it was called
replaceand noted that default is to merge configs.