summaryrefslogtreecommitdiffstats
path: root/pkg/version/version.go
blob: 5135fdcb05212e09064d63654b932ad27d796bf4 (plain)
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package version

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"runtime"
	"runtime/debug"
	"strconv"
	"strings"
)

type VersionInfo struct {
	Version string
	Commit  string
}

// Version determines version and commit information based on multiple data sources:
//  - Version information dynamically added by `git archive` in the remaining to parameters.
//  - A hardcoded version number passed as first parameter.
//  - Commit information added to the binary by `go build`.
//
// It's supposed to be called like this in combination with setting the `export-subst` attribute for the corresponding
// file in .gitattributes:
//
//     var Version = version.Version("1.0.0-rc2", "$Format:%(describe)$", "$Format:%H$")
//
// When exported using `git archive`, the placeholders are replaced in the file and this version information is
// preferred. Otherwise the hardcoded version is used and augmented with commit information from the build metadata.
func Version(version, gitDescribe, gitHash string) *VersionInfo {
	const hashLen = 7 // Same truncation length for the commit hash as used by git describe.

	if !strings.HasPrefix(gitDescribe, "$") && !strings.HasPrefix(gitHash, "$") {
		if strings.HasPrefix(gitDescribe, "%") {
			// Only Git 2.32+ supports %(describe), older versions don't expand it but keep it as-is.
			// Fall back to the hardcoded version augmented with the commit hash.
			gitDescribe = version

			if len(gitHash) >= hashLen {
				gitDescribe += "-g" + gitHash[:hashLen]
			}
		}

		return &VersionInfo{
			Version: gitDescribe,
			Commit:  gitHash,
		}
	} else {
		commit := ""

		if info, ok := debug.ReadBuildInfo(); ok {
			modified := false

			for _, setting := range info.Settings {
				switch setting.Key {
				case "vcs.revision":
					commit = setting.Value
				case "vcs.modified":
					modified, _ = strconv.ParseBool(setting.Value)
				}
			}

			if len(commit) >= hashLen {
				version += "-g" + commit[:hashLen]

				if modified {
					version += "-dirty"
					commit += " (modified)"
				}
			}
		}

		return &VersionInfo{
			Version: version,
			Commit:  commit,
		}
	}
}

// Print writes verbose version output to stdout.
func (v *VersionInfo) Print() {
	fmt.Println("Icinga DB version:", v.Version)
	fmt.Println()

	fmt.Println("Build information:")
	fmt.Printf("  Go version: %s (%s, %s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
	if v.Commit != "" {
		fmt.Println("  Git commit:", v.Commit)
	}

	if r, err := readOsRelease(); err == nil {
		fmt.Println()
		fmt.Println("System information:")
		fmt.Println("  Platform:", r.Name)
		fmt.Println("  Platform version:", r.DisplayVersion())
	}
}

// osRelease contains the information obtained from the os-release file.
type osRelease struct {
	Name      string
	Version   string
	VersionId string
	BuildId   string
}

// DisplayVersion returns the most suitable version information for display purposes.
func (o *osRelease) DisplayVersion() string {
	if o.Version != "" {
		// Most distributions set VERSION
		return o.Version
	} else if o.VersionId != "" {
		// Some only set VERSION_ID (Alpine Linux for example)
		return o.VersionId
	} else if o.BuildId != "" {
		// Others only set BUILD_ID (Arch Linux for example)
		return o.BuildId
	} else {
		return "(unknown)"
	}
}

// readOsRelease reads and parses the os-release file.
func readOsRelease() (*osRelease, error) {
	for _, path := range []string{"/etc/os-release", "/usr/lib/os-release"} {
		f, err := os.Open(path)
		if err != nil {
			if os.IsNotExist(err) {
				continue // Try next path.
			} else {
				return nil, err
			}
		}

		o := &osRelease{
			Name: "Linux", // Suggested default as per os-release(5) man page.
		}

		scanner := bufio.NewScanner(f)
		for scanner.Scan() {
			line := scanner.Text()
			if strings.HasPrefix(line, "#") {
				continue // Ignore comment.
			}

			parts := strings.SplitN(line, "=", 2)
			if len(parts) != 2 {
				continue // Ignore empty or possibly malformed line.
			}

			key := parts[0]
			val := parts[1]

			// Unquote strings. This isn't fully compliant with the specification which allows using some shell escape
			// sequences. However, typically quotes are only used to allow whitespace within the value.
			if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[0] == val[len(val)-1] {
				val = val[1 : len(val)-1]
			}

			switch key {
			case "NAME":
				o.Name = val
			case "VERSION":
				o.Version = val
			case "VERSION_ID":
				o.VersionId = val
			case "BUILD_ID":
				o.BuildId = val
			}
		}
		if err := scanner.Err(); err != nil {
			return nil, err
		}

		return o, nil
	}

	return nil, errors.New("os-release file not found")
}