-
Notifications
You must be signed in to change notification settings - Fork 288
Expand file tree
/
Copy pathroot.go
More file actions
266 lines (237 loc) · 7.35 KB
/
root.go
File metadata and controls
266 lines (237 loc) · 7.35 KB
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/*
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package cmd defines command line utilities for gcluster
package cmd
import (
"errors"
"fmt"
"hpc-toolkit/pkg/config"
"hpc-toolkit/pkg/logging"
"hpc-toolkit/pkg/shell"
"os"
"os/exec"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra"
)
// Git references when use Makefile
var (
GitTagVersion string
GitBranch string
GitCommitInfo string
GitCommitHash string
GitInitialHash string
)
var (
annotation = make(map[string]string)
rootCmd = &cobra.Command{
Use: "gcluster",
Short: "A blueprint and deployment engine for HPC clusters in GCP.",
Long: `Google Cloud Cluster Toolkit is an open source tool that makes it easy to create and manage repeatable AI/ML and HPC clusters on Google Cloud.`,
Run: func(cmd *cobra.Command, args []string) {
if err := cmd.Help(); err != nil {
logging.Fatal("cmd.Help function failed: %s", err)
}
},
Version: config.GetToolkitVersion(),
Annotations: annotation,
}
)
func init() {
addColorFlag(rootCmd.PersistentFlags())
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
initColor()
}
}
// Execute the root command
func Execute() error {
mismatch, branch, hash, dir := checkGitHashMismatch()
if mismatch {
logging.Error("WARNING: gcluster binary was built from a different commit (%s/%s) than the current git branch in %s (%s/%s). You can rebuild the binary by running 'make'",
GitBranch, GitCommitHash[0:7], dir, branch, hash[0:7])
}
if len(GitCommitInfo) > 0 {
if len(GitTagVersion) == 0 {
GitTagVersion = "- not built from official release"
}
if len(GitBranch) == 0 {
GitBranch = "detached HEAD"
}
annotation["version"] = GitTagVersion
annotation["branch"] = GitBranch
annotation["commitInfo"] = GitCommitInfo
tmpl := `gcluster version {{index .Annotations "version"}}
Built from '{{index .Annotations "branch"}}' branch.
Commit info: {{index .Annotations "commitInfo"}}
`
tfVersion, _ := shell.TfVersion()
if tfVersion != "" {
annotation["tfVersion"] = tfVersion
tmpl += `Terraform version: {{index .Annotations "tfVersion"}}
`
}
rootCmd.SetVersionTemplate(tmpl)
}
return rootCmd.Execute()
}
// checkGitHashMismatch will compare the hash of the git repository vs the git
// hash the gcluster binary was compiled against, if the git repository if found and
// a mismatch is identified, then the function returns a positive bool along with
// the branch details, and false for all other cases.
func checkGitHashMismatch() (mismatch bool, branch, hash, dir string) {
// binary does not contain build-time git info
if len(GitCommitHash) == 0 {
return false, "", "", ""
}
// could not find hpcToolkitRepo
repo, dir, err := hpcToolkitRepo()
if err != nil {
return false, "", "", ""
}
// failed to open git
head, err := repo.Head()
if err != nil {
return false, "", "", ""
}
// found hpc-toolkit git repo and hash does not match
if GitCommitHash != head.Hash().String() {
mismatch = true
branch = head.Name().Short()
hash = head.Hash().String()
return
}
return false, "", "", ""
}
// hpcToolkitRepo will find the path of the directory containing the hpc-toolkit
// starting with the working directory and evaluating the parent directories until
// the toolkit repository is found. If the Cluster Toolkit repository is not found by
// traversing the path, then the executable directory is checked.
func hpcToolkitRepo() (repo *git.Repository, dir string, err error) {
// first look in the working directory and it's parents until a git repo is
// found. If it's the hpc-toolkit repo, return it.
// repo := new(git.Repository)
dir, err = os.Getwd()
if err != nil {
return nil, "", err
}
subdir := filepath.Dir(dir)
o := git.PlainOpenOptions{DetectDotGit: true}
repo, err = git.PlainOpenWithOptions(dir, &o)
if err == nil && isHpcToolkitRepo(*repo) {
return
} else if err == nil && !isHpcToolkitRepo(*repo) {
// found a repo that is not the hpc-toolkit repo. likely a submodule
// or another git repo checked out under ./hpc-toolkit. Keep walking
// the parents' path to find the hpc-toolkit repo until we hit root of
// filesystem
for dir != subdir {
dir = filepath.Dir(dir)
subdir = filepath.Dir(dir)
repo, err = git.PlainOpen(dir)
if err == nil && isHpcToolkitRepo(*repo) {
return repo, dir, nil
}
}
}
// fall back to the executable's directory
e, err := os.Executable()
if err != nil {
return nil, "", err
}
dir = filepath.Dir(e)
repo, err = git.PlainOpen(dir)
if err != nil {
return nil, "", err
}
if isHpcToolkitRepo(*repo) {
return repo, dir, nil
}
return nil, "", errors.New("gcluster executable found in a git repo other than the hpc-toolkit git repo")
}
// isHpcToolkitRepo will verify that the found git repository has a commit with
// the known hash of the initial commit of the Cluster Toolkit repository
func isHpcToolkitRepo(r git.Repository) bool {
h := plumbing.NewHash(GitInitialHash)
_, err := r.CommitObject(h)
return err == nil
}
// Best effort to find the path of the executable
// Possible return values:
// * "gcluster" if the executable is in the PATH
// AND resolved path matches Args[0];
// * Args[0].
// If error occurs returns "gcluster"
func execPath() string {
const nice string = "gcluster"
args0 := os.Args[0]
if args0 == nice { // trivial case
// but it's important to terminate here to prevent
// "simplification" of `gcluster` to `./gcluster`
return nice
}
// Code below assumes that `args0` contains path to file, not a
// executable name from PATH.
{ // Find shortest & nicest form of args0
cwd, err := os.Getwd()
if err != nil {
return nice
}
absPath, err := filepath.Abs(args0)
if err != nil {
return nice
}
relPath, err := filepath.Rel(cwd, absPath)
if err != nil {
return nice
}
if dir, _ := filepath.Split(relPath); dir == "" {
// relPath is file in cwd, change "relPath" to "./relPath"
relPath = fmt.Sprintf(".%c%s", filepath.Separator, relPath)
}
// Choose shortest path, tie goes to absolute path
if len(relPath) < len(absPath) {
args0 = relPath
} else {
args0 = absPath
}
}
found, err := exec.LookPath("gcluster")
if err != nil { // not found in PATH
return args0
}
// see if found points to the same file as args0
// can't use simple string comparison because of symlinks
args0Info, err := os.Stat(args0)
if err != nil {
return nice
}
foundInfo, err := os.Stat(found)
if err != nil {
return nice
}
if os.SameFile(args0Info, foundInfo) {
return nice
}
return args0
}
// checkErr is similar to cobra.CheckErr, but with renderError and logging.Fatal
// NOTE: this function uses empty YamlCtx, so if you have one, use renderError directly.
func checkErr(err error, ctx *config.YamlCtx) {
if ctx == nil {
ctx = &config.YamlCtx{}
}
if err != nil {
logging.Fatal("%s", renderError(err, *ctx))
}
}