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
3 changes: 1 addition & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -145,7 +144,7 @@ func initConfig() {
}

func loadConfig(filename string) error {
data, err := ioutil.ReadFile(filename)
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("error reading configuration file: %w", err)
}
Expand Down
9 changes: 1 addition & 8 deletions pkg/age/age.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,8 @@ func mergeAndEncryptYAMLValues(plain, encrypted interface{}, identity *age.X2551
// Values are the same, keep existing encrypted value (idempotent)
return encryptedStr, nil
}
// If decryption fails or values differ, encrypt new value
} else if encryptedStr == plainVal {
// Both are plain strings and equal, but we need encrypted version
// Encrypt the plain value
} else {
// Values differ, encrypt new value
}

// Encrypt the new value
// Encrypt the new value (if decryption fails, values differ, or both are plain)
return encryptYAMLValues(plain, identity.Recipient())

default:
Expand Down
29 changes: 0 additions & 29 deletions pkg/commands/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@
package commands

import (
"bufio"
"context"
"errors"
"fmt"
"os"
"time"

"github.com/cozystack/talm/pkg/engine"
Expand Down Expand Up @@ -188,33 +186,6 @@ func apply(args []string) error {
return nil
}

// readFirstLine reads and returns the first line of the file specified by the filename.
// It returns an error if opening or reading the file fails.
func readFirstLine(filename string) (string, error) {
// Open the file
file, err := os.Open(filename)
if err != nil {
return "", fmt.Errorf("error opening file: %v", err)
}
defer file.Close() // Ensure the file is closed after reading

// Create a scanner to read the file
scanner := bufio.NewScanner(file)

// Read the first line
if scanner.Scan() {
return scanner.Text(), nil
}

// Check for errors during scanning
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("error reading file: %v", err)
}

// If no lines in the file, return an empty string
return "", nil
}

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)")
Expand Down
13 changes: 4 additions & 9 deletions pkg/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,8 @@ var initCmd = &cobra.Command{

// If encrypted file exists, decrypt it (don't require key - will generate if needed)
if encryptedTalosconfigFileExists && !talosconfigFileExists {
_, err := handleTalosconfigEncryption(false)
if err != nil {
// If decryption fails (e.g., no key), continue to generate
}
// If decryption fails (e.g., no key), continue to generate
_, _ = handleTalosconfigEncryption(false)
talosconfigFileExists = fileExists(talosconfigFile)
Comment on lines +327 to 329
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate decryption errors instead of ignoring them.

handleTalosconfigEncryption(false) already returns nil when the key is missing and decryption should be skipped. Ignoring all errors can hide corruption/invalid key issues and leave talosconfig.encrypted stale while a new plaintext is generated.

🛠️ Proposed fix
-			// If decryption fails (e.g., no key), continue to generate
-			_, _ = handleTalosconfigEncryption(false)
+			// If decryption fails for any reason other than missing key, return the error
+			if _, err := handleTalosconfigEncryption(false); err != nil {
+				return err
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If decryption fails (e.g., no key), continue to generate
_, _ = handleTalosconfigEncryption(false)
talosconfigFileExists = fileExists(talosconfigFile)
// If decryption fails for any reason other than missing key, return the error
if _, err := handleTalosconfigEncryption(false); err != nil {
return err
}
talosconfigFileExists = fileExists(talosconfigFile)
🤖 Prompt for AI Agents
In `@pkg/commands/init.go` around lines 327 - 329, The call to
handleTalosconfigEncryption(false) currently discards all errors; instead
capture its returned error and only ignore the specific "no key / skip
decryption" case, while propagating any other errors so corruption/invalid-key
conditions are not hidden. Concretely, call err :=
handleTalosconfigEncryption(false), then if err != nil check for the
sentinel/no-key condition (e.g., errors.Is(err, ErrNoKey) or a typed error your
function returns) and continue only in that case; otherwise return or surface
the error. After handling the error, update talosconfigFileExists =
fileExists(talosconfigFile) as before. Use the existing function names
handleTalosconfigEncryption, talosconfigFileExists and talosconfigFile to locate
the change.

}

Expand All @@ -347,7 +345,6 @@ var initCmd = &cobra.Command{
if err = writeToDestination(data, talosconfigFile, 0o600); err != nil {
return err
}
talosconfigFileExists = true
}

// Encrypt talosconfig if needed
Expand Down Expand Up @@ -385,7 +382,6 @@ var initCmd = &cobra.Command{
if err != nil {
return fmt.Errorf("failed to generate key: %w", err)
}
keyFileExists = true // Update flag after creation
keyWasCreated = keyCreated
}

Expand Down Expand Up @@ -417,7 +413,7 @@ var initCmd = &cobra.Command{
if chartName == initCmdFlags.preset {
file := filepath.Join(Config.RootDir, filepath.Join(parts[1:]...))
if parts[len(parts)-1] == "Chart.yaml" {
writeToDestination([]byte(fmt.Sprintf(content, clusterName, Config.InitOptions.Version)), file, 0o644)
err = writeToDestination([]byte(fmt.Sprintf(content, clusterName, Config.InitOptions.Version)), file, 0o644)
} else {
err = writeToDestination([]byte(content), file, 0o644)
}
Expand All @@ -429,7 +425,7 @@ var initCmd = &cobra.Command{
if chartName == "talm" {
file := filepath.Join(Config.RootDir, filepath.Join("charts", path))
if parts[len(parts)-1] == "Chart.yaml" {
writeToDestination([]byte(fmt.Sprintf(content, "talm", Config.InitOptions.Version)), file, 0o644)
err = writeToDestination([]byte(fmt.Sprintf(content, "talm", Config.InitOptions.Version)), file, 0o644)
} else {
err = writeToDestination([]byte(content), file, 0o644)
}
Expand Down Expand Up @@ -856,7 +852,6 @@ func handleTalosconfigEncryption(requireKeyForDecrypt bool) (bool, error) {
if keyCreated {
fmt.Fprintf(os.Stderr, "Generated new encryption key: talm.key\n")
}
keyFileExists = true
}

// Encrypt talosconfig
Expand Down
4 changes: 0 additions & 4 deletions pkg/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import (
"github.com/siderolabs/talos/pkg/machinery/client"
)

var kubernetesFlag bool

// GlobalArgs is the common arguments for the root command.
var GlobalArgs global.Args

Expand Down Expand Up @@ -72,8 +70,6 @@ var Config struct {
}
}

const pathAutoCompleteLimit = 500

// WithClientNoNodes wraps common code to initialize Talos client and provide cancellable context.
//
// WithClientNoNodes doesn't set any node information on the request context.
Expand Down
84 changes: 84 additions & 0 deletions pkg/commands/rotate_ca_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright Cozystack Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package commands

import (
"fmt"

"github.com/spf13/cobra"
)

// wrapRotateCACommand adds special handling for rotate-ca command:
// - validates that only one node/endpoint is provided (must be a control-plane node)
// - updates command description
func wrapRotateCACommand(wrappedCmd *cobra.Command) {
// Update command description to be more helpful
wrappedCmd.Long = `Rotates Talos and/or Kubernetes root Certificate Authorities.

This command must be run against a SINGLE control-plane node. The specified node
will be used to coordinate the CA rotation across the entire cluster.

The command works by:
1. Auto-discovering all cluster nodes (control-plane and workers)
2. Generating new CA certificates
3. Gracefully rolling out the new CAs to all nodes
4. Updating the talosconfig with new credentials

IMPORTANT: You must specify exactly ONE control-plane node via --endpoints/-e or --nodes/-n
flags, or through a single config file (-f). The node must be a control-plane node.

By default, both Talos API CA and Kubernetes API CA are rotated. Use --talos=false
or --kubernetes=false to rotate only one of them.

The command runs in dry-run mode by default. Use --dry-run=false to perform actual rotation.`

wrappedCmd.Example = ` # Dry-run CA rotation (recommended first step)
talm rotate-ca -e 192.168.1.10

# Rotate CAs using config file (must contain single control-plane node)
talm rotate-ca -f nodes/controlplane-1.yaml

# Actually perform the rotation
talm rotate-ca -f nodes/controlplane-1.yaml --dry-run=false

# Rotate only Talos API CA
talm rotate-ca -f nodes/controlplane-1.yaml --kubernetes=false --dry-run=false

# Rotate only Kubernetes API CA
talm rotate-ca -f nodes/controlplane-1.yaml --talos=false --dry-run=false`

// Store original PreRunE to chain it
originalPreRunE := wrappedCmd.PreRunE

wrappedCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
// Run original PreRunE first (processes modeline, syncs GlobalArgs, etc.)
if originalPreRunE != nil {
if err := originalPreRunE(cmd, args); err != nil {
return err
}
}

// Validate that only one endpoint/node is provided
if len(GlobalArgs.Endpoints) > 1 {
return fmt.Errorf("rotate-ca requires exactly one control-plane node, but %d endpoints were provided\n\nThe rotate-ca command coordinates CA rotation across the entire cluster from a single\ncontrol-plane node. Please specify only one endpoint using -e flag or a single config file", len(GlobalArgs.Endpoints))
}

if len(GlobalArgs.Nodes) > 1 {
return fmt.Errorf("rotate-ca requires exactly one control-plane node, but %d nodes were provided\n\nThe rotate-ca command coordinates CA rotation across the entire cluster from a single\ncontrol-plane node. Please specify only one node using -n flag or a single config file", len(GlobalArgs.Nodes))
}
Comment thread
kvaps marked this conversation as resolved.

return nil
}
}
10 changes: 7 additions & 3 deletions pkg/commands/talosctl_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ func wrapTalosCommand(cmd *cobra.Command, cmdName string) *cobra.Command {
}
if baseCmdName == "kubeconfig" {
if !cmd.Flags().Changed("force") {
if err := cmd.Flags().Set("force", "true"); err != nil {
// Flag might not exist, ignore error
}
// Flag might not exist, ignore error
_ = cmd.Flags().Set("force", "true")
}
Comment on lines +97 to 99
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n pkg/commands/talosctl_wrapper.go | sed -n '85,110p'

Repository: cozystack/talm

Length of output: 1268


🏁 Script executed:

# Let's see more context to understand what this code does
cat -n pkg/commands/talosctl_wrapper.go | sed -n '1,50p'

Repository: cozystack/talm

Length of output: 2054


🌐 Web query:

spf13/cobra pflag FlagSet.Set() error handling missing flags

💡 Result:

(*pflag.FlagSet).Set(name, value) does return an error when the flag doesn’t exist in that FlagSet; the concrete type is *pflag.NotExistError (“error returned when trying to access a flag that does not exist in the FlagSet”).[1][2]

Practical handling pattern

fs := cmd.Flags() // in Cobra: local + persistent from parents
if fs.Lookup("my-flag") == nil {
    // flag not registered on this command (or its parents)
    return nil // or return a custom error
}

if err := fs.Set("my-flag", "123"); err != nil {
    var ne *pflag.NotExistError
    if errors.As(err, &ne) {
        // handle missing flag explicitly
        return nil
    }
    return err // invalid value, etc.
}

Cobra-specific “missing flag” gotcha

If you call Set() on the wrong flag set, you’ll get “flag accessed but not defined” even though you defined it elsewhere (e.g., as a persistent flag on a parent). In Cobra, cmd.Flags() is the combined set (“local and persistent declared here and by all parents”), so it’s usually the right one to use when setting/reading flags during execution.[3]

Sources:
[1] https://pkg.go.dev/github.com/spf13/pflag#NotExistError
[2] https://pkg.go.dev/github.com/spf13/pflag#FlagSet.Set
[3] https://pkg.go.dev/github.com/spf13/cobra#Command.Flags


🏁 Script executed:

rg -A 5 -B 5 "force" pkg/commands/talosctl_wrapper.go | head -40

Repository: cozystack/talm

Length of output: 798


🏁 Script executed:

# Find the kubeconfig command definition in talosctl
fd -type f -name "*.go" | xargs rg -l "kubeconfig" | head -5

Repository: cozystack/talm

Length of output: 362


🏁 Script executed:

# Search for where force flag is defined
rg "AddBoolFlag.*force" --type go

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Check the original talosctl kubeconfig command to see if it has force flag
rg -B 3 -A 3 "kubeconfig.*Command" pkg/commands/talosctl_wrapper.go

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

rg "force.*Bool" . --max-count 20

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Look for force flag in kubeconfig_handler.go
cat pkg/commands/kubeconfig_handler.go | head -100

Repository: cozystack/talm

Length of output: 3346


🏁 Script executed:

# Search for force flag registration in talos package imports
rg -B 5 -A 5 "cmd\.Flags\(\)\.BoolVar.*force" .

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Search for all places where force flag is referenced
rg "\"force\"" pkg/commands/ -B 2 -A 2

Repository: cozystack/talm

Length of output: 2248


🏁 Script executed:

# Search for where the kubeconfig command is created/wrapped
rg -B 10 -A 10 "wrapTalosCommand.*kubeconfig" pkg/commands/

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Check the talosctl import to understand the command structure
rg "taloscommands" pkg/commands/talosctl_wrapper.go -A 5 -B 5

Repository: cozystack/talm

Length of output: 1038


🏁 Script executed:

# Check if force flag is typically a persistent flag in talosctl
rg -A 20 "func wrapTalosCommand" pkg/commands/talosctl_wrapper.go | head -50

Repository: cozystack/talm

Length of output: 845


🏁 Script executed:

# Check if kubeconfig command is called with force flag anywhere
rg "kubeconfig.*--force" .

Repository: cozystack/talm

Length of output: 181


🏁 Script executed:

# Look for how the original talosctl kubeconfig is defined
rg -B 5 -A 15 "kubeconfig.*Command" pkg/commands/root.go

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Check the Apply command which does have force flag to see the pattern
cat -n pkg/commands/apply.go | sed -n '1,100p'

Repository: cozystack/talm

Length of output: 3712


🏁 Script executed:

# Search for kubeconfig command in talosctl package reference
git ls-files | xargs grep -l "kubeconfig" | head -5

Repository: cozystack/talm

Length of output: 254


🏁 Script executed:

# Check the initialization of talosctl commands and if force flag is added
cat -n pkg/commands/talosctl_wrapper.go | sed -n '160,180p'

Repository: cozystack/talm

Length of output: 695


🏁 Script executed:

# Look for any flag additions or modifications to kubeconfig specifically
rg -B 10 -A 10 "wrapKubeconfigCommand" pkg/commands/

Repository: cozystack/talm

Length of output: 2799


🏁 Script executed:

# Look at the full context of how kubeconfig is being wrapped, including the PreRunE logic
cat -n pkg/commands/talosctl_wrapper.go | sed -n '87,135p'

Repository: cozystack/talm

Length of output: 2055


🏁 Script executed:

# Search for where force flag might be added to kubeconfig
rg -A 30 "func wrapKubeconfigCommand" pkg/commands/kubeconfig_handler.go

Repository: cozystack/talm

Length of output: 1188


Check flag existence before calling Set() and handle errors.

Silently ignoring Set() errors masks failures: the flag might not exist, or Set() could fail for invalid values or type mismatches. Use the idiomatic pflag pattern—check with Lookup() first, then handle errors from Set().

🛠️ Proposed fix
-			// Flag might not exist, ignore error
-			_ = cmd.Flags().Set("force", "true")
+			if cmd.Flags().Lookup("force") != nil {
+				if err := cmd.Flags().Set("force", "true"); err != nil {
+					return err
+				}
+			}
🤖 Prompt for AI Agents
In `@pkg/commands/talosctl_wrapper.go` around lines 97 - 99, Check whether the
"force" flag exists via cmd.Flags().Lookup("force") before calling
cmd.Flags().Set("force","true"), and do not ignore errors from Set(); if Lookup
returns nil skip setting, otherwise call Set and handle its error (e.g., return
it or log it) so type-mismatch/invalid-value failures are surfaced; update the
code around cmd.Flags().Set, using cmd.Flags().Lookup and proper error handling
for Set() instead of assigning its result to _.

}

Expand Down Expand Up @@ -164,6 +163,11 @@ func wrapTalosCommand(cmd *cobra.Command, cmdName string) *cobra.Command {
wrapUpgradeCommand(wrappedCmd, originalRunE)
}

// Special handling for rotate-ca command
if baseCmdName == "rotate-ca" {
wrapRotateCACommand(wrappedCmd)
}

// Copy all subcommands
for _, subCmd := range cmd.Commands() {
wrappedCmd.AddCommand(wrapTalosCommand(subCmd, subCmd.Name()))
Expand Down
33 changes: 0 additions & 33 deletions pkg/commands/track.go

This file was deleted.

2 changes: 1 addition & 1 deletion pkg/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ func applyPatchesAndRenderConfig(ctx context.Context, opts Options, configPatche
if err := encoder.Encode(&targetNode); err != nil {
return nil, err
}
encoder.Close()
_ = encoder.Close()

// Append extra documents (like UserVolumeConfig) that are not part of Talos config
for _, extraDoc := range extraDocs {
Expand Down
2 changes: 1 addition & 1 deletion pkg/modeline/modeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func ReadAndParseModeline(filePath string) (*Config, error) {
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
defer file.Close()
defer func() { _ = file.Close() }()

scanner := bufio.NewScanner(file)
if scanner.Scan() {
Expand Down
2 changes: 1 addition & 1 deletion pkg/yamltools/yamltools.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func DiffYAMLs(original, modified []byte) ([]byte, error) {
if err := encoder.Encode(diff); err != nil {
return nil, err
}
encoder.Close()
_ = encoder.Close()

return buffer.Bytes(), nil
}
Expand Down
Loading