golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gomote/group.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "os" 13 "path/filepath" 14 "sort" 15 ) 16 17 func group(args []string) error { 18 cm := map[string]struct { 19 run func([]string) error 20 desc string 21 }{ 22 "create": {createGroup, "create a new group"}, 23 "destroy": {destroyGroup, "destroy an existing group (does not destroy gomotes)"}, 24 "add": {addToGroup, "add an existing instance to a group"}, 25 "remove": {removeFromGroup, "remove an existing instance from a group"}, 26 "list": {listGroups, "list existing groups and their details"}, 27 } 28 if len(args) == 0 { 29 var cmds []string 30 for cmd := range cm { 31 cmds = append(cmds, cmd) 32 } 33 sort.Strings(cmds) 34 fmt.Fprintf(os.Stderr, "Usage of gomote group: gomote [global-flags] group <cmd> [cmd-flags]\n\n") 35 fmt.Fprintf(os.Stderr, "Commands:\n\n") 36 for _, name := range cmds { 37 fmt.Fprintf(os.Stderr, " %-8s %s\n", name, cm[name].desc) 38 } 39 fmt.Fprintln(os.Stderr) 40 os.Exit(1) 41 } 42 subCmd := args[0] 43 sc, ok := cm[subCmd] 44 if !ok { 45 return fmt.Errorf("unknown sub-command %q\n", subCmd) 46 } 47 return sc.run(args[1:]) 48 } 49 50 func createGroup(args []string) error { 51 usage := func() { 52 fmt.Fprintln(os.Stderr, "group create usage: gomote group create <name>") 53 os.Exit(1) 54 } 55 if len(args) != 1 { 56 usage() 57 } 58 _, err := doCreateGroup(args[0]) 59 return err 60 } 61 62 func doCreateGroup(name string) (*groupData, error) { 63 if _, err := loadGroup(name); err == nil { 64 return nil, fmt.Errorf("group %q already exists", name) 65 } 66 g := &groupData{Name: name} 67 return g, storeGroup(g) 68 } 69 70 func destroyGroup(args []string) error { 71 usage := func() { 72 fmt.Fprintln(os.Stderr, "group destroy usage: gomote group destroy <name>") 73 os.Exit(1) 74 } 75 if len(args) != 1 { 76 usage() 77 } 78 name := args[0] 79 _, err := loadGroup(name) 80 if errors.Is(err, os.ErrNotExist) { 81 return fmt.Errorf("group %q does not exist", name) 82 } else if err != nil { 83 return fmt.Errorf("loading group %q: %w", name, err) 84 } 85 if err := deleteGroup(name); err != nil { 86 return err 87 } 88 if os.Getenv("GOMOTE_GROUP") == name { 89 fmt.Fprintln(os.Stderr, "You may wish to now clear GOMOTE_GROUP.") 90 } 91 return nil 92 } 93 94 func addToGroup(args []string) error { 95 usage := func() { 96 fmt.Fprintln(os.Stderr, "group add usage: gomote group add [instances ...]") 97 os.Exit(1) 98 } 99 if len(args) == 0 { 100 usage() 101 } 102 if activeGroup == nil { 103 fmt.Fprintln(os.Stderr, "No active group found. Use -group or GOMOTE_GROUP.") 104 usage() 105 } 106 ctx := context.Background() 107 for _, inst := range args { 108 if err := doPing(ctx, inst); err != nil { 109 return fmt.Errorf("instance %q: %w", inst, err) 110 } 111 activeGroup.Instances = append(activeGroup.Instances, inst) 112 } 113 return storeGroup(activeGroup) 114 } 115 116 func removeFromGroup(args []string) error { 117 usage := func() { 118 fmt.Fprintln(os.Stderr, "group add usage: gomote group add [instances ...]") 119 os.Exit(1) 120 } 121 if len(args) == 0 { 122 usage() 123 } 124 if activeGroup == nil { 125 fmt.Fprintln(os.Stderr, "No active group found. Use -group or GOMOTE_GROUP.") 126 usage() 127 } 128 newInstances := make([]string, 0, len(activeGroup.Instances)) 129 for _, inst := range activeGroup.Instances { 130 remove := false 131 for _, rmInst := range args { 132 if inst == rmInst { 133 remove = true 134 break 135 } 136 } 137 if remove { 138 continue 139 } 140 newInstances = append(newInstances, inst) 141 } 142 activeGroup.Instances = newInstances 143 return storeGroup(activeGroup) 144 } 145 146 func listGroups(args []string) error { 147 usage := func() { 148 fmt.Fprintln(os.Stderr, "group list usage: gomote group list") 149 os.Exit(1) 150 } 151 if len(args) != 0 { 152 usage() 153 } 154 groups, err := loadAllGroups() 155 if err != nil { 156 return err 157 } 158 // N.B. Glob ignores I/O errors, so no matches also means the directory 159 // does not exist. 160 emit := func(name, inst string) { 161 fmt.Printf("%s\t%s\t\n", name, inst) 162 } 163 emit("Name", "Instances") 164 for _, g := range groups { 165 sort.Strings(g.Instances) 166 emitted := false 167 for _, inst := range g.Instances { 168 if !emitted { 169 emit(g.Name, inst) 170 } else { 171 emit("", inst) 172 } 173 emitted = true 174 } 175 if !emitted { 176 emit(g.Name, "(none)") 177 } 178 } 179 if len(groups) == 0 { 180 fmt.Println("(none)") 181 } 182 return nil 183 } 184 185 type groupData struct { 186 // User-provided name of the group. 187 Name string `json:"name"` 188 189 // Instances is a list of instances in the group. 190 Instances []string `json:"instances"` 191 } 192 193 func (g *groupData) has(inst string) bool { 194 for _, i := range g.Instances { 195 if inst == i { 196 return true 197 } 198 } 199 return false 200 } 201 202 func loadAllGroups() ([]*groupData, error) { 203 dir, err := groupDir() 204 if err != nil { 205 return nil, fmt.Errorf("acquiring group directory: %w", err) 206 } 207 // N.B. Glob ignores I/O errors, so no matches also means the directory 208 // does not exist. 209 matches, _ := filepath.Glob(filepath.Join(dir, "*.json")) 210 var groups []*groupData 211 for _, match := range matches { 212 g, err := loadGroupFromFile(match) 213 if err != nil { 214 return nil, fmt.Errorf("reading group file for %q: %w", match, err) 215 } 216 groups = append(groups, g) 217 } 218 return groups, nil 219 } 220 221 func loadGroup(name string) (*groupData, error) { 222 fname, err := groupFilePath(name) 223 if err != nil { 224 return nil, fmt.Errorf("loading group %q: %w", name, err) 225 } 226 g, err := loadGroupFromFile(fname) 227 if err != nil { 228 return nil, fmt.Errorf("loading group %q: %w", name, err) 229 } 230 return g, nil 231 } 232 233 func loadGroupFromFile(fname string) (*groupData, error) { 234 f, err := os.Open(fname) 235 if err != nil { 236 return nil, err 237 } 238 defer f.Close() 239 g := new(groupData) 240 if err := json.NewDecoder(f).Decode(g); err != nil { 241 return nil, err 242 } 243 // On every load, ping for liveness and prune. 244 // 245 // Otherwise, we can get into situations where we sometimes 246 // don't have an accurate record. 247 ctx := context.Background() 248 newInstances := make([]string, 0, len(g.Instances)) 249 for _, inst := range g.Instances { 250 err := doPing(ctx, inst) 251 if instanceDoesNotExist(err) { 252 continue 253 } else if err != nil { 254 return nil, err 255 } 256 newInstances = append(newInstances, inst) 257 } 258 g.Instances = newInstances 259 return g, storeGroup(g) 260 } 261 262 func storeGroup(data *groupData) error { 263 fname, err := groupFilePath(data.Name) 264 if err != nil { 265 return fmt.Errorf("storing group %q: %w", data.Name, err) 266 } 267 if err := os.MkdirAll(filepath.Dir(fname), 0755); err != nil { 268 return fmt.Errorf("storing group %q: %w", data.Name, err) 269 } 270 f, err := os.Create(fname) 271 if err != nil { 272 return fmt.Errorf("storing group %q: %w", data.Name, err) 273 } 274 defer f.Close() 275 if err := json.NewEncoder(f).Encode(data); err != nil { 276 return fmt.Errorf("storing group %q: %w", data.Name, err) 277 } 278 return nil 279 } 280 281 func deleteGroup(name string) error { 282 fname, err := groupFilePath(name) 283 if err != nil { 284 return fmt.Errorf("deleting group %q: %w", name, err) 285 } 286 if err := os.Remove(fname); err != nil { 287 return fmt.Errorf("deleting group %q: %w", name, err) 288 } 289 return nil 290 } 291 292 func groupFilePath(name string) (string, error) { 293 dir, err := groupDir() 294 if err != nil { 295 return "", err 296 } 297 return filepath.Join(dir, fmt.Sprintf("%s.json", name)), nil 298 } 299 300 func groupDir() (string, error) { 301 cfgDir, err := os.UserConfigDir() 302 if err != nil { 303 return "", err 304 } 305 return filepath.Join(cfgDir, "gomote", "groups"), nil 306 }