Skip to main content

helm_schema_cli/
lib.rs

1//! Command-line argument model and invocation policy for `helm-schema`.
2
3/// Typed command-line arguments and option validation.
4pub mod cli;
5mod config;
6mod diag_emit;
7
8use std::io::{BufWriter, Write};
9use std::path::Path;
10
11use helm_schema::chart_source::RootChartSource;
12use helm_schema::diagnostics::DiagnosticSink;
13use helm_schema::output::{FetchPolicy, LoadBudget, PolicyInputOptions, write_schema_json};
14use helm_schema::{AnalysisSession, EngineResult};
15use tracing_subscriber::Layer as _;
16use tracing_subscriber::layer::SubscriberExt as _;
17
18pub use cli::Cli;
19pub use helm_schema::generation::{GenerateOptions, SchemaProfile};
20pub use helm_schema::provider::ProviderOptions;
21pub use helm_schema::{CliError, flatten, schema_override};
22
23/// Run the CLI.
24///
25/// # Errors
26///
27/// Returns an error if chart discovery fails, a template/values file cannot be
28/// read/parsed, the schema cannot be generated, or output cannot be written.
29pub fn run(cli: Cli) -> EngineResult<()> {
30    let trace_output = cli.perf.trace_output.clone();
31    if let Some(trace_output) = trace_output {
32        let trace_file = create_output_file(&trace_output)?;
33        let perfetto_layer =
34            tracing_perfetto::PerfettoLayer::new(std::sync::Mutex::new(trace_file))
35                .with_debug_annotations(true)
36                .with_filter(tracing_subscriber::filter::filter_fn(|metadata| {
37                    metadata.target().starts_with("helm_schema")
38                        || metadata
39                            .target()
40                            .starts_with("helm_schema_json_schema_minify")
41                }));
42        let subscriber = tracing_subscriber::registry().with(perfetto_layer);
43        let dispatch = tracing::Dispatch::new(subscriber);
44        return tracing::dispatcher::with_default(&dispatch, || run_inner(cli));
45    }
46
47    run_inner(cli)
48}
49
50fn run_inner(cli: Cli) -> EngineResult<()> {
51    let run_span = tracing::info_span!(
52        "helm_schema_run",
53        chart_dir = %cli.chart_dir.display()
54    );
55    let _entered = run_span.enter();
56
57    let root_source = RootChartSource::open(&cli.chart_dir, LoadBudget::default())?;
58    let effective_config = config::resolve(
59        &root_source,
60        &cli.chart_dir,
61        cli.config.as_deref(),
62        cli.no_config,
63        cli.profile,
64        cli.emission,
65    )?;
66
67    let diagnostics = DiagnosticSink::new();
68    if !effective_config.file_weakening.is_empty() {
69        diagnostics.push(
70            helm_schema::diagnostics::Diagnostic::DiscoveredConfigWeakensEmission {
71                disabled_knobs: effective_config
72                    .file_weakening
73                    .iter()
74                    .map(|knob| (*knob).to_string())
75                    .collect(),
76                explicit: effective_config.file_weakening_is_explicit,
77            },
78        );
79    }
80    if cli.print_effective_config {
81        let stdout = std::io::stdout();
82        let mut out = BufWriter::new(stdout.lock());
83        out.write_all(effective_config.to_yaml()?.as_bytes())?;
84        out.flush()?;
85        diag_emit::emit_to_stderr(&diagnostics, cli.diag.diag_format);
86        return Ok(());
87    }
88
89    let generated = (|| {
90        cli.crd.validate().map_err(CliError::CliValidation)?;
91        let fallback_window = cli
92            .k8s
93            .resolved_fallback_window()
94            .map_err(CliError::CliValidation)?;
95        let chart_dir = root_source.into_chart_dir();
96        let provider_options = ProviderOptions {
97            k8s_versions: cli.k8s.k8s_version.clone(),
98            k8s_version_fallback_window: fallback_window,
99            k8s_schema_mirrors: cli.k8s.k8s_schema_mirror.clone(),
100            k8s_schema_cache_dir: cli.k8s.k8s_schema_cache_dir.clone(),
101            no_cache: cli.k8s.no_cache,
102            allow_net: !cli.k8s.offline,
103            disable_k8s_schemas: cli.k8s.no_k8s_schemas,
104            crd_lookup_loose: matches!(cli.crd.lookup_mode(), cli::CrdVersionLookup::Loose),
105            crd_catalog_mirrors: cli.crd.crd_catalog_mirror.clone(),
106            crd_catalog_cache_dir: cli.crd.crd_catalog_cache_dir.clone(),
107            crd_override_dir: cli.crd.crd_override_dir.clone(),
108            local_schema_universe: helm_schema::provider::LocalSchemaUniverse::default(),
109            crd_cache_record_source: cli.crd.crd_cache_record_source,
110            api_version_guess: cli.inference.enabled(),
111        };
112        let opts = GenerateOptions {
113            chart_dir,
114            include_tests: !cli.chart.exclude_tests,
115            include_subchart_values: !cli.chart.no_subchart_values,
116            values_files: cli.chart.values_files.clone(),
117            infer_required: cli.chart.infer_required,
118            emission: effective_config.selection,
119            provider: provider_options,
120        };
121        let session = AnalysisSession::with_diagnostics(opts, diagnostics.clone());
122        let policy_input_options = PolicyInputOptions {
123            fetch_policy: FetchPolicy::input_assembly(!cli.k8s.offline),
124            load_budget: LoadBudget::default(),
125        };
126        session.emit_with_policy_paths(
127            &cli.override_schema,
128            policy_input_options,
129            cli.output.emit_request(),
130        )
131    })();
132    diag_emit::emit_to_stderr(&diagnostics, cli.diag.diag_format);
133    let schema = generated?;
134
135    let json_format = cli.output.json_format();
136
137    if let Some(path) = cli.output.output {
138        let mut out = BufWriter::new(create_output_file(&path)?);
139        write_schema_json(&mut out, &schema, json_format)
140            .map_err(|err| write_output_error_with_path(err, &path))?;
141        out.flush().map_err(|err| CliError::WriteOutput {
142            path: path.clone(),
143            source: err,
144        })?;
145    } else {
146        let stdout = std::io::stdout();
147        let mut out = BufWriter::new(stdout.lock());
148        write_schema_json(&mut out, &schema, json_format)?;
149        out.flush()?;
150    }
151
152    Ok(())
153}
154
155fn create_output_file(path: &Path) -> EngineResult<std::fs::File> {
156    if let Some(parent) = path.parent() {
157        std::fs::create_dir_all(parent).map_err(|err| CliError::CreateOutputDir {
158            path: parent.to_path_buf(),
159            source: err,
160        })?;
161    }
162    std::fs::File::create(path).map_err(|err| CliError::WriteOutput {
163        path: path.to_path_buf(),
164        source: err,
165    })
166}
167
168fn write_output_error_with_path(err: CliError, path: &Path) -> CliError {
169    match err {
170        CliError::Io(source) => CliError::WriteOutput {
171            path: path.to_path_buf(),
172            source,
173        },
174        err => err,
175    }
176}