package converter import ( "encoding/json" "fmt" "os" "sort" "github.com/nvm/kportal/internal/config" "gopkg.in/yaml.v3" ) // KFTrayConfig represents a single port-forward entry from kftray JSON format type KFTrayConfig struct { Service string `json:"service"` Namespace string `json:"namespace"` LocalPort int `json:"local_port"` RemotePort int `json:"remote_port"` Context string `json:"context"` WorkloadType string `json:"workload_type"` Protocol string `json:"protocol"` Alias string `json:"alias"` } // ConvertKFTrayToKPortal converts kftray JSON configuration to kportal YAML format func ConvertKFTrayToKPortal(inputFile, outputFile string) error { // Read kftray JSON config // #nosec G304 -- inputFile is from command line argument for explicit conversion data, err := os.ReadFile(inputFile) if err != nil { return fmt.Errorf("failed to read input file: %w", err) } var kftrayConfigs []KFTrayConfig if err := json.Unmarshal(data, &kftrayConfigs); err != nil { return fmt.Errorf("failed to parse JSON: %w", err) } // Convert to kportal format kportalConfig := convertToKPortal(kftrayConfigs) // Write kportal YAML config yamlData, err := yaml.Marshal(kportalConfig) if err != nil { return fmt.Errorf("failed to generate YAML: %w", err) } // Add header comment header := "# kportal configuration converted from kftray format\n# Generated by kportal --convert\n\n" yamlData = append([]byte(header), yamlData...) if err := os.WriteFile(outputFile, yamlData, 0600); err != nil { return fmt.Errorf("failed to write output file: %w", err) } return nil } // GetConversionSummary returns statistics about the kftray configuration func GetConversionSummary(inputFile string) (map[string]map[string]int, int, error) { // #nosec G304 -- inputFile is from command line argument for explicit conversion data, err := os.ReadFile(inputFile) if err != nil { return nil, 0, fmt.Errorf("failed to read input file: %w", err) } var kftrayConfigs []KFTrayConfig if err := json.Unmarshal(data, &kftrayConfigs); err != nil { return nil, 0, fmt.Errorf("failed to parse JSON: %w", err) } contextMap := make(map[string]map[string]int) for _, cfg := range kftrayConfigs { if _, ok := contextMap[cfg.Context]; !ok { contextMap[cfg.Context] = make(map[string]int) } contextMap[cfg.Context][cfg.Namespace]++ } return contextMap, len(kftrayConfigs), nil } func convertToKPortal(kftrayConfigs []KFTrayConfig) config.Config { // Group by context and namespace contextMap := make(map[string]map[string][]forwardEntry) for _, cfg := range kftrayConfigs { // Initialize context if not exists if _, ok := contextMap[cfg.Context]; !ok { contextMap[cfg.Context] = make(map[string][]forwardEntry) } // Build resource string based on workload type resource := fmt.Sprintf("%s/%s", cfg.WorkloadType, cfg.Service) // Create forward entry forward := forwardEntry{ Resource: resource, Protocol: cfg.Protocol, Port: cfg.RemotePort, LocalPort: cfg.LocalPort, Alias: cfg.Alias, } // Add to namespace contextMap[cfg.Context][cfg.Namespace] = append( contextMap[cfg.Context][cfg.Namespace], forward, ) } // Convert map to structured config var contexts []contextEntry // Sort contexts for consistent output contextNames := make([]string, 0, len(contextMap)) for name := range contextMap { contextNames = append(contextNames, name) } sort.Strings(contextNames) for _, contextName := range contextNames { namespaceMap := contextMap[contextName] // Sort namespaces namespaceNames := make([]string, 0, len(namespaceMap)) for name := range namespaceMap { namespaceNames = append(namespaceNames, name) } sort.Strings(namespaceNames) var namespaces []namespaceEntry for _, namespaceName := range namespaceNames { forwards := namespaceMap[namespaceName] // Sort forwards by local port for consistent output sort.Slice(forwards, func(i, j int) bool { return forwards[i].LocalPort < forwards[j].LocalPort }) namespaces = append(namespaces, namespaceEntry{ Name: namespaceName, Forwards: forwards, }) } contexts = append(contexts, contextEntry{ Name: contextName, Namespaces: namespaces, }) } return config.Config{ Contexts: convertToConfigContexts(contexts), } } // Internal types for conversion (to avoid circular dependencies) type contextEntry struct { Name string Namespaces []namespaceEntry } type namespaceEntry struct { Name string Forwards []forwardEntry } type forwardEntry struct { Resource string `yaml:"resource"` Protocol string `yaml:"protocol"` Port int `yaml:"port"` LocalPort int `yaml:"localPort"` Alias string `yaml:"alias,omitempty"` } // Convert internal types to config package types func convertToConfigContexts(contexts []contextEntry) []config.Context { var result []config.Context for _, ctx := range contexts { var namespaces []config.Namespace for _, ns := range ctx.Namespaces { var forwards []config.Forward for _, fwd := range ns.Forwards { forwards = append(forwards, config.Forward{ Resource: fwd.Resource, Protocol: fwd.Protocol, Port: fwd.Port, LocalPort: fwd.LocalPort, Alias: fwd.Alias, }) } namespaces = append(namespaces, config.Namespace{ Name: ns.Name, Forwards: forwards, }) } result = append(result, config.Context{ Name: ctx.Name, Namespaces: namespaces, }) } return result }