Skip to content

Commit 9161b94

Browse files
authored
Merge pull request #293 from timescale/feat/remove-pipeline-inline-sources
feat!: remove pipeline-embedded sources: blocks
2 parents 02aa762 + 6c9cfb6 commit 9161b94

48 files changed

Lines changed: 773 additions & 945 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,18 +251,20 @@ jobs:
251251
--pipeline tests/fixtures/dynamic-pipelines/pipelines/multi_format.yml \
252252
--pipeline tests/fixtures/dynamic-pipelines/pipelines/extract_languages.yml \
253253
--pipeline tests/fixtures/dynamic-pipelines/pipelines/include_expansion.yml \
254+
--source tests/fixtures/dynamic-pipelines/source-files/ \
254255
--resolve-sources --verbose
255256
- name: Golden file comparison for rsigma pipeline resolve
256257
run: |
257258
failed=0
258259
for pipeline in tests/fixtures/dynamic-pipelines/pipelines/*.yml; do
259260
name=$(basename "$pipeline" .yml)
260261
golden="tests/fixtures/dynamic-pipelines/golden/${name}.json"
262+
source_file="tests/fixtures/dynamic-pipelines/source-files/${name}.yml"
261263
if [ ! -f "$golden" ]; then
262264
echo "SKIP: no golden file for ${name}"
263265
continue
264266
fi
265-
actual=$(./target/release/rsigma pipeline resolve --pipeline "$pipeline" --pretty)
267+
actual=$(./target/release/rsigma pipeline resolve --pipeline "$pipeline" --source-file "$source_file" --pretty)
266268
if ! diff -u "$golden" <(echo "$actual") > /tmp/diff_${name}.txt 2>&1; then
267269
echo "FAIL: ${name} resolve output differs from golden file"
268270
cat /tmp/diff_${name}.txt

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to RSigma are documented in this file. Each entry correspond
44

55
## [Unreleased]
66

7+
### Removed pipeline-embedded `sources:` blocks (#293)
8+
9+
Dynamic source declarations no longer live inside pipeline files. A pipeline that still declares an inline `sources:` block is now rejected with a hard parse error that points at `rsigma rule migrate-sources`; source declarations come exclusively from standalone `--source` files, and a pipeline only references them with `${source.<id>}`. This completes the deprecation cycle started in v0.12.0 (#135, visible-deprecated) and continued in v0.13.0 (#136, hidden from docs).
10+
11+
- **Library API.** `rsigma_eval::Pipeline` drops its `sources` field; `Pipeline::is_dynamic()` is now driven purely by `${source.*}` references, and `validate_source_refs` no longer takes a pipeline-local declaration set. `parse_sources` is now exported for tooling that reads a raw `sources:` block. The runtime `RuntimeEngine` gains `set_external_sources`, resolving and expanding references against the external declarations (carried across hot-reload), and `expand_includes` takes the external sources for its remote-include check.
12+
- **Reference detection fix.** List-valued pipeline `vars` (the common `value_placeholders` shape, e.g. `malicious_commands: ["${source.cmd_list}"]`) are now correctly recognized as dynamic source references; previously only scalar var values were scanned, which the removed inline `sources:` block had masked.
13+
- **`rule migrate-sources`** reads the inline `sources:` block directly (rather than through the now-rejecting pipeline parser) so it keeps working as the migration path.
14+
- **Docs and tests** move to the external-only model throughout; the runtime `pipeline_deprecation` module and its stderr warning are gone.
15+
716
### Removed the deprecated flat CLI aliases (#292)
817

918
The twelve flat top-level subcommands (`eval`, `daemon`, `parse`, `validate`, `lint`, `fields`, `condition`, `stdin`, `convert`, `list-targets`, `list-formats`, `resolve`) are removed. They shipped as visible-deprecated forwarders in v0.12.0 (#124), were hidden from `rsigma --help` in v0.13.0 (#125), and reach end-of-life here. Invoking a removed alias now fails with clap's `unrecognized subcommand` error and lists the available command groups. Use the noun-led groups instead: `engine eval`, `engine daemon`, `rule parse`, `rule validate`, `rule lint`, `rule fields`, `rule condition`, `rule stdin`, `backend convert`, `backend targets`, `backend formats`, and `pipeline resolve`. The per-alias forwarding dispatch and the stderr deprecation warning are gone; the group enums remain the single source of truth for every argument.

crates/rsigma-cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ cat rule.yml | rsigma rule stdin
10411041

10421042
### `rule migrate-sources`: Extract pipeline sources into standalone files
10431043

1044-
Extract pipeline-embedded `sources:` blocks into standalone source files. Pipeline-embedded sources are deprecated; this tool automates the migration to the `--source` flag.
1044+
Extract pipeline-embedded `sources:` blocks into standalone source files. Pipeline-embedded sources were removed in v1.0; this tool automates the migration to the `--source` flag for any pipeline that still declares them.
10451045

10461046
| Argument | Type | Default | Description |
10471047
|----------|------|---------|-------------|
@@ -1197,7 +1197,7 @@ rsigma engine daemon -r rules/ -p pipelines/ --source sources.d/ # loads all *
11971197

11981198
External source files decouple source configuration from pipeline logic, so pipelines stay reusable across environments. Source IDs must be unique across every `--source` file. The flag is repeatable, so multiple files can be combined (each with its own per-team or per-data-source ownership).
11991199

1200-
> **Deprecated.** Declaring `sources:` inline in a pipeline file is deprecated and will be removed in v1.0 (tracked in [#137](https://github.com/timescale/rsigma/issues/137)). The parser still accepts it but prints a `warning:` line on stderr at every load. Migrate with `rsigma rule migrate-sources -p <dir-or-file> -o sources.yml` and load the result via `--source sources.yml`.
1200+
> **Removed in v1.0.** Declaring `sources:` inline in a pipeline file is no longer accepted (tracked in [#137](https://github.com/timescale/rsigma/issues/137)); the parser rejects such a pipeline with a hard error pointing at the migration tool. Migrate with `rsigma rule migrate-sources -p <dir-or-file> -o sources.yml` and load the result via `--source sources.yml`.
12011201

12021202
### Source types
12031203

crates/rsigma-cli/src/commands/daemon.rs

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,28 +1484,15 @@ fn run_daemon(
14841484
process::exit(exit_code::CONFIG_ERROR);
14851485
});
14861486

1487-
let pipeline_sources: Vec<_> = pipelines
1488-
.iter()
1489-
.flat_map(|p| {
1490-
p.sources
1491-
.iter()
1492-
.map(|s| (s.clone(), p.name.clone()))
1493-
.collect::<Vec<_>>()
1494-
})
1495-
.collect();
1496-
1497-
// The pipeline-embedded `sources:` deprecation warning is emitted from
1498-
// `load_pipelines` (called above), which de-duplicates across hot-reloads
1499-
// and covers every CLI entry point that loads a pipeline file.
1500-
1501-
let source_registry = rsigma_runtime::sources::registry::DaemonSourceRegistry::new(
1502-
external_sources,
1503-
pipeline_sources,
1504-
)
1505-
.unwrap_or_else(|e| {
1506-
eprintln!("Source ID collision: {e}");
1507-
process::exit(exit_code::CONFIG_ERROR);
1508-
});
1487+
// Source declarations come exclusively from external `--source` files;
1488+
// pipelines only reference them. The registry therefore holds only the
1489+
// external sources.
1490+
let source_registry =
1491+
rsigma_runtime::sources::registry::DaemonSourceRegistry::from_external(external_sources)
1492+
.unwrap_or_else(|e| {
1493+
eprintln!("Source ID collision: {e}");
1494+
process::exit(exit_code::CONFIG_ERROR);
1495+
});
15091496

15101497
// `value_parser` (and the config schema) restrict this to off/summary/full.
15111498
let match_detail = match_detail

crates/rsigma-cli/src/commands/migrate_sources.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::collections::HashMap;
55
use std::path::PathBuf;
66

77
use clap::Args;
8-
use rsigma_eval::parse_pipeline_file;
8+
use rsigma_eval::parse_sources;
99

1010
/// Arguments for `rsigma rule migrate-sources`.
1111
#[derive(Args, Debug)]
@@ -79,23 +79,26 @@ pub(crate) fn cmd_migrate_sources(args: MigrateSourcesArgs) {
7979
}
8080
};
8181

82-
let pipeline = match parse_pipeline_file(path) {
83-
Ok(p) => p,
82+
// The pipeline parser rejects inline `sources:` blocks, so read the
83+
// block straight out of the raw YAML instead. A pipeline that no
84+
// longer declares sources is simply skipped.
85+
let sources = match extract_inline_sources(&content) {
86+
Ok(sources) => sources,
8487
Err(e) => {
85-
eprintln!("Error parsing pipeline {}: {e}", path.display());
88+
eprintln!("Error parsing sources in {}: {e}", path.display());
8689
std::process::exit(crate::exit_code::RULE_ERROR);
8790
}
8891
};
8992

90-
if pipeline.sources.is_empty() {
93+
if sources.is_empty() {
9194
pipelines_without_sources += 1;
9295
continue;
9396
}
9497

9598
pipelines_with_sources += 1;
9699

97100
let mut extracted = Vec::new();
98-
for source in &pipeline.sources {
101+
for source in &sources {
99102
if let Some(prev_pipeline) = seen_ids.get(&source.id) {
100103
eprintln!(
101104
"Error: source ID '{}' declared in both '{}' and '{}'. \
@@ -211,6 +214,24 @@ struct ExtractedSource {
211214
raw_yaml: String,
212215
}
213216

217+
/// Parse a pipeline file's raw YAML and return its inline `sources:`
218+
/// declarations, or an empty vector when the file declares none. Errors only
219+
/// on a malformed `sources:` block (an unknown source type, a missing `id`,
220+
/// etc.), so a clean, already-migrated pipeline is a no-op.
221+
fn extract_inline_sources(
222+
content: &str,
223+
) -> Result<Vec<rsigma_eval::pipeline::sources::DynamicSource>, rsigma_eval::EvalError> {
224+
let value: yaml_serde::Value = yaml_serde::from_str(content)
225+
.map_err(|e| rsigma_eval::EvalError::InvalidModifiers(format!("pipeline YAML: {e}")))?;
226+
let Some(node) = value
227+
.as_mapping()
228+
.and_then(|m| m.get(yaml_serde::Value::String("sources".to_string())))
229+
else {
230+
return Ok(Vec::new());
231+
};
232+
parse_sources(node)
233+
}
234+
214235
/// Extract the raw YAML text for a single source entry from a pipeline file.
215236
/// Falls back to a simple serialization if the source can't be found by ID.
216237
fn extract_source_yaml(content: &str, source_id: &str) -> String {

crates/rsigma-cli/src/commands/resolve.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ async fn resolve_async(
8383
}
8484
}
8585

86+
// Pipelines only reference sources now; the declarations come from the
87+
// `--source-file` flags loaded above. Parse each pipeline so a stale
88+
// inline `sources:` block still surfaces its migration error, and note
89+
// any pipeline that references no sources (the command's output is
90+
// driven entirely by the loaded source declarations either way).
8691
for path in &pipeline_paths {
8792
let pipeline = match parse_pipeline_file(path) {
8893
Ok(p) => p,
@@ -92,25 +97,11 @@ async fn resolve_async(
9297
}
9398
};
9499

95-
if !pipeline.sources.is_empty() {
96-
rsigma_runtime::warn_pipeline_inline_sources(path, &pipeline.name);
97-
}
98-
99-
if !pipeline.is_dynamic() && source_files.is_empty() {
100+
if !pipeline.is_dynamic() {
100101
eprintln!(
101-
"Pipeline '{}' has no dynamic sources, skipping.",
102+
"Pipeline '{}' references no dynamic sources.",
102103
pipeline.name
103104
);
104-
continue;
105-
}
106-
107-
for source in &pipeline.sources {
108-
if let Some(ref filter) = source_filter
109-
&& source.id != *filter
110-
{
111-
continue;
112-
}
113-
all_sources.push((pipeline.name.clone(), source.clone()));
114105
}
115106
}
116107

crates/rsigma-cli/src/commands/validate.rs

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -171,35 +171,34 @@ fn resolve_validate_sources(
171171
let mut resolved_pipelines = Vec::with_capacity(pipelines.len());
172172
let mut source_errors: Vec<String> = Vec::new();
173173

174-
// Resolve external sources first so they populate the cache
175-
if !external_sources.is_empty()
176-
&& let Err(e) = rt.block_on(rsigma_runtime::sources::resolve_all(
177-
&resolver,
178-
&external_sources,
179-
))
180-
{
181-
source_errors.push(format!("external sources: {e}"));
182-
}
174+
// Resolve the external sources once into a shared data map that
175+
// every dynamic pipeline's `${source.*}` references expand against.
176+
let resolved_data = match rt.block_on(rsigma_runtime::sources::resolve_all(
177+
&resolver,
178+
&external_sources,
179+
)) {
180+
Ok(data) => data,
181+
Err(e) => {
182+
source_errors.push(format!("external sources: {e}"));
183+
std::collections::HashMap::new()
184+
}
185+
};
183186

184187
for pipeline in &pipelines {
185188
if pipeline.is_dynamic() {
186-
match rt.block_on(rsigma_runtime::sources::resolve_all(
187-
&resolver,
188-
&pipeline.sources,
189-
)) {
190-
Ok(resolved_data) => {
191-
let expanded =
192-
rsigma_runtime::sources::template::TemplateExpander::expand(
193-
pipeline,
194-
&resolved_data,
195-
);
196-
resolved_pipelines.push(expanded);
197-
}
198-
Err(e) => {
199-
source_errors.push(format!("pipeline '{}': {e}", pipeline.name));
200-
resolved_pipelines.push(pipeline.clone());
201-
}
189+
let mut expanded = rsigma_runtime::sources::template::TemplateExpander::expand(
190+
pipeline,
191+
&resolved_data,
192+
);
193+
if let Err(e) = rsigma_runtime::sources::include::expand_includes(
194+
&mut expanded,
195+
&resolved_data,
196+
&external_sources,
197+
false,
198+
) {
199+
source_errors.push(format!("pipeline '{}': {e}", pipeline.name));
202200
}
201+
resolved_pipelines.push(expanded);
203202
} else {
204203
resolved_pipelines.push(pipeline.clone());
205204
}

crates/rsigma-cli/src/daemon/server.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,17 @@ pub async fn run_daemon(config: DaemonConfig) {
384384
let resolver: Arc<dyn rsigma_runtime::sources::SourceResolver> = instrumented;
385385
engine.set_source_resolver(resolver.clone());
386386

387+
// Feed the engine the external source declarations so it can resolve
388+
// and expand `${source.*}` references in the pipelines.
389+
engine.set_external_sources(
390+
config
391+
.source_registry
392+
.sources()
393+
.into_iter()
394+
.cloned()
395+
.collect(),
396+
);
397+
387398
// Resolve dynamic sources at startup (blocks on required sources)
388399
if let Err(e) = engine.resolve_dynamic_pipelines().await {
389400
tracing::error!(error = %e, "Failed to resolve required dynamic sources at startup");

crates/rsigma-cli/src/main.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -568,16 +568,14 @@ pub(crate) fn load_pipelines(paths: &[PathBuf]) -> Vec<Pipeline> {
568568
Ok(p) => {
569569
eprintln!("Loaded pipeline: {} (priority {})", p.name, p.priority);
570570
if p.is_dynamic() {
571-
let source_ids: Vec<&str> =
572-
p.sources.iter().map(|s| s.id.as_str()).collect();
573-
eprintln!(" dynamic source(s): {}", source_ids.join(", "));
574-
}
575-
// The inline-sources deprecation warning lives in
576-
// rsigma-runtime, which is only linked with the `daemon`
577-
// feature. Builds without it cannot resolve sources anyway.
578-
#[cfg(feature = "daemon")]
579-
if !p.sources.is_empty() {
580-
rsigma_runtime::warn_pipeline_inline_sources(path, &p.name);
571+
let source_ids: Vec<&str> = p
572+
.dynamic_references()
573+
.iter()
574+
.map(|r| r.source_id.as_str())
575+
.collect::<std::collections::BTreeSet<&str>>()
576+
.into_iter()
577+
.collect();
578+
eprintln!(" dynamic source ref(s): {}", source_ids.join(", "));
581579
}
582580
pipelines.push(p);
583581
}

0 commit comments

Comments
 (0)