Utility tool for upgrading talos nodes.

fix detection match rules

evan.jarrett.net 57587fe2 1fcd1f55

verified
+1
internal/cmd/status.go
··· 166 166 SystemProductName: hwInfo.SystemProductName, 167 167 ProcessorManufacturer: hwInfo.ProcessorManufacturer, 168 168 ProcessorProductName: hwInfo.ProcessorProductName, 169 + Arch: hwInfo.Arch, 169 170 } 170 171 171 172 // Detect profile
+1
internal/cmd/upgrade.go
··· 391 391 SystemProductName: hwInfo.SystemProductName, 392 392 ProcessorManufacturer: hwInfo.ProcessorManufacturer, 393 393 ProcessorProductName: hwInfo.ProcessorProductName, 394 + Arch: hwInfo.Arch, 394 395 } 395 396 396 397 // Detect profile
+1
internal/cmd/upgrade_node.go
··· 116 116 SystemProductName: hwInfo.SystemProductName, 117 117 ProcessorManufacturer: hwInfo.ProcessorManufacturer, 118 118 ProcessorProductName: hwInfo.ProcessorProductName, 119 + Arch: hwInfo.Arch, 119 120 } 120 121 121 122 profileName, profile := cfg.DetectProfile(cfgHwInfo)
+18 -2
internal/config/config.go
··· 116 116 if _, ok := c.Profiles[rule.Profile]; !ok { 117 117 return fmt.Errorf("detection rule %d: references unknown profile %s", i, rule.Profile) 118 118 } 119 - if rule.Match.SystemManufacturer == "" && rule.Match.ProcessorManufacturer == "" { 120 - return fmt.Errorf("detection rule %d: at least one match criterion required", i) 119 + 120 + // Must have either match or match_either 121 + if rule.Match == nil && len(rule.MatchEither) == 0 { 122 + return fmt.Errorf("detection rule %d: must have match or match_either", i) 123 + } 124 + 125 + // Validate match criteria 126 + if rule.Match != nil { 127 + if rule.Match.SystemManufacturer == "" && rule.Match.ProcessorManufacturer == "" && rule.Match.Arch == "" { 128 + return fmt.Errorf("detection rule %d: at least one match criterion required", i) 129 + } 130 + } 131 + 132 + // Validate match_either criteria 133 + for j, m := range rule.MatchEither { 134 + if m.SystemManufacturer == "" && m.ProcessorManufacturer == "" && m.Arch == "" { 135 + return fmt.Errorf("detection rule %d, match_either %d: at least one match criterion required", i, j) 136 + } 121 137 } 122 138 } 123 139 }
+287 -12
internal/config/config_test.go
··· 683 683 Rules: []DetectionRule{ 684 684 { 685 685 Profile: "test", 686 - Match: DetectionMatch{SystemManufacturer: "Dell"}, 686 + Match: &DetectionMatch{SystemManufacturer: "Dell"}, 687 687 }, 688 688 }, 689 689 }, ··· 700 700 Rules: []DetectionRule{ 701 701 { 702 702 Profile: "", 703 - Match: DetectionMatch{SystemManufacturer: "Dell"}, 703 + Match: &DetectionMatch{SystemManufacturer: "Dell"}, 704 704 }, 705 705 }, 706 706 }, ··· 719 719 Rules: []DetectionRule{ 720 720 { 721 721 Profile: "nonexistent", 722 - Match: DetectionMatch{SystemManufacturer: "Dell"}, 722 + Match: &DetectionMatch{SystemManufacturer: "Dell"}, 723 723 }, 724 724 }, 725 725 }, ··· 738 738 Rules: []DetectionRule{ 739 739 { 740 740 Profile: "test", 741 - Match: DetectionMatch{}, 741 + Match: &DetectionMatch{}, 742 742 }, 743 743 }, 744 744 }, ··· 757 757 cfg := &Config{ 758 758 Detection: &Detection{ 759 759 Rules: []DetectionRule{ 760 - {Profile: "test", Match: DetectionMatch{SystemManufacturer: "Dell"}}, 760 + {Profile: "test", Match: &DetectionMatch{SystemManufacturer: "Dell"}}, 761 761 }, 762 762 }, 763 763 } ··· 794 794 Rules: []DetectionRule{ 795 795 { 796 796 Profile: "intel-profile", 797 - Match: DetectionMatch{ProcessorManufacturer: "Intel"}, 797 + Match: &DetectionMatch{ProcessorManufacturer: "Intel"}, 798 798 }, 799 799 { 800 800 Profile: "amd-profile", 801 - Match: DetectionMatch{ProcessorManufacturer: "AMD"}, 801 + Match: &DetectionMatch{ProcessorManufacturer: "AMD"}, 802 802 }, 803 803 { 804 804 Profile: "dell-profile", 805 - Match: DetectionMatch{SystemManufacturer: "Dell"}, 805 + Match: &DetectionMatch{SystemManufacturer: "Dell"}, 806 806 }, 807 807 }, 808 808 }, ··· 866 866 }, 867 867 Detection: &Detection{ 868 868 Rules: []DetectionRule{ 869 - {Profile: "first", Match: DetectionMatch{SystemManufacturer: "Dell"}}, 870 - {Profile: "second", Match: DetectionMatch{SystemManufacturer: "Dell"}}, 869 + {Profile: "first", Match: &DetectionMatch{SystemManufacturer: "Dell"}}, 870 + {Profile: "second", Match: &DetectionMatch{SystemManufacturer: "Dell"}}, 871 871 }, 872 872 }, 873 873 } ··· 883 883 }, 884 884 Detection: &Detection{ 885 885 Rules: []DetectionRule{ 886 - {Profile: "missing", Match: DetectionMatch{SystemManufacturer: "Dell"}}, 886 + {Profile: "missing", Match: &DetectionMatch{SystemManufacturer: "Dell"}}, 887 887 }, 888 888 }, 889 889 } ··· 903 903 Rules: []DetectionRule{ 904 904 { 905 905 Profile: "dell-intel", 906 - Match: DetectionMatch{ 906 + Match: &DetectionMatch{ 907 907 SystemManufacturer: "Dell", 908 908 ProcessorManufacturer: "Intel", 909 909 }, ··· 942 942 assert.Nil(t, profile) 943 943 }) 944 944 } 945 + 946 + // ============================================================================ 947 + // Arch Matching Tests 948 + // ============================================================================ 949 + 950 + func TestDetectProfile_ArchMatch(t *testing.T) { 951 + cfg := &Config{ 952 + Profiles: map[string]Profile{ 953 + "amd64-profile": {Arch: "amd64", Platform: "metal"}, 954 + "arm64-profile": {Arch: "arm64", Platform: "metal"}, 955 + }, 956 + Detection: &Detection{ 957 + Rules: []DetectionRule{ 958 + { 959 + Profile: "amd64-profile", 960 + Match: &DetectionMatch{Arch: "amd64"}, 961 + }, 962 + { 963 + Profile: "arm64-profile", 964 + Match: &DetectionMatch{Arch: "arm64"}, 965 + }, 966 + }, 967 + }, 968 + } 969 + 970 + t.Run("match amd64 arch", func(t *testing.T) { 971 + hw := &HardwareInfo{Arch: "amd64"} 972 + name, profile := cfg.DetectProfile(hw) 973 + assert.Equal(t, "amd64-profile", name) 974 + require.NotNil(t, profile) 975 + }) 976 + 977 + t.Run("match arm64 arch", func(t *testing.T) { 978 + hw := &HardwareInfo{Arch: "arm64"} 979 + name, profile := cfg.DetectProfile(hw) 980 + assert.Equal(t, "arm64-profile", name) 981 + require.NotNil(t, profile) 982 + }) 983 + 984 + t.Run("arch case insensitive", func(t *testing.T) { 985 + hw := &HardwareInfo{Arch: "AMD64"} 986 + name, profile := cfg.DetectProfile(hw) 987 + assert.Equal(t, "amd64-profile", name) 988 + require.NotNil(t, profile) 989 + }) 990 + 991 + t.Run("no arch match", func(t *testing.T) { 992 + hw := &HardwareInfo{Arch: "riscv64"} 993 + name, profile := cfg.DetectProfile(hw) 994 + assert.Empty(t, name) 995 + assert.Nil(t, profile) 996 + }) 997 + } 998 + 999 + func TestDetectProfile_ArchAndManufacturer(t *testing.T) { 1000 + cfg := &Config{ 1001 + Profiles: map[string]Profile{ 1002 + "arm64-rpi": {Arch: "arm64", Platform: "metal"}, 1003 + }, 1004 + Detection: &Detection{ 1005 + Rules: []DetectionRule{ 1006 + { 1007 + Profile: "arm64-rpi", 1008 + Match: &DetectionMatch{ 1009 + Arch: "arm64", 1010 + SystemManufacturer: "raspberrypi", 1011 + }, 1012 + }, 1013 + }, 1014 + }, 1015 + } 1016 + 1017 + t.Run("both arch and manufacturer match", func(t *testing.T) { 1018 + hw := &HardwareInfo{ 1019 + Arch: "arm64", 1020 + SystemManufacturer: "raspberrypi", 1021 + } 1022 + name, profile := cfg.DetectProfile(hw) 1023 + assert.Equal(t, "arm64-rpi", name) 1024 + require.NotNil(t, profile) 1025 + }) 1026 + 1027 + t.Run("arch matches but manufacturer doesn't", func(t *testing.T) { 1028 + hw := &HardwareInfo{ 1029 + Arch: "arm64", 1030 + SystemManufacturer: "turing", 1031 + } 1032 + name, profile := cfg.DetectProfile(hw) 1033 + assert.Empty(t, name) 1034 + assert.Nil(t, profile) 1035 + }) 1036 + 1037 + t.Run("manufacturer matches but arch doesn't", func(t *testing.T) { 1038 + hw := &HardwareInfo{ 1039 + Arch: "amd64", 1040 + SystemManufacturer: "raspberrypi", 1041 + } 1042 + name, profile := cfg.DetectProfile(hw) 1043 + assert.Empty(t, name) 1044 + assert.Nil(t, profile) 1045 + }) 1046 + } 1047 + 1048 + // ============================================================================ 1049 + // MatchEither (OR Logic) Tests 1050 + // ============================================================================ 1051 + 1052 + func TestDetectProfile_MatchEither(t *testing.T) { 1053 + cfg := &Config{ 1054 + Profiles: map[string]Profile{ 1055 + "amd64-profile": {Arch: "amd64", Platform: "metal"}, 1056 + }, 1057 + Detection: &Detection{ 1058 + Rules: []DetectionRule{ 1059 + { 1060 + Profile: "amd64-profile", 1061 + MatchEither: []DetectionMatch{ 1062 + {ProcessorManufacturer: "Intel"}, 1063 + {ProcessorManufacturer: "Advanced Micro Devices"}, 1064 + }, 1065 + }, 1066 + }, 1067 + }, 1068 + } 1069 + 1070 + t.Run("matches first option (Intel)", func(t *testing.T) { 1071 + hw := &HardwareInfo{ProcessorManufacturer: "Intel Corporation"} 1072 + name, profile := cfg.DetectProfile(hw) 1073 + assert.Equal(t, "amd64-profile", name) 1074 + require.NotNil(t, profile) 1075 + }) 1076 + 1077 + t.Run("matches second option (AMD)", func(t *testing.T) { 1078 + hw := &HardwareInfo{ProcessorManufacturer: "Advanced Micro Devices, Inc."} 1079 + name, profile := cfg.DetectProfile(hw) 1080 + assert.Equal(t, "amd64-profile", name) 1081 + require.NotNil(t, profile) 1082 + }) 1083 + 1084 + t.Run("no match for ARM", func(t *testing.T) { 1085 + hw := &HardwareInfo{ProcessorManufacturer: "ARM Holdings"} 1086 + name, profile := cfg.DetectProfile(hw) 1087 + assert.Empty(t, name) 1088 + assert.Nil(t, profile) 1089 + }) 1090 + } 1091 + 1092 + func TestDetectProfile_MatchEitherWithArch(t *testing.T) { 1093 + cfg := &Config{ 1094 + Profiles: map[string]Profile{ 1095 + "arm64-sbc": {Arch: "arm64", Platform: "metal"}, 1096 + }, 1097 + Detection: &Detection{ 1098 + Rules: []DetectionRule{ 1099 + { 1100 + Profile: "arm64-sbc", 1101 + MatchEither: []DetectionMatch{ 1102 + {Arch: "arm64", SystemManufacturer: "raspberrypi"}, 1103 + {Arch: "arm64", SystemManufacturer: "turing"}, 1104 + }, 1105 + }, 1106 + }, 1107 + }, 1108 + } 1109 + 1110 + t.Run("matches raspberry pi", func(t *testing.T) { 1111 + hw := &HardwareInfo{ 1112 + Arch: "arm64", 1113 + SystemManufacturer: "raspberrypi", 1114 + } 1115 + name, profile := cfg.DetectProfile(hw) 1116 + assert.Equal(t, "arm64-sbc", name) 1117 + require.NotNil(t, profile) 1118 + }) 1119 + 1120 + t.Run("matches turing", func(t *testing.T) { 1121 + hw := &HardwareInfo{ 1122 + Arch: "arm64", 1123 + SystemManufacturer: "turing", 1124 + } 1125 + name, profile := cfg.DetectProfile(hw) 1126 + assert.Equal(t, "arm64-sbc", name) 1127 + require.NotNil(t, profile) 1128 + }) 1129 + 1130 + t.Run("wrong arch even if manufacturer matches", func(t *testing.T) { 1131 + hw := &HardwareInfo{ 1132 + Arch: "amd64", 1133 + SystemManufacturer: "raspberrypi", 1134 + } 1135 + name, profile := cfg.DetectProfile(hw) 1136 + assert.Empty(t, name) 1137 + assert.Nil(t, profile) 1138 + }) 1139 + } 1140 + 1141 + func TestValidate_MatchEither(t *testing.T) { 1142 + t.Run("valid match_either", func(t *testing.T) { 1143 + cfg := &Config{ 1144 + Profiles: map[string]Profile{ 1145 + "test": {Arch: "amd64", Platform: "metal"}, 1146 + }, 1147 + Detection: &Detection{ 1148 + Rules: []DetectionRule{ 1149 + { 1150 + Profile: "test", 1151 + MatchEither: []DetectionMatch{ 1152 + {ProcessorManufacturer: "Intel"}, 1153 + {ProcessorManufacturer: "AMD"}, 1154 + }, 1155 + }, 1156 + }, 1157 + }, 1158 + } 1159 + assert.NoError(t, cfg.Validate()) 1160 + }) 1161 + 1162 + t.Run("empty match_either item fails", func(t *testing.T) { 1163 + cfg := &Config{ 1164 + Profiles: map[string]Profile{ 1165 + "test": {Arch: "amd64", Platform: "metal"}, 1166 + }, 1167 + Detection: &Detection{ 1168 + Rules: []DetectionRule{ 1169 + { 1170 + Profile: "test", 1171 + MatchEither: []DetectionMatch{ 1172 + {ProcessorManufacturer: "Intel"}, 1173 + {}, // Empty match 1174 + }, 1175 + }, 1176 + }, 1177 + }, 1178 + } 1179 + err := cfg.Validate() 1180 + assert.Error(t, err) 1181 + assert.Contains(t, err.Error(), "match_either 1: at least one match criterion required") 1182 + }) 1183 + 1184 + t.Run("no match or match_either fails", func(t *testing.T) { 1185 + cfg := &Config{ 1186 + Profiles: map[string]Profile{ 1187 + "test": {Arch: "amd64", Platform: "metal"}, 1188 + }, 1189 + Detection: &Detection{ 1190 + Rules: []DetectionRule{ 1191 + { 1192 + Profile: "test", 1193 + // Neither Match nor MatchEither set 1194 + }, 1195 + }, 1196 + }, 1197 + } 1198 + err := cfg.Validate() 1199 + assert.Error(t, err) 1200 + assert.Contains(t, err.Error(), "must have match or match_either") 1201 + }) 1202 + } 1203 + 1204 + func TestValidate_ArchOnly(t *testing.T) { 1205 + cfg := &Config{ 1206 + Profiles: map[string]Profile{ 1207 + "test": {Arch: "amd64", Platform: "metal"}, 1208 + }, 1209 + Detection: &Detection{ 1210 + Rules: []DetectionRule{ 1211 + { 1212 + Profile: "test", 1213 + Match: &DetectionMatch{Arch: "amd64"}, 1214 + }, 1215 + }, 1216 + }, 1217 + } 1218 + assert.NoError(t, cfg.Validate()) 1219 + }
+31 -4
internal/config/types.go
··· 49 49 50 50 // DetectionRule maps hardware characteristics to a profile 51 51 type DetectionRule struct { 52 - Profile string `yaml:"profile"` 53 - Match DetectionMatch `yaml:"match"` 52 + Profile string `yaml:"profile"` 53 + Match *DetectionMatch `yaml:"match,omitempty"` // AND logic: all conditions must match 54 + MatchEither []DetectionMatch `yaml:"match_either,omitempty"` // OR logic: any condition can match 54 55 } 55 56 56 57 // DetectionMatch defines the hardware characteristics to match 57 58 type DetectionMatch struct { 58 59 SystemManufacturer string `yaml:"system_manufacturer,omitempty"` 59 60 ProcessorManufacturer string `yaml:"processor_manufacturer,omitempty"` 61 + Arch string `yaml:"arch,omitempty"` // e.g., "amd64", "arm64" 60 62 } 61 63 62 64 // HardwareInfo represents detected hardware information ··· 65 67 SystemProductName string 66 68 ProcessorManufacturer string 67 69 ProcessorProductName string 70 + Arch string // e.g., "amd64", "arm64" 68 71 } 69 72 70 73 // DetectProfile finds the matching profile for given hardware info ··· 74 77 } 75 78 76 79 for _, rule := range c.Detection.Rules { 77 - if matchesRule(hw, &rule.Match) { 80 + matched := false 81 + 82 + // Check Match (AND logic - all conditions must match) 83 + if rule.Match != nil && matchesRule(hw, rule.Match) { 84 + matched = true 85 + } 86 + 87 + // Check MatchEither (OR logic - any condition can match) 88 + if !matched && len(rule.MatchEither) > 0 { 89 + for i := range rule.MatchEither { 90 + if matchesRule(hw, &rule.MatchEither[i]) { 91 + matched = true 92 + break 93 + } 94 + } 95 + } 96 + 97 + if matched { 78 98 if profile, ok := c.Profiles[rule.Profile]; ok { 79 99 return rule.Profile, &profile 80 100 } ··· 85 105 86 106 // matchesRule checks if hardware info matches a detection rule 87 107 func matchesRule(hw *HardwareInfo, match *DetectionMatch) bool { 108 + // Check arch (case-insensitive exact match) 109 + if match.Arch != "" { 110 + if !strings.EqualFold(hw.Arch, match.Arch) { 111 + return false 112 + } 113 + } 114 + 88 115 // Check system manufacturer (case-insensitive contains match) 89 116 if match.SystemManufacturer != "" { 90 117 if !strings.Contains(strings.ToLower(hw.SystemManufacturer), strings.ToLower(match.SystemManufacturer)) { ··· 100 127 } 101 128 102 129 // At least one match criterion must be specified 103 - if match.SystemManufacturer == "" && match.ProcessorManufacturer == "" { 130 + if match.SystemManufacturer == "" && match.ProcessorManufacturer == "" && match.Arch == "" { 104 131 return false 105 132 } 106 133
+11
internal/talos/client.go
··· 739 739 nodeCtx := talosclient.WithNode(ctx, nodeIP) 740 740 info := &HardwareInfo{} 741 741 742 + // Get architecture from Version API 743 + versionResp, err := c.talos.Version(nodeCtx) 744 + if err == nil { 745 + for _, msg := range versionResp.Messages { 746 + if msg.Version != nil && msg.Version.Arch != "" { 747 + info.Arch = msg.Version.Arch 748 + break 749 + } 750 + } 751 + } 752 + 742 753 // Query SystemInformation resource 743 754 sysInfoList, err := c.talos.COSIList(nodeCtx, resource.NewMetadata(hardware.NamespaceName, hardware.SystemInformationType, "", resource.VersionUndefined)) 744 755 if err != nil {
+1
internal/talos/interfaces.go
··· 30 30 SystemProductName string // e.g., "Raspberry Pi Compute Module 4" 31 31 ProcessorManufacturer string // e.g., "Intel(R) Corporation", "Advanced Micro Devices" 32 32 ProcessorProductName string // e.g., "Intel(R) Core(TM) i9-10850K" 33 + Arch string // e.g., "amd64", "arm64" 33 34 } 34 35 35 36 // TalosClientInterface defines the interface for interacting with Talos nodes.