Skip to content

Commit 04d3422

Browse files
authored
Merge pull request #561 from cbgbt/multicall-schnauzer
schnauzer: use multicall binary for v1 and v2
2 parents 468919d + 5e97421 commit 04d3422

File tree

11 files changed

+405
-176
lines changed

11 files changed

+405
-176
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Implementing Features
2+
3+
- Whenever you implement a feature or create a code change you MUST follow this process
4+
5+
## Implementation Process
6+
7+
1. You MUST implement the code required changes without waiting for human input.
8+
2. You MUST run the build to ensure tests pass.
9+
3. You MUST debug and fix any failures.
10+
4. You MUST thoroughly and pedantically follow any provided style guides, paying special attention to MUST, SHOULD, and MAY statements.
11+
5. When the code is finished, you MUST ensure that the following checks have been completed:
12+
- You MUST perform a build, based on the programming language (e.g. cargo build)
13+
- You MUST run tests based on language (e.g. cargo test or pytest)
14+
- You MUST check lints using the programming language preferences (e.g. cargo clippy, mypy + ruff)
15+
- You MUST format the changes (e.g. cargo fmt or black)
16+
6. You MUST print a status update summarizing the work done (you MUST NOT show any code, just do a verbal summary)
17+
7. You MUST perform a code-review using the style guide as a reference, paying special attention to SHOULD, MUST, and MAY statements. You MUST print a summary of the review (for code samples, you MUST NOT print large sections of code. You MAY only show handfuls of lines when it makes sense)
18+
8. You MUST implement the changes suggested by the code review by going back to step 1.
19+
9. You MUST iterate on the code until all elements of point 5. are complete.

.amazonq/rules/02-using-git.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## Using Git
2+
3+
- You MUST never use `git push` or any command that would send code off the host.
4+
- You MUST never perform deletions.
5+
- You MUST never commit any `.gitignore`d content to git.
6+
- You MUST create local commits with `git add` and `git commit`.
7+
- If anything goes wrong with Git, you MUST NOT delete data or attempt destructive operations.
8+
- You MUST NOT rebase changes or modify Git history in any way.
9+
- You MUST NOT use `git filter-branch`, `git reset --hard`, or similar commands.
10+
- If you encounter Git issues, you MUST immediately report them and exit gracefully.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Bottlerocket Git Commit Convention
2+
3+
* Commits MUST NOT follow the conventional commit format
4+
* Commits MUST follow this format:
5+
```
6+
<component>: <description>
7+
8+
<body>
9+
```
10+
11+
Where:
12+
* <component> MUST be the subsystem you modified (e.g., "prompt-plan", "implement", "code-review")
13+
* <description> MUST use present imperative tense (e.g., "add", "fix", "update", not "adds", "fixed", "updated")
14+
* <description> MUST be lowercase and MUST NOT end with a period
15+
* The commit message SHOULD complete the sentence: "If applied, this commit will ________"
16+
* If needed, you MAY add a body separated by a blank line, with lines no longer than 72 characters
17+
18+
Example:
19+
```
20+
prompt-plan: implement structured format parser
21+
22+
This change adds a parser for the structured prompt plan format that
23+
extracts prompt information and status.
24+
`

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ packages/*/*.sig
2424
packages/*/*-sig.txt
2525
packages/*/*.sign
2626
!packages/*/gpgkey-*.asc
27+
planning/

packages/os/os.spec

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ install -d %{buildroot}%{_cross_bindir}
595595
install -d %{buildroot}%{_cross_fips_bindir}
596596
for p in \
597597
apiserver \
598-
sundog schnauzer schnauzer-v2 bork \
598+
sundog schnauzer bork \
599599
corndog thar-be-settings thar-be-updates host-containers \
600600
storewolf settings-committer \
601601
migrator prairiedog certdog \
@@ -610,6 +610,9 @@ for p in \
610610
install -p -m 0755 %{__cargo_outdir}/${p} %{buildroot}%{_cross_bindir}
611611
done
612612

613+
# Create symlink for schnauzer-v2 to enable multicall behavior
614+
ln -s schnauzer %{buildroot}%{_cross_bindir}/schnauzer-v2
615+
613616
ln -s brush %{buildroot}%{_cross_bindir}/sh
614617
install -d %{buildroot}%{_cross_libexecdir}/brush/allowed-programs
615618

sources/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sources/api/schnauzer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ cidr.workspace = true
5050
# Workaround to enable a feature during integration tests.
5151
schnauzer = { workspace = true, features = ["testfakes"] }
5252
test-case.workspace = true
53+
tempfile.workspace = true
5354

5455
[build-dependencies]
5556
generate-readme.workspace = true

sources/api/schnauzer/src/bin/schnauzer-v2.rs

Lines changed: 0 additions & 15 deletions
This file was deleted.

sources/api/schnauzer/src/main.rs

