Skip to main content

add-sugar

Add syntactic sugar features to Butane experimental spec versions

来源信息

仓库
coreos/ignition
最近来源活动
2026年7月28日 20:13
检测到的 SKILL.md 语言
英语
星标
976
分支
298

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
add-sugar
description
Add syntactic sugar features to Butane experimental spec versions
# Add Sugar Feature ## What it does Guides and scaffolds the addition of a new syntactic sugar feature to a Butane experimental spec version: 1. Gathers requirements from the user (spec type, field design, translation behavior) 2. Validates prerequisites (experimental spec exists, git status clean) 3. Adds struct definitions to `schema.go` 4. Implements translation (desugaring) logic in `translate.go` 5. Adds test cases in `translate_test.go` 6. Adds validation logic in `validate.go` and tests in `validate_test.go` (if needed) 7. Adds error constants to `config/common/errors.go` 8. Updates documentation descriptors in `internal/doc/butane.yaml` 9. Runs `./generate` to regenerate spec docs 10. Adds examples to `docs/examples.md` 11. Adds a release note to `docs/release-notes.md` 12. Runs `./test` to validate everything compiles and passes ## Prerequisites - Go toolchain installed - Target experimental spec version exists (directory ends with `_exp`) - Understanding of what the sugar should do (what YAML the user writes, what Ignition config it generates) ## Usage ```bash # Interactive mode - will ask for details /add-sugar # Target a specific spec /add-sugar --spec fcos/v1_8_exp --field boot_device.luks.method # Base spec sugar (distro-independent) /add-sugar --spec base/v0_8_exp --field storage.files.parent ``` ## Workflow ### Step 1: Gather Requirements If not provided via arguments, ask the user: 1. **Target spec**: Where should the sugar live? - **Base spec** (`base/v0_8_exp`): distro-independent, will appear in all variants - **Distro spec** (e.g., `config/fcos/v1_8_exp`, `config/openshift/v4_22_exp`): distro-specific 2. **Feature description**: What does the sugar do? - What YAML fields does the user write? - What Ignition config does it expand to? - Any validation constraints? 3. **Schema design**: Ask the user to describe or confirm: - Field name(s) and types - Whether it's a new top-level field or nested within an existing struct - Any new struct types needed 4. **Translation approach**: Per `docs/development.md:62`: - **Config merging** (recommended): Generate a fresh Ignition config struct, then use `baseutil.MergeTranslatedConfigs()` to merge with the user's config. The desugared struct is the merge parent, user config is child. - **Direct modification**: Only if config merging is not expressive enough. 5. **Validation needs**: What input constraints exist? - Required fields - Valid value ranges - Mutually exclusive options ### Step 2: Pre-flight Validation Run these checks: ```bash # Verify experimental spec directory exists ls -la base/{version}/ || ls -la config/{distro}/{version}/ # Check that the version ends with _exp # CRITICAL: Sugar must ONLY be added to experimental specs # Check git status git status --porcelain ``` **Stop if**: - Target spec does not exist - Target spec is NOT experimental (name must end with `_exp`) - Working directory has unexpected uncommitted changes ### Step 3: Update Schema **File**: `{spec_dir}/schema.go` Read the existing schema file first to understand the current struct layout. #### 3a: Adding a new top-level field to Config If the sugar is a new top-level section (like `boot_device` or `grub`), add a field to the `Config` struct: ```go type Config struct { base.Config `yaml:",inline"` BootDevice BootDevice `yaml:"boot_device"` Grub Grub `yaml:"grub"` NewSugar NewSugar `yaml:"new_sugar"` // ADD THIS } ``` Then add the new struct type(s): ```go type NewSugar struct { FieldOne *string `yaml:"field_one"` FieldTwo *bool `yaml:"field_two"` } ``` #### 3b: Adding a field to an existing struct in base If extending an existing base struct (like adding `parent` to `File`), modify the struct in `base/{version}/schema.go`: ```go type File struct { // ... existing fields ... NewField NewFieldType `yaml:"new_field"` // ADD THIS } ``` **Conventions**: - Use `*string`, `*bool`, `*int` for optional scalar fields - Use `[]Type` for lists - Use struct types for nested objects - YAML tags use `snake_case` - Add ` butane:"auto_skip"` tag for fields not in the Ignition spec that should be automatically filtered from the output (see `config/util/filter.go`) ### Step 4: Implement Translation **File**: `{spec_dir}/translate.go` Read the existing translate.go to understand the current translation pipeline. #### 4a: Config Merging Pattern (Recommended) This is the recommended approach per `docs/development.md:62`. The desugared config is the merge parent, user config is the child, so users can override sugar-generated values. For **distro specs** (e.g., `config/fcos/v1_8_exp/translate.go`): Add a new processing function and call it from `ToIgn3_7Unvalidated()`: ```go func (c Config) ToIgn3_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { ret, ts, r := c.Config.ToIgn3_7Unvalidated(options) if r.IsFatal() { return types.Config{}, translate.TranslationSet{}, r } // Existing sugar processing... r.Merge(c.processBootDevice(&ret, &ts, options)) // ADD: Call new sugar processing retp, tsp, rp := c.processNewSugar(options) retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) ret = retConfig.(types.Config) r.Merge(rp) return ret, ts, r } ``` Implement the processing function: ```go func (c Config) processNewSugar(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { rendered := types.Config{} ts := translate.NewTranslationSet("yaml", "json") var r report.Report // Early return if sugar is not being used if /* sugar not configured */ { return rendered, ts, r } yamlPath := path.New("yaml", "new_sugar") // Generate Ignition config elements // Example: creating a file file := types.File{ Node: types.Node{ Path: "/path/to/generated/file", }, FileEmbedded1: types.FileEmbedded1{ Contents: types.Resource{ Source: util.StrToPtr("data:,generated-content"), }, }, } rendered.Storage.Files = append(rendered.Storage.Files, file) // Track translations for error reporting ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) return rendered, ts, r } ``` For **base specs** (e.g., `base/v0_8_exp/translate.go`): The pattern is the same, but the processing function is called from the base `ToIgn3_7Unvalidated()` and operates on base types. When modifying translation at the base level, you may need to: - Create or modify a custom translator function (e.g., `translateStorage()`) - Register it with `tr.AddCustomTranslator()` #### 4b: Direct Modification Pattern (Alternative) Only use this when config merging isn't expressive enough: ```go func (c Config) processNewSugar(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { var r report.Report if /* sugar not configured */ { return r } // Directly modify the Ignition config config.Storage.Files = append(config.Storage.Files, types.File{...}) // Track translations yamlPath := path.New("yaml", "new_sugar") jsonPath := path.New("json", "storage", "files", len(config.Storage.Files)-1) ts.AddFromCommonSource(yamlPath, jsonPath, config.Storage.Files[len(config.Storage.Files)-1]) return r } ``` **Key imports** (add as needed): ```go import ( baseutil "github.com/coreos/butane/base/util" "github.com/coreos/butane/config/common" "github.com/coreos/butane/translate" "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/config/v3_7_experimental/types" "github.com/coreos/vcontext/path" "github.com/coreos/vcontext/report" ) ``` **IMPORTANT**: The Ignition types import version must match the one already used in the file. Check the existing imports before adding new ones. ### Step 5: Write Tests **File**: `{spec_dir}/translate_test.go` Read the existing test file to understand the test patterns used. Tests follow a table-driven pattern. Add a new test function: ```go func TestTranslateNewSugar(t *testing.T) { tests := []struct { in Config out types.Config }{ // empty / no-op case { in: Config{}, out: types.Config{ Ignition: types.Ignition{ Version: "3.7.0-experimental", }, }, }, // basic sugar usage { in: Config{ NewSugar: NewSugar{ FieldOne: util.StrToPtr("value"), }, }, out: types.Config{ Ignition: types.Ignition{ Version: "3.7.0-experimental", }, Storage: types.Storage{ Files: []types.File{ { Node: types.Node{ Path: "/path/to/generated/file", }, FileEmbedded1: types.FileEmbedded1{ Contents: types.Resource{ Source: util.StrToPtr("data:,generated-content"), }, }, }, }, }, }, }, } for i, test := range tests { t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { out, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.in, r) assert.Equal(t, test.out, out, "bad output") assert.Equal(t, report.Report{}, r, "expected empty report") assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") }) } } ``` **IMPORTANT**: The Ignition version in test expectations (e.g., `"3.7.0-experimental"`) must match the version used in the spec's translate.go. Check the existing tests for the correct value. **Test categories to cover**: - Empty/no-op: sugar not configured, should produce default output - Basic usage: simplest valid configuration - Complex usage: all options exercised - User overrides: verify user can override sugar-generated values (for merge pattern) - Edge cases: boundary conditions - Error cases: invalid inputs (test separately in validate tests) ### Step 6: Add Validation (If Needed) **File**: `{spec_dir}/validate.go` Read the existing validate.go to understand validation patterns. Add a `Validate` method on the new sugar type: ```go func (s NewSugar) Validate(c path.ContextPath) (r report.Report) { if s.FieldOne != nil && *s.FieldOne == "" { r.AddOnError(c.Append("field_one"), common.ErrNewSugarFieldOneEmpty) } // ... more validations return } ``` Or add validation to an existing `Validate` method on `Config`: ```go func (conf Config) Validate(c path.ContextPath) (r report.Report) { // ... existing validations ... // New sugar validation if someCondition { r.AddOnError(c.Append("new_sugar", "field"), common.ErrSomething) } return } ``` **Validation test file**: `{spec_dir}/validate_test.go` ```go func TestValidateNewSugar(t *testing.T) { tests := []struct { in NewSugar out error errPath path.ContextPath }{ // valid config { in: NewSugar{FieldOne: util.StrToPtr("valid")}, out: nil, errPath: path.New("yaml"), }, // invalid config { in: NewSugar{FieldOne: util.StrToPtr("")}, out: common.ErrNewSugarFieldOneEmpty, errPath: path.New("yaml", "field_one"), }, } for i, test := range tests { t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { actual := test.in.Validate(path.New("yaml")) baseutil.VerifyReport(t, test.in, actual) expected := report.Report{} expected.AddOnError(test.errPath, test.out) assert.Equal(t, expected, actual, "bad validation report") }) } } ``` ### Step 7: Add Error Constants **File**: `config/common/errors.go` Read the existing errors.go to understand the naming pattern. Add new error variables in the appropriate section: ```go var ( // ... existing errors ... // New sugar ErrNewSugarFieldOneEmpty = errors.New("field_one must not be empty") ErrNewSugarInvalidCombo = errors.New("field_one and field_two are mutually exclusive") ) ``` **Naming convention**: `Err` + CamelCase description. Error messages should be lowercase, concise, and actionable. ### Step 8: Update Documentation Descriptors **File**: `internal/doc/butane.yaml` Read the existing butane.yaml to understand the YAML structure for field documentation. Add documentation descriptors for new fields. Place them in the correct location within the document hierarchy.
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看