Skip to content

Conversation

@IvanHunters
Copy link

@IvanHunters IvanHunters commented Dec 25, 2025

Summary by CodeRabbit

  • New Features
    • Added interactive wizard for cluster initialization with step-by-step configuration
    • Added network scanning to discover and gather information about nodes
    • Added template discovery and rendering capabilities for configuration management
    • Added node information retrieval with hardware and status details
    • Added configuration generation for Talos clusters
    • Added performance caching layer for frequently accessed data

✏️ Tip: You can customize this high-level summary in your review settings.

ohotnikov.ivan added 3 commits December 23, 2025 10:33
Signed-off-by: ohotnikov.ivan <[email protected]>
Signed-off-by: ohotnikov.ivan <[email protected]>
Signed-off-by: ohotnikov.ivan <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Dec 25, 2025

📝 Walkthrough

Walkthrough

Introduces a comprehensive interactive initialization wizard system spanning multiple packages. New generator package provides configuration generation and file persistence workflows. Interactive package adds node management, template rendering, and wizard UI components. Large initwizard subsystem implements a complete TUI-based wizard with network scanning, hardware discovery, data validation, caching, error handling, and multi-step state-driven UI flows. Adds CLI command for interactive initialization.

Changes

Cohort / File(s) Summary
Generator Package
internal/pkg/generator/generate.go, internal/pkg/generator/write.go
Orchestrates Talos configuration generation with optional version parsing, secrets bundle creation, YAML marshaling, and file outputs. Provides safe file writing with optional overwrite control and directory creation. Implements preset file writing with dynamic content formatting.
Interactive Package
internal/pkg/interactive/nodes.go
Implements NodeManager for interactive node handling with state management, loading from YAML/alternative structures, and ExecuteNodeCommand routing to specialized handlers (version, list, memory, processes, mounts, disks, health, stats). Provides node discovery and metadata enrichment via Talos client interactions.
Interactive Package
internal/pkg/interactive/template.go
Introduces TemplateManager for Helm-like templating workflows: template/value discovery, offline rendering with custom values, output persistence, modeline generation and loading, and preset-based template filtering.
Interactive Package
internal/pkg/interactive/wizard.go
Provides comprehensive interactive wizard wiring tview UI components, InitWizard, NodeManager, and TemplateManager. Main menu orchestrates initialization, node info, templates, with dynamic enabling/disabling, modal-based feedback, and input capture for Ctrl+C/Escape/Ctrl+Q.
InitWizard Core Types
internal/pkg/ui/initwizard/types.go, internal/pkg/ui/initwizard/interfaces.go
Defines core data structures (InitData, NodeInfo, Hardware, ValuesYAML, ChartYAML) with JSON/YAML annotations. Establishes interfaces for Wizard, Validator, DataProcessor, Generator, NetworkScanner, Presenter, and UIHelper to decouple components.
InitWizard Error & State
internal/pkg/ui/initwizard/errors.go, internal/pkg/ui/initwizard/wizard_state.go, internal/pkg/ui/initwizard/wizard_transitions.go, internal/pkg/ui/initwizard/wizard_controller.go
Structured error taxonomy (ErrorType) with specialized constructors (validation, network, filesystem, etc.) and error introspection methods. Wizard state enum with string representation, allowed transition mapping, and WizardController for state management.
InitWizard Caching & Validation
internal/pkg/ui/initwizard/cache.go, internal/pkg/ui/initwizard/validator.go
TTL-based generic Cache with thread-safe access and statistics; specialized NodeCache, HardwareCache, ConfigCache wrappers with auto-cleanup. ValidatorImpl performs network CIDR, cluster name, hostname, IP/VIP, DNS, node type, preset, and API server URL validation with structured error codes.
InitWizard Network & Factory
internal/pkg/ui/initwizard/network.go, internal/pkg/ui/initwizard/factory.go
Connection pooling with idle/lifetime tracking and metrics; NetworkClient with timeout handling; RateLimiter for concurrent access control. WizardBuilder implements fluent configuration; WizardComponents aggregates validator, scanner, processor, generator, presenter, caches, network stack with lifecycle and stats accessors.
InitWizard Processing & Scanning
internal/pkg/ui/initwizard/processor.go, internal/pkg/ui/initwizard/scanner.go
DataProcessorImpl provides node filtering, deduplication, sorting, resource computation, compatibility validation, and cluster summarization. NetworkScannerImpl discovers Talos nodes via port scanning, collects enhanced hardware info (memory, disks, processors, interfaces), supports progress-based scanning, and implements parallel worker pool with context awareness.
InitWizard Presentation & Generation
internal/pkg/ui/initwizard/presenter.go, internal/pkg/ui/initwizard/generate.go
PresenterImpl orchestrates multi-step UI forms for preset/generic/cozystack config, node selection/details, network/VIP setup, and modals for errors/success/progress. GenerateFromTUI validates presets, parses versions, creates secrets bundles, generates and persists machine/node configs with dynamic content formatting and file substitutions.
InitWizard Wizard & Public Entry
internal/pkg/ui/initwizard/wizard.go, internal/pkg/ui/initwizard/initwizard.go
WizardImpl provides complete wizard lifecycle with network scanning, validation, config generation, bootstrap flows, node processing, logging setup, and dependency accessors. Public RunInitWizard, CheckExistingFiles, RunInitWizardWithConfig, and NewInitWizard functions expose wizard functionality.
CLI Command
pkg/commands/interactive_init.go
Cobra-based interactive CLI command with flags for config files (--file/-f), update interval (--update-interval/-d), and insecure mode (--insecure/-i). PreRunE processes modeline files; RunE invokes initwizard.RunInitWizard().

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Wizard as Wizard Coordinator
    participant Validator
    participant Scanner
    participant Processor
    participant Generator
    participant Presenter
    participant FileSystem

    User->>Wizard: Start wizard
    Wizard->>Presenter: Show preset form
    Presenter-->>User: Display UI
    User->>Presenter: Select preset/config
    Presenter->>Validator: Validate network CIDR
    Validator-->>Presenter: ✓/✗
    alt Validation OK
        Presenter->>Scanner: Scan network (progress)
        Scanner->>Scanner: Discover Talos nodes
        Scanner->>Scanner: Collect enhanced hardware
        Scanner-->>Presenter: Node list
        Presenter-->>User: Display nodes
        User->>Presenter: Select nodes
        Presenter->>Processor: Process/filter nodes
        Processor-->>Presenter: Processed nodes
        Presenter->>Generator: Generate config
        rect rgb(200, 220, 255)
        Note over Generator: Parse version, create secrets,<br/>generate machine/node configs
        end
        Generator->>FileSystem: Write secrets.yaml
        Generator->>FileSystem: Write talosconfig
        Generator->>FileSystem: Write node files
        Generator-->>Presenter: ✓ Config saved
        Presenter-->>User: Success modal
    else Validation Failed
        Presenter-->>User: Error modal
    end
    User->>Wizard: Exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰 A whisker-twitch and code so bright,
Twenty files dance through the night,
State machines hop, networks scan,
With caches, templates, and a master plan!
The wizard's born—let clusters shine,
A fluffy feat of design! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title '[tui] Create talm ui' is vague and non-descriptive, using generic terms like 'talm ui' that don't convey the actual scope of the substantial changes being introduced. Provide a more specific and descriptive title that highlights the primary change, such as '[tui] Add interactive wizard with node management and template rendering' or similar that better reflects the extensive new UI infrastructure being added.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 85.51% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @IvanHunters, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

Этот запрос на вытягивание представляет собой значительное дополнение к инструменту talm, вводя интерактивный пользовательский интерфейс (TUI) для упрощения инициализации и управления кластерами Talos. Он предоставляет пошаговый мастер, который помогает пользователям в настройке кластера, обнаружении узлов, сборе информации об оборудовании и генерации необходимых файлов конфигурации. Модульная архитектура обеспечивает гибкость и расширяемость, а также включает в себя надежную обработку ошибок и управление состоянием для обеспечения бесперебойной работы пользователя.

Highlights

  • Новый TUI для talm: Представлен комплексный текстовый пользовательский интерфейс (TUI) для talm, обеспечивающий интерактивную инициализацию и управление кластером.
  • Модульная архитектура: TUI построен с модульной архитектурой, разделяющей обязанности на отдельные компоненты, такие как Validator, NetworkScanner, DataProcessor, Generator и Presenter, которые координируются реализацией Wizard.
  • Интерактивная инициализация кластера: Предоставляет пошаговый мастер для инициализации кластера Talos, включая выбор пресетов (generic, cozystack), настройку сетевых параметров и генерацию необходимых файлов конфигурации (talosconfig, secrets.yaml, Chart.yaml, values.yaml).
  • Обнаружение и настройка узлов: Включает сканирование сети для обнаружения узлов Talos, собирает подробную информацию об оборудовании (ЦП, ОЗУ, диски, сетевые интерфейсы) и позволяет интерактивно настраивать отдельные узлы (роль, имя хоста, диск, сетевые параметры, VIP).
  • Обработка ошибок и управление состоянием: Реализована структурированная обработка ошибок с пользовательскими типами AppError и WizardController для управления переходами состояний TUI, обеспечивая надежный пользовательский опыт.
  • Сетевые утилиты: Включает ConnectionPool, NetworkClient и RateLimiter для эффективных и контролируемых сетевых операций во время обнаружения узлов и сбора информации.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a large new feature, a TUI for talm. While the feature itself is a great addition, the implementation has several significant issues that need to be addressed. The most critical one is the mix of Russian and English in comments, user-facing strings, and log messages across almost all new files. This severely impacts maintainability and should be standardized to English. Additionally, there are uses of deprecated packages (io/ioutil), placeholder logic that is functionally incorrect (e.g., disk size calculation), hardcoded values, and overly permissive file modes. I've added specific comments on these issues. Please address them to improve the quality and maintainability of the code.

Comment on lines +20 to +534
// NodeInfo структура для хранения информации об узле
type NodeInfo struct {
IP string
Hostname string
Status string
Version string
}

// NodeManager менеджер для работы с узлами
type NodeManager struct {
rootDir string
nodes []NodeInfo
}

// NewNodeManager создает новый менеджер узлов
func NewNodeManager(rootDir string) *NodeManager {
return &NodeManager{
rootDir: rootDir,
nodes: []NodeInfo{},
}
}

// LoadNodes загружает список узлов из конфигурации
func (nm *NodeManager) LoadNodes() error {
// Пытаемся загрузить узлы из values.yaml
valuesFile := fmt.Sprintf("%s/values.yaml", nm.rootDir)
data, err := os.ReadFile(valuesFile)
if err != nil {
// Если файл не найден, пробуем другие способы
return nm.loadNodesFromAlternative()
}

// Парсим YAML (упрощенно)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.Contains(line, "nodes:") {
// Найден раздел nodes, парсим его
nm.parseNodesSection(lines)
break
}
}

return nil
}

// loadNodesFromAlternative загружает узлы из альтернативных источников
func (nm *NodeManager) loadNodesFromAlternative() error {
// Проверяем директорию nodes/
nodesDir := fmt.Sprintf("%s/nodes", nm.rootDir)
files, err := os.ReadDir(nodesDir)
if err != nil {
return fmt.Errorf("не удалось прочитать директорию nodes: %v", err)
}

// Читаем каждый файл узла
for _, file := range files {
if strings.HasSuffix(file.Name(), ".yaml") {
nodeFile := fmt.Sprintf("%s/%s", nodesDir, file.Name())
nodeInfo, err := nm.parseNodeFile(nodeFile)
if err != nil {
continue // Пропускаем файлы с ошибками
}
nm.nodes = append(nm.nodes, nodeInfo)
}
}

return nil
}

// parseNodesSection парсит раздел nodes из values.yaml
func (nm *NodeManager) parseNodesSection(lines []string) {
inNodesSection := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "nodes:" {
inNodesSection = true
continue
}
if inNodesSection {
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, " ") {
// Парсим строку узла
parts := strings.Split(trimmed, ":")
if len(parts) >= 2 {
nodeName := strings.TrimSpace(parts[0])
// Здесь можно добавить парсинг IP и типа узла
nm.nodes = append(nm.nodes, NodeInfo{
Hostname: nodeName,
Status: "unknown",
})
}
} else {
// Выходим из раздела nodes
break
}
}
}
}

// parseNodeFile парсит файл конфигурации узла
func (nm *NodeManager) parseNodeFile(filename string) (NodeInfo, error) {
data, err := os.ReadFile(filename)
if err != nil {
return NodeInfo{}, err
}

content := string(data)
nodeInfo := NodeInfo{
Hostname: strings.TrimSuffix(strings.TrimPrefix(filename, fmt.Sprintf("%s/nodes/", nm.rootDir)), ".yaml"),
Status: "configured",
}

// Извлекаем IP адрес из конфигурации
lines := strings.Split(content, "\n")
for _, line := range lines {
if strings.Contains(line, "address:") || strings.Contains(line, "ip:") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
nodeInfo.IP = strings.TrimSpace(parts[1])
}
}
}

