Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

### Added

- Add Liquid template rendering support for `picodata.yaml` config files (see [picodata.yaml](README.md#picodatayaml))
- Add configurable manifest.yaml.template path for build helper

### Changed
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ PICODATA_HTTP_LISTEN = "127.0.0.1:{{ 8000 | plus: instance_id }}"

Пайк позволяет использовать файл конфигурации Пикодаты вместе с запущенным кластером. Пример файла сразу генерируется командами `new` и `init`. Документацию к параметрам можно найти в [документации к Пикодате](https://docs.picodata.io/picodata/stable/reference/config/).

Файл `picodata.yaml` поддерживает [Liquid-шаблоны](https://shopify.dev/docs/api/liquid). При запуске кластера Пайк рендерит конфиг для каждого инстанса отдельно, подставляя переменные из контекста. Доступные переменные:

- `instance_id` — порядковый номер инстанса (начиная с 1)

Пример использования:

```yaml
instance:
name: "my-instance-{{ instance_id }}"
```

#### Настройка нескольких тиров

Для настройки необходимо указать нужные тиры в файле топологии topology.toml.
Expand Down
48 changes: 47 additions & 1 deletion src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,12 @@ impl PicodataInstance {

let config_path = run_params.plugin_path.join(&run_params.config_path);
if config_path.exists() {
// Template the config file with instance_id
let instance_config_path =
Self::template_config(&config_path, &instance_data_dir, &env_templates_ctx)?;
child.args([
"--config",
config_path.to_str().unwrap_or("./picodata.yaml"),
instance_config_path.to_str().unwrap_or("./picodata.yaml"),
]);
} else {
warn!(
Expand Down Expand Up @@ -538,6 +541,49 @@ impl PicodataInstance {
.collect()
}

/// Templates a picodata.yaml config file with Liquid variables (e.g., `instance_id`).
/// Returns the path to the templated config file in the instance data directory.
fn template_config(
config_path: &Path,
instance_data_dir: &Path,
ctx: &liquid::Object,
) -> Result<PathBuf> {
let config_content = fs::read_to_string(config_path).with_context(|| {
format!(
"failed to read picodata config at {}",
config_path.display()
)
})?;

let parser = liquid::ParserBuilder::with_stdlib()
.build()
.context("failed to build Liquid parser")?;

let template = parser.parse(&config_content).with_context(|| {
format!(
"failed to parse picodata config as Liquid template at {}",
config_path.display()
)
})?;

let rendered = template.render(ctx).with_context(|| {
format!(
"failed to render picodata config template at {}",
config_path.display()
)
})?;

let instance_config_path = instance_data_dir.join("picodata.yaml");
fs::write(&instance_config_path, &rendered).with_context(|| {
format!(
"failed to write templated picodata config to {}",
instance_config_path.display()
)
})?;

Ok(instance_config_path)
}

fn capture_logs(&mut self) -> Result<()> {
let mut rnd = rand::rng();
let instance_name_color = colored::CustomColor::new(
Expand Down
5 changes: 2 additions & 3 deletions tests/plugin_pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ fn find_archive(dir: &Path, name: &str, version: &str) -> PathBuf {
}
assert!(
!matches.is_empty(),
"No archive found in {} with prefix {}",
dir.display(),
prefix
"No archive found in {} with prefix {prefix}",
dir.display()
);
assert_eq!(
matches.len(),
Expand Down
78 changes: 78 additions & 0 deletions tests/plugin_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,81 @@ fn test_plugin_run_tests() {
String::from_utf8_lossy(&output.stderr)
);
}

#[test]
fn test_picodata_config_template_rendering() {
let plugin_path = Path::new("./tests/tmp/plugin-template-config-test");
init_plugin("plugin-template-config-test");

// Create a picodata.yaml config template with Liquid variables
let config_path = plugin_path.join("picodata.yaml");
let template_content = r#"cluster:
name: "test-cluster"
tier:
default:
can_vote: true
default_replication_factor: 2

instance:
name: "instance-{{ instance_id }}"
log:
level: warn
format: plain
memtx:
memory: 67108864
"#;

fs::write(&config_path, template_content).expect("Failed to write templated picodata.yaml");

let config_content = fs::read_to_string(&config_path).expect("Failed to read picodata.yaml");

// Verify that the template contains Liquid template variables
assert!(
config_content.contains("{{ instance_id }}"),
"picodata.yaml should contain {{ instance_id }} template variable"
);

// Test template rendering with Liquid
let parser = liquid::ParserBuilder::with_stdlib()
.build()
.expect("Failed to build Liquid parser");

let template = parser
.parse(&config_content)
.expect("Failed to parse picodata.yaml as Liquid template");

// Render with instance_id = 1
let ctx = liquid::object!({
"instance_id": 1,
});
let rendered = template
.render(&ctx)
.expect("Failed to render template with instance_id=1");

// Verify that the rendered output contains the substituted value
assert!(
rendered.contains("instance-1"),
"Rendered config should contain 'instance-1', got:\n{rendered}"
);
assert!(
!rendered.contains("{{ instance_id }}"),
"Rendered config should not contain template variable, got:\n{rendered}"
);

// Render with instance_id = 42
let ctx = liquid::object!({
"instance_id": 42,
});
let rendered = template
.render(&ctx)
.expect("Failed to render template with instance_id=42");

assert!(
rendered.contains("instance-42"),
"Rendered config should contain 'instance-42', got:\n{rendered}"
);
assert!(
!rendered.contains("{{ instance_id }}"),
"Rendered config should not contain template variable, got:\n{rendered}"
);
}
5 changes: 2 additions & 3 deletions tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ fn find_os_suffixed_archive(dir: &Path, name: &str, version: &str) -> PathBuf {
}
assert!(
!matches.is_empty(),
"No archive found in {} with prefix {}",
dir.display(),
prefix
"No archive found in {} with prefix {prefix}",
dir.display()
);
assert_eq!(
matches.len(),
Expand Down
Loading