forked from k0sproject/rig
-
Notifications
You must be signed in to change notification settings - Fork 0
/
resolver.go
145 lines (123 loc) · 4.15 KB
/
resolver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package rig
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/k0sproject/rig/errstring"
"github.com/k0sproject/rig/log"
ps "github.com/k0sproject/rig/powershell"
)
type resolveFunc func(*Connection) (OSVersion, error)
var (
// Resolvers exposes an array of resolve functions where you can add your own if you need to detect some OS rig doesn't already know about
// (consider making a PR)
Resolvers = []resolveFunc{resolveLinux, resolveDarwin, resolveWindows}
errAbort = errstring.New("base os detected, version resolving failed")
)
type windowsVersion struct {
Caption string
Version string
}
// GetOSVersion runs through the Resolvers and tries to figure out the OS version information
func GetOSVersion(conn *Connection) (OSVersion, error) {
for _, r := range Resolvers {
os, err := r(conn)
if err == nil {
return os, nil
}
if errors.Is(err, errAbort) {
return OSVersion{}, ErrNotSupported.Wrap(err)
}
log.Tracef("resolver failed: %v", err)
}
return OSVersion{}, ErrNotSupported.Wrapf("unable to determine host os")
}
func resolveLinux(conn *Connection) (OSVersion, error) {
if err := conn.Exec("uname | grep -q Linux"); err != nil {
return OSVersion{}, ErrCommandFailed.Wrapf("not a linux host: %w", err)
}
output, err := conn.ExecOutput("cat /etc/os-release || cat /usr/lib/os-release")
if err != nil {
// at this point it is known that this is a linux host, so any error from here on should signal the resolver to not try the next
return OSVersion{}, errAbort.Wrapf("unable to read os-release file: %w", err)
}
var version OSVersion
if err := parseOSReleaseFile(output, &version); err != nil {
return OSVersion{}, errAbort.Wrap(err)
}
return version, nil
}
func resolveWindows(conn *Connection) (OSVersion, error) {
if !conn.IsWindows() {
return OSVersion{}, ErrCommandFailed.Wrapf("not a windows host")
}
script := ps.Cmd("Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object Caption, Version | ConvertTo-Json")
output, err := conn.ExecOutput(script)
if err != nil {
return OSVersion{}, errAbort.Wrapf("unable to get windows version: %w", err)
}
var winver windowsVersion
if err := json.Unmarshal([]byte(output), &winver); err != nil {
return OSVersion{}, errAbort.Wrapf("unable to parse windows version: %w", err)
}
return OSVersion{
ID: "windows",
IDLike: "windows",
Version: winver.Version,
Name: winver.Caption,
}, nil
}
func resolveDarwin(conn *Connection) (OSVersion, error) {
if err := conn.Exec("uname | grep -q Darwin"); err != nil {
return OSVersion{}, ErrCommandFailed.Wrapf("not a darwin host: %w", err)
}
// at this point it is known that this is a windows host, so any error from here on should signal the resolver to not try the next
version, err := conn.ExecOutput("sw_vers -productVersion")
if err != nil {
return OSVersion{}, errAbort.Wrapf("unable to determine darwin version: %w", err)
}
var name string
if n, err := conn.ExecOutput(`grep "SOFTWARE LICENSE AGREEMENT FOR " "/System/Library/CoreServices/Setup Assistant.app/Contents/Resources/en.lproj/OSXSoftwareLicense.rtf" | sed -E "s/^.*SOFTWARE LICENSE AGREEMENT FOR (.+)\\\/\1/"`); err == nil {
name = fmt.Sprintf("%s %s", n, version)
}
os := OSVersion{
ID: "darwin",
IDLike: "darwin",
Version: version,
Name: name,
}
return os, nil
}
func unquote(s string) string {
if u, err := strconv.Unquote(s); err == nil {
return u
}
return s
}
func parseOSReleaseFile(s string, version *OSVersion) error {
scanner := bufio.NewScanner(strings.NewReader(s))
for scanner.Scan() {
fields := strings.SplitN(scanner.Text(), "=", 2)
switch fields[0] {
case "ID":
version.ID = unquote(fields[1])
case "ID_LIKE":
version.IDLike = unquote(fields[1])
case "VERSION_ID":
version.Version = unquote(fields[1])
case "PRETTY_NAME":
version.Name = unquote(fields[1])
}
}
// ArchLinux has no versions
if version.ID == "arch" || version.IDLike == "arch" {
version.Version = "0.0.0"
}
if version.ID == "" || version.Version == "" {
return ErrNotSupported.Wrapf("invalid or incomplete os-release file contents, at least ID and VERSION_ID required")
}
return nil
}