return nodeInfo, nil
}

// GetNodes возвращает список узлов
func (nm *NodeManager) GetNodes() []NodeInfo {
return nm.nodes
}

// ExecuteNodeCommand выполняет команду на узле
func (nm *NodeManager) ExecuteNodeCommand(ctx context.Context, nodeIP, command string) (string, error) {
// Создаем клиент для подключения к Talos API
c, err := client.New(ctx, client.WithEndpoints(nodeIP))
if err != nil {
return "", fmt.Errorf("не удалось создать клиент: %v", err)
}
defer c.Close()

// Выполняем команду в зависимости от типа
switch command {
case "version":
return nm.executeVersionCommand(ctx, c)
case "list":
return nm.executeListCommand(ctx, c)
case "memory":
return nm.executeMemoryCommand(ctx, c)
case "processes":
return nm.executeProcessesCommand(ctx, c)
case "mounts":
return nm.executeMountsCommand(ctx, c)
case "disks":
return nm.executeDisksCommand(ctx, c)
case "health":
return nm.executeHealthCommand(ctx, c)
case "stats":
return nm.executeStatsCommand(ctx, c)
default:
return "", fmt.Errorf("неизвестная команда: %s", command)
}
}

// executeVersionCommand выполняет команду version
func (nm *NodeManager) executeVersionCommand(ctx context.Context, c *client.Client) (string, error) {
var remotePeer peer.Peer

resp, err := c.Version(ctx, grpc.Peer(&remotePeer))
if err != nil {
return "", fmt.Errorf("ошибка получения версии: %v", err)
}

var output strings.Builder

for _, msg := range resp.Messages {
node := client.AddrFromPeer(&remotePeer)
if msg.Metadata != nil {
node = msg.Metadata.Hostname
}

output.WriteString(fmt.Sprintf("Узел: %s\n", node))

// Используем встроенную функцию для вывода версии
var versionBuf strings.Builder
fmt.Fprintf(&versionBuf, "\t")
version.PrintLongVersionFromExisting(msg.Version)
versionStr := versionBuf.String()
// Убираем лишние отступы
versionStr = strings.ReplaceAll(versionStr, "\n\t", "\n")
output.WriteString(versionStr)

var enabledFeatures []string
if msg.Features != nil {
if msg.Features.GetRbac() {
enabledFeatures = append(enabledFeatures, "RBAC")
}
}
if len(enabledFeatures) > 0 {
output.WriteString(fmt.Sprintf("\tВключенные функции: %s\n", strings.Join(enabledFeatures, ", ")))
}
output.WriteString("\n")
}

return output.String(), nil
}

// executeListCommand выполняет команду list
func (nm *NodeManager) executeListCommand(ctx context.Context, c *client.Client) (string, error) {
// Получаем список файлов в корневой директории
stream, err := c.LS(ctx, &machine.ListRequest{
Root: "/",
Recurse: false,
RecursionDepth: 1,
})
if err != nil {
return "", fmt.Errorf("ошибка получения списка файлов: %v", err)
}

var files []string
for {
info, err := stream.Recv()
if err != nil {
break
}

if info.Error != "" {
continue // Пропускаем файлы с ошибками
}

// Определяем тип файла
typeName := "файл"
if info.Mode&040000 != 0 {
typeName = "директория"
} else if info.Mode&120000 != 0 {
typeName = "символическая ссылка"
}

fileInfo := fmt.Sprintf("• %s (%s, %s)", info.RelativeName, typeName, humanize.Bytes(uint64(info.Size)))
files = append(files, fileInfo)
}

output := "Список файлов и директорий:\n"
if len(files) == 0 {
output += "Файлы не найдены или нет доступа\n"
} else {
output += strings.Join(files, "\n") + "\n"
}
output += "\n(Реализовано через talm list)"

return output, nil
}

// executeMemoryCommand выполняет команду memory
func (nm *NodeManager) executeMemoryCommand(ctx context.Context, c *client.Client) (string, error) {
var remotePeer peer.Peer

resp, err := c.Memory(ctx, grpc.Peer(&remotePeer))
if err != nil {
return "", fmt.Errorf("ошибка получения информации о памяти: %v", err)
}

var output strings.Builder

for _, msg := range resp.Messages {
node := client.AddrFromPeer(&remotePeer)
if msg.Metadata != nil {
node = msg.Metadata.Hostname
}

output.WriteString(fmt.Sprintf("Узел: %s\n", node))
output.WriteString(fmt.Sprintf("Общая память: %s\n", humanize.Bytes(uint64(msg.Meminfo.Memtotal))))
output.WriteString(fmt.Sprintf("Свободная память: %s\n", humanize.Bytes(uint64(msg.Meminfo.Memfree))))
output.WriteString(fmt.Sprintf("Доступная память: %s\n", humanize.Bytes(uint64(msg.Meminfo.Memavailable))))
output.WriteString(fmt.Sprintf("Буферы: %s\n", humanize.Bytes(uint64(msg.Meminfo.Buffers))))
output.WriteString(fmt.Sprintf("Кэш: %s\n", humanize.Bytes(uint64(msg.Meminfo.Cached))))
output.WriteString(fmt.Sprintf("Общий SWAP: %s\n", humanize.Bytes(uint64(msg.Meminfo.Swaptotal))))
output.WriteString(fmt.Sprintf("Свободный SWAP: %s\n\n", humanize.Bytes(uint64(msg.Meminfo.Swapfree))))
}

return output.String(), nil
}

// executeProcessesCommand выполняет команду processes
func (nm *NodeManager) executeProcessesCommand(ctx context.Context, c *client.Client) (string, error) {
var remotePeer peer.Peer

resp, err := c.Processes(ctx, grpc.Peer(&remotePeer))
if err != nil {
return "", fmt.Errorf("ошибка получения информации о процессах: %v", err)
}

var output strings.Builder
output.WriteString("Выполняющиеся процессы:\n")
output.WriteString("PID\t\tИМЯ\t\t\tCPU\tПАМЯТЬ\tСОСТОЯНИЕ\n")
output.WriteString(strings.Repeat("-", 80) + "\n")

for _, msg := range resp.Messages {
node := client.AddrFromPeer(&remotePeer)
if msg.Metadata != nil {
node = msg.Metadata.Hostname
}

if msg.Metadata != nil {
output.WriteString(fmt.Sprintf("\nУзел: %s\n", node))
}

// Показываем только первые 20 процессов для читаемости
processes := msg.Processes
if len(processes) > 20 {
processes = processes[:20]
output.WriteString("(Показаны первые 20 процессов)\n")
}

for _, p := range processes {
// Форматируем команду
command := p.Executable
if p.Args != "" {
args := strings.Fields(p.Args)
if len(args) > 0 && command != "" {
if strings.Contains(args[0], command) {
command = p.Args
} else {
command = command + " " + p.Args
}
}
}

// Ограничиваем длину имени процесса
if len(command) > 30 {
command = command[:27] + "..."
}

output.WriteString(fmt.Sprintf("%d\t\t%s\t\t%.2f\t%s\t%c\n",
p.Pid,
command,
p.CpuTime,
humanize.Bytes(uint64(p.ResidentMemory)),
p.State,
))
}
}

output.WriteString("\n(Реализовано через talm processes)")
return output.String(), nil
}

// executeMountsCommand выполняет команду mounts
func (nm *NodeManager) executeMountsCommand(ctx context.Context, c *client.Client) (string, error) {
// Упрощенная реализация - используем форматировщик напрямую
output := "Точки монтирования:\n"
output += "(Команда mounts временно отключена для упрощения)\n"
output += "\nИспользуйте talm mounts для получения подробной информации\n"
return output, nil
}

// executeDisksCommand выполняет команду disks
func (nm *NodeManager) executeDisksCommand(ctx context.Context, c *client.Client) (string, error) {
// Для получения информации о дисках используем get disks через Talos API
// В настоящее время диски получаются через c.GetDisks() или подобный метод
// Здесь используем упрощенную реализацию через LS директории /sys/block

stream, err := c.LS(ctx, &machine.ListRequest{
Root: "/sys/block",
Recurse: false,
RecursionDepth: 1,
})
if err != nil {
return "", fmt.Errorf("ошибка получения информации о дисках: %v", err)
}

var output strings.Builder
output.WriteString("Информация о дисках:\n")
output.WriteString("УСТРОЙСТВО\tРАЗМЕР\t\tТИП\n")
output.WriteString(strings.Repeat("-", 50) + "\n")

for {
info, err := stream.Recv()
if err != nil {
break
}

if info.Error != "" || info.Mode&040000 == 0 {
continue // Пропускаем файлы и ошибки
}

// Парсим размер (в секторах по 512 байт) - упрощенно
sectors := int64(1024*1024) // По умолчанию 512MB для демонстрации
// В реальной реализации нужно читать файл /sys/block/{device}/size
if err != nil {
continue
}

sizeBytes := sectors * 512
sizeHuman := humanize.Bytes(uint64(sizeBytes))

// Определяем тип диска (упрощенно)
diskType := "неизвестен"
if strings.Contains(info.RelativeName, "nvme") {
diskType = "NVMe SSD"
} else if strings.Contains(info.RelativeName, "sd") {
diskType = "SATA SSD/HDD"
} else if strings.Contains(info.RelativeName, "vd") {
diskType = "Виртуальный диск"
}

output.WriteString(fmt.Sprintf("%s\t\t%s\t%s\n", info.RelativeName, sizeHuman, diskType))
}

output.WriteString("\n(Реализовано через /sys/block)")
return output.String(), nil
}

// executeHealthCommand выполняет команду health
func (nm *NodeManager) executeHealthCommand(ctx context.Context, c *client.Client) (string, error) {
// Упрощенная проверка здоровья - проверяем доступность API и базовую информацию
var remotePeer peer.Peer

// Проверяем доступность через version command
versionResp, err := c.Version(ctx, grpc.Peer(&remotePeer))
if err != nil {
return fmt.Sprintf("Статус здоровья:\nЗдоров: false\nПричина: API недоступен - %v\n\n(Ошибка при проверке через talm version)", err), nil
}

var output strings.Builder
output.WriteString("Статус здоровья:\n")

for _, msg := range versionResp.Messages {
node := client.AddrFromPeer(&remotePeer)
if msg.Metadata != nil {
node = msg.Metadata.Hostname
}

output.WriteString(fmt.Sprintf("Узел: %s\n", node))
output.WriteString(fmt.Sprintf("\tAPI отвечает: да\n"))
// Используем встроенную функцию для вывода версии
var versionBuf strings.Builder
fmt.Fprintf(&versionBuf, "\t")
version.PrintLongVersionFromExisting(msg.Version)
versionStr := versionBuf.String()
// Убираем лишние отступы
versionStr = strings.ReplaceAll(versionStr, "\n\t", "\n")
output.WriteString(versionStr)

// Проверяем дополнительные компоненты
output.WriteString("\tПроверка компонентов:\n")
output.WriteString("\t\t• API: ОК\n")
output.WriteString("\t\t• Версия: ОК\n")
output.WriteString("\t\t• Платформа: ОК\n")
output.WriteString("\n")
}

output.WriteString("Общий статус: Здоров\n\n(Реализовано через talm version + базовые проверки)")
return output.String(), nil
}

