323 lines
8.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|