-
Notifications
You must be signed in to change notification settings - Fork 23
fix(commands): apply command ignores modeline templates and --skip-verify bypasses node context #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(commands): apply command ignores modeline templates and --skip-verify bypasses node context #113
Changes from all commits
1cb3a4a
42bc0c3
db218cc
d6d5651
805a25d
64bfcd8
1c5b611
b393aa5
d51b9dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,8 +16,9 @@ package commands | |
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/cozystack/talm/pkg/engine" | ||
|
|
@@ -77,7 +78,7 @@ var applyCmd = &cobra.Command{ | |
| } | ||
| applyCmdFlags.nodesFromArgs = len(GlobalArgs.Nodes) > 0 | ||
| applyCmdFlags.endpointsFromArgs = len(GlobalArgs.Endpoints) > 0 | ||
| // Set dummy endpoint to avoid errors on building clinet | ||
| // Set dummy endpoint to avoid errors on building client | ||
| if len(GlobalArgs.Endpoints) == 0 { | ||
| GlobalArgs.Endpoints = append(GlobalArgs.Endpoints, "127.0.0.1") | ||
| } | ||
|
|
@@ -104,54 +105,51 @@ func apply(args []string) error { | |
| } | ||
|
|
||
| for _, configFile := range expandedFiles { | ||
| if err := processModelineAndUpdateGlobals(configFile, applyCmdFlags.nodesFromArgs, applyCmdFlags.endpointsFromArgs, true); err != nil { | ||
| modelineTemplates, err := processModelineAndUpdateGlobals(configFile, applyCmdFlags.nodesFromArgs, applyCmdFlags.endpointsFromArgs, true) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Resolve secrets.yaml path relative to project root if not absolute | ||
| withSecretsPath := ResolveSecretsPath(applyCmdFlags.withSecrets) | ||
|
|
||
| opts := engine.Options{ | ||
| TalosVersion: applyCmdFlags.talosVersion, | ||
| WithSecrets: withSecretsPath, | ||
| KubernetesVersion: applyCmdFlags.kubernetesVersion, | ||
| Debug: applyCmdFlags.debug, | ||
| } | ||
|
|
||
| patches := []string{"@" + configFile} | ||
| configBundle, machineType, err := engine.FullConfigProcess(ctx, opts, patches) | ||
| if err != nil { | ||
| return fmt.Errorf("full config processing error: %s", err) | ||
| } | ||
| var result []byte | ||
| if len(modelineTemplates) > 0 { | ||
| // Template rendering path: render templates via Helm engine to produce full config | ||
| opts := buildApplyRenderOptions(modelineTemplates, withSecretsPath) | ||
| result, err = engine.Render(ctx, nil, opts) | ||
| if err != nil { | ||
| return fmt.Errorf("template rendering error: %w", err) | ||
| } | ||
| } else { | ||
| // Direct patch path: apply config file as patch against empty bundle | ||
| opts := buildApplyPatchOptions(withSecretsPath) | ||
| patches := []string{"@" + configFile} | ||
| configBundle, machineType, err := engine.FullConfigProcess(ctx, opts, patches) | ||
| if err != nil { | ||
| return fmt.Errorf("full config processing error: %w", err) | ||
| } | ||
|
|
||
| result, err := engine.SerializeConfiguration(configBundle, machineType) | ||
| if err != nil { | ||
| return fmt.Errorf("error serializing configuration: %s", err) | ||
| result, err = engine.SerializeConfiguration(configBundle, machineType) | ||
| if err != nil { | ||
| return fmt.Errorf("error serializing configuration: %w", err) | ||
| } | ||
| } | ||
|
|
||
| withClient := func(f func(ctx context.Context, c *client.Client) error) error { | ||
| if applyCmdFlags.insecure { | ||
| // Maintenance mode connects directly to the node IP without talosconfig; | ||
| // node context injection is not needed — the maintenance client handles | ||
| // node targeting internally via GlobalArgs.Nodes. | ||
| return WithClientMaintenance(applyCmdFlags.certFingerprints, f) | ||
| } | ||
|
|
||
| wrappedF := wrapWithNodeContext(f) | ||
|
|
||
| if GlobalArgs.SkipVerify { | ||
| return WithClientSkipVerify(f) | ||
| return WithClientSkipVerify(wrappedF) | ||
| } | ||
|
|
||
| return WithClientNoNodes(func(ctx context.Context, cli *client.Client) error { | ||
| if len(GlobalArgs.Nodes) < 1 { | ||
| configContext := cli.GetConfigContext() | ||
| if configContext == nil { | ||
| return errors.New("failed to resolve config context") | ||
| } | ||
|
|
||
| GlobalArgs.Nodes = configContext.Nodes | ||
| } | ||
|
|
||
| ctx = client.WithNodes(ctx, GlobalArgs.Nodes...) | ||
|
|
||
| return f(ctx, cli) | ||
| }) | ||
| return WithClientNoNodes(wrappedF) | ||
| } | ||
|
|
||
| err = withClient(func(ctx context.Context, c *client.Client) error { | ||
|
|
@@ -186,6 +184,109 @@ func apply(args []string) error { | |
| return nil | ||
| } | ||
|
|
||
| // buildApplyRenderOptions constructs engine.Options for the template rendering path. | ||
| // Offline is set to true because at this point we don't have a Talos client for | ||
| // Helm lookup functions. Templates that use lookup() should be rendered via | ||
| // 'talm template' which supports online mode. | ||
| func buildApplyRenderOptions(modelineTemplates []string, withSecretsPath string) engine.Options { | ||
| resolvedTemplates := resolveTemplatePaths(modelineTemplates, Config.RootDir) | ||
| return engine.Options{ | ||
| TalosVersion: applyCmdFlags.talosVersion, | ||
| WithSecrets: withSecretsPath, | ||
| KubernetesVersion: applyCmdFlags.kubernetesVersion, | ||
| Debug: applyCmdFlags.debug, | ||
| Full: true, | ||
| Offline: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoding
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. The Talos client is not available at rendering time — it is created later in Added a documenting comment in b393aa5 and filed #114 to track this as a follow-up. |
||
| Root: Config.RootDir, | ||
| TemplateFiles: resolvedTemplates, | ||
| } | ||
| } | ||
|
|
||
| // buildApplyPatchOptions constructs engine.Options for the direct patch path. | ||
| func buildApplyPatchOptions(withSecretsPath string) engine.Options { | ||
| return engine.Options{ | ||
| TalosVersion: applyCmdFlags.talosVersion, | ||
| WithSecrets: withSecretsPath, | ||
| KubernetesVersion: applyCmdFlags.kubernetesVersion, | ||
| Debug: applyCmdFlags.debug, | ||
| } | ||
| } | ||
|
|
||
| // wrapWithNodeContext wraps a client action function to resolve and inject node | ||
| // context. If GlobalArgs.Nodes is already set, uses those directly. Otherwise, | ||
| // attempts to resolve nodes from the client's config context. | ||
| // This function does not mutate GlobalArgs. It reads GlobalArgs.Nodes at | ||
| // invocation time (not at wrapper creation time) and makes a defensive copy. | ||
| func wrapWithNodeContext(f func(ctx context.Context, c *client.Client) error) func(ctx context.Context, c *client.Client) error { | ||
| return func(ctx context.Context, c *client.Client) error { | ||
| nodes := append([]string(nil), GlobalArgs.Nodes...) | ||
| if len(nodes) < 1 { | ||
| if c == nil { | ||
| return fmt.Errorf("failed to resolve config context: no client available") | ||
| } | ||
| configContext := c.GetConfigContext() | ||
| if configContext == nil { | ||
| return fmt.Errorf("failed to resolve config context") | ||
| } | ||
| nodes = configContext.Nodes | ||
| } | ||
|
Comment on lines
+223
to
+232
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic to resolve nodes from the client's config context appears to be unreachable in the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentionally kept as defensive code. |
||
|
|
||
| ctx = client.WithNodes(ctx, nodes...) | ||
| return f(ctx, c) | ||
| } | ||
| } | ||
|
|
||
| // resolveTemplatePaths resolves template file paths relative to the project root, | ||
| // normalizing them for the Helm engine (forward slashes). | ||
| // Relative paths from the modeline are resolved against rootDir, not CWD. | ||
| // | ||
| // Note: template.go has similar path resolution in generateOutput() but resolves | ||
| // against CWD via filepath.Abs and has an additional fallback that tries | ||
| // templates/<basename> when a path resolves outside the root. This function | ||
| // intentionally resolves against rootDir (modeline paths are root-relative by | ||
| // convention) and does not perform the basename fallback to avoid silently | ||
| // substituting a different file. | ||
| func resolveTemplatePaths(templates []string, rootDir string) []string { | ||
| resolved := make([]string, len(templates)) | ||
| if rootDir == "" { | ||
| // No rootDir specified — normalize paths only, don't resolve against CWD | ||
| for i, p := range templates { | ||
| resolved[i] = engine.NormalizeTemplatePath(p) | ||
| } | ||
| return resolved | ||
| } | ||
| absRootDir, rootErr := filepath.Abs(rootDir) | ||
| if rootErr != nil { | ||
| for i, p := range templates { | ||
| resolved[i] = engine.NormalizeTemplatePath(p) | ||
| } | ||
| return resolved | ||
| } | ||
|
|
||
| for i, templatePath := range templates { | ||
| var absTemplatePath string | ||
| if filepath.IsAbs(templatePath) { | ||
| absTemplatePath = templatePath | ||
| } else { | ||
| // Resolve relative paths against rootDir, not CWD | ||
| absTemplatePath = filepath.Join(absRootDir, templatePath) | ||
| } | ||
| relPath, relErr := filepath.Rel(absRootDir, absTemplatePath) | ||
| if relErr != nil { | ||
| resolved[i] = engine.NormalizeTemplatePath(templatePath) | ||
| continue | ||
| } | ||
| relPath = filepath.Clean(relPath) | ||
| if strings.HasPrefix(relPath, "..") { | ||
| // Path goes outside project root — use original path as-is | ||
| resolved[i] = engine.NormalizeTemplatePath(templatePath) | ||
| continue | ||
| } | ||
| resolved[i] = engine.NormalizeTemplatePath(relPath) | ||
| } | ||
| return resolved | ||
| } | ||
|
|
||
| func init() { | ||
| applyCmd.Flags().BoolVarP(&applyCmdFlags.insecure, "insecure", "i", false, "apply using the insecure (encrypted with no auth) maintenance service") | ||
| applyCmd.Flags().StringSliceVarP(&applyCmdFlags.configFiles, "file", "f", nil, "specify config files or patches in a YAML file (can specify multiple)") | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't turn modeline-bearing
-ffiles into metadata-only inputs.Lines 116-123 render only
modelineTemplates;configFileis never merged in this branch. SinceprocessModelineAndUpdateGlobalsonly extracts the modeline metadata, any YAML overrides that remain in the same node file are dropped, sotalm apply -f nodes/node0.yamlapplies the shared templates but loses the node-specific patch from the file the user passed in. Please keepconfigFilein the merge step after rendering the template set.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional and matches the existing behavior of
talm template -f.The
templateWithFilesfunction intemplate.go(lines 127-218) also does NOT pass configFile as a patch toengine.Render. The node file serves as a modeline container (nodes, endpoints, templates metadata) and as the output target for--in-placerendering. The actual cluster configuration is produced entirely by the Helm templates +values.yaml.The two-step workaround
talm template -f node.yaml | talm apply -f -works the same way:templaterenders full config from templates,applysends it to the node. This PR makestalm apply -fdo both steps internally.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lexfrei, thank you for the clear explanation! That makes sense — the node file is purely a modeline carrier (nodes, endpoints, template references) and the full configuration is produced entirely by the Helm templates andvalues.yaml, consistent with howtemplateWithFilesworks. I've noted this design pattern for future reviews.✏️ Learnings added