// executeStatsCommand выполняет команду stats
func (nm *NodeManager) executeStatsCommand(ctx context.Context, c *client.Client) (string, error) {
var remotePeer peer.Peer

// Получаем статистику системных контейнеров
resp, err := c.Stats(ctx, constants.SystemContainerdNamespace, common.ContainerDriver_CONTAINERD, grpc.Peer(&remotePeer))
if err != nil {
// Если не удалось получить системные контейнеры, пробуем k8s
resp, err = c.Stats(ctx, constants.K8sContainerdNamespace, common.ContainerDriver_CRI, grpc.Peer(&remotePeer))
if err != nil {
return "", fmt.Errorf("ошибка получения статистики контейнеров: %v", err)
}
}

var output strings.Builder
output.WriteString("Статистика контейнеров:\n")
output.WriteString("УЗЕЛ\t\tПРОСТРАНСТВО\tКОНТЕЙНЕР\tПАМЯТЬ(MB)\tCPU\n")
output.WriteString(strings.Repeat("-", 80) + "\n")

for _, msg := range resp.Messages {
node := client.AddrFromPeer(&remotePeer)
if msg.Metadata != nil {
node = msg.Metadata.Hostname
}

if len(msg.Stats) == 0 {
output.WriteString(fmt.Sprintf("%s\t\tНет активных контейнеров\n", node))
continue
}

for _, stat := range msg.Stats {
// Отображаем информацию о контейнере
displayID := stat.Id
if stat.Id != stat.PodId {
// Контейнер в поде
displayID = "└─ " + stat.Id
}

// Ограничиваем длину для читаемости
if len(displayID) > 15 {
displayID = displayID[:12] + "..."
}

// Память в MB
memoryMB := float64(stat.MemoryUsage) / 1024.0 / 1024.0

output.WriteString(fmt.Sprintf("%s\t\t%s\t%s\t\t%.2f\t%d\n",
node,
stat.Namespace,
displayID,
memoryMB,
stat.CpuUsage,
))
}
}

output.WriteString("\n(Реализовано через talm stats)")
return output.String(), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This file contains numerous comments and user-facing strings in Russian (e.g., lines 20, 28, 34, 42, 71, 181, etc.). For consistency, accessibility, and maintainability in an open-source project, all code, comments, and UI text should be in English. This issue is present in most of the new files in this pull request and should be addressed globally.

Comment on lines +408 to +412
sectors := int64(1024*1024) // По умолчанию 512MB для демонстрации
// В реальной реализации нужно читать файл /sys/block/{device}/size
if err != nil {
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The logic for determining disk size is a placeholder and functionally incorrect. It defaults to a hardcoded value of 512MB (1024*1024) and ignores the actual disk size. The comment // В реальной реализации нужно читать файл /sys/block/{device}/size confirms this is a known issue, but checking in placeholder code that produces incorrect results is a bug. This needs to be implemented correctly.

Comment on lines +67 to +69
if opts.APIServerURL == "" {
opts.APIServerURL = "https://192.168.0.1:6443"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The API server URL is hardcoded to https://192.168.0.1:6443 as a default. This is not a safe assumption and can lead to configuration errors in different network environments. It would be better to make this a required field or derive it from a more reliable source.

import (
"context"
"fmt"
"io/ioutil"
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The io/ioutil package has been deprecated since Go 1.16. Please use functions from the os and io packages instead. For example, ioutil.ReadDir should be replaced with os.ReadDir, and ioutil.WriteFile with os.WriteFile.

Comment on lines +1 to +2
DEBUG: 2025/12/21 22:57:20 Запуск мастера инициализации
DEBUG: 2025/12/21 22:57:20 Проверка существующих файлов: false
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Log files should not be checked into the version control system. Please remove this file and add *.log to the .gitignore file to prevent this from happening in the future.

Comment on lines +372 to +481
func (s *NetworkScannerImpl) getDisksViaNodeManager(ctx context.Context, ip string) ([]Blockdevice, error) {
log.Printf("[FALLBACK] Получение информации о дисках через NodeManager для %s", ip)

output, err := s.commandExecutor.ExecuteNodeCommand(ctx, ip, "disks")
if err != nil {
log.Printf("[FALLBACK] Ошибка выполнения команды disks для %s: %v", ip, err)
return nil, err
}

log.Printf("[FALLBACK] Получен вывод команды disks для %s: %s", ip, output)

var disks []Blockdevice
lines := strings.Split(output, "\n")

for lineNum, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}

log.Printf("[FALLBACK] Обработка строки %d: %s", lineNum, line)

// Парсим строки вида: "sda\t\t32 GB\tSATA SSD"
parts := strings.Split(line, "\t")
if len(parts) >= 2 {
deviceName := strings.TrimSpace(parts[0])
sizeStr := strings.TrimSpace(parts[1])

log.Printf("[FALLBACK] Парсинг диска: device=%s, sizeStr=%s", deviceName, sizeStr)

// Конвертируем размер в байты
var sizeBytes int
if strings.Contains(sizeStr, "GB") {
var sizeGB int
if _, err := fmt.Sscanf(sizeStr, "%d GB", &sizeGB); err == nil {
sizeBytes = sizeGB * 1024 * 1024 * 1024
log.Printf("[FALLBACK] Конвертирован размер: %d GB -> %d байт", sizeGB, sizeBytes)
}
} else if strings.Contains(sizeStr, "TB") {
var sizeTB int
if _, err := fmt.Sscanf(sizeStr, "%d TB", &sizeTB); err == nil {
sizeBytes = sizeTB * 1024 * 1024 * 1024 * 1024
log.Printf("[FALLBACK] Конвертирован размер: %d TB -> %d байт", sizeTB, sizeBytes)
}
} else {
log.Printf("[FALLBACK] Неизвестный формат размера: %s", sizeStr)
}

// Фильтруем слишком маленькие диски (меньше 3GB) и нежелательные устройства
minSize := 3 * 1024 * 1024 * 1024 // 3GB в байтах
isUnwantedDevice := strings.HasPrefix(deviceName, "zd") ||
strings.HasPrefix(deviceName, "drbd") ||
strings.HasPrefix(deviceName, "loop") ||
strings.HasPrefix(deviceName, "sr")

if sizeBytes > 0 && sizeBytes >= minSize && !isUnwantedDevice {
disk := Blockdevice{
Name: deviceName,
Size: sizeBytes,
DevPath: "/dev/" + deviceName,
Metadata: struct {
ID string `json:"id"`
}{ID: deviceName},
}

// Добавляем тип диска если есть
if len(parts) >= 3 {
disk.Transport = strings.TrimSpace(parts[2])
}

// Пытаемся определить модель диска если есть дополнительная информация
if len(parts) >= 4 {
disk.Model = strings.TrimSpace(parts[3])
}

// [COMPATIBILITY] Диагностические логи для fallback метода
log.Printf("[COMPAT-FALLBACK] Blockdevice создано через fallback для %s:", ip)
log.Printf("[COMPAT-FALLBACK] - Name: %s (JSON tag: %s)", disk.Name, "исключено из JSON")
log.Printf("[COMPAT-FALLBACK] - DevPath: %s (JSON tag: %s)", disk.DevPath, "dev_path")
log.Printf("[COMPAT-FALLBACK] - Size: %d (JSON tag: %s)", disk.Size, "size")
log.Printf("[COMPAT-FALLBACK] - Model: %s (JSON tag: %s)", disk.Model, "model")
log.Printf("[COMPAT-FALLBACK] - Transport: %s (JSON tag: %s)", disk.Transport, "transport")
log.Printf("[COMPAT-FALLBACK] - Metadata.ID: %s (JSON tag: %s)", disk.Metadata.ID, "id")

log.Printf("[FALLBACK] Создан диск: Name=%s, Size=%d, DevPath=%s, Transport=%s, Model=%s",
disk.Name, disk.Size, disk.DevPath, disk.Transport, disk.Model)

disks = append(disks, disk)
} else {
reasons := []string{}
if sizeBytes == 0 {
reasons = append(reasons, "нулевой размер")
}
if sizeBytes > 0 && sizeBytes < minSize {
reasons = append(reasons, fmt.Sprintf("размер %d байт меньше минимального %d байт", sizeBytes, minSize))
}
if isUnwantedDevice {
reasons = append(reasons, "нежелательное устройство")
}

log.Printf("[FALLBACK] Диск %s пропущен: %v", deviceName, strings.Join(reasons, ", "))
}
} else {
log.Printf("[FALLBACK] Строка %d не содержит достаточно частей: %v", lineNum, parts)
}
}

log.Printf("[FALLBACK] Итого найдено %d дисков через NodeManager для %s", len(disks), ip)
return disks, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The function getDisksViaNodeManager parses the plain text output of a command to extract disk information. This approach is very brittle and can easily break if the command's output format changes. It would be much more robust to use a structured output format like JSON (-o json) from the command if available, and then unmarshal the structured data. The current implementation is also quite complex and hard to maintain.

}
}

if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using os.ModePerm (0777) for creating directories is overly permissive and a security risk. Please use a more restrictive permission set, such as 0755, to adhere to the principle of least privilege.

Suggested change
if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {

// LoadNodes загружает список узлов из конфигурации
func (nm *NodeManager) LoadNodes() error {
// Пытаемся загрузить узлы из values.yaml
valuesFile := fmt.Sprintf("%s/values.yaml", nm.rootDir)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using fmt.Sprintf for file path construction is not portable across operating systems. It's recommended to use filepath.Join to ensure the correct path separators are used. This applies to other instances in this file as well (e.g., lines 68, 77).

Suggested change
valuesFile := fmt.Sprintf("%s/values.yaml", nm.rootDir)
valuesFile := filepath.Join(nm.rootDir, "values.yaml")

Comment on lines +369 to +374
func (nm *NodeManager) executeMountsCommand(ctx context.Context, c *client.Client) (string, error) {
// Упрощенная реализация - используем форматировщик напрямую
output := "Точки монтирования:\n"
output += "(Команда mounts временно отключена для упрощения)\n"
output += "\nИспользуйте talm mounts для получения подробной информации\n"
return output, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The function executeMountsCommand is a stub that returns a hardcoded message indicating it's disabled. Incomplete or placeholder features should be fully implemented or removed before merging to avoid confusion and dead code.

Comment on lines +29 to +33
// Если указан rootDir, меняем рабочую директорию
if rootDir != "." {
// В реальном приложении здесь можно добавить логику
// для работы с другими директориями
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The rootDir parameter in NewInitWizard is accepted but not used within the function body. The comment suggests it might be for future use, but unused parameters should be avoided to prevent confusion. Please either implement the logic that uses this parameter or remove it for now.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (14)
internal/pkg/interactive/nodes.go-52-60 (1)

52-60: Manual YAML parsing is fragile and error-prone.

The current approach uses string splitting to parse YAML, which will fail for complex structures, quoted values, or multi-line content. Use a proper YAML parser:

🔎 Proposed fix using yaml.Unmarshal
+import "gopkg.in/yaml.v3"
+
+type valuesYAML struct {
+    Nodes map[string]struct {
+        IP   string `yaml:"ip"`
+        Type string `yaml:"type"`
+    } `yaml:"nodes"`
+}
+
 func (nm *NodeManager) LoadNodes() error {
     valuesFile := fmt.Sprintf("%s/values.yaml", nm.rootDir)
     data, err := os.ReadFile(valuesFile)
     if err != nil {
         return nm.loadNodesFromAlternative()
     }
-    // Парсим YAML (упрощенно)
-    lines := strings.Split(string(data), "\n")
-    for _, line := range lines {
-        if strings.Contains(line, "nodes:") {
-            nm.parseNodesSection(lines)
-            break
-        }
-    }
+    var values valuesYAML
+    if err := yaml.Unmarshal(data, &values); err != nil {
+        return nm.loadNodesFromAlternative()
+    }
+    for name, node := range values.Nodes {
+        nm.nodes = append(nm.nodes, NodeInfo{
+            Hostname: name,
+            IP:       node.IP,
+            Status:   "configured",
+        })
+    }
     return nil
 }
internal/pkg/interactive/nodes.go-204-211 (1)

204-211: Bug: version.PrintLongVersionFromExisting prints to stdout, not to versionBuf.

The function PrintLongVersionFromExisting writes directly to stdout, so versionBuf will remain empty. The version information won't appear in the returned output.

🔎 Proposed fix
-       // Используем встроенную функцию для вывода версии
-       var versionBuf strings.Builder
-       fmt.Fprintf(&versionBuf, "\t")
-       version.PrintLongVersionFromExisting(msg.Version)
-       versionStr := versionBuf.String()
-       // Убираем лишние отступы
-       versionStr = strings.ReplaceAll(versionStr, "\n\t", "\n")
-       output.WriteString(versionStr)
+       if msg.Version != nil {
+           output.WriteString(fmt.Sprintf("\tTag: %s\n", msg.Version.Tag))
+           output.WriteString(fmt.Sprintf("\tSHA: %s\n", msg.Version.Sha))
+       }

Committable suggestion skipped: line range outside the PR's diff.

internal/pkg/ui/initwizard/generate.go-120-127 (1)

120-127: Secrets file should have restrictive permissions.

secrets.yaml contains sensitive cryptographic material. Using 0o644 makes it world-readable. Use 0o600 instead.

Proposed fix
-return writeToDestination(bundleBytes, "secrets.yaml", 0o644)
+return writeToDestination(bundleBytes, "secrets.yaml", 0o600)
internal/pkg/ui/initwizard/generate.go-76-76 (1)

76-76: Potential nil pointer dereference when accessing Contexts.

Same issue as in generator/generate.go. Add defensive nil checks before accessing map keys.

Proposed fix
-configBundle.TalosConfig().Contexts[data.ClusterName].Endpoints = []string{"127.0.0.1"}
+talosConfig := configBundle.TalosConfig()
+if talosConfig != nil && talosConfig.Contexts != nil {
+    if ctx, ok := talosConfig.Contexts[data.ClusterName]; ok && ctx != nil {
+        ctx.Endpoints = []string{"127.0.0.1"}
+    }
+}
internal/pkg/ui/initwizard/cache.go-34-56 (1)

34-56: Race condition: metrics modified outside mutex protection.

hits, misses, and evicted are incremented at lines 40, 49-50, and 54 outside the mutex lock, causing data races under concurrent access.

Proposed fix
 func (c *Cache) Get(key string) (interface{}, bool) {
     c.mutex.RLock()
     entry, exists := c.data[key]
     c.mutex.RUnlock()

     if !exists {
-        c.misses++
+        c.mutex.Lock()
+        c.misses++
+        c.mutex.Unlock()
         return nil, false
     }

     if time.Now().After(entry.ExpiresAt) {
         c.mutex.Lock()
         delete(c.data, key)
+        c.evicted++
+        c.misses++
         c.mutex.Unlock()
-        c.evicted++
-        c.misses++
         return nil, false
     }

+    c.mutex.Lock()
     c.hits++
+    c.mutex.Unlock()
     return entry.Value, true
 }

Alternatively, use atomic.Int64 for all metrics.

internal/pkg/ui/initwizard/cache.go-112-124 (1)

112-124: Goroutine leak: StartCleanup runs forever without shutdown mechanism.

The cleanup goroutine has no way to be stopped, causing a resource leak if the cache is discarded. Accept a context or return a stop function.

Proposed fix with context
-func (c *Cache) StartCleanup(interval time.Duration) {
+func (c *Cache) StartCleanup(ctx context.Context, interval time.Duration) {
     ticker := time.NewTicker(interval)
     go func() {
         defer ticker.Stop()
         for {
             select {
+            case <-ctx.Done():
+                return
             case <-ticker.C:
                 c.Cleanup()
             }
         }
     }()
 }
internal/pkg/ui/initwizard/wizard.go-385-397 (1)

385-397: File handle leak in SetupLogging.

The opened file is never closed since the handle isn't stored. This leaks file descriptors.

Proposed fix
 type WizardImpl struct {
     data      *InitData
     app       *tview.Application
     pages     *tview.Pages
     validator Validator
     scanner   NetworkScanner
     processor DataProcessor
     generator Generator
     presenter Presenter
+    logFile   *os.File
 }

-func (w *WizardImpl) SetupLogging(logFile string) error {
-    file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+func (w *WizardImpl) SetupLogging(logFilePath string) error {
+    file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
     if err != nil {
         return fmt.Errorf("failed to open log file: %v", err)
     }
     
+    w.logFile = file
     log.SetOutput(file)
     log.SetFlags(log.LstdFlags)
     log.SetPrefix("DEBUG: ")
     
     return nil
 }

 func (w *WizardImpl) Shutdown() {
     log.Printf("Shutting down initialization wizard")
+    if w.logFile != nil {
+        w.logFile.Close()
+    }
     if w.app != nil {
         w.app.Stop()
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

internal/pkg/ui/initwizard/generate.go-233-237 (1)

233-237: YAML unmarshals integers as float64, not int.

When unmarshaling into map[string]interface{}, YAML/JSON libraries typically decode numbers as float64. The type assertion values["nr_hugepages"].(int) will always fail.

Proposed fix
-if nr, ok := values["nr_hugepages"].(int); ok && nr == 0 {
-    if data.NrHugepages > 0 {
-        values["nr_hugepages"] = data.NrHugepages
-    }
+if nrVal, ok := values["nr_hugepages"]; ok {
+    var nr int
+    switch v := nrVal.(type) {
+    case int:
+        nr = v
+    case float64:
+        nr = int(v)
+    }
+    if nr == 0 && data.NrHugepages > 0 {
+        values["nr_hugepages"] = data.NrHugepages
+    }
 }
internal/pkg/generator/generate.go-84-84 (1)

84-84: Potential nil pointer dereference.

If configBundle.TalosConfig() returns nil, or if Contexts doesn't contain clusterName, this line will panic. Add defensive checks.

Proposed fix
-configBundle.TalosConfig().Contexts[clusterName].Endpoints = []string{"127.0.0.1"}
+talosConfig := configBundle.TalosConfig()
+if talosConfig != nil && talosConfig.Contexts != nil {
+    if ctx, ok := talosConfig.Contexts[clusterName]; ok && ctx != nil {
+        ctx.Endpoints = []string{"127.0.0.1"}
+    }
+}
internal/pkg/ui/initwizard/network.go-20-28 (1)

20-28: Metrics fields have data races without atomic operations.

PoolMetrics fields are modified without synchronization while the mutex only protects connections map access. Use atomic.Int64 or protect metric updates under the mutex.

Proposed fix using sync/atomic
+import "sync/atomic"

 type PoolMetrics struct {
-    Created    int64
-    Reused     int64
-    Closed     int64
-    Active     int64
-    GetCalls   int64
-    PutCalls   int64
+    Created    atomic.Int64
+    Reused     atomic.Int64
+    Closed     atomic.Int64
+    Active     atomic.Int64
+    GetCalls   atomic.Int64
+    PutCalls   atomic.Int64
 }

Then update usages like p.metrics.Created++ to p.metrics.Created.Add(1).

Committable suggestion skipped: line range outside the PR's diff.

internal/pkg/ui/initwizard/factory.go-285-295 (1)

285-295: Cache cleanup goroutine is started but never stopped - potential goroutine leak.

StartCaches spawns a goroutine for cleanup, but StopCaches is a no-op. This will leak goroutines if WizardComponents is created and destroyed multiple times.

Proposed fix

Add a stop channel to the cache or use context-based cancellation:

+type WizardComponents struct {
+	// ... existing fields ...
+	stopCleanup chan struct{}
+}

 // StartCaches запускает фоновые процессы кэшей
 func (wc *WizardComponents) StartCaches() {
+	wc.stopCleanup = make(chan struct{})
 	if wc.NodeCache != nil {
-		go wc.NodeCache.cache.StartCleanup(1 * time.Minute)
+		go wc.NodeCache.cache.StartCleanupWithStop(1 * time.Minute, wc.stopCleanup)
 	}
 }

 // StopCaches останавливает кэши
 func (wc *WizardComponents) StopCaches() {
-	// Кэши останавливаются автоматически при очистке
+	if wc.stopCleanup != nil {
+		close(wc.stopCleanup)
+	}
 }

Committable suggestion skipped: line range outside the PR's diff.

internal/pkg/ui/initwizard/presenter.go-566-572 (1)

566-572: Hardcoded network configuration will fail for non-192.168.1.x networks.

The gateway and DNS servers are hardcoded, which won't work for nodes on different network segments. Consider deriving the gateway from the node's IP address or prompting the user.

Proposed approach
 		AddButton("OK", func() {
-			// Автоматически устанавливаем сетевую конфигурацию
-			data.Addresses = data.SelectedNode + "/24"
-			data.Gateway = "192.168.1.1"
-			data.DNSServers = "8.8.8.8,1.1.1.1"
+			// Derive gateway from selected node's IP
+			data.Addresses = data.SelectedNode + "/24"
+			parts := strings.Split(data.SelectedNode, ".")
+			if len(parts) == 4 {
+				data.Gateway = fmt.Sprintf("%s.%s.%s.1", parts[0], parts[1], parts[2])
+			}
+			data.DNSServers = "8.8.8.8,1.1.1.1"
 
 			p.ShowConfigConfirmation(data)
 		}).
internal/pkg/ui/initwizard/scanner.go-410-415 (1)

410-415: Integer overflow risk when converting TB to bytes.

The expression sizeTB * 1024 * 1024 * 1024 * 1024 can overflow int on 32-bit systems (where int is 32 bits). 1 TB = ~1.1e12 bytes, which exceeds int32 max (~2.1e9).

Use int64 for size calculations.

Proposed fix
-			} else if strings.Contains(sizeStr, "TB") {
-				var sizeTB int
-				if _, err := fmt.Sscanf(sizeStr, "%d TB", &sizeTB); err == nil {
-					sizeBytes = sizeTB * 1024 * 1024 * 1024 * 1024
+			} else if strings.Contains(sizeStr, "TB") {
+				var sizeTB int64
+				if _, err := fmt.Sscanf(sizeStr, "%d TB", &sizeTB); err == nil {
+					sizeBytes = int(sizeTB * 1024 * 1024 * 1024 * 1024)

Note: Consider using int64 for sizeBytes throughout to avoid truncation for very large disks.

internal/pkg/ui/initwizard/factory.go-50-55 (1)

50-55: Update default Talos version to match latest stable release.

The default TalosVersion is set to "v1.7.0", which is outdated. The latest stable version is v1.11.5 (released November 2025). Even the v1.10.5 referenced in the presenter code is behind the current stable. Update the default to v1.11.5 or the latest available version at the time of release.

🟡 Minor comments (8)
internal/pkg/generator/write.go-47-54 (1)

47-54: Potential panic if path doesn't contain a "/".

strings.SplitN(path, "/", 2) returns a slice of length 1 if path has no "/". Accessing parts[1] (line 54) would panic.

🔎 Proposed fix
     for path, content := range generated.PresetFiles {
         parts := strings.SplitN(path, "/", 2)
+        if len(parts) < 2 {
+            continue // skip malformed paths
+        }
         chartName := parts[0]

         if chartName != opts.Preset && chartName != "talm" {
             continue
         }

         out := filepath.Join(opts.RootDir, parts[1])
pkg/commands/interactive_init.go-43-44 (1)

43-44: Flags --update-interval and --insecure are defined but never used.

These flags are parsed but not passed to initwizard.RunInitWizard(). Either remove them or pass them to the wizard:

🔎 Option 1: Remove unused flags
-   interactiveCmd.Flags().DurationVarP(&interactiveCmdFlags.interval, "update-interval", "d", 3*time.Second, "interval between updates")
-   interactiveCmd.Flags().BoolVarP(&interactiveCmdFlags.insecure, "insecure", "i", false, "use Talos insecure maintenance mode (no talosconfig required)")
🔎 Option 2: Pass to wizard (requires API change)
 RunE: func(cmd *cobra.Command, args []string) error {
-    return initwizard.RunInitWizard()
+    return initwizard.RunInitWizardWithOptions(initwizard.Options{
+        Interval: interactiveCmdFlags.interval,
+        Insecure: interactiveCmdFlags.insecure,
+    })
 },
internal/pkg/interactive/nodes.go-134-142 (1)

134-142: IPv6 addresses will break the IP extraction logic.

Splitting on : will incorrectly parse IPv6 addresses (e.g., 2001:db8::1). Consider using a more robust approach:

🔎 Proposed fix
     for _, line := range lines {
         if strings.Contains(line, "address:") || strings.Contains(line, "ip:") {
-            parts := strings.Split(line, ":")
-            if len(parts) >= 2 {
-                nodeInfo.IP = strings.TrimSpace(parts[1])
-            }
+            // Handle both IPv4 and IPv6 by finding the last colon for the key
+            idx := strings.Index(line, ":")
+            if idx != -1 {
+                nodeInfo.IP = strings.TrimSpace(line[idx+1:])
+            }
         }
     }
internal/pkg/interactive/nodes.go-406-412 (1)

406-412: Dead code: unused error check after hardcoded value.

Line 408 sets sectors to a hardcoded value, then line 410 checks if err != nil where err is from the stream.Recv() call above (line 398), not from any sectors-related operation. This check is misleading.

🔎 Proposed fix
         // Парсим размер (в секторах по 512 байт) - упрощенно
         sectors := int64(1024*1024) // По умолчанию 512MB для демонстрации
         // В реальной реализации нужно читать файл /sys/block/{device}/size
-        if err != nil {
-            continue
-        }
internal/pkg/ui/initwizard/initwizard.go-24-36 (1)

24-36: NewInitWizard doesn't use the rootDir parameter.

The function accepts rootDir but the conditional block is empty. Either implement the directory handling or remove the parameter:

🔎 Option 1: Implement rootDir handling
 func NewInitWizard(rootDir string) Wizard {
-    // Создаем мастер с настройками по умолчанию
-    wizard := NewWizard()
-    
-    // Если указан rootDir, меняем рабочую директорию
-    if rootDir != "." {
-        // В реальном приложении здесь можно добавить логику
-        // для работы с другими директориями
-    }
-    
-    return wizard
+    wizard := NewWizard()
+    wizard.SetRootDir(rootDir) // Requires adding this method to WizardImpl
+    return wizard
 }
🔎 Option 2: Remove unused parameter
-func NewInitWizard(rootDir string) Wizard {
+func NewInitWizard() Wizard {
     wizard := NewWizard()
-    if rootDir != "." {
-    }
     return wizard
 }
internal/pkg/ui/initwizard/wizard_state.go-21-37 (1)

21-37: Potential panic if WizardState value is out of range.

The String() method uses direct array indexing which will panic if s is negative or >= 13. Consider adding bounds checking or using a switch/map.

Proposed fix with bounds checking
 func (s WizardState) String() string {
+     names := [...]string{
-     return [...]string{
          "preset",
          "endpoint",
          "scanning",
          "node_select",
          "node_config",
          "confirm",
          "generate",
          "done",
          "add_node_scan",
          "cozystack_scan",
          "vip_config",
          "network_config",
          "node_details",
-     }[s]
+     }
+     if s < 0 || int(s) >= len(names) {
+         return fmt.Sprintf("unknown(%d)", s)
+     }
+     return names[s]
 }

Committable suggestion skipped: line range outside the PR's diff.

internal/pkg/ui/initwizard/network.go-49-55 (1)

49-55: Type assertion without safety check may panic.

If the connection in the map is not a *timedConn (e.g., after refactoring or corruption), this will panic. Use the comma-ok idiom.

Proposed fix
 if conn, exists := p.connections[key]; exists {
-    // Проверяем, не истекло ли соединение
-    if time.Since(conn.(*timedConn).lastUsed) < p.maxIdle {
+    tc, ok := conn.(*timedConn)
+    if !ok {
+        conn.Close()
+        delete(p.connections, key)
+        p.metrics.Closed++
+    } else if time.Since(tc.lastUsed) < p.maxIdle {
         p.metrics.Reused++
-        conn.(*timedConn).lastUsed = time.Now()
+        tc.lastUsed = time.Now()
         return conn, nil
+    } else {
+        conn.Close()
+        delete(p.connections, key)
+        p.metrics.Closed++
     }
-    // Закрываем истекшее соединение
-    conn.Close()
-    delete(p.connections, key)
-    p.metrics.Closed++
 }
internal/pkg/interactive/template.go-61-64 (1)

61-64: Ignored error from filepath.Rel could cause incorrect paths.

The error from filepath.Rel is silently ignored. If this fails, relPath could be empty or incorrect, leading to wrong template paths being stored.

Proposed fix
-			relPath, _ := filepath.Rel(tm.rootDir, path)
-			templates = append(templates, relPath)
+			relPath, err := filepath.Rel(tm.rootDir, path)
+			if err != nil {
+				return fmt.Errorf("failed to get relative path for %s: %v", path, err)
+			}
+			templates = append(templates, relPath)
🧹 Nitpick comments (30)
internal/pkg/ui/initwizard/types.go (2)

33-45: Duplicate NodeInfo type across packages.

There's a NodeInfo type in internal/pkg/interactive/nodes.go with different fields (IP, Hostname, Status, Version). This could cause confusion when both packages are used together. Consider consolidating into a single shared type or renaming one to clarify its purpose (e.g., InitNodeInfo vs RuntimeNodeInfo).

Additionally, NodeInfo lacks JSON/YAML struct tags, unlike the nested Hardware types. If this struct will be serialized, add appropriate tags for consistency.


4-30: Consider adding struct tags to InitData.

If InitData will be serialized to JSON/YAML (e.g., for caching wizard state or configuration persistence), add appropriate struct tags. The nested types like Hardware and ValuesYAML have tags, but InitData does not.

internal/pkg/interactive/nodes.go (2)

20-21: Inconsistent comment language.

Comments are in Russian, but code identifiers and some other project files use English. Consider translating to English for consistency across the codebase.

Also applies to: 28-29, 34-35, 42-43


251-257: Use constants for file mode bit checks.

Magic numbers like 040000 and 120000 reduce readability. Use os.ModeDir and os.ModeSymlink:

🔎 Proposed fix
+import "io/fs"
+
         typeName := "файл"
-        if info.Mode&040000 != 0 {
+        if fs.FileMode(info.Mode).IsDir() {
             typeName = "директория"
-        } else if info.Mode&120000 != 0 {
+        } else if fs.FileMode(info.Mode)&fs.ModeSymlink != 0 {
             typeName = "символическая ссылка"
         }
internal/pkg/ui/initwizard/errors.go (2)

213-218: Stubbed getCallerLocation() provides no useful information.

The function always returns "initwizard", which defeats the purpose of location tracking. Consider implementing it properly using runtime.Caller:

🔎 Proposed fix
+import "runtime"
+
 func getCallerLocation() string {
-    // Упрощенная версия для получения местоположения
-    // В реальном приложении можно использовать более продвинутые методы
-    return "initwizard"
+    _, file, line, ok := runtime.Caller(2)
+    if !ok {
+        return "unknown"
+    }
+    return fmt.Sprintf("%s:%d", filepath.Base(file), line)
 }

40-41: StackTrace field is declared but never populated.

The StackTrace field in AppError is never set by any constructor or method. Either implement stack trace capture or remove the unused field.

pkg/commands/interactive_init.go (1)

21-24: Missing Short description for the command.

The command has Long but no Short description, which is shown in the parent command's help output:

🔎 Proposed fix
 var interactiveCmd = &cobra.Command{
     Use:   "interactive",
+    Short: "Start interactive TUI for cluster configuration",
     Long:  `Start a terminal-based UI (TUI) similar to talos-bootstrap.`,
     Args:  cobra.NoArgs,
internal/pkg/ui/initwizard/wizard_transitions.go (1)

38-46: Missing back/cancel transitions for VIP, Network, and NodeDetails states.

StateVIPConfig, StateNetworkConfig, and StateNodeDetails only have forward transitions. Users may get stuck if they need to go back:

🔎 Suggested additions
     StateVIPConfig: {
         StateNetworkConfig,
+        StateNodeConfig,  // back
     },
     StateNetworkConfig: {
         StateNodeDetails,
+        StateVIPConfig,  // back
     },
     StateNodeDetails: {
         StateConfirm,
+        StateNetworkConfig,  // back
     },
internal/pkg/ui/initwizard/validator.go (2)

52-59: Compile regexes at package level to avoid repeated compilation.

Regexes are compiled on every validation call. Move to package-level variables:

🔎 Proposed fix
+var (
+    validClusterNameRegex = regexp.MustCompile(`^[a-z0-9-]+$`)
+    validHostnameRegex    = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`)
+)
+
 func (v *ValidatorImpl) ValidateClusterName(name string) error {
     // ...
-    validName := regexp.MustCompile(`^[a-z0-9-]+$`)
-    if !validName.MatchString(name) {
+    if !validClusterNameRegex.MatchString(name) {

Also applies to: 84-91


54-67: Mixed language in error messages.

Lines 56-67 use Russian while other error messages use English. Standardize to one language for consistency.

internal/pkg/generator/write.go (1)

56-59: Hardcoded version "0.1.0" should be configurable.

The chart version is hardcoded. Consider making it configurable via Options or deriving it from the project version:

-        content = fmt.Sprintf(content, clusterName, "0.1.0")
+        content = fmt.Sprintf(content, clusterName, opts.ChartVersion)
internal/pkg/ui/initwizard/wizard_controller.go (2)

8-11: Consider adding a getter for the data field.

The data field is stored but not accessible outside the controller. If other components need to access the InitData, consider adding a GetData() method. Alternatively, if data is intentionally private and accessed only internally, this is fine as-is.


14-19: Consider validating the data parameter.

The constructor accepts *InitData without nil validation. If nil is passed, it could cause issues when the data is eventually accessed.

Proposed fix
 func NewWizardController(data *InitData) *WizardController {
+    if data == nil {
+        data = &InitData{}
+    }
     return &WizardController{
         state: StatePreset,
         data:  data,
     }
 }
internal/pkg/generator/generate.go (1)

52-57: Wrap error with context for consistency.

Other errors in this function include context (e.g., "invalid talos-version"), but this one doesn't.

Proposed fix
 absolutePath, err := filepath.Abs(opts.RootDir)
 if err != nil {
-    return err
+    return fmt.Errorf("failed to resolve absolute path for %s: %w", opts.RootDir, err)
 }
internal/pkg/ui/initwizard/generate.go (1)

172-177: Silent error recovery may mask configuration issues.

When YAML parsing fails, the function silently returns the original content. Consider logging a warning so users are aware of the issue.

Proposed fix
 if err := yaml.Unmarshal([]byte(content), &values); err != nil {
-    // Если не удалось распарсить, возвращаем оригинальный контент
+    log.Printf("WARNING: Failed to parse values.yaml for formatting: %v", err)
     return content
 }
internal/pkg/ui/initwizard/cache.go (1)

240-244: InvalidateClusterConfig clears entire cache, not just cluster entries.

The comment says "Simplified implementation" but this could cause unintended data loss. Consider implementing prefix-based deletion or documenting this behavior clearly.

Proposed fix for prefix-based deletion
 func (cc *ConfigCache) InvalidateClusterConfig(clusterName string) {
-    // Simplified implementation - in a real application, prefixes can be used
-    cc.cache.Clear()
+    cc.cache.mutex.Lock()
+    defer cc.cache.mutex.Unlock()
+    prefix := clusterName + ":"
+    for key := range cc.cache.data {
+        if strings.HasPrefix(key, prefix) {
+            delete(cc.cache.data, key)
+        }
+    }
 }
internal/pkg/ui/initwizard/wizard.go (1)

231-250: Inconsistent node filename pattern.

First node uses "nodes/node1.yaml" (line 230), but additional nodes use "nodes/node-%d.yaml" (line 238). This inconsistency may cause confusion.

Proposed fix for consistency
-nodeFileName := fmt.Sprintf("nodes/node-%d.yaml", len(w.getExistingNodes())+1)
+nodeFileName := fmt.Sprintf("nodes/node%d.yaml", len(w.getExistingNodes())+1)
internal/pkg/interactive/wizard.go (2)

62-80: Navigation logic may cause confusion.

When Escape/Ctrl+C is pressed on non-main pages, it hides "main" and shows "menu". However, if the user is on a different page (e.g., "nodes_info"), hiding "main" has no effect. Consider using SwitchToPage or tracking the current page.

Proposed fix
 case tcell.KeyCtrlC, tcell.KeyEscape:
-    if w.pages.GetPageCount() == 1 {
+    name, _ := w.pages.GetFrontPage()
+    if name == "menu" {
         w.app.Stop()
         return nil
     }
-    w.pages.HidePage("main")
-    w.pages.ShowPage("menu")
+    w.pages.SwitchToPage("menu")
     return nil

452-491: Use cancellable context for long-running operations.

context.Background() prevents cancellation if the user navigates away. Pass a cancellable context that can be cancelled on page hide.

Proposed fix
 func (w *Wizard) renderTemplates() {
     w.pages.HidePage("template")
     
+    ctx, cancel := context.WithCancel(context.Background())
+    defer cancel()
+    
     go func() {
-        ctx := context.Background()
         templates := w.templateManager.GetTemplateFiles()
         
         if len(templates) == 0 {
             w.showErrorModal("Нет шаблонов для рендеринга")
             w.pages.ShowPage("template")
             return
         }
         
         result, err := w.templateManager.RenderTemplates(ctx, templates, nil)
         // ...
     }()
 }
internal/pkg/ui/initwizard/network.go (1)

278-284: Remove the custom min function and use Go's builtin instead.

The project targets Go 1.24.0, which includes the builtin min function introduced in Go 1.21. Replace the custom implementation with the stdlib function.

internal/pkg/ui/initwizard/processor.go (2)

73-86: Duplicate removal is performed twice.

ProcessScanResults calls RemoveDuplicatesByMAC and then FilterAndSortNodes. However, FilterAndSortNodes (lines 31-43) already contains duplicate removal logic by MAC address. This results in redundant processing.

Consider removing the duplicate detection from FilterAndSortNodes and keeping it only in RemoveDuplicatesByMAC, or vice versa.

Proposed fix
 // ProcessScanResults обрабатывает результаты сканирования сети
 func (p *DataProcessorImpl) ProcessScanResults(results []NodeInfo) []NodeInfo {
 	if len(results) == 0 {
 		return results
 	}
 
-	// Удаляем дубликаты
-	processed := p.RemoveDuplicatesByMAC(results)
-
 	// Фильтруем и сортируем
-	processed = p.FilterAndSortNodes(processed)
+	processed := p.FilterAndSortNodes(results)
 
 	return processed
 }

168-219: Performance concern: CalculateResourceStats called repeatedly during sorting.

The sorting methods (SortNodesByCPU, SortNodesByRAM, SortNodesByDisks) call CalculateResourceStats inside the comparison function. For a list of N nodes, this results in O(N log N) calls to CalculateResourceStats per sort, with each call iterating over hardware processors.

For better performance, pre-compute resource stats once before sorting.

Example optimization for SortNodesByCPU
 // SortNodesByCPU сортирует ноды по количеству CPU ядер
 func (p *DataProcessorImpl) SortNodesByCPU(nodes []NodeInfo, ascending bool) []NodeInfo {
 	sorted := make([]NodeInfo, len(nodes))
 	copy(sorted, nodes)
+
+	// Pre-compute CPU stats to avoid repeated calculations
+	cpuStats := make([]int, len(sorted))
+	for i := range sorted {
+		cpuStats[i], _, _ = p.CalculateResourceStats(sorted[i])
+	}
 	
 	sort.Slice(sorted, func(i, j int) bool {
-		cpuI, _, _ := p.CalculateResourceStats(sorted[i])
-		cpuJ, _, _ := p.CalculateResourceStats(sorted[j])
-		
 		if ascending {
-			return cpuI < cpuJ
+			return cpuStats[i] < cpuStats[j]
 		}
-		return cpuI > cpuJ
+		return cpuStats[i] > cpuStats[j]
 	})
 	
 	return sorted
 }
internal/pkg/interactive/template.go (1)

3-13: Replace deprecated io/ioutil with os package.

The io/ioutil package has been deprecated since Go 1.16. Use os.ReadDir instead of ioutil.ReadDir and os.WriteFile instead of ioutil.WriteFile.

Proposed fix
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"

Then update usages:

  • Line 41: ioutil.ReadDiros.ReadDir
  • Line 80: ioutil.ReadDiros.ReadDir
  • Line 165: ioutil.WriteFileos.WriteFile
internal/pkg/ui/initwizard/interfaces.go (2)

10-18: Private methods in public interface restrict external implementations.

The Wizard interface exposes private methods (getData, getApp, getPages, setupInputCapture) which can only be implemented by types in the same package. This is likely intentional for internal use, but if external implementations are ever needed, these would need to be exported.


55-60: Inconsistent return types for secrets bundle operations.

LoadSecretsBundle returns interface{} while SaveSecretsBundle accepts *secrets.Bundle. This asymmetry requires callers to perform type assertions after loading.

Consider using consistent types:

Proposed fix
 	// Secrets management
 	GenerateSecretsBundle(data *InitData) error
-	LoadSecretsBundle() (interface{}, error)
+	LoadSecretsBundle() (*secrets.Bundle, error)
 	ValidateSecretsBundle() error
 	SaveSecretsBundle(bundle *secrets.Bundle) error
internal/pkg/ui/initwizard/presenter.go (2)

199-199: strings.Title is deprecated since Go 1.18.

Use cases.Title from golang.org/x/text/cases package instead.

Proposed fix

Add import:

import "golang.org/x/text/cases"
import "golang.org/x/text/language"

Replace usage:

-	form.SetBorder(true).SetTitle(fmt.Sprintf("%s Preset - Дополнительная Конфигурация", strings.Title(data.Preset))).SetTitleAlign(tview.AlignLeft)
+	caser := cases.Title(language.English)
+	form.SetBorder(true).SetTitle(fmt.Sprintf("%s Preset - Дополнительная Конфигурация", caser.String(data.Preset))).SetTitleAlign(tview.AlignLeft)

1336-1350: Redundant conditional branches with identical values.

Both generic and cozystack presets set identical default values for PodSubnets and ServiceSubnets. The conditional is unnecessary.

Proposed simplification
 	if data.PodSubnets == "" {
-		if data.Preset == "cozystack" {
-			data.PodSubnets = "10.244.0.0/16"
-		} else {
-			data.PodSubnets = "10.244.0.0/16"
-		}
+		data.PodSubnets = "10.244.0.0/16"
 	}
 
 	if data.ServiceSubnets == "" {
-		if data.Preset == "cozystack" {
-			data.ServiceSubnets = "10.96.0.0/16"
-		} else {
-			data.ServiceSubnets = "10.96.0.0/16"
-		}
+		data.ServiceSubnets = "10.96.0.0/16"
 	}
internal/pkg/ui/initwizard/scanner.go (3)

570-573: Context variable shadowing may cause confusion.

Line 572 creates a new context with cancel that shadows the parent ctx. While child context cancellation works correctly (parent cancel propagates to child), the new cancel variable shadows access to the parent's cancel. This is technically correct but can be confusing.

Consider using a different variable name for clarity.

Proposed fix
-	ctx, cancel := context.WithCancel(ctx)
-	defer cancel()
+	scanCtx, scanCancel := context.WithCancel(ctx)
+	defer scanCancel()
 	for w := 0; w < numWorkers; w++ {

Then update usages of ctx within the worker goroutines to use scanCtx.


714-725: Wasteful command creation pattern.

A command is created on line 715, then immediately replaced on line 725 with a context-aware version. This is inefficient.

Proposed fix
 	// Первая попытка с таймаутом
 	{
 		log.Printf("[FIXED] Выполняем первую команду nmap...")
-		cmd := exec.Command("nmap", "-p", "50000", "--open", "-oG", "-", cidr)
 
 		// Проверяем отмену перед выполнением команды
 		select {
 		case <-ctx.Done():
 			log.Printf("[FIXED] Сканирование отменено перед первой командой nmap")
 			return nil, fmt.Errorf("сканирование отменено: %v", ctx.Err())
 		default:
 		}
 
-		cmd = exec.CommandContext(ctx, cmd.Args[0], cmd.Args[1:]...)
+		cmd := exec.CommandContext(ctx, "nmap", "-p", "50000", "--open", "-oG", "-", cidr)
 
 		output, err = cmd.Output()

566-569: Magic number for early termination threshold.

targetNodes := 3 is hardcoded. Consider making this configurable via NetworkScannerImpl fields or a config parameter.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f34474 and de23384.

⛔ Files ignored due to path filters (1)
  • internal/pkg/ui/initwizard/debug.log is excluded by !**/*.log
📒 Files selected for processing (22)
  • internal/pkg/generator/generate.go
  • internal/pkg/generator/write.go
  • internal/pkg/interactive/nodes.go
  • internal/pkg/interactive/template.go
  • internal/pkg/interactive/wizard.go
  • internal/pkg/ui/initwizard/cache.go
  • internal/pkg/ui/initwizard/errors.go
  • internal/pkg/ui/initwizard/factory.go
  • internal/pkg/ui/initwizard/generate.go
  • internal/pkg/ui/initwizard/initwizard.go
  • internal/pkg/ui/initwizard/interfaces.go
  • internal/pkg/ui/initwizard/network.go
  • internal/pkg/ui/initwizard/presenter.go
  • internal/pkg/ui/initwizard/processor.go
  • internal/pkg/ui/initwizard/scanner.go
  • internal/pkg/ui/initwizard/types.go
  • internal/pkg/ui/initwizard/validator.go
  • internal/pkg/ui/initwizard/wizard.go
  • internal/pkg/ui/initwizard/wizard_controller.go
  • internal/pkg/ui/initwizard/wizard_state.go
  • internal/pkg/ui/initwizard/wizard_transitions.go
  • pkg/commands/interactive_init.go
🧰 Additional context used
🧬 Code graph analysis (14)
internal/pkg/generator/write.go (2)
internal/pkg/generator/generate.go (1)
  • Options (16-23)
pkg/generated/presets.go (1)
  • PresetFiles (3-435)
internal/pkg/ui/initwizard/initwizard.go (4)
internal/pkg/ui/initwizard/wizard.go (1)
  • NewWizard (27-61)
internal/pkg/interactive/wizard.go (2)
  • NewWizard (26-47)
  • Wizard (16-23)
internal/pkg/ui/initwizard/types.go (1)
  • InitData (4-30)
internal/pkg/ui/initwizard/interfaces.go (1)
  • Wizard (11-18)
internal/pkg/ui/initwizard/validator.go (2)
internal/pkg/ui/initwizard/interfaces.go (1)
  • Validator (21-33)
internal/pkg/ui/initwizard/errors.go (2)
  • NewValidationError (124-126)
  • NewValidationErrorWithCause (129-131)
internal/pkg/ui/initwizard/types.go (1)
internal/pkg/interactive/nodes.go (1)
  • NodeInfo (21-26)
internal/pkg/ui/initwizard/generate.go (2)
internal/pkg/ui/initwizard/types.go (1)
  • InitData (4-30)
pkg/generated/presets.go (2)
  • AvailablePresets (437-440)
  • PresetFiles (3-435)
internal/pkg/ui/initwizard/network.go (1)
internal/pkg/ui/initwizard/errors.go (3)
  • NewNetworkErrorWithCause (139-141)
  • WrapError (221-226)
  • ErrNetwork (15-15)
internal/pkg/ui/initwizard/wizard_controller.go (2)
internal/pkg/ui/initwizard/wizard_state.go (2)
  • WizardState (3-3)
  • StatePreset (6-6)
internal/pkg/ui/initwizard/types.go (1)
  • InitData (4-30)
internal/pkg/ui/initwizard/cache.go (2)
internal/pkg/ui/initwizard/types.go (2)
  • NodeInfo (33-45)
  • Hardware (53-58)
internal/pkg/interactive/nodes.go (1)
  • NodeInfo (21-26)
internal/pkg/ui/initwizard/wizard_transitions.go (1)
internal/pkg/ui/initwizard/wizard_state.go (13)
  • WizardState (3-3)
  • StatePreset (6-6)
  • StateEndpoint (7-7)
  • StateScanning (8-8)
  • StateNodeSelect (9-9)
  • StateNodeConfig (10-10)
  • StateConfirm (11-11)
  • StateGenerate (12-12)
  • StateDone (13-13)
  • StateCozystackScan (15-15)
  • StateVIPConfig (16-16)
  • StateNetworkConfig (17-17)
  • StateNodeDetails (18-18)
internal/pkg/interactive/template.go (2)
internal/pkg/generator/generate.go (1)
  • Options (16-23)
pkg/modeline/modeline.go (2)
  • GenerateModeline (75-97)
  • ReadAndParseModeline (54-72)
internal/pkg/ui/initwizard/interfaces.go (2)
internal/pkg/ui/initwizard/types.go (5)
  • InitData (4-30)
  • NodeInfo (33-45)
  • Hardware (53-58)
  • ChartYAML (122-130)
  • ValuesYAML (97-113)
internal/pkg/interactive/nodes.go (1)
  • NodeInfo (21-26)
pkg/commands/interactive_init.go (2)
pkg/commands/root.go (1)
  • GlobalArgs (24-24)
internal/pkg/ui/initwizard/initwizard.go (1)
  • RunInitWizard (7-10)
internal/pkg/ui/initwizard/processor.go (2)
internal/pkg/ui/initwizard/interfaces.go (1)
  • DataProcessor (36-42)
internal/pkg/ui/initwizard/types.go (4)
  • NodeInfo (33-45)
  • Hardware (53-58)
  • Memory (68-70)
  • Hostname (48-50)
internal/pkg/ui/initwizard/presenter.go (5)
internal/pkg/ui/initwizard/wizard_state.go (14)
  • WizardState (3-3)
  • StatePreset (6-6)
  • StateEndpoint (7-7)
  • StateScanning (8-8)
  • StateNodeSelect (9-9)
  • StateNodeConfig (10-10)
  • StateConfirm (11-11)
  • StateGenerate (12-12)
  • StateDone (13-13)
  • StateAddNodeScan (14-14)
  • StateCozystackScan (15-15)
  • StateVIPConfig (16-16)
  • StateNetworkConfig (17-17)
  • StateNodeDetails (18-18)
internal/pkg/ui/initwizard/types.go (3)
  • InitData (4-30)
  • Hostname (48-50)
  • Interface (90-94)
internal/pkg/ui/initwizard/interfaces.go (1)
  • Wizard (11-18)
internal/pkg/ui/initwizard/wizard_controller.go (2)
  • WizardController (8-11)
  • NewWizardController (14-19)
internal/pkg/ui/initwizard/generate.go (1)
  • GenerateFromTUI (21-105)
🔇 Additional comments (22)
internal/pkg/ui/initwizard/types.go (1)

96-130: LGTM!

ValuesYAML and ChartYAML are well-structured with appropriate YAML tags for Helm chart generation.

internal/pkg/ui/initwizard/errors.go (2)

91-97: Is() method uses OR logic for Type and Code matching.

The current implementation returns true if either Type or Code matches, which could cause unexpected behavior with errors.Is(). Typically, both should match for equality. Consider whether this is the intended semantics.

// Current: matches if EITHER Type OR Code matches
return e.Type == appErr.Type || e.Code == appErr.Code

43-131: LGTM on error type hierarchy and constructors.

The typed error constructors (NewValidationError, NewNetworkError, etc.) provide a clean API for creating domain-specific errors with consistent structure.

internal/pkg/ui/initwizard/wizard_transitions.go (1)

49-56: LGTM on isAllowed() implementation.

Simple and correct linear search for the allowed transitions lookup.

internal/pkg/ui/initwizard/validator.go (1)

196-211: Both "controlplane" and "control-plane" are valid node types.

This allows two spellings which could cause issues downstream. Consider normalizing to a single canonical form:

// Option: Normalize input before validation
nodeType = strings.ReplaceAll(nodeType, "-", "")
internal/pkg/generator/write.go (1)

25-42: LGTM on writeFile implementation.

Proper handling of Force flag, directory creation, and error wrapping.

internal/pkg/ui/initwizard/initwizard.go (2)

7-10: LGTM on RunInitWizard wrapper.

Clean delegation to the wizard's Run method.


12-16: No action needed. The checkExistingFiles() method exists as an unexported method on WizardImpl (line 148 of wizard.go) and is correctly called on the concrete type returned by NewWizard(). It is appropriately not included in the Wizard interface since it's an internal implementation detail.

internal/pkg/ui/initwizard/wizard_controller.go (1)

21-34: LGTM!

The Transition method correctly validates state transitions and provides detailed debug logging. The error message includes both source and target states which aids debugging.

internal/pkg/ui/initwizard/wizard_state.go (1)

5-19: LGTM!

State enumeration is well-defined with clear, descriptive names for each wizard step.

internal/pkg/generator/generate.go (2)

16-23: LGTM!

Options struct is well-defined with clear field names for configuration.


67-69: Hardcoded default API server URL may not be appropriate.

The default https://192.168.0.1:6443 is a private IP that may not match the user's environment. Consider either requiring this field or documenting the default behavior clearly.

internal/pkg/ui/initwizard/generate.go (2)

20-22: LGTM on function signature and documentation.

Clear entry point for TUI-driven configuration generation.


296-312: LGTM!

The writeToDestination function properly checks for existing files and creates parent directories as needed.

internal/pkg/ui/initwizard/network.go (1)

11-18: LGTM on ConnectionPool structure.

Well-designed pool with idle timeout, lifetime management, and metrics tracking.

internal/pkg/ui/initwizard/cache.go (1)

15-23: LGTM on Cache struct design.

Clean TTL-based cache with metric tracking.

internal/pkg/ui/initwizard/wizard.go (2)

26-61: LGTM on wizard construction.

Clean dependency injection with all components properly wired together.


64-119: LGTM on Run method.

Good defensive checks for nil presenter and data, with proper logging and error handling.

internal/pkg/interactive/wizard.go (1)

25-47: LGTM on wizard initialization.

Clean setup with proper component instantiation.

internal/pkg/ui/initwizard/scanner.go (1)

36-64: Good implementation of network scanning with proper error handling.

The ScanNetwork method follows a clear flow: scan for nodes with open ports, then collect detailed information in parallel. The diagnostic logging and error propagation are well-structured.

internal/pkg/ui/initwizard/processor.go (1)

66-71: Unused stub method in interface - either implement or remove from DataProcessor.

ExtractHardwareInfo is declared in the DataProcessor interface but not called anywhere in the codebase. The current implementation returns an error unconditionally. Either remove the method from the interface if it's not needed, or implement it properly if it's required for the interface contract.

internal/pkg/interactive/template.go (1)

133-139: engine.Render returns []byte, not a map. The conversion logic is flawed.

The code iterates over result (which is []byte) using range, which yields indices (int) and byte values (uint8). Converting these to strings produces keys like "0", "1", etc., with corresponding byte representations as values. This does not meaningfully convert the rendered template output into a usable map[string]string. If the intent is to parse YAML or similar structured content, use the appropriate unmarshaling function instead.

Likely an incorrect or invalid review comment.

Comment on lines +147 to +160
// startInitWizard запускает мастер инициализации
func (w *Wizard) startInitWizard() {
w.pages.HidePage("menu")

// Запускаем init wizard
go func() {
if err := w.initWizard.Run(); err != nil {
w.showErrorModal(fmt.Sprintf("Ошибка инициализации: %v", err))
} else {
w.showSuccessModal("Проект успешно инициализирован!")
}
w.pages.ShowPage("menu")
}()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the repository structure
git ls-files | grep -E "(wizard|init)" | head -20

Repository: cozystack/talm

Length of output: 888


🏁 Script executed:

# Run the provided verification script to check if NewInitWizard creates tview.Application
ast-grep --pattern $'func NewInitWizard($_) $_ {
  $$$
  tview.NewApplication()
  $$$
}'

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Also search for NewInitWizard definition
rg "func NewInitWizard" -A 10

Repository: cozystack/talm

Length of output: 809


🏁 Script executed:

# Search for the initWizard.Run() implementation
rg "func.*Run\(\)" -A 20 | grep -A 20 "initWizard"

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

# Check the imports and struct definitions in wizard.go
head -50 internal/pkg/interactive/wizard.go

Repository: cozystack/talm

Length of output: 1161


🏁 Script executed:

# Search for NewWizard in the initwizard package
rg "func NewWizard" internal/pkg/ui/initwizard/ -A 15

Repository: cozystack/talm

Length of output: 3353


🏁 Script executed:

# Look for the Run() method in initwizard package
rg "func.*\(w.*\).*Run\(\)" internal/pkg/ui/initwizard/ -A 20

Repository: cozystack/talm

Length of output: 1569


🏁 Script executed:

# Check the Wizard interface definition in initwizard package
rg "type Wizard" internal/pkg/ui/initwizard/ -A 10

Repository: cozystack/talm

Length of output: 4786


🏁 Script executed:

# Check if there are any tview.Application creations in the initwizard package
rg "tview.NewApplication" internal/pkg/ui/initwizard/

Repository: cozystack/talm

Length of output: 125


🏁 Script executed:

# Get more details from wizard.go in initwizard package
head -100 internal/pkg/ui/initwizard/wizard.go

Repository: cozystack/talm

Length of output: 2622


🏁 Script executed:

# Get the full Run() method implementation
sed -n '149,250p' internal/pkg/ui/initwizard/wizard.go

Repository: cozystack/talm

Length of output: 3282


🏁 Script executed:

# Check if there's any app.Run() or app.SetRoot() call in initwizard
rg "\.Run\(\)|\.SetRoot" internal/pkg/ui/initwizard/ -B 2 -A 2

Repository: cozystack/talm

Length of output: 1143


🏁 Script executed:

# Check the entire startInitWizard function in wizard.go
sed -n '147,165p' internal/pkg/interactive/wizard.go

Repository: cozystack/talm

Length of output: 540


🏁 Script executed:

# Search for how the outer wizard's app is managed during initWizard execution
rg "app\.Run|app\.Stop" internal/pkg/interactive/wizard.go -B 3 -A 3

Repository: cozystack/talm

Length of output: 808


Lifecycle conflict: two tview.Application instances running concurrently.

initWizard.Run() creates and runs its own tview.Application (via app.SetRoot(w.pages, true).SetFocus(w.pages).Run() at line ~144 of initwizard/wizard.go). Running this in a goroutine while the outer wizard's app.Run() is already active will cause terminal conflicts and race conditions—tview applications require exclusive terminal access and cannot coexist.

The inner and outer wizards need to share the same tview.Application instance or the architecture must be redesigned to not nest application lifecycles.

🤖 Prompt for AI Agents
internal/pkg/interactive/wizard.go lines 147-160: the current code starts
initWizard.Run() in a goroutine even though initWizard.Run creates and runs its
own tview.Application, causing two concurrent tview applications; instead,
refactor to reuse the parent tview.Application by (1) changing the init wizard
API to accept an existing *tview.Application (or provide a RunWithApp(app
*tview.Application) / StartWithApp method) that only sets the root and focus
(SetRoot/SetFocus) without calling app.Run(), (2) pass the outer wizard's app
into the init wizard and call the new RunWithApp synchronously (or via
app.QueueUpdateDraw) rather than starting a separate goroutine, and (3) remove
the inner app.Run to ensure a single application lifecycle is used.

Comment on lines +236 to +255
// Создаем wizard
wizard := &WizardImpl{
data: data,
app: nil, // Будет создан в презентере
pages: nil, // Будет создан в презентере
validator: components.Validator,
scanner: components.Scanner,
processor: components.Processor,
generator: components.Generator,

}

// Создаем презентер
// Создаем презентер с базовыми параметрами
// Презентер будет настроен в презентере
components.Presenter = nil // Временное значение
wizard.presenter = components.Presenter

return wizard, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how WizardImpl is initialized and used
rg -n "BuildWizard|WizardImpl\{" --type go -A 10

Repository: cozystack/talm

Length of output: 4540


🏁 Script executed:

#!/bin/bash
# View the complete BuildWizard function
sed -n '215,260p' internal/pkg/ui/initwizard/factory.go

Repository: cozystack/talm

Length of output: 1262


🏁 Script executed:

#!/bin/bash
# Find WizardImpl struct definition
rg -n "^type WizardImpl struct" --type go -A 15

Repository: cozystack/talm

Length of output: 1072


🏁 Script executed:

#!/bin/bash
# Find all WizardImpl methods
rg -n "func \(w \*WizardImpl\)" --type go | head -20

Repository: cozystack/talm

Length of output: 2007


🏁 Script executed:

#!/bin/bash
# Check the Run() method which is likely the main entry point
sed -n '64,121p' internal/pkg/ui/initwizard/wizard.go

Repository: cozystack/talm

Length of output: 1952


🏁 Script executed:

#!/bin/bash
# Search for where app and pages are set in WizardImpl
rg -n "w\.app\s*=" --type go

Repository: cozystack/talm

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check getApp() and getPages() implementations
sed -n '127,136p' internal/pkg/ui/initwizard/wizard.go

Repository: cozystack/talm

Length of output: 264


🏁 Script executed:

#!/bin/bash
# Search for any setter methods for app or pages
rg -n "SetApp|SetPages|set.*app|set.*pages" --type go -i

Repository: cozystack/talm

Length of output: 2644


🏁 Script executed:

#!/bin/bash
# Search for where tview.Application and tview.Pages are created
rg -n "NewApplication|tview\.Application\{|tview\.NewPages|tview\.Pages\{" --type go

Repository: cozystack/talm

Length of output: 703


🏁 Script executed:

#!/bin/bash
# View the full NewWizard function to compare with BuildWizard
sed -n '27,62p' internal/pkg/ui/initwizard/wizard.go

Repository: cozystack/talm

Length of output: 881


🏁 Script executed:

#!/bin/bash
# Find all usages of BuildWizard in the codebase
rg -n "BuildWizard\(\)" --type go -B 2 -A 5

Repository: cozystack/talm

Length of output: 2453


🏁 Script executed:

#!/bin/bash
# Find usages of CreateDefaultWizard, CreateWizardWithConfig, CreateMinimalWizard
rg -n "CreateDefaultWizard|CreateWizardWithConfig|CreateMinimalWizard" --type go | grep -v "^internal/pkg/ui/initwizard/factory.go:[0-9]*:func"

Repository: cozystack/talm

Length of output: 380


🏁 Script executed:

#!/bin/bash
# Check if there are any actual calls to the factory methods or if NewWizard is used instead
rg -n "NewWizard\(\)|CreateDefaultWizard\(\)|Factory\(\)" --type go | grep -v "^internal/pkg/ui/initwizard/factory.go" | grep -v "^internal/pkg/ui/initwizard/wizard.go"

Repository: cozystack/talm

Length of output: 435


BuildWizard returns WizardImpl with all UI components (app, pages, presenter) set to nil with no initialization mechanism.

The created WizardImpl has app, pages, and presenter all set to nil. Comments suggest these will be "created in the presenter," but presenter itself is nil, making this impossible. More critically, the Run() method calls w.app.SetRoot(w.pages, true) without any nil checks, guaranteeing a nil pointer panic when invoked. There are no setter methods or other mechanisms to initialize these fields after creation.

Consider either:

  • Implementing proper initialization like NewWizard() does (creating tview.NewApplication(), tview.NewPages(), and NewPresenter())
  • Removing this broken factory code if it is not actively used

Comment on lines +323 to +336
func (wc *WizardComponents) getNetworkStats() NetworkStats {
var poolMetrics PoolMetrics
if wc.ConnectionPool != nil {
poolMetrics = wc.ConnectionPool.GetMetrics()
}

return NetworkStats{
PoolSize: wc.ConnectionPool.Size(),
PoolMetrics: poolMetrics,
RateLimiterEnabled: wc.RateLimiter != nil,
MaxWorkers: wc.config.MaxWorkers,
RequestTimeout: wc.config.RequestTimeout,
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Potential nil pointer dereference in getNetworkStats.

ConnectionPool.Size() is called unconditionally on line 330, but line 325-327 only check if it's nil for GetMetrics(). If ConnectionPool is nil, line 330 will panic.

Proposed fix
 func (wc *WizardComponents) getNetworkStats() NetworkStats {
 	var poolMetrics PoolMetrics
+	var poolSize int
 	if wc.ConnectionPool != nil {
 		poolMetrics = wc.ConnectionPool.GetMetrics()
+		poolSize = wc.ConnectionPool.Size()
 	}
 	
 	return NetworkStats{
-		PoolSize: wc.ConnectionPool.Size(),
+		PoolSize: poolSize,
 		PoolMetrics: poolMetrics,
 		RateLimiterEnabled: wc.RateLimiter != nil,
 		MaxWorkers: wc.config.MaxWorkers,
 		RequestTimeout: wc.config.RequestTimeout,
 	}
 }
📝 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
func (wc *WizardComponents) getNetworkStats() NetworkStats {
var poolMetrics PoolMetrics
if wc.ConnectionPool != nil {
poolMetrics = wc.ConnectionPool.GetMetrics()
}
return NetworkStats{
PoolSize: wc.ConnectionPool.Size(),
PoolMetrics: poolMetrics,
RateLimiterEnabled: wc.RateLimiter != nil,
MaxWorkers: wc.config.MaxWorkers,
RequestTimeout: wc.config.RequestTimeout,
}
}
func (wc *WizardComponents) getNetworkStats() NetworkStats {
var poolMetrics PoolMetrics
var poolSize int
if wc.ConnectionPool != nil {
poolMetrics = wc.ConnectionPool.GetMetrics()
poolSize = wc.ConnectionPool.Size()
}
return NetworkStats{
PoolSize: poolSize,
PoolMetrics: poolMetrics,
RateLimiterEnabled: wc.RateLimiter != nil,
MaxWorkers: wc.config.MaxWorkers,
RequestTimeout: wc.config.RequestTimeout,
}
}
🤖 Prompt for AI Agents
In internal/pkg/ui/initwizard/factory.go around lines 323 to 336,
getNetworkStats calls wc.ConnectionPool.Size() without checking for nil which
can panic; modify the function to check if wc.ConnectionPool != nil before
calling Size() (similar to how GetMetrics() is handled), set a default PoolSize
(e.g., 0) when ConnectionPool is nil, and return that value in the NetworkStats
struct so all uses of ConnectionPool are guarded against nil.

Comment on lines +183 to +223
// ScanWithPool сканирование с использованием пула соединений
func (nc *NetworkClient) ScanWithPool(ctx context.Context, cidr string, operation func(context.Context, string, net.Conn) error) error {
// Упрощенная реализация - получаем список IP из CIDR
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return NewNetworkErrorWithCause(
"NET_002",
"некорректная CIDR нотация",
fmt.Sprintf("CIDR: %s", cidr),
err,
)
}

// Создаем канал для ограничения параллелизма
const maxWorkers = 10
workerChan := make(chan struct{}, maxWorkers)
defer close(workerChan)

// Обрабатываем IP адреса
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = nextIP(ip) {
select {
case <-ctx.Done():
return WrapError(ctx.Err(), ErrNetwork, "NET_003", "сканирование отменено", "операция была отменена пользователем")
case workerChan <- struct{}{}:
go func(ip net.IP) {
defer func() { <-workerChan }()

addr := ip.String() + ":50000"
err := nc.ExecuteWithConnection("tcp", addr, func(conn net.Conn) error {
return operation(ctx, addr, conn)
})
if err != nil {
// Логируем ошибку, но продолжаем
// В реальном приложении можно добавить более продвинутую обработку
}
}(ip)
}
}

return nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

ScanWithPool doesn't wait for goroutines to complete before returning.

The function spawns goroutines for each IP but returns immediately after iteration. This means:

  1. The caller won't know when scanning is complete
  2. defer close(workerChan) may close the channel while goroutines are still running
  3. Results from ongoing scans are lost
Proposed fix using sync.WaitGroup
 func (nc *NetworkClient) ScanWithPool(ctx context.Context, cidr string, operation func(context.Context, string, net.Conn) error) error {
     _, ipNet, err := net.ParseCIDR(cidr)
     if err != nil {
         return NewNetworkErrorWithCause(
             "NET_002", 
             "некорректная CIDR нотация", 
             fmt.Sprintf("CIDR: %s", cidr), 
             err,
         )
     }

     const maxWorkers = 10
     workerChan := make(chan struct{}, maxWorkers)
-    defer close(workerChan)
+    var wg sync.WaitGroup

     for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = nextIP(ip) {
         select {
         case <-ctx.Done():
+            wg.Wait()
             return WrapError(ctx.Err(), ErrNetwork, "NET_003", "сканирование отменено", "операция была отменена пользователем")
         case workerChan <- struct{}{}:
+            wg.Add(1)
             go func(ip net.IP) {
-                defer func() { <-workerChan }()
+                defer func() {
+                    <-workerChan
+                    wg.Done()
+                }()
                 
                 addr := ip.String() + ":50000"
                 err := nc.ExecuteWithConnection("tcp", addr, func(conn net.Conn) error {
                     return operation(ctx, addr, conn)
                 })
                 if err != nil {
                     // Log error but continue
                 }
             }(ip)
         }
     }

+    wg.Wait()
     return nil
 }
📝 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
// ScanWithPool сканирование с использованием пула соединений
func (nc *NetworkClient) ScanWithPool(ctx context.Context, cidr string, operation func(context.Context, string, net.Conn) error) error {
// Упрощенная реализация - получаем список IP из CIDR
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return NewNetworkErrorWithCause(
"NET_002",
"некорректная CIDR нотация",
fmt.Sprintf("CIDR: %s", cidr),
err,
)
}
// Создаем канал для ограничения параллелизма
const maxWorkers = 10
workerChan := make(chan struct{}, maxWorkers)
defer close(workerChan)
// Обрабатываем IP адреса
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = nextIP(ip) {
select {
case <-ctx.Done():
return WrapError(ctx.Err(), ErrNetwork, "NET_003", "сканирование отменено", "операция была отменена пользователем")
case workerChan <- struct{}{}:
go func(ip net.IP) {
defer func() { <-workerChan }()
addr := ip.String() + ":50000"
err := nc.ExecuteWithConnection("tcp", addr, func(conn net.Conn) error {
return operation(ctx, addr, conn)
})
if err != nil {
// Логируем ошибку, но продолжаем
// В реальном приложении можно добавить более продвинутую обработку
}
}(ip)
}
}
return nil
}
// ScanWithPool сканирование с использованием пула соединений
func (nc *NetworkClient) ScanWithPool(ctx context.Context, cidr string, operation func(context.Context, string, net.Conn) error) error {
// Упрощенная реализация - получаем список IP из CIDR
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return NewNetworkErrorWithCause(
"NET_002",
"некорректная CIDR нотация",
fmt.Sprintf("CIDR: %s", cidr),
err,
)
}
// Создаем канал для ограничения параллелизма
const maxWorkers = 10
workerChan := make(chan struct{}, maxWorkers)
var wg sync.WaitGroup
// Обрабатываем IP адреса
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = nextIP(ip) {
select {
case <-ctx.Done():
wg.Wait()
return WrapError(ctx.Err(), ErrNetwork, "NET_003", "сканирование отменено", "операция была отменена пользователем")
case workerChan <- struct{}{}:
wg.Add(1)
go func(ip net.IP) {
defer func() {
<-workerChan
wg.Done()
}()
addr := ip.String() + ":50000"
err := nc.ExecuteWithConnection("tcp", addr, func(conn net.Conn) error {
return operation(ctx, addr, conn)
})
if err != nil {
// Логируем ошибку, но продолжаем
// В реальном приложении можно добавить более продвинутую обработку
}
}(ip)
}
}
wg.Wait()
return nil
}
🤖 Prompt for AI Agents
internal/pkg/ui/initwizard/network.go lines 183-223: ScanWithPool spawns
goroutines for each IP but returns immediately and closes workerChan while
goroutines may still run, losing results and causing races; fix by using a
sync.WaitGroup: create a wg before the loop, call wg.Add(1) for each goroutine
before launching, defer nothing on workerChan (do not close it), have each
goroutine call defer wg.Done() and release the worker token, and after the loop
call wg.Wait() (and check ctx.Done() where appropriate) so the function only
returns after all goroutines complete; remove defer close(workerChan) to avoid
closing the channel while workers still use it.

Comment on lines +1015 to +1024
func (p *PresenterImpl) showCozystackScanningModal(data *InitData) {
log.Printf("[FIXED-UI] Запускаем showCozystackScanningModal")

// Создаем контекст с возможностью отмены
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Сохраняем cancel функцию для использования при отмене
p.cancelScan = cancel

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Context cancellation issue in goroutine.

The defer cancel() on line 1020 will cancel the context when showCozystackScanningModal returns, but the scanning goroutine (starting at line 1116) continues to use ctx. This means the scan will be cancelled prematurely when the modal is displayed.

Move the cancel call to dismissModal or after the goroutine completes.

Proposed fix
 func (p *PresenterImpl) showCozystackScanningModal(data *InitData) {
 	log.Printf("[FIXED-UI] Запускаем showCozystackScanningModal")
 
 	// Создаем контекст с возможностью отмены
 	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
 
 	// Сохраняем cancel функцию для использования при отмене
 	p.cancelScan = cancel

The cancel is already called in dismissModal, so the defer cancel() is both redundant and harmful.

🤖 Prompt for AI Agents
internal/pkg/ui/initwizard/presenter.go around lines 1015-1024: the function
creates a cancellable context and assigns p.cancelScan but also calls defer
cancel(), which cancels the context as soon as showCozystackScanningModal
returns and prematurely stops the scanning goroutine; remove the defer cancel()
so the context remains valid for the goroutine, keep assigning p.cancelScan =
cancel, and ensure dismissModal (or the modal close path) calls p.cancelScan()
(with a nil-check) to perform cancellation when the modal is dismissed.

Comment on lines +1597 to +1606

// Сохраняем конфигурацию в файл
if err := os.WriteFile(configFilename, configBytes, 0o644); err != nil {
log.Printf("[MACHINE-CONFIG] Ошибка сохранения файла конфигурации: %v", err)
p.app.QueueUpdateDraw(func() {
p.ShowErrorModal(fmt.Sprintf("Ошибка сохранения конфигурации: %v", err))
})
return
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Type mismatch: engine.Render returns map, but code expects []byte.

engine.Render returns map[string][]byte (or similar), but line 1599 passes configBytes directly to os.WriteFile as if it were []byte. This will cause a compilation error or runtime panic.

Proposed fix
 	// Выполняем рендеринг через engine.Render
 	ctx := context.Background()
-	configBytes, err := engine.Render(ctx, nil, opts)
+	resultMap, err := engine.Render(ctx, nil, opts)
 	if err != nil {
 		log.Printf("[MACHINE-CONFIG] Ошибка рендеринга шаблона: %v", err)
 		p.app.QueueUpdateDraw(func() {
 			p.ShowErrorModal(fmt.Sprintf("Ошибка генерации конфигурации: %v", err))
 		})
 		return
 	}
+
+	// Extract the rendered content for the template
+	var configBytes []byte
+	for _, content := range resultMap {
+		configBytes = content
+		break
+	}
+
+	if len(configBytes) == 0 {
+		log.Printf("[MACHINE-CONFIG] Рендеринг вернул пустой результат")
+		p.app.QueueUpdateDraw(func() {
+			p.ShowErrorModal("Рендеринг шаблона вернул пустой результат")
+		})
+		return
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
internal/pkg/ui/initwizard/presenter.go around lines 1597-1606: engine.Render
returns a map (map[string][]byte) not a single []byte, so passing configBytes
directly to os.WriteFile is a type mismatch; update the code to extract the
intended byte slice from the render map (e.g., rendered["config.json"] or the
appropriate key), validate that the key exists and is not nil, and then call
os.WriteFile with that []byte; if you intend to write multiple files, iterate
the map and write each entry to disk, handling and reporting errors for missing
keys or write failures.

Comment on lines +249 to +256
// Check that the URL contains a port
if !strings.Contains(url, ":") {
return NewValidationError(
"VAL_017",
"API server URL must contain a port (e.g., :6443)",
fmt.Sprintf("provided URL: %s", url),
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Port validation incorrectly passes URLs with protocol but no port.

strings.Contains(url, ":") returns true for https://example.com because of the :// in the protocol. This allows URLs without explicit ports.

🔎 Proposed fix
     // Check that the URL contains a port
-    if !strings.Contains(url, ":") {
+    // Extract host:port portion after protocol
+    urlWithoutProtocol := strings.TrimPrefix(strings.TrimPrefix(url, "https://"), "http://")
+    if !strings.Contains(urlWithoutProtocol, ":") {
         return NewValidationError(
             "VAL_017",
             "API server URL must contain a port (e.g., :6443)",
             fmt.Sprintf("provided URL: %s", url),
         )
     }
🤖 Prompt for AI Agents
In internal/pkg/ui/initwizard/validator.go around lines 249 to 256, the current
check uses strings.Contains(url, ":") which incorrectly treats
"https://example.com" as having a port due to "://"; replace this logic by
parsing the URL with net/url (url.Parse) and then inspect the parsed URL's Host
and Port (use u.Port() or net.SplitHostPort on u.Host) to ensure an explicit
port is present; also handle cases where parsing fails by returning a validation
error and treat empty u.Port() as missing port so you return the same VAL_017
error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants