deb-bootc-image-builder-new/bib/internal/ux/validation.go
2025-09-05 07:10:12 -07:00

323 lines
8.3 KiB
Go

package ux
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// Validator provides input validation functionality
type Validator struct {
verbose bool
}
// NewValidator creates a new validator
func NewValidator(verbose bool) *Validator {
return &Validator{verbose: verbose}
}
// ValidationResult represents the result of a validation
type ValidationResult struct {
Valid bool
Message string
Suggestions []string
}
// ValidateImageReference validates a container image reference
func (v *Validator) ValidateImageReference(imageRef string) *ValidationResult {
if imageRef == "" {
return &ValidationResult{
Valid: false,
Message: "Image reference cannot be empty",
Suggestions: []string{
"Provide a valid container image reference",
"Example: git.raines.xyz/particle-os/debian-bootc:latest",
"Example: docker.io/debian:bookworm-slim",
},
}
}
// Basic validation for image reference format
// Should contain at least one colon or slash
if !strings.Contains(imageRef, ":") && !strings.Contains(imageRef, "/") {
return &ValidationResult{
Valid: false,
Message: "Invalid image reference format",
Suggestions: []string{
"Image reference should contain registry, namespace, and tag",
"Example: registry.example.com/namespace/image:tag",
"Example: image:tag",
},
}
}
// Check for valid characters (allow hyphens in addition to other valid chars)
validChars := regexp.MustCompile(`^[a-zA-Z0-9._/:+-]+$`)
if !validChars.MatchString(imageRef) {
return &ValidationResult{
Valid: false,
Message: "Image reference contains invalid characters",
Suggestions: []string{
"Use only alphanumeric characters, dots, underscores, slashes, hyphens, and colons",
"Example: git.raines.xyz/particle-os/debian-bootc:latest",
},
}
}
return &ValidationResult{Valid: true}
}
// ValidateImageTypes validates image type specifications
func (v *Validator) ValidateImageTypes(imageTypes []string) *ValidationResult {
if len(imageTypes) == 0 {
return &ValidationResult{
Valid: false,
Message: "At least one image type must be specified",
Suggestions: []string{
"Specify one or more image types: qcow2, ami, vmdk, raw, debian-installer, calamares",
"Example: --type qcow2 --type ami",
},
}
}
validTypes := map[string]bool{
"qcow2": true,
"ami": true,
"vmdk": true,
"raw": true,
"debian-installer": true,
"calamares": true,
}
var invalidTypes []string
for _, imgType := range imageTypes {
if !validTypes[imgType] {
invalidTypes = append(invalidTypes, imgType)
}
}
if len(invalidTypes) > 0 {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Invalid image types: %s", strings.Join(invalidTypes, ", ")),
Suggestions: []string{
"Supported image types: qcow2, ami, vmdk, raw, debian-installer, calamares",
"Check spelling and case sensitivity",
},
}
}
return &ValidationResult{Valid: true}
}
// ValidateArchitecture validates target architecture
func (v *Validator) ValidateArchitecture(arch string) *ValidationResult {
if arch == "" {
return &ValidationResult{
Valid: false,
Message: "Target architecture cannot be empty",
Suggestions: []string{
"Specify a valid architecture: amd64, arm64, armhf, ppc64el, s390x",
"Example: --target-arch amd64",
},
}
}
validArchs := map[string]bool{
"amd64": true,
"arm64": true,
"armhf": true,
"ppc64el": true,
"s390x": true,
}
if !validArchs[arch] {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Unsupported architecture: %s", arch),
Suggestions: []string{
"Supported architectures: amd64, arm64, armhf, ppc64el, s390x",
"Use --target-arch amd64 for x86_64 systems",
},
}
}
return &ValidationResult{Valid: true}
}
// ValidateRootfsType validates root filesystem type
func (v *Validator) ValidateRootfsType(rootfsType string) *ValidationResult {
if rootfsType == "" {
// Empty is valid (will use default)
return &ValidationResult{Valid: true}
}
validTypes := map[string]bool{
"ext4": true,
"xfs": true,
"btrfs": true,
}
if !validTypes[rootfsType] {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Unsupported root filesystem type: %s", rootfsType),
Suggestions: []string{
"Supported filesystem types: ext4, xfs, btrfs",
"Leave empty to use default (ext4)",
},
}
}
return &ValidationResult{Valid: true}
}
// ValidateDirectory validates a directory path
func (v *Validator) ValidateDirectory(path, purpose string) *ValidationResult {
if path == "" {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("%s path cannot be empty", purpose),
Suggestions: []string{
fmt.Sprintf("Specify a valid directory path for %s", purpose),
"Use absolute or relative paths",
},
}
}
// Check if path exists and is a directory
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
// Try to create the directory
if err := os.MkdirAll(path, 0755); err != nil {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Cannot create %s directory: %v", purpose, err),
Suggestions: []string{
"Check parent directory permissions",
"Use a different directory path",
"Run with appropriate permissions",
},
}
}
return &ValidationResult{Valid: true}
}
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Cannot access %s directory: %v", purpose, err),
Suggestions: []string{
"Check directory permissions",
"Ensure the path is accessible",
},
}
}
if !info.IsDir() {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("%s path is not a directory: %s", purpose, path),
Suggestions: []string{
"Specify a directory path, not a file",
"Check the path exists and is a directory",
},
}
}
return &ValidationResult{Valid: true}
}
// ValidateConfigFile validates a configuration file path
func (v *Validator) ValidateConfigFile(configPath string) *ValidationResult {
if configPath == "" {
// Empty is valid (will use default)
return &ValidationResult{Valid: true}
}
// Check if file exists
info, err := os.Stat(configPath)
if err != nil {
if os.IsNotExist(err) {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Configuration file not found: %s", configPath),
Suggestions: []string{
"Check the file path is correct",
"Create a configuration file at the specified path",
"Leave empty to use default configuration",
},
}
}
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Cannot access configuration file: %v", err),
Suggestions: []string{
"Check file permissions",
"Ensure the file is accessible",
},
}
}
if info.IsDir() {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Configuration path is a directory, not a file: %s", configPath),
Suggestions: []string{
"Specify a file path, not a directory",
"Example: .config/registry.yaml",
},
}
}
// Check file extension
ext := strings.ToLower(filepath.Ext(configPath))
if ext != ".yaml" && ext != ".yml" {
return &ValidationResult{
Valid: false,
Message: fmt.Sprintf("Configuration file should be YAML format: %s", configPath),
Suggestions: []string{
"Use .yaml or .yml extension",
"Example: .config/registry.yaml",
},
}
}
return &ValidationResult{Valid: true}
}
// ValidateDistroDefPath validates distribution definition path
func (v *Validator) ValidateDistroDefPath(path string) *ValidationResult {
if path == "" {
return &ValidationResult{
Valid: false,
Message: "Distribution definition path cannot be empty",
Suggestions: []string{
"Specify a valid directory path containing package definitions",
"Example: ./data/defs",
},
}
}
return v.ValidateDirectory(path, "distribution definition")
}
// PrintValidationResult prints validation results with suggestions
func (v *Validator) PrintValidationResult(result *ValidationResult) {
if result.Valid {
if v.verbose {
PrintInfo(os.Stdout, "Validation passed")
}
return
}
PrintError(os.Stdout, result.Message)
if len(result.Suggestions) > 0 {
fmt.Println("💡 Suggestions:")
for _, suggestion := range result.Suggestions {
fmt.Printf(" • %s\n", suggestion)
}
}
}