Lines changed: 77 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,180 +1,97 @@
11
/*!
2-
# Introduction
2+
# Multicall Binary for schnauzer
33
4-
schnauzer is called by sundog as a setting generator.
5-
Its sole parameter is the name of the setting to generate.
6-
7-
The setting we're generating is expected to have a metadata key already set: "template".
8-
"template" is an arbitrary string with mustache template variables that reference other settings.
4+
This binary serves as both schnauzer (v1) and schnauzer-v2 based on how it's invoked.
5+
The dispatch mechanism checks argv[0] to determine which tool to run.
96
10-
For example, if we're generating "settings.x" and we have template "foo-{{ settings.bar }}", we look up the value of "settings.bar" in the API.
11-
If the returned value is "baz", our generated value will be "foo-baz".
7+
schnauzer is called by sundog as a setting generator.
8+
schnauzer-v2 is a more advanced settings generator for rendering handlebars templates.
129
13-
(The name "schnauzer" comes from the fact that Schnauzers are search and rescue dogs (similar to this search and replace task) and because they have mustaches.)
10+
(The name "schnauzer" comes from the fact that Schnauzers are search and rescue dogs
11+
(similar to this search and replace task) and because they have mustaches.)
1412
*/
1513

16-
use snafu::{ensure, OptionExt, ResultExt};
17-
use std::collections::HashMap;
18-
use std::string::String;
19-
use std::{env, process};
20-
21-
// Setting generators do not require dynamic socket paths at this moment.
22-
const API_METADATA_URI_BASE: &str = "/metadata/";
23-
24-
mod error {
25-
use http::StatusCode;
26-
use snafu::Snafu;
27-
28-
#[derive(Debug, Snafu)]
29-
#[snafu(visibility(pub(super)))]
30-
pub(super) enum Error {
31-
#[snafu(display("Error {}ing to {}: {}", method, uri, source))]
32-
APIRequest {
33-
method: String,
34-
uri: String,
35-
#[snafu(source(from(apiclient::Error, Box::new)))]
36-
source: Box<apiclient::Error>,
37-
},
38-
39-
#[snafu(display("Error {} when {}ing to '{}': {}", code, method, uri, response_body))]
40-
Response {
41-
method: String,
42-
uri: String,
43-
code: StatusCode,
44-
response_body: String,
45-
},
46-
47-
#[snafu(display("Error deserializing to JSON: {}", source))]
48-
DeserializeJson { source: serde_json::error::Error },
49-
50-
#[snafu(display("Error serializing to JSON '{}': {}", output, source))]
51-
SerializeOutput {
52-
output: String,
53-
source: serde_json::error::Error,
54-
},
55-
56-
#[snafu(display("Missing metadata {} for key: {}", meta, key))]
57-
MissingMetadata { meta: String, key: String },
58-
59-
#[snafu(display("Metadata {} expected to be {}, got: {}", meta, expected, value))]
60-
MetadataWrongType {
61-
meta: String,
62-
expected: String,
63-
value: String,
64-
},
65-
66-
#[snafu(display("Failed to build template registry: {}", source))]
67-
BuildTemplateRegistry { source: schnauzer::v1::Error },
14+
const SCHNAUZER_NAME: &str = "schnauzer";
15+
const SCHNAUZER_V2_NAME: &str = "schnauzer-v2";
16+
const DEFAULT_TOOL_NAME: &str = SCHNAUZER_V2_NAME;
6817

69-
#[snafu(display("Failed to get settings from API: {}", source))]
70-
GetSettings { source: schnauzer::v1::Error },
18+
use snafu::ResultExt;
19+
use std::env;
7120

72-
#[snafu(display(
73-
"Failed to render setting '{}' from template '{}': {}",
74-
setting_name,
75-
template,
76-
source
77-
))]
78-
RenderTemplate {
79-
setting_name: String,
80-
template: String,
81-
#[snafu(source(from(handlebars::RenderError, Box::new)))]
82-
source: Box<handlebars::RenderError>,
83-
},
84-
}
21+
/// Enumeration of available tools in this multicall binary
22+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23+
enum ToolName {
24+
SchnauzerV1,
25+
SchnauzerV2,
8526
}
86-
type Result<T> = std::result::Result<T, error::Error>;
8727

