diff --git a/.github/workflows/hakari.yml b/.github/workflows/hakari.yml index a9beb49ed5..6e847ce8c4 100644 --- a/.github/workflows/hakari.yml +++ b/.github/workflows/hakari.yml @@ -24,7 +24,7 @@ jobs: with: toolchain: stable - name: Install cargo-hakari - uses: taiki-e/install-action@0256b3ea9ae3d751755a35cbb0608979a842f1d2 # v2 + uses: taiki-e/install-action@996330bfc2ff267dc45a3d59354705b61547df0b # v2 with: tool: cargo-hakari - name: Check workspace-hack Cargo.toml is up-to-date diff --git a/Cargo.lock b/Cargo.lock index 9a202fb04a..61660b973f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1018,9 +1018,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -1028,9 +1028,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -1041,9 +1041,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1929,6 +1929,7 @@ dependencies = [ "camino-tempfile", "chrono", "clap", + "dns-server-api", "dns-service-client", "dropshot", "expectorate", @@ -1958,6 +1959,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "dns-server-api" +version = "0.1.0" +dependencies = [ + "chrono", + "dropshot", + "omicron-workspace-hack", + "schemars", + "serde", +] + [[package]] name = "dns-service-client" version = "0.1.0" @@ -6098,6 +6110,7 @@ dependencies = [ "atomicwrites", "camino", "clap", + "dns-server-api", "dropshot", "fs-err", "indent_write", @@ -9280,11 +9293,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools 0.12.1", "nom", "unicode_categories", ] diff --git a/Cargo.toml b/Cargo.toml index 13149ca637..379aa7f549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "dev-tools/releng", "dev-tools/xtask", "dns-server", + "dns-server-api", "end-to-end-tests", "gateway-cli", "gateway-test-utils", @@ -119,6 +120,7 @@ default-members = [ # hakari to not work as well and build times to be longer. # See omicron#4392. "dns-server", + "dns-server-api", # Do not include end-to-end-tests in the list of default members, as its # tests only work on a deployed control plane. "gateway-cli", @@ -279,6 +281,7 @@ derive-where = "1.2.7" diesel = { version = "2.1.6", features = ["postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] } diesel-dtrace = { git = "https://github.com/oxidecomputer/diesel-dtrace", branch = "main" } dns-server = { path = "dns-server" } +dns-server-api = { path = "dns-server-api" } dns-service-client = { path = "clients/dns-service-client" } dpd-client = { path = "clients/dpd-client" } dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main", features = [ "usdt-probes" ] } @@ -480,7 +483,7 @@ sp-sim = { path = "sp-sim" } sprockets-common = { git = "https://github.com/oxidecomputer/sprockets", rev = "77df31efa5619d0767ffc837ef7468101608aee9" } sprockets-host = { git = "https://github.com/oxidecomputer/sprockets", rev = "77df31efa5619d0767ffc837ef7468101608aee9" } sprockets-rot = { git = "https://github.com/oxidecomputer/sprockets", rev = "77df31efa5619d0767ffc837ef7468101608aee9" } -sqlformat = "0.2.3" +sqlformat = "0.2.4" sqlparser = { version = "0.45.0", features = [ "visitor" ] } static_assertions = "1.1.0" # Please do not change the Steno version to a Git dependency. It makes it diff --git a/dev-tools/openapi-manager/Cargo.toml b/dev-tools/openapi-manager/Cargo.toml index 654a36dd4e..db3152c604 100644 --- a/dev-tools/openapi-manager/Cargo.toml +++ b/dev-tools/openapi-manager/Cargo.toml @@ -12,6 +12,7 @@ anyhow.workspace = true atomicwrites.workspace = true camino.workspace = true clap.workspace = true +dns-server-api.workspace = true dropshot.workspace = true fs-err.workspace = true indent_write.workspace = true diff --git a/dev-tools/openapi-manager/src/spec.rs b/dev-tools/openapi-manager/src/spec.rs index 8883742154..5ad991e353 100644 --- a/dev-tools/openapi-manager/src/spec.rs +++ b/dev-tools/openapi-manager/src/spec.rs @@ -15,25 +15,34 @@ use openapiv3::OpenAPI; pub fn all_apis() -> Vec { vec![ ApiSpec { - title: "Installinator API".to_string(), - version: "0.0.1".to_string(), + title: "Internal DNS", + version: "0.0.1", + description: "API for the internal DNS server", + boundary: ApiBoundary::Internal, + api_description: + dns_server_api::dns_server_api::stub_api_description, + filename: "dns-server.json", + extra_validation: None, + }, + ApiSpec { + title: "Installinator API", + version: "0.0.1", description: "API for installinator to fetch artifacts \ - and report progress" - .to_string(), + and report progress", boundary: ApiBoundary::Internal, api_description: installinator_api::installinator_api::stub_api_description, - filename: "installinator.json".to_string(), + filename: "installinator.json", extra_validation: None, }, ApiSpec { - title: "Nexus internal API".to_string(), - version: "0.0.1".to_string(), - description: "Nexus internal API".to_string(), + title: "Nexus internal API", + version: "0.0.1", + description: "Nexus internal API", boundary: ApiBoundary::Internal, api_description: nexus_internal_api::nexus_internal_api_mod::stub_api_description, - filename: "nexus-internal.json".to_string(), + filename: "nexus-internal.json", extra_validation: None, }, // Add your APIs here! Please keep this list sorted by filename. @@ -42,13 +51,13 @@ pub fn all_apis() -> Vec { pub struct ApiSpec { /// The title. - pub title: String, + pub title: &'static str, /// The version. - pub version: String, + pub version: &'static str, /// The description string. - pub description: String, + pub description: &'static str, /// Whether this API is internal or external. pub boundary: ApiBoundary, @@ -59,7 +68,7 @@ pub struct ApiSpec { fn() -> Result, ApiDescriptionBuildErrors>, /// The JSON filename to write the API description to. - pub filename: String, + pub filename: &'static str, /// Extra validation to perform on the OpenAPI spec, if any. pub extra_validation: Option anyhow::Result<()>>, diff --git a/dns-server-api/Cargo.toml b/dns-server-api/Cargo.toml new file mode 100644 index 0000000000..c87af14e0d --- /dev/null +++ b/dns-server-api/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dns-server-api" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[lints] +workspace = true + +[dependencies] +chrono.workspace = true +dropshot.workspace = true +omicron-workspace-hack.workspace = true +schemars.workspace = true +serde.workspace = true diff --git a/dns-server-api/src/lib.rs b/dns-server-api/src/lib.rs new file mode 100644 index 0000000000..2c59caf0c5 --- /dev/null +++ b/dns-server-api/src/lib.rs @@ -0,0 +1,160 @@ +// 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/. + +//! Dropshot API for configuring DNS namespace. +//! +//! ## Shape of the API +//! +//! The DNS configuration API has just two endpoints: PUT and GET of the entire +//! DNS configuration. This is pretty anti-REST. But it's important to think +//! about how this server fits into the rest of the system. When changes are +//! made to DNS data, they're grouped together and assigned a monotonically +//! increasing generation number. The DNS data is first stored into CockroachDB +//! and then propagated from a distributed fleet of Nexus instances to a +//! distributed fleet of these DNS servers. If we accepted individual updates to +//! DNS names, then propagating a particular change would be non-atomic, and +//! Nexus would have to do a lot more work to ensure (1) that all changes were +//! propagated (even if it crashes) and (2) that they were propagated in the +//! correct order (even if two Nexus instances concurrently propagate separate +//! changes). +//! +//! This DNS server supports hosting multiple zones. We could imagine supporting +//! separate endpoints to update the DNS data for a particular zone. That feels +//! nicer (although it's not clear what it would buy us). But as with updates to +//! multiple names, Nexus's job is potentially much easier if the entire state +//! for all zones is updated at once. (Otherwise, imagine how Nexus would +//! implement _renaming_ one zone to another without loss of service. With +//! a combined endpoint and generation number for all zones, all that's necessary +//! is to configure a new zone with all the same names, and then remove the old +//! zone later in another update. That can be managed by the same mechanism in +//! Nexus that manages regular name updates. On the other hand, if there were +//! separate endpoints with separate generation numbers, then Nexus has more to +//! keep track of in order to do the rename safely.) +//! +//! See RFD 367 for more on DNS propagation. +//! +//! ## ETags and Conditional Requests +//! +//! It's idiomatic in HTTP use ETags and conditional requests to provide +//! synchronization. We could define an ETag to be just the current generation +//! number of the server and honor standard `if-match` headers to fail requests +//! where the generation number doesn't match what the client expects. This +//! would be fine, but it's rather annoying: +//! +//! 1. When the client wants to propagate generation X, the client would have +//! make an extra request just to fetch the current ETag, just so it can put +//! it into the conditional request. +//! +//! 2. If some other client changes the configuration in the meantime, the +//! conditional request would fail and the client would have to take another +//! lap (fetching the current config and potentially making another +//! conditional PUT). +//! +//! 3. This approach would make synchronization opt-in. If a client (or just +//! one errant code path) neglected to set the if-match header, we could do +//! the wrong thing and cause the system to come to rest with the wrong DNS +//! data. +//! +//! Since the semantics here are so simple (we only ever want to move the +//! generation number forward), we don't bother with ETags or conditional +//! requests. Instead we have the server implement the behavior we want, which +//! is that when a request comes in to update DNS data to generation X, the +//! server replies with one of: +//! +//! (1) the update has been applied and the server is now running generation X +//! (client treats this as success) +//! +//! (2) the update was not applied because the server is already at generation X +//! (client treats this as success) +//! +//! (3) the update was not applied because the server is already at a newer +//! generation +//! (client probably starts the whole propagation process over because its +//! current view of the world is out of date) +//! +//! This way, the DNS data can never move backwards and the client only ever has +//! to make one request. +//! +//! ## Concurrent updates +//! +//! Given that we've got just one API to update the all DNS zones, and given +//! that might therefore take a minute for a large zone, and also that there may +//! be multiple Nexus instances trying to do it at the same time, we need to +//! think a bit about what should happen if two Nexus do try to do it at the same +//! time. Spoiler: we immediately fail any request to update the DNS data if +//! there's already an update in progress. +//! +//! What else could we do? We could queue the incoming request behind the +//! in-progress one. How large do we allow that queue to grow? At some point +//! we'll need to stop queueing them. So why bother at all? + +use std::{ + collections::HashMap, + net::{Ipv4Addr, Ipv6Addr}, +}; + +use dropshot::{HttpError, HttpResponseOk, RequestContext}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[dropshot::api_description] +pub trait DnsServerApi { + type Context; + + #[endpoint( + method = GET, + path = "/config", + )] + async fn dns_config_get( + rqctx: RequestContext, + ) -> Result, HttpError>; + + #[endpoint( + method = PUT, + path = "/config", + )] + async fn dns_config_put( + rqctx: RequestContext, + rq: dropshot::TypedBody, + ) -> Result; +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct DnsConfigParams { + pub generation: u64, + pub time_created: chrono::DateTime, + pub zones: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct DnsConfig { + pub generation: u64, + pub time_created: chrono::DateTime, + pub time_applied: chrono::DateTime, + pub zones: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct DnsConfigZone { + pub zone_name: String, + pub records: HashMap>, +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(tag = "type", content = "data")] +pub enum DnsRecord { + A(Ipv4Addr), + AAAA(Ipv6Addr), + SRV(SRV), +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename = "Srv")] +pub struct SRV { + pub prio: u16, + pub weight: u16, + pub port: u16, + pub target: String, +} diff --git a/dns-server/Cargo.toml b/dns-server/Cargo.toml index 237d2a2fbb..d11dabaf85 100644 --- a/dns-server/Cargo.toml +++ b/dns-server/Cargo.toml @@ -12,6 +12,7 @@ anyhow.workspace = true camino.workspace = true chrono.workspace = true clap.workspace = true +dns-server-api.workspace = true dns-service-client.workspace = true dropshot.workspace = true http.workspace = true diff --git a/dns-server/src/bin/apigen.rs b/dns-server/src/bin/apigen.rs deleted file mode 100644 index e130ee0211..0000000000 --- a/dns-server/src/bin/apigen.rs +++ /dev/null @@ -1,29 +0,0 @@ -// 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/. - -//! Generate the OpenAPI spec for the DNS server - -use anyhow::{bail, Result}; -use dns_server::http_server::api; -use std::fs::File; -use std::io; - -fn usage(args: &[String]) -> String { - format!("{} [output path]", args[0]) -} - -fn main() -> Result<()> { - let args: Vec = std::env::args().collect(); - - let mut out = match args.len() { - 1 => Box::new(io::stdout()) as Box, - 2 => Box::new(File::create(args[1].clone())?) as Box, - _ => bail!(usage(&args)), - }; - - let api = api(); - let openapi = api.openapi("Internal DNS", "v0.1.0"); - openapi.write(&mut out)?; - Ok(()) -} diff --git a/dns-server/src/dns_server.rs b/dns-server/src/dns_server.rs index 01a8430b62..5c761f2aa3 100644 --- a/dns-server/src/dns_server.rs +++ b/dns-server/src/dns_server.rs @@ -7,12 +7,12 @@ //! The facilities here handle binding a UDP socket, receiving DNS messages on //! that socket, and replying to them. -use crate::dns_types::DnsRecord; use crate::storage; use crate::storage::QueryError; use crate::storage::Store; use anyhow::anyhow; use anyhow::Context; +use dns_server_api::DnsRecord; use pretty_hex::*; use serde::Deserialize; use slog::{debug, error, info, o, trace, Logger}; @@ -234,12 +234,7 @@ fn dns_record_to_record( Ok(aaaa) } - DnsRecord::SRV(crate::dns_types::SRV { - prio, - weight, - port, - target, - }) => { + DnsRecord::SRV(dns_server_api::SRV { prio, weight, port, target }) => { let tgt = Name::from_str(&target).map_err(|error| { RequestError::ServFail(anyhow!( "serialization failed due to bad SRV target {:?}: {:#}", diff --git a/dns-server/src/dns_types.rs b/dns-server/src/dns_types.rs deleted file mode 100644 index 941124feb6..0000000000 --- a/dns-server/src/dns_types.rs +++ /dev/null @@ -1,50 +0,0 @@ -// 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/. - -//! types describing DNS records and configuration - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::net::Ipv6Addr; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct DnsConfigParams { - pub generation: u64, - pub time_created: chrono::DateTime, - pub zones: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct DnsConfig { - pub generation: u64, - pub time_created: chrono::DateTime, - pub time_applied: chrono::DateTime, - pub zones: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct DnsConfigZone { - pub zone_name: String, - pub records: HashMap>, -} - -#[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(tag = "type", content = "data")] -pub enum DnsRecord { - A(Ipv4Addr), - AAAA(Ipv6Addr), - SRV(SRV), -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(rename = "Srv")] -pub struct SRV { - pub prio: u16, - pub weight: u16, - pub port: u16, - pub target: String, -} diff --git a/dns-server/src/http_server.rs b/dns-server/src/http_server.rs index e50346d828..84ffbc90e9 100644 --- a/dns-server/src/http_server.rs +++ b/dns-server/src/http_server.rs @@ -4,102 +4,12 @@ //! Dropshot server for configuring DNS namespace -// Shape of the API -// ------------------------------ -// -// The DNS configuration API has just two endpoints: PUT and GET of the entire -// DNS configuration. This is pretty anti-REST. But it's important to think -// about how this server fits into the rest of the system. When changes are -// made to DNS data, they're grouped together and assigned a monotonically -// increasing generation number. The DNS data is first stored into CockroachDB -// and then propagated from a distributed fleet of Nexus instances to a -// distributed fleet of these DNS servers. If we accepted individual updates to -// DNS names, then propagating a particular change would be non-atomic, and -// Nexus would have to do a lot more work to ensure (1) that all changes were -// propagated (even if it crashes) and (2) that they were propagated in the -// correct order (even if two Nexus instances concurrently propagate separate -// changes). -// -// This DNS server supports hosting multiple zones. We could imagine supporting -// separate endpoints to update the DNS data for a particular zone. That feels -// nicer (although it's not clear what it would buy us). But as with updates to -// multiple names, Nexus's job is potentially much easier if the entire state -// for all zones is updated at once. (Otherwise, imagine how Nexus would -// implement _renaming_ one zone to another without loss of service. With -// a combined endpoint and generation number for all zones, all that's necessary -// is to configure a new zone with all the same names, and then remove the old -// zone later in another update. That can be managed by the same mechanism in -// Nexus that manages regular name updates. On the other hand, if there were -// separate endpoints with separate generation numbers, then Nexus has more to -// keep track of in order to do the rename safely.) -// -// See RFD 367 for more on DNS propagation. -// -// -// ETags and Conditional Requests -// ------------------------------ -// -// It's idiomatic in HTTP use ETags and conditional requests to provide -// synchronization. We could define an ETag to be just the current generation -// number of the server and honor standard `if-match` headers to fail requests -// where the generation number doesn't match what the client expects. This -// would be fine, but it's rather annoying: -// -// (1) When the client wants to propagate generation X, the client would have -// make an extra request just to fetch the current ETag, just so it can put -// it into the conditional request. -// -// (2) If some other client changes the configuration in the meantime, the -// conditional request would fail and the client would have to take another -// lap (fetching the current config and potentially making another -// conditional PUT). -// -// (3) This approach would make synchronization opt-in. If a client (or just -// one errant code path) neglected to set the if-match header, we could do -// the wrong thing and cause the system to come to rest with the wrong DNS -// data. -// -// Since the semantics here are so simple (we only ever want to move the -// generation number forward), we don't bother with ETags or conditional -// requests. Instead we have the server implement the behavior we want, which -// is that when a request comes in to update DNS data to generation X, the -// server replies with one of: -// -// (1) the update has been applied and the server is now running generation X -// (client treats this as success) -// -// (2) the update was not applied because the server is already at generation X -// (client treats this as success) -// -// (3) the update was not applied because the server is already at a newer -// generation -// (client probably starts the whole propagation process over because its -// current view of the world is out of date) -// -// This way, the DNS data can never move backwards and the client only ever has -// to make one request. -// -// -// Concurrent updates -// ------------------ -// -// Given that we've got just one API to update the all DNS zones, and given -// that might therefore take a minute for a large zone, and also that there may -// be multiple Nexus instances trying to do it at the same time, we need to -// think a bit about what should happen if two Nexus do try to do it at the same -// time. Spoiler: we immediately fail any request to update the DNS data if -// there's already an update in progress. -// -// What else could we do? We could queue the incoming request behind the -// in-progress one. How large do we allow that queue to grow? At some point -// we'll need to stop queueing them. So why bother at all? - -use crate::dns_types::{DnsConfig, DnsConfigParams}; use crate::storage::{self, UpdateError}; +use dns_server_api::{DnsConfig, DnsConfigParams, DnsServerApi}; use dns_service_client::{ ERROR_CODE_BAD_UPDATE_GENERATION, ERROR_CODE_UPDATE_IN_PROGRESS, }; -use dropshot::{endpoint, RequestContext}; +use dropshot::RequestContext; pub struct Context { store: storage::Store, @@ -112,41 +22,40 @@ impl Context { } pub fn api() -> dropshot::ApiDescription { - let mut api = dropshot::ApiDescription::new(); - - api.register(dns_config_get).expect("register dns_config_get"); - api.register(dns_config_put).expect("register dns_config_update"); - api + dns_server_api::dns_server_api::api_description::() + .expect("registered DNS server entrypoints") } -#[endpoint( - method = GET, - path = "/config", -)] -async fn dns_config_get( - rqctx: RequestContext, -) -> Result, dropshot::HttpError> { - let apictx = rqctx.context(); - let config = apictx.store.dns_config().await.map_err(|e| { - dropshot::HttpError::for_internal_error(format!( - "internal error: {:?}", - e - )) - })?; - Ok(dropshot::HttpResponseOk(config)) -} +enum DnsServerApiImpl {} + +impl DnsServerApi for DnsServerApiImpl { + type Context = Context; -#[endpoint( - method = PUT, - path = "/config", -)] -async fn dns_config_put( - rqctx: RequestContext, - rq: dropshot::TypedBody, -) -> Result { - let apictx = rqctx.context(); - apictx.store.dns_config_update(&rq.into_inner(), &rqctx.request_id).await?; - Ok(dropshot::HttpResponseUpdatedNoContent()) + async fn dns_config_get( + rqctx: RequestContext, + ) -> Result, dropshot::HttpError> { + let apictx = rqctx.context(); + let config = apictx.store.dns_config().await.map_err(|e| { + dropshot::HttpError::for_internal_error(format!( + "internal error: {:?}", + e + )) + })?; + Ok(dropshot::HttpResponseOk(config)) + } + + async fn dns_config_put( + rqctx: RequestContext, + rq: dropshot::TypedBody, + ) -> Result + { + let apictx = rqctx.context(); + apictx + .store + .dns_config_update(&rq.into_inner(), &rqctx.request_id) + .await?; + Ok(dropshot::HttpResponseUpdatedNoContent()) + } } impl From for dropshot::HttpError { diff --git a/dns-server/src/lib.rs b/dns-server/src/lib.rs index ea8625a667..a2b1fda0d7 100644 --- a/dns-server/src/lib.rs +++ b/dns-server/src/lib.rs @@ -43,7 +43,6 @@ //! the persistent DNS data pub mod dns_server; -pub mod dns_types; pub mod http_server; pub mod storage; diff --git a/dns-server/src/storage.rs b/dns-server/src/storage.rs index 21fb9ebdc6..85b2e79b8b 100644 --- a/dns-server/src/storage.rs +++ b/dns-server/src/storage.rs @@ -92,9 +92,9 @@ // backwards-compatible way (but obviously one wouldn't get the scaling benefits // while continuing to use the old API). -use crate::dns_types::{DnsConfig, DnsConfigParams, DnsConfigZone, DnsRecord}; use anyhow::{anyhow, Context}; use camino::Utf8PathBuf; +use dns_server_api::{DnsConfig, DnsConfigParams, DnsConfigZone, DnsRecord}; use serde::{Deserialize, Serialize}; use sled::transaction::ConflictableTransactionError; use slog::{debug, error, info, o, warn}; @@ -777,13 +777,13 @@ impl<'a, 'b> Drop for UpdateGuard<'a, 'b> { #[cfg(test)] mod test { use super::{Config, Store, UpdateError}; - use crate::dns_types::DnsConfigParams; - use crate::dns_types::DnsConfigZone; - use crate::dns_types::DnsRecord; use crate::storage::QueryError; use anyhow::Context; use camino::Utf8PathBuf; use camino_tempfile::Utf8TempDir; + use dns_server_api::DnsConfigParams; + use dns_server_api::DnsConfigZone; + use dns_server_api::DnsRecord; use omicron_test_utils::dev::test_setup_log; use std::collections::BTreeSet; use std::collections::HashMap; diff --git a/dns-server/tests/openapi_test.rs b/dns-server/tests/openapi_test.rs deleted file mode 100644 index 490680eda4..0000000000 --- a/dns-server/tests/openapi_test.rs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 expectorate::assert_contents; -use omicron_test_utils::dev::test_cmds::assert_exit_code; -use omicron_test_utils::dev::test_cmds::path_to_executable; -use omicron_test_utils::dev::test_cmds::run_command; -use omicron_test_utils::dev::test_cmds::EXIT_SUCCESS; -use openapiv3::OpenAPI; -use subprocess::Exec; - -const CMD_API_GEN: &str = env!("CARGO_BIN_EXE_apigen"); - -#[test] -fn test_dns_server_openapi() { - let exec = Exec::cmd(path_to_executable(CMD_API_GEN)); - let (exit_status, stdout, stderr) = run_command(exec); - assert_exit_code(exit_status, EXIT_SUCCESS, &stderr); - - let spec: OpenAPI = - serde_json::from_str(&stdout).expect("stdout was not valid OpenAPI"); - let errors = openapi_lint::validate(&spec); - assert!(errors.is_empty(), "{}", errors.join("\n\n")); - - assert_contents("../openapi/dns-server.json", &stdout); -} diff --git a/docs/how-to-run-simulated.adoc b/docs/how-to-run-simulated.adoc index de19b70f04..86f7a0915b 100644 --- a/docs/how-to-run-simulated.adoc +++ b/docs/how-to-run-simulated.adoc @@ -94,6 +94,10 @@ omicron-dev: external DNS: [::1]:54342 === Running the pieces by hand +There are many reasons it's useful to run the pieces of the stack by hand, especially during development and debugging: to test stopping and starting a component while the rest of the stack remains online; to run one component in a custom environment; to use a custom binary; to use a custom config file; to run under the debugger or with extra tracing enabled; etc. + +CAUTION: This process does not currently work. See https://github.com/oxidecomputer/omicron/issues/4421[omicron#4421] for details. The pieces here may still be useful for reference. + . Start CockroachDB using `omicron-dev db-run`: + [source,text] @@ -181,6 +185,8 @@ omicron-dev: using /tmp/.tmpFH6v8h and /tmp/.tmpkUjDji for ClickHouse data stora $ cargo run --bin=nexus -- nexus/examples/config.toml ---- Nexus can also serve the web console. Instructions for downloading (or building) the console's static assets and pointing Nexus to them are https://github.com/oxidecomputer/console/blob/main/docs/serve-from-nexus.md[here]. Without console assets, Nexus will still start and run normally as an API. A few link:./nexus/src/external_api/console_api.rs[console-specific routes] will 404. ++ +CAUTION: This step does not currently work. See https://github.com/oxidecomputer/omicron/issues/4421[omicron#4421] for details. . `dns-server` is run similar to Nexus, except that the bind addresses are specified on the command line: + @@ -207,9 +213,98 @@ Dec 02 18:00:01.093 DEBG registered endpoint, path: /producers, method: POST, lo ... ---- +=== Using both `omicron-dev run-all` and running Nexus manually + +While it's often useful to run _some_ part of the stack by hand (see above), if you only want to run your own Nexus, one option is to run `omicron-dev run-all` first to get a whole simulated stack up, then run a second Nexus by hand with a custom config file. + +To do this, first run `omicron-dev run-all`: + +[source,text] +---- +$ cargo run --bin=omicron-dev -- run-all + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s + Running `target/debug/omicron-dev run-all` +omicron-dev: setting up all services ... +log file: /dangerzone/omicron_tmp/omicron-dev-omicron-dev.29765.0.log +note: configured to log to "/dangerzone/omicron_tmp/omicron-dev-omicron-dev.29765.0.log" +DB URL: postgresql://root@[::1]:43256/omicron?sslmode=disable +DB address: [::1]:43256 +log file: /dangerzone/omicron_tmp/omicron-dev-omicron-dev.29765.2.log +note: configured to log to "/dangerzone/omicron_tmp/omicron-dev-omicron-dev.29765.2.log" +log file: /dangerzone/omicron_tmp/omicron-dev-omicron-dev.29765.3.log +note: configured to log to "/dangerzone/omicron_tmp/omicron-dev-omicron-dev.29765.3.log" +omicron-dev: services are running. +omicron-dev: nexus external API: 127.0.0.1:12220 +omicron-dev: nexus internal API: [::1]:12221 +omicron-dev: cockroachdb pid: 29769 +omicron-dev: cockroachdb URL: postgresql://root@[::1]:43256/omicron?sslmode=disable +omicron-dev: cockroachdb directory: /dangerzone/omicron_tmp/.tmpikyLO8 +omicron-dev: internal DNS HTTP: http://[::1]:39841 +omicron-dev: internal DNS: [::1]:54025 +omicron-dev: external DNS name: oxide-dev.test +omicron-dev: external DNS HTTP: http://[::1]:63482 +omicron-dev: external DNS: [::1]:45276 +omicron-dev: e.g. `dig @::1 -p 45276 test-suite-silo.sys.oxide-dev.test` +omicron-dev: management gateway: http://[::1]:49188 (switch0) +omicron-dev: management gateway: http://[::1]:39352 (switch1) +omicron-dev: silo name: test-suite-silo +omicron-dev: privileged user name: test-privileged +---- + +You'll need to note: + +* the TCP ports for the two management gateways (`49188` and `39352` here for switch0 and switch1, respectively) +* the TCP port for internal DNS (`54025` here) +* the TCP port in the CockroachDB URL (`43256` here) + +Next, you'll need to customize the Nexus configuration file. Start with nexus/examples/config-second.toml (_not_ nexus/examples/config.toml, which uses various values that conflict with what `omicron-dev run-all` uses). You should only need to modify the block at the **bottom** of the file: + +[source,toml] +---- +################################################################################ +# INSTRUCTIONS: To run Nexus against an existing stack started with # +# `omicron-dev run-all`, you should only have to modify values in this # +# section. # +# # +# Modify the port numbers below based on the output of `omicron-dev run-all` # +################################################################################ + +[mgd] +# Look for "management gateway: http://[::1]:49188 (switch0)" +# The "http://" does not go in this string -- just the socket address. +switch0.address = "[::1]:49188" + +# Look for "management gateway: http://[::1]:39352 (switch1)" +# The "http://" does not go in this string -- just the socket address. +switch1.address = "[::1]:39352" + +[deployment.internal_dns] +# Look for "internal DNS: [::1]:54025" +# and adjust the port number below. +address = "[::1]:54025" +# You should not need to change this. +type = "from_address" + +[deployment.database] +# Look for "cockroachdb URL: postgresql://root@[::1]:43256/omicron?sslmode=disable" +# and adjust the port number below. +url = "postgresql://root@[::1]:43256/omicron?sslmode=disable" +# You should not need to change this. +type = "from_url" +################################################################################ +---- + +So it's: + +* Copy the example config file: `cp nexus/examples/config-second.toml config-second.toml` +* Edit as described above: `vim config-second.toml` +* Start Nexus like above, but with this config file: `cargo run --bin=nexus -- config-second.toml` + +=== Using the stack + Once everything is up and running, you can use the system in a few ways: -* Use the browser-based console. The Nexus log output will show what IP address and port it's listening on. This is also configured in the config file. If you're using the defaults, you can reach the console at `http://127.0.0.1:12220/projects`. Depending on the environment where you're running this, you may need an ssh tunnel or the like to reach this from your browser. +* Use the browser-based console. The Nexus log output will show what IP address and port it's listening on. This is also configured in the config file. If you're using the defaults with `omicron-dev run-all`, you can reach the console at `http://127.0.0.1:12220/projects`. If you ran a second Nexus using the `config-second.toml` config file, it will be on port `12222` instead (because that config file specifies port 12222). Depending on the environment where you're running this, you may need an ssh tunnel or the like to reach this from your browser. * Use the xref:cli.adoc[`oxide` CLI]. == Running with TLS diff --git a/nexus-config/src/nexus_config.rs b/nexus-config/src/nexus_config.rs index 5ca1d2d6ed..4bdee4ab4e 100644 --- a/nexus-config/src/nexus_config.rs +++ b/nexus-config/src/nexus_config.rs @@ -1174,6 +1174,12 @@ mod test { let example_config = NexusConfig::from_file(config_path) .expect("example config file is not valid"); + // The second example config file should be valid. + let config_path = "../nexus/examples/config-second.toml"; + println!("checking {:?}", config_path); + let _ = NexusConfig::from_file(config_path) + .expect("second example config file is not valid"); + // The config file used for the tests should also be valid. The tests // won't clear the runway anyway if this file isn't valid. But it's // helpful to verify this here explicitly as well. diff --git a/nexus/db-queries/src/db/datastore/bgp.rs b/nexus/db-queries/src/db/datastore/bgp.rs index d73e7ff327..1244184c1d 100644 --- a/nexus/db-queries/src/db/datastore/bgp.rs +++ b/nexus/db-queries/src/db/datastore/bgp.rs @@ -572,14 +572,14 @@ impl DataStore { &self, opctx: &OpContext, port_settings_id: Uuid, - interface_name: &String, + interface_name: &str, addr: IpNetwork, ) -> ListResultVec { use db::schema::switch_port_settings_bgp_peer_config_communities::dsl; let results = dsl::switch_port_settings_bgp_peer_config_communities .filter(dsl::port_settings_id.eq(port_settings_id)) - .filter(dsl::interface_name.eq(interface_name.clone())) + .filter(dsl::interface_name.eq(interface_name.to_owned())) .filter(dsl::addr.eq(addr)) .load_async(&*self.pool_connection_authorized(opctx).await?) .await @@ -592,7 +592,7 @@ impl DataStore { &self, opctx: &OpContext, port_settings_id: Uuid, - interface_name: &String, + interface_name: &str, addr: IpNetwork, ) -> LookupResult>> { use db::schema::switch_port_settings_bgp_peer_config as db_peer; @@ -619,7 +619,8 @@ impl DataStore { dsl::switch_port_settings_bgp_peer_config_allow_export .filter(db_allow::port_settings_id.eq(port_settings_id)) .filter( - db_allow::interface_name.eq(interface_name.clone()), + db_allow::interface_name + .eq(interface_name.to_owned()), ) .filter(db_allow::addr.eq(addr)) .load_async(&conn) @@ -637,7 +638,7 @@ impl DataStore { &self, opctx: &OpContext, port_settings_id: Uuid, - interface_name: &String, + interface_name: &str, addr: IpNetwork, ) -> LookupResult>> { use db::schema::switch_port_settings_bgp_peer_config as db_peer; @@ -664,7 +665,8 @@ impl DataStore { dsl::switch_port_settings_bgp_peer_config_allow_import .filter(db_allow::port_settings_id.eq(port_settings_id)) .filter( - db_allow::interface_name.eq(interface_name.clone()), + db_allow::interface_name + .eq(interface_name.to_owned()), ) .filter(db_allow::addr.eq(addr)) .load_async(&conn) diff --git a/nexus/examples/config-second.toml b/nexus/examples/config-second.toml new file mode 100644 index 0000000000..5dadb329cd --- /dev/null +++ b/nexus/examples/config-second.toml @@ -0,0 +1,180 @@ +# +# Example configuration file for running a second Nexus instance locally +# alongside the stack started by `omicron-dev run-all`. See the +# how-to-run-simulated instructions for details. +# + +################################################################################ +# INSTRUCTIONS: To run Nexus against an existing stack started with # +# `omicron-dev run-all`, see the very bottom of this file. # +################################################################################ + +[console] +# Directory for static assets. Absolute path or relative to CWD. +static_dir = "out/console-assets" +session_idle_timeout_minutes = 480 # 8 hours +session_absolute_timeout_minutes = 1440 # 24 hours + +# List of authentication schemes to support. +[authn] +schemes_external = ["session_cookie", "access_token"] + +[log] +# Show log messages of this level and more severe +level = "info" + +# Example output to a terminal (with colors) +mode = "stderr-terminal" + +# Example output to a file, appending if it already exists. +#mode = "file" +#path = "logs/server.log" +#if_exists = "append" + +# Configuration for interacting with the timeseries database +[timeseries_db] +address = "[::1]:8123" + + + +[deployment] +# Identifier for this instance of Nexus +id = "a4ef738a-1fb0-47b1-9da2-4919c7ec7c7f" +rack_id = "c19a698f-c6f9-4a17-ae30-20d711b8f7dc" +# Since we expect to be the second instance of Nexus running on this system, +# pick any available port. +techport_external_server_port = 0 + +# Nexus may need to resolve external hosts (e.g. to grab IdP metadata). +# These are the DNS servers it should use. +external_dns_servers = ["1.1.1.1", "9.9.9.9"] + +[deployment.dropshot_external] +# IP Address and TCP port on which to listen for the external API +# This config file uses 12222 to avoid colliding with the usual 12220 that's +# used by `omicron-dev run-all` +bind_address = "127.0.0.1:12222" +# Allow large request bodies to support uploading TUF archives. The number here +# is picked based on the typical size for tuf-mupdate.zip as of 2024-01 +# (~1.5GiB) and multiplying it by 2. +# +# This should be brought back down to a more reasonable value once per-endpoint +# request body limits are implemented. +request_body_max_bytes = 3221225472 +# To have Nexus's external HTTP endpoint use TLS, uncomment the line below. You +# will also need to provide an initial TLS certificate during rack +# initialization. If you're using this config file, you're probably running a +# simulated system. In that case, the initial certificate is provided to the +# simulated sled agent (acting as RSS) via command-line arguments. +#tls = true + +[deployment.dropshot_internal] +# IP Address and TCP port on which to listen for the internal API +# This config file uses 12223 to avoid colliding with the usual 12221 that's +# used by `omicron-dev run-all` +bind_address = "[::1]:12223" +request_body_max_bytes = 1048576 + +#[deployment.internal_dns] +## These values are overridden at the bottom of this file. +#type = "from_address" +#address = "[::1]:3535" + +#[deployment.database] +## These values are overridden at the bottom of this file. +#type = "from_url" +#url = "postgresql://root@[::1]:32221/omicron?sslmode=disable" + +# Tunable configuration parameters, for testing or experimentation +[tunables] + +# The maximum allowed prefix (thus smallest size) for a VPC Subnet's +# IPv4 subnetwork. This size allows for ~60 hosts. +max_vpc_ipv4_subnet_prefix = 26 + +# Configuration for interacting with the dataplane daemon +[dendrite.switch0] +address = "[::1]:12224" + +[background_tasks] +dns_internal.period_secs_config = 60 +dns_internal.period_secs_servers = 60 +dns_internal.period_secs_propagation = 60 +dns_internal.max_concurrent_server_updates = 5 +dns_external.period_secs_config = 60 +dns_external.period_secs_servers = 60 +dns_external.period_secs_propagation = 60 +dns_external.max_concurrent_server_updates = 5 +metrics_producer_gc.period_secs = 60 +# How frequently we check the list of stored TLS certificates. This is +# approximately an upper bound on how soon after updating the list of +# certificates it will take _other_ Nexus instances to notice and stop serving +# them (on a sunny day). +external_endpoints.period_secs = 60 +nat_cleanup.period_secs = 30 +bfd_manager.period_secs = 30 +# How frequently to collect hardware/software inventory from the whole system +# (even if we don't have reason to believe anything has changed). +inventory.period_secs = 600 +# Maximum number of past collections to keep in the database +inventory.nkeep = 5 +# Disable inventory collection altogether (for emergencies) +inventory.disable = false +phantom_disks.period_secs = 30 +physical_disk_adoption.period_secs = 30 +blueprints.period_secs_load = 10 +blueprints.period_secs_execute = 60 +blueprints.period_secs_collect_crdb_node_ids = 180 +sync_service_zone_nat.period_secs = 30 +switch_port_settings_manager.period_secs = 30 +region_replacement.period_secs = 30 +region_replacement_driver.period_secs = 10 +# How frequently to query the status of active instances. +instance_watcher.period_secs = 30 +service_firewall_propagation.period_secs = 300 +v2p_mapping_propagation.period_secs = 30 +abandoned_vmm_reaper.period_secs = 60 +lookup_region_port.period_secs = 60 + +[default_region_allocation_strategy] +# allocate region on 3 random distinct zpools, on 3 random distinct sleds. +type = "random_with_distinct_sleds" + +# the same as random_with_distinct_sleds, but without requiring distinct sleds +# type = "random" + +# setting `seed` to a fixed value will make dataset selection ordering use the +# same shuffling order for every region allocation. +# seed = 0 + +################################################################################ +# INSTRUCTIONS: To run Nexus against an existing stack started with # +# `omicron-dev run-all`, you should only have to modify values in this # +# section. # +# # +# Modify the port numbers below based on the output of `omicron-dev run-all` # +################################################################################ + +[mgd] +# Look for "management gateway: http://[::1]:49188 (switch0)" +# The "http://" does not go in this string -- just the socket address. +switch0.address = "[::1]:49188" + +# Look for "management gateway: http://[::1]:39352 (switch1)" +# The "http://" does not go in this string -- just the socket address. +switch1.address = "[::1]:39352" + +[deployment.internal_dns] +# Look for "internal DNS: [::1]:54025" +# and adjust the port number below. +address = "[::1]:54025" +# You should not need to change this. +type = "from_address" + +[deployment.database] +# Look for "cockroachdb URL: postgresql://root@[::1]:43256/omicron?sslmode=disable" +# and adjust the port number below. +url = "postgresql://root@[::1]:43256/omicron?sslmode=disable" +# You should not need to change this. +type = "from_url" +################################################################################ diff --git a/nexus/src/app/background/tasks/sync_switch_configuration.rs b/nexus/src/app/background/tasks/sync_switch_configuration.rs index e8f07726a5..20a12d1127 100644 --- a/nexus/src/app/background/tasks/sync_switch_configuration.rs +++ b/nexus/src/app/background/tasks/sync_switch_configuration.rs @@ -63,6 +63,7 @@ use std::{ }; const DPD_TAG: Option<&'static str> = Some(OMICRON_DPD_TAG); +const PHY0: &str = "phy0"; // This is more of an implementation detail of the BGP implementation. It // defines the maximum time the peering engine will wait for external messages @@ -999,7 +1000,7 @@ impl BackgroundTask for SwitchPortSettingsManager { .communities_for_peer( opctx, port.port_settings_id.unwrap(), - &peer.port, + PHY0, //TODO https://github.com/oxidecomputer/omicron/issues/3062 IpNetwork::from(IpAddr::from(peer.addr)) ).await { Ok(cs) => cs.iter().map(|c| c.community.0).collect(), @@ -1017,7 +1018,7 @@ impl BackgroundTask for SwitchPortSettingsManager { let allow_import = match self.datastore.allow_import_for_peer( opctx, port.port_settings_id.unwrap(), - &peer.port, + PHY0, //TODO https://github.com/oxidecomputer/omicron/issues/3062 IpNetwork::from(IpAddr::from(peer.addr)), ).await { Ok(cs) => cs, @@ -1041,7 +1042,7 @@ impl BackgroundTask for SwitchPortSettingsManager { let allow_export = match self.datastore.allow_export_for_peer( opctx, port.port_settings_id.unwrap(), - &peer.port, + PHY0, //TODO https://github.com/oxidecomputer/omicron/issues/3062 IpNetwork::from(IpAddr::from(peer.addr)), ).await { Ok(cs) => cs, diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index 38cdac5fcb..18efe40e27 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -118,7 +118,7 @@ pub struct ControlPlaneTestContext { pub sled_agent2: sim::Server, pub oximeter: Oximeter, pub producer: ProducerServer, - pub gateway: HashMap, + pub gateway: BTreeMap, pub dendrite: HashMap, pub mgd: HashMap, pub external_dns_zone_name: String, @@ -280,7 +280,7 @@ pub struct ControlPlaneTestContextBuilder<'a, N: NexusServer> { pub sled_agent2: Option, pub oximeter: Option, pub producer: Option, - pub gateway: HashMap, + pub gateway: BTreeMap, pub dendrite: HashMap, pub mgd: HashMap, @@ -330,7 +330,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> { sled_agent2: None, oximeter: None, producer: None, - gateway: HashMap::new(), + gateway: BTreeMap::new(), dendrite: HashMap::new(), mgd: HashMap::new(), nexus_internal: None, diff --git a/nexus/tests/config.test.toml b/nexus/tests/config.test.toml index 8415a192b1..dfcaec2157 100644 --- a/nexus/tests/config.test.toml +++ b/nexus/tests/config.test.toml @@ -37,7 +37,7 @@ max_vpc_ipv4_subnet_prefix = 29 [deployment] # Identifier for this instance of Nexus. # NOTE: The test suite always overrides this. -id = "e6bff1ff-24fb-49dc-a54e-c6a350cd4d6c" +id = "913233fe-92a8-4635-9572-183f495429c4" rack_id = "c19a698f-c6f9-4a17-ae30-20d711b8f7dc" techport_external_server_port = 0 diff --git a/openapi/dns-server.json b/openapi/dns-server.json index 1b02199b76..0252c1538a 100644 --- a/openapi/dns-server.json +++ b/openapi/dns-server.json @@ -2,7 +2,12 @@ "openapi": "3.0.3", "info": { "title": "Internal DNS", - "version": "v0.1.0" + "description": "API for the internal DNS server", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "0.0.1" }, "paths": { "/config": { diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 796cf0bf63..cc12f6d032 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -31,8 +31,8 @@ byteorder = { version = "1.5.0" } bytes = { version = "1.6.0", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.4", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.2", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.5.9", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.5.9", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } console = { version = "0.15.8" } const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.18" } @@ -136,8 +136,8 @@ byteorder = { version = "1.5.0" } bytes = { version = "1.6.0", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.4", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.2", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.5.9", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.5.9", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } console = { version = "0.15.8" } const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.18" }