Files
2025-09-29 10:43:51 -07:00

633 lines
20 KiB
Go

package convert
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
// V1Beta2ToV1Beta3Result holds the conversion results
type V1Beta2ToV1Beta3Result struct {
TemplatedSpec string `yaml:"-"`
ValuesFile string `yaml:"-"`
Values map[string]interface{} `yaml:"-"`
}
// ConvertToV1Beta3 converts a v1beta2 preflight spec to v1beta3 format with templating
func ConvertToV1Beta3(doc []byte) (*V1Beta2ToV1Beta3Result, error) {
var parsed map[string]interface{}
err := yaml.Unmarshal(doc, &parsed)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal yaml")
}
// Check if it's already v1beta3
if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == "troubleshoot.sh/v1beta3" {
return nil, errors.New("document is already v1beta3")
}
// Check if it's v1beta2
if apiVersion, ok := parsed["apiVersion"]; !ok || apiVersion != "troubleshoot.sh/v1beta2" {
return nil, errors.Errorf("unsupported apiVersion: %v", apiVersion)
}
// Check if it's a preflight spec
if kind, ok := parsed["kind"]; !ok || kind != "Preflight" {
return nil, errors.Errorf("unsupported kind: %v", kind)
}
// Extract values and create templated spec
values := make(map[string]interface{})
converter := &v1beta3Converter{
values: values,
spec: parsed,
}
templatedSpec, err := converter.convert()
if err != nil {
return nil, errors.Wrap(err, "failed to convert spec")
}
// Marshal values
valuesBytes, err := yaml.Marshal(values)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal values")
}
return &V1Beta2ToV1Beta3Result{
TemplatedSpec: templatedSpec,
ValuesFile: string(valuesBytes),
Values: values,
}, nil
}
type v1beta3Converter struct {
values map[string]interface{}
spec map[string]interface{}
}
func (c *v1beta3Converter) convert() (string, error) {
// Initialize values structure
c.initializeValues()
// Get metadata name
metadataName := "converted-from-v1beta2"
if metadata, ok := c.spec["metadata"].(map[interface{}]interface{}); ok {
if name, ok := metadata["name"].(string); ok {
metadataName = name
}
}
// Process spec
var analyzers []interface{}
if spec, ok := c.spec["spec"].(map[interface{}]interface{}); ok {
if analyzersList, ok := spec["analyzers"].([]interface{}); ok {
convertedAnalyzers, err := c.convertAnalyzers(analyzersList)
if err != nil {
return "", errors.Wrap(err, "failed to convert analyzers")
}
analyzers = convertedAnalyzers
}
}
// Build the templated spec string
var buf bytes.Buffer
// Header
buf.WriteString("apiVersion: troubleshoot.sh/v1beta3\n")
buf.WriteString("kind: Preflight\n")
buf.WriteString("metadata:\n")
buf.WriteString(fmt.Sprintf(" name: %s\n", metadataName))
buf.WriteString("spec:\n")
buf.WriteString(" analyzers:\n")
// Add each analyzer
for _, analyzer := range analyzers {
if analyzerStr, ok := analyzer.(string); ok {
// This is already a templated string
buf.WriteString(" ")
buf.WriteString(strings.ReplaceAll(analyzerStr, "\n", "\n "))
buf.WriteString("\n")
} else {
// Convert to YAML and add as-is
analyzerBytes, err := yaml.Marshal(analyzer)
if err != nil {
return "", errors.Wrap(err, "failed to marshal analyzer")
}
lines := strings.Split(string(analyzerBytes), "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" {
buf.WriteString(" - ")
buf.WriteString(line)
buf.WriteString("\n")
}
}
}
}
return buf.String(), nil
}
func (c *v1beta3Converter) initializeValues() {
c.values["kubernetes"] = map[string]interface{}{
"enabled": false,
"minVersion": "1.20.0",
"recommendedVersion": "1.22.0",
}
c.values["storage"] = map[string]interface{}{
"enabled": false,
"className": "default",
}
c.values["cluster"] = map[string]interface{}{
"minNodes": 3,
"recommendedNodes": 5,
"minCPU": 4,
}
c.values["node"] = map[string]interface{}{
"minMemoryGi": 8,
"recommendedMemoryGi": 32,
"minEphemeralGi": 40,
"recommendedEphemeralGi": 100,
}
c.values["ingress"] = map[string]interface{}{
"enabled": false,
"type": "Contour",
}
c.values["runtime"] = map[string]interface{}{
"enabled": false,
}
c.values["distribution"] = map[string]interface{}{
"enabled": false,
}
c.values["nodeChecks"] = map[string]interface{}{
"enabled": false,
"count": map[string]interface{}{
"enabled": false,
},
"cpu": map[string]interface{}{
"enabled": false,
},
"memory": map[string]interface{}{
"enabled": false,
},
"ephemeral": map[string]interface{}{
"enabled": false,
},
}
}
func (c *v1beta3Converter) convertAnalyzers(analyzers []interface{}) ([]interface{}, error) {
var result []interface{}
for _, analyzer := range analyzers {
if analyzerMap, ok := analyzer.(map[interface{}]interface{}); ok {
converted, err := c.convertAnalyzer(analyzerMap)
if err != nil {
return nil, err
}
if converted != nil {
result = append(result, converted)
}
}
}
return result, nil
}
func (c *v1beta3Converter) convertAnalyzer(analyzer map[interface{}]interface{}) (interface{}, error) {
// Convert analyzer based on type
if _, exists := analyzer["clusterVersion"]; exists {
return c.convertClusterVersion(analyzer)
}
if _, exists := analyzer["customResourceDefinition"]; exists {
return c.convertCustomResourceDefinition(analyzer)
}
if _, exists := analyzer["containerRuntime"]; exists {
return c.convertContainerRuntime(analyzer)
}
if _, exists := analyzer["storageClass"]; exists {
return c.convertStorageClass(analyzer)
}
if _, exists := analyzer["distribution"]; exists {
return c.convertDistribution(analyzer)
}
if _, exists := analyzer["nodeResources"]; exists {
return c.convertNodeResources(analyzer)
}
// For unrecognized analyzers, return as-is with warning comment
return c.wrapWithWarning(analyzer, "Unknown analyzer type - manual review required")
}
func (c *v1beta3Converter) convertClusterVersion(analyzer map[interface{}]interface{}) (interface{}, error) {
// Enable kubernetes checks
c.setNestedValue("kubernetes.enabled", true)
// Extract version requirements from outcomes
if cv, ok := analyzer["clusterVersion"].(map[interface{}]interface{}); ok {
if outcomes, ok := cv["outcomes"].([]interface{}); ok {
c.extractVersionRequirements(outcomes)
}
}
return c.createTemplatedAnalyzer("kubernetes", analyzer, "")
}
func (c *v1beta3Converter) convertCustomResourceDefinition(analyzer map[interface{}]interface{}) (interface{}, error) {
c.setNestedValue("ingress.enabled", true)
if crd, ok := analyzer["customResourceDefinition"].(map[interface{}]interface{}); ok {
if crdName, ok := crd["customResourceDefinitionName"].(string); ok {
if strings.Contains(crdName, "contour") {
c.setNestedValue("ingress.type", "Contour")
}
}
}
return c.createTemplatedAnalyzer("ingress", analyzer, "")
}
func (c *v1beta3Converter) convertContainerRuntime(analyzer map[interface{}]interface{}) (interface{}, error) {
c.setNestedValue("runtime.enabled", true)
return c.createTemplatedAnalyzer("runtime", analyzer, "")
}
func (c *v1beta3Converter) convertStorageClass(analyzer map[interface{}]interface{}) (interface{}, error) {
c.setNestedValue("storage.enabled", true)
// Extract storage class name
if sc, ok := analyzer["storageClass"].(map[interface{}]interface{}); ok {
if className, ok := sc["storageClassName"].(string); ok {
c.setNestedValue("storage.className", className)
}
}
// Update the analyzer to use template
if sc, ok := analyzer["storageClass"].(map[interface{}]interface{}); ok {
sc["storageClassName"] = "{{ .Values.storage.className }}"
}
return c.createTemplatedAnalyzer("storage", analyzer, "")
}
func (c *v1beta3Converter) convertDistribution(analyzer map[interface{}]interface{}) (interface{}, error) {
c.setNestedValue("distribution.enabled", true)
return c.createTemplatedAnalyzer("distribution", analyzer, "")
}
func (c *v1beta3Converter) convertNodeResources(analyzer map[interface{}]interface{}) (interface{}, error) {
if nr, ok := analyzer["nodeResources"].(map[interface{}]interface{}); ok {
checkName := ""
if name, ok := nr["checkName"].(string); ok {
checkName = strings.ToLower(name)
}
// Determine node resource type and enable appropriate check
if strings.Contains(checkName, "node") && strings.Contains(checkName, "count") {
c.setNestedValue("nodeChecks.enabled", true)
c.setNestedValue("nodeChecks.count.enabled", true)
c.extractNodeCountRequirements(nr)
return c.createTemplatedAnalyzer("nodeChecks.count", analyzer, "")
}
if strings.Contains(checkName, "cpu") || strings.Contains(checkName, "core") {
c.setNestedValue("nodeChecks.enabled", true)
c.setNestedValue("nodeChecks.cpu.enabled", true)
c.extractCPURequirements(nr)
return c.createTemplatedAnalyzer("nodeChecks.cpu", analyzer, "")
}
if strings.Contains(checkName, "memory") {
c.setNestedValue("nodeChecks.enabled", true)
c.setNestedValue("nodeChecks.memory.enabled", true)
c.extractMemoryRequirements(nr)
c.templatizeMemoryOutcomes(analyzer)
return c.createTemplatedAnalyzer("nodeChecks.memory", analyzer, "")
}
if strings.Contains(checkName, "ephemeral") || strings.Contains(checkName, "storage") {
c.setNestedValue("nodeChecks.enabled", true)
c.setNestedValue("nodeChecks.ephemeral.enabled", true)
c.extractEphemeralRequirements(nr)
c.templatizeEphemeralOutcomes(analyzer)
return c.createTemplatedAnalyzer("nodeChecks.ephemeral", analyzer, "")
}
}
// Default case - enable general node checks
c.setNestedValue("nodeChecks.enabled", true)
return c.createTemplatedAnalyzer("nodeChecks", analyzer, "")
}
func (c *v1beta3Converter) createTemplatedAnalyzer(checkType string, originalAnalyzer map[interface{}]interface{}, docString string) (interface{}, error) {
// Convert map[interface{}]interface{} to map[string]interface{} for proper YAML output
convertedAnalyzer := c.convertMapKeys(originalAnalyzer)
// Add placeholder docString - user should replace with their actual requirements
convertedAnalyzer["docString"] = "# TODO: Add docString with Title, Requirement, and rationale for this check"
// Marshal the analyzer to YAML
analyzerBytes, err := yaml.Marshal(convertedAnalyzer)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal analyzer")
}
// Create template string with proper indentation
analyzerYAML := strings.TrimSuffix(string(analyzerBytes), "\n")
// Add conditional wrapper
condition := fmt.Sprintf("{{- if .Values.%s.enabled }}", checkType)
endCondition := "{{- end }}"
templateStr := fmt.Sprintf("%s\n- %s\n%s", condition,
strings.ReplaceAll(analyzerYAML, "\n", "\n "),
endCondition)
return templateStr, nil
}
func (c *v1beta3Converter) wrapWithWarning(analyzer map[interface{}]interface{}, warning string) (interface{}, error) {
convertedAnalyzer := c.convertMapKeys(analyzer)
convertedAnalyzer["docString"] = fmt.Sprintf("# TODO: Manual Review Required - %s", warning)
return convertedAnalyzer, nil
}
func (c *v1beta3Converter) convertMapKeys(m map[interface{}]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range m {
strKey := fmt.Sprintf("%v", k)
switch val := v.(type) {
case map[interface{}]interface{}:
result[strKey] = c.convertMapKeys(val)
case []interface{}:
result[strKey] = c.convertSlice(val)
default:
result[strKey] = val
}
}
return result
}
func (c *v1beta3Converter) convertSlice(s []interface{}) []interface{} {
result := make([]interface{}, len(s))
for i, v := range s {
switch val := v.(type) {
case map[interface{}]interface{}:
result[i] = c.convertMapKeys(val)
case []interface{}:
result[i] = c.convertSlice(val)
default:
result[i] = val
}
}
return result
}
// Helper methods for extracting requirements from outcomes
func (c *v1beta3Converter) extractVersionRequirements(outcomes []interface{}) {
for _, outcome := range outcomes {
if outcomeMap, ok := outcome.(map[interface{}]interface{}); ok {
if fail, ok := outcomeMap["fail"].(map[interface{}]interface{}); ok {
if when, ok := fail["when"].(string); ok {
if version := c.extractVersionFromWhen(when); version != "" {
c.setNestedValue("kubernetes.minVersion", version)
}
}
}
if warn, ok := outcomeMap["warn"].(map[interface{}]interface{}); ok {
if when, ok := warn["when"].(string); ok {
if version := c.extractVersionFromWhen(when); version != "" {
c.setNestedValue("kubernetes.recommendedVersion", version)
}
}
}
}
}
}
func (c *v1beta3Converter) extractVersionFromWhen(when string) string {
// Simple version extraction from conditions like "< 1.22.0"
when = strings.TrimSpace(when)
if strings.HasPrefix(when, "<") {
version := strings.TrimSpace(strings.TrimPrefix(when, "<"))
version = strings.Trim(version, `"`)
return version
}
return ""
}
func (c *v1beta3Converter) extractNodeCountRequirements(nr map[interface{}]interface{}) {
if outcomes, ok := nr["outcomes"].([]interface{}); ok {
for _, outcome := range outcomes {
if outcomeMap, ok := outcome.(map[interface{}]interface{}); ok {
if fail, ok := outcomeMap["fail"].(map[interface{}]interface{}); ok {
if when, ok := fail["when"].(string); ok {
if count := c.extractNumberFromWhen(when, "count()"); count > 0 {
c.setNestedValue("cluster.minNodes", count)
}
}
}
if warn, ok := outcomeMap["warn"].(map[interface{}]interface{}); ok {
if when, ok := warn["when"].(string); ok {
if count := c.extractNumberFromWhen(when, "count()"); count > 0 {
c.setNestedValue("cluster.recommendedNodes", count)
}
}
}
}
}
}
}
func (c *v1beta3Converter) extractCPURequirements(nr map[interface{}]interface{}) {
if outcomes, ok := nr["outcomes"].([]interface{}); ok {
for _, outcome := range outcomes {
if outcomeMap, ok := outcome.(map[interface{}]interface{}); ok {
if fail, ok := outcomeMap["fail"].(map[interface{}]interface{}); ok {
if when, ok := fail["when"].(string); ok {
if cpu := c.extractNumberFromWhen(when, "sum(cpuCapacity)"); cpu > 0 {
c.setNestedValue("cluster.minCPU", cpu)
}
}
}
}
}
}
}
func (c *v1beta3Converter) extractMemoryRequirements(nr map[interface{}]interface{}) {
if outcomes, ok := nr["outcomes"].([]interface{}); ok {
for _, outcome := range outcomes {
if outcomeMap, ok := outcome.(map[interface{}]interface{}); ok {
if fail, ok := outcomeMap["fail"].(map[interface{}]interface{}); ok {
if when, ok := fail["when"].(string); ok {
if memory := c.extractMemoryFromWhen(when); memory > 0 {
c.setNestedValue("node.minMemoryGi", memory)
}
}
}
if warn, ok := outcomeMap["warn"].(map[interface{}]interface{}); ok {
if when, ok := warn["when"].(string); ok {
if memory := c.extractMemoryFromWhen(when); memory > 0 {
c.setNestedValue("node.recommendedMemoryGi", memory)
}
}
}
}
}
}
}
func (c *v1beta3Converter) extractEphemeralRequirements(nr map[interface{}]interface{}) {
if outcomes, ok := nr["outcomes"].([]interface{}); ok {
for _, outcome := range outcomes {
if outcomeMap, ok := outcome.(map[interface{}]interface{}); ok {
if fail, ok := outcomeMap["fail"].(map[interface{}]interface{}); ok {
if when, ok := fail["when"].(string); ok {
if storage := c.extractStorageFromWhen(when); storage > 0 {
c.setNestedValue("node.minEphemeralGi", storage)
}
}
}
if warn, ok := outcomeMap["warn"].(map[interface{}]interface{}); ok {
if when, ok := warn["when"].(string); ok {
if storage := c.extractStorageFromWhen(when); storage > 0 {
c.setNestedValue("node.recommendedEphemeralGi", storage)
}
}
}
}
}
}
}
func (c *v1beta3Converter) extractNumberFromWhen(when, prefix string) int {
when = strings.TrimSpace(when)
if strings.Contains(when, prefix) {
// Extract number from conditions like "count() < 3"
parts := strings.Split(when, "<")
if len(parts) == 2 {
numStr := strings.TrimSpace(parts[1])
if num, err := strconv.Atoi(numStr); err == nil {
return num
}
}
}
return 0
}
func (c *v1beta3Converter) extractMemoryFromWhen(when string) int {
when = strings.TrimSpace(when)
// Handle conditions like "min(memoryCapacity) < 8Gi"
if strings.Contains(when, "memoryCapacity") {
parts := strings.Split(when, "<")
if len(parts) == 2 {
sizeStr := strings.TrimSpace(parts[1])
sizeStr = strings.TrimSuffix(sizeStr, "Gi")
if num, err := strconv.Atoi(sizeStr); err == nil {
return num
}
}
}
return 0
}
func (c *v1beta3Converter) extractStorageFromWhen(when string) int {
when = strings.TrimSpace(when)
// Handle conditions like "min(ephemeralStorageCapacity) < 40Gi"
if strings.Contains(when, "ephemeralStorageCapacity") {
parts := strings.Split(when, "<")
if len(parts) == 2 {
sizeStr := strings.TrimSpace(parts[1])
sizeStr = strings.TrimSuffix(sizeStr, "Gi")
if num, err := strconv.Atoi(sizeStr); err == nil {
return num
}
}
}
return 0
}
func (c *v1beta3Converter) templatizeMemoryOutcomes(analyzer map[interface{}]interface{}) {
c.templatizeNodeResourceOutcomes(analyzer, "memoryCapacity", "node.minMemoryGi", "node.recommendedMemoryGi")
}
func (c *v1beta3Converter) templatizeEphemeralOutcomes(analyzer map[interface{}]interface{}) {
c.templatizeNodeResourceOutcomes(analyzer, "ephemeralStorageCapacity", "node.minEphemeralGi", "node.recommendedEphemeralGi")
}
func (c *v1beta3Converter) templatizeNodeResourceOutcomes(analyzer map[interface{}]interface{}, capacity, minKey, recKey string) {
if nr, ok := analyzer["nodeResources"].(map[interface{}]interface{}); ok {
if outcomes, ok := nr["outcomes"].([]interface{}); ok {
for _, outcome := range outcomes {
if outcomeMap, ok := outcome.(map[interface{}]interface{}); ok {
// Update fail condition
if fail, ok := outcomeMap["fail"].(map[interface{}]interface{}); ok {
if when, ok := fail["when"].(string); ok && strings.Contains(when, capacity) {
fail["when"] = fmt.Sprintf("min(%s) < {{ .Values.%s }}Gi", capacity, minKey)
}
if _, ok := fail["message"].(string); ok {
parts := strings.Split(minKey, ".")
fail["message"] = fmt.Sprintf("All nodes must have at least {{ .Values.%s }} GiB of %s.", minKey, parts[len(parts)-1])
}
}
// Update warn condition
if warn, ok := outcomeMap["warn"].(map[interface{}]interface{}); ok {
if when, ok := warn["when"].(string); ok && strings.Contains(when, capacity) {
warn["when"] = fmt.Sprintf("min(%s) < {{ .Values.%s }}Gi", capacity, recKey)
}
if _, ok := warn["message"].(string); ok {
parts := strings.Split(recKey, ".")
warn["message"] = fmt.Sprintf("All nodes are recommended to have at least {{ .Values.%s }} GiB of %s.", recKey, parts[len(parts)-1])
}
}
// Update pass message
if pass, ok := outcomeMap["pass"].(map[interface{}]interface{}); ok {
if _, ok := pass["message"].(string); ok {
parts := strings.Split(recKey, ".")
pass["message"] = fmt.Sprintf("All nodes have at least {{ .Values.%s }} GiB of %s.", recKey, parts[len(parts)-1])
}
}
}
}
}
}
}
func (c *v1beta3Converter) setNestedValue(path string, value interface{}) {
parts := strings.Split(path, ".")
current := c.values
for _, part := range parts[:len(parts)-1] {
if _, ok := current[part]; !ok {
current[part] = make(map[string]interface{})
}
if nextMap, ok := current[part].(map[string]interface{}); ok {
current = nextMap
} else {
// Path exists but isn't a map, need to handle this case
return
}
}
current[parts[len(parts)-1]] = value
}