88-
/// Returns the value of a metadata key for a given data key, erroring if the value is not a
89-
/// string or is empty.
90-
async fn get_metadata(key: &str, meta: &str) -> Result<String> {
91-
let uri = &format!("{API_METADATA_URI_BASE}{meta}?keys={key}");
92-
let method = "GET";
93-
let (code, response_body) = apiclient::raw_request(constants::API_SOCKET, &uri, method, None)
94-
.await
95-
.context(error::APIRequestSnafu { method, uri })?;
96-
ensure!(
97-
code.is_success(),
98-
error::ResponseSnafu {
99-
method,
100-
uri,
101-
code,
102-
response_body
28+
impl ToolName {
29+
/// Determine which tool to run based on the program name
30+
fn from_program_name(name: &str) -> Self {
31+
match name {
32+
SCHNAUZER_NAME => Self::SchnauzerV1,
33+
_ => Self::SchnauzerV2,
10334
}
104-
);
105-
106-
// Metadata responses are of the form `{"data_key": METADATA}` so we pull out the value.
107-
let mut response_map: HashMap<String, serde_json::Value> =
108-
serde_json::from_str(&response_body).context(error::DeserializeJsonSnafu)?;
109-
let response_val = response_map
110-
.remove(key)
111-
.context(error::MissingMetadataSnafu { meta, key })?;
112-
113-
// Ensure it's a non-empty string
114-
let response_str = response_val
115-
.as_str()
116-
.with_context(|| error::MetadataWrongTypeSnafu {
117-
meta,
118-
expected: "string",
119-
value: response_val.to_string(),
120-
})?;
121-
ensure!(
122-
!response_str.is_empty(),
123-
error::MissingMetadataSnafu { meta, key }
124-
);
125-
Ok(response_str.to_string())
35+
}
12636
}
12737

128-
/// Print usage message.
129-
fn usage() -> ! {
130-
let program_name = env::args().next().unwrap_or_else(|| "program".to_string());
131-
eprintln!("Usage: {program_name} SETTING_KEY");
132-
process::exit(2);
38+
/// Extract program name from argv[0] for tool dispatch
39+
fn extract_program_name() -> String {
40+
env::args()
41+
.next()
42+
.and_then(|path| {
43+
std::path::Path::new(&path)
44+
.file_name()
45+
.and_then(|name| name.to_str())
46+
.map(|s| s.to_string())
47+
})
48+
.unwrap_or_else(|| DEFAULT_TOOL_NAME.to_string())
13349
}
13450

135-
/// Parses args for the setting key name.
136-
fn parse_args(mut args: env::Args) -> String {
137-
let arg = args.nth(1).unwrap_or_else(|| "--help".to_string());
138-
if arg == "--help" || arg == "-h" {
139-
usage()
51+
#[snafu::report]
52+
#[tokio::main]
53+
async fn main() -> Result<(), snafu::Whatever> {
54+
let program_name = extract_program_name();
55+
let tool = ToolName::from_program_name(&program_name);
56+
57+
// Dispatch to the appropriate CLI module
58+
match tool {
59+
ToolName::SchnauzerV1 => schnauzer::v1::cli::run()
60+
.await
61+
.whatever_context("schnauzer v1 execution failed"),
62+
ToolName::SchnauzerV2 => schnauzer::v2::cli::run()
63+
.await
64+
.whatever_context("schnauzer v2 execution failed"),
14065
}
141-
arg
142-
}
143-
144-
async fn run() -> Result<()> {
145-
let setting_name = parse_args(env::args());
146-
147-
let registry =
148-
schnauzer::v1::build_template_registry().context(error::BuildTemplateRegistrySnafu)?;
149-
let template = get_metadata(&setting_name, "templates").await?;
150-
let settings = schnauzer::v1::get_settings(constants::API_SOCKET)
151-
.await
152-
.context(error::GetSettingsSnafu)?;
153-
154-
let setting =
155-
registry
156-
.render_template(&template, &settings)
157-
.context(error::RenderTemplateSnafu {
158-
setting_name,
159-
template,
160-
})?;
161-
162-
// sundog expects JSON-serialized output so that many types can be represented, allowing the
163-
// API model to use more accurate types.
164-
let output = serde_json::to_string(&setting)
165-
.context(error::SerializeOutputSnafu { output: &setting })?;
166-
167-
println!("{output}");
168-
Ok(())
16966
}
17067

171-
// Returning a Result from main makes it print a Debug representation of the error, but with Snafu
172-
// we have nice Display representations of the error, so we wrap "main" (run) and print any error.
173-
// https://github.com/shepmaster/snafu/issues/110
174-
#[tokio::main]
175-
async fn main() {
176-
if let Err(e) = run().await {
177-
eprintln!("{e}");
178-
process::exit(1);
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
72+
#[test]
73+
fn test_tool_name_from_program_name() {
74+
// Given Various program names
75+
// When Converting program names to ToolName enum
76+
// Then Should correctly identify schnauzer v1 vs v2
77+
78+
// Test schnauzer v1 detection
79+
assert_eq!(
80+
ToolName::from_program_name("schnauzer"),
81+
ToolName::SchnauzerV1
82+
);
83+
84+
// Test schnauzer v2 detection
85+
assert_eq!(
86+
ToolName::from_program_name("schnauzer-v2"),
87+
ToolName::SchnauzerV2
88+
);
89+
90+
// Test default behavior (unknown names default to v2)
91+
assert_eq!(
92+
ToolName::from_program_name("unknown"),
93+
ToolName::SchnauzerV2
94+
);
95+
assert_eq!(ToolName::from_program_name(""), ToolName::SchnauzerV2);
17996
}
18097
}

0 commit comments

Comments
 (0)