package talos import ( "bytes" "encoding/json" "fmt" "net/http" "os/exec" "strconv" "strings" "time" ) const ( defaultPlatform = "nocloud" defaultTarget = "cloud" ) var defaultExtensions = []string{ "siderolabs/iscsi-tools", "siderolabs/nfs-utils", "siderolabs/qemu-guest-agent", "siderolabs/util-linux-tools", } type releaseResponse struct { TagName string `json:"tag_name"` } type schematicRequest struct { Customization struct { SystemExtensions struct { OfficialExtensions []string `json:"officialExtensions"` } `json:"systemExtensions"` } `json:"customization"` } type schematicResponse struct { ID string `json:"id"` } func LatestVersion() string { client := http.Client{Timeout: 10 * time.Second} request, err := http.NewRequest(http.MethodGet, "https://api.github.com/repos/siderolabs/talos/releases/latest", nil) if err != nil { return "v1.13.6" } request.Header.Set("Accept", "application/vnd.github+json") response, err := client.Do(request) if err != nil { return "v1.13.6" } defer response.Body.Close() if response.StatusCode != http.StatusOK { return "v1.13.6" } var release releaseResponse if err := json.NewDecoder(response.Body).Decode(&release); err != nil || release.TagName == "" { return "v1.13.6" } return release.TagName } func NormalizeVersion(version string) string { if strings.HasPrefix(version, "v") { return version } return "v" + version } func RequireCLICompatibility(version string) error { version = NormalizeVersion(version) if err := requireTalhelper(version); err != nil { return err } return requireTalosctl(version) } func requireTalhelper(version string) error { output, err := exec.Command("talhelper", "--version").Output() if err != nil { return fmt.Errorf("talhelper is required for Talos %s; install a compatible version and try again", version) } major, minor, ok := parseToolVersion(string(output)) if !ok || major < 3 || (major == 3 && minor < 1) { return fmt.Errorf("talhelper %s may be incompatible with Talos %s; check that you have the right talhelper version", strings.TrimSpace(string(output)), version) } return nil } func requireTalosctl(version string) error { output, err := exec.Command("talosctl", "version", "--client", "--short").Output() if err != nil { return fmt.Errorf("talosctl is required for Talos %s; install a compatible version and try again", version) } major, minor, ok := parseToolVersion(string(output)) if !ok { return fmt.Errorf("unable to parse talosctl version %q; check that it matches Talos %s", strings.TrimSpace(string(output)), version) } talosMajor, talosMinor, ok := parseToolVersion(version) if !ok || major != talosMajor || minor != talosMinor { return fmt.Errorf("talosctl %s may be incompatible with Talos %s; check that you have the right talosctl version", strings.TrimSpace(string(output)), version) } return nil } func parseToolVersion(raw string) (int, int, bool) { raw = strings.TrimSpace(raw) fields := strings.Fields(raw) if len(fields) > 0 { raw = fields[len(fields)-1] } raw = strings.TrimPrefix(raw, "v") parts := strings.Split(raw, ".") if len(parts) < 2 { return 0, 0, false } major, err := strconv.Atoi(parts[0]) if err != nil { return 0, 0, false } minor, err := strconv.Atoi(parts[1]) if err != nil { return 0, 0, false } return major, minor, true } func FactoryURL(version, arch string) string { version = NormalizeVersion(version) return fmt.Sprintf("https://factory.talos.dev/?arch=%s&extensions=-&extensions=%s&extensions=%s&extensions=%s&extensions=%s&platform=%s&target=%s&version=%s", arch, "siderolabs%2Fiscsi-tools", "siderolabs%2Fnfs-utils", "siderolabs%2Fqemu-guest-agent", "siderolabs%2Futil-linux-tools", defaultPlatform, defaultTarget, strings.TrimPrefix(version, "v"), ) } func ResolveSchematicID() (string, error) { requestBody := schematicRequest{} requestBody.Customization.SystemExtensions.OfficialExtensions = defaultExtensions payload, err := json.Marshal(requestBody) if err != nil { return "", err } client := http.Client{Timeout: 15 * time.Second} request, err := http.NewRequest(http.MethodPost, "https://factory.talos.dev/schematics", bytes.NewReader(payload)) if err != nil { return "", err } request.Header.Set("Content-Type", "application/json") response, err := client.Do(request) if err != nil { return "", err } defer response.Body.Close() if response.StatusCode >= 300 { return "", fmt.Errorf("talos factory returned %s", response.Status) } var schematic schematicResponse if err := json.NewDecoder(response.Body).Decode(&schematic); err != nil { return "", err } if schematic.ID == "" { return "", fmt.Errorf("talos factory returned empty schematic id") } return schematic.ID, nil }