blob: 99fd27e7a5eb43ab5ff45de64568551e89746b74 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
)
func group(args []string) error {
cm := map[string]struct {
run func([]string) error
desc string
}{
"create": {createGroup, "create a new group"},
"destroy": {destroyGroup, "destroy an existing group (does not destroy gomotes)"},
"add": {addToGroup, "add an existing instance to a group"},
"remove": {removeFromGroup, "remove an existing instance from a group"},
"list": {listGroups, "list existing groups and their details"},
}
if len(args) == 0 {
var cmds []string
for cmd := range cm {
cmds = append(cmds, cmd)
}
sort.Strings(cmds)
fmt.Fprintf(os.Stderr, "Usage of gomote group: gomote [global-flags] group <cmd> [cmd-flags]\n\n")
fmt.Fprintf(os.Stderr, "Commands:\n\n")
for _, name := range cmds {
fmt.Fprintf(os.Stderr, " %-8s %s\n", name, cm[name].desc)
}
fmt.Fprintln(os.Stderr)
os.Exit(1)
}
subCmd := args[0]
sc, ok := cm[subCmd]
if !ok {
return fmt.Errorf("unknown sub-command %q\n", subCmd)
}
return sc.run(args[1:])
}
func createGroup(args []string) error {
usage := func() {
fmt.Fprintln(os.Stderr, "group create usage: gomote group create <name>")
os.Exit(1)
}
if len(args) != 1 {
usage()
}
_, err := doCreateGroup(args[0])
return err
}
func doCreateGroup(name string) (*groupData, error) {
if _, err := loadGroup(name); err == nil {
return nil, fmt.Errorf("group %q already exists", name)
}
g := &groupData{Name: name}
return g, storeGroup(g)
}
func destroyGroup(args []string) error {
usage := func() {
fmt.Fprintln(os.Stderr, "group destroy usage: gomote group destroy <name>")
os.Exit(1)
}
if len(args) != 1 {
usage()
}
name := args[0]
_, err := loadGroup(name)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("group %q does not exist", name)
} else if err != nil {
return fmt.Errorf("loading group %q: %w", name, err)
}
if err := deleteGroup(name); err != nil {
return err
}
if os.Getenv("GOMOTE_GROUP") == name {
fmt.Fprintln(os.Stderr, "You may wish to now clear GOMOTE_GROUP.")
}
return nil
}
func addToGroup(args []string) error {
usage := func() {
fmt.Fprintln(os.Stderr, "group add usage: gomote group add [instances ...]")
os.Exit(1)
}
if len(args) == 0 {
usage()
}
if activeGroup == nil {
fmt.Fprintln(os.Stderr, "No active group found. Use -group or GOMOTE_GROUP.")
usage()
}
ctx := context.Background()
for _, inst := range args {
if err := doPing(ctx, inst); err != nil {
return fmt.Errorf("instance %q: %w", inst, err)
}
activeGroup.Instances = append(activeGroup.Instances, inst)
}
return storeGroup(activeGroup)
}
func removeFromGroup(args []string) error {
usage := func() {
fmt.Fprintln(os.Stderr, "group add usage: gomote group add [instances ...]")
os.Exit(1)
}
if len(args) == 0 {
usage()
}
if activeGroup == nil {
fmt.Fprintln(os.Stderr, "No active group found. Use -group or GOMOTE_GROUP.")
usage()
}
newInstances := make([]string, 0, len(activeGroup.Instances))
for _, inst := range activeGroup.Instances {
remove := false
for _, rmInst := range args {
if inst == rmInst {
remove = true
break
}
}
if remove {
continue
}
newInstances = append(newInstances, inst)
}
activeGroup.Instances = newInstances
return storeGroup(activeGroup)
}
func listGroups(args []string) error {
usage := func() {
fmt.Fprintln(os.Stderr, "group list usage: gomote group list")
os.Exit(1)
}
if len(args) != 0 {
usage()
}
groups, err := loadAllGroups()
if err != nil {
return err
}
// N.B. Glob ignores I/O errors, so no matches also means the directory
// does not exist.
emit := func(name, inst string) {
fmt.Printf("%s\t%s\t\n", name, inst)
}
emit("Name", "Instances")
for _, g := range groups {
sort.Strings(g.Instances)
emitted := false
for _, inst := range g.Instances {
if !emitted {
emit(g.Name, inst)
} else {
emit("", inst)
}
emitted = true
}
if !emitted {
emit(g.Name, "(none)")
}
}
if len(groups) == 0 {
fmt.Println("(none)")
}
return nil
}
type groupData struct {
// User-provided name of the group.
Name string `json:"name"`
// Instances is a list of instances in the group.
Instances []string `json:"instances"`
}
func (g *groupData) has(inst string) bool {
for _, i := range g.Instances {
if inst == i {
return true
}
}
return false
}
func loadAllGroups() ([]*groupData, error) {
dir, err := groupDir()
if err != nil {
return nil, fmt.Errorf("acquiring group directory: %w", err)
}
// N.B. Glob ignores I/O errors, so no matches also means the directory
// does not exist.
matches, _ := filepath.Glob(filepath.Join(dir, "*.json"))
var groups []*groupData
for _, match := range matches {
g, err := loadGroupFromFile(match)
if err != nil {
return nil, fmt.Errorf("reading group file for %q: %w", match, err)
}
groups = append(groups, g)
}
return groups, nil
}
func loadGroup(name string) (*groupData, error) {
fname, err := groupFilePath(name)
if err != nil {
return nil, fmt.Errorf("loading group %q: %w", name, err)
}
g, err := loadGroupFromFile(fname)
if err != nil {
return nil, fmt.Errorf("loading group %q: %w", name, err)
}
return g, nil
}
func loadGroupFromFile(fname string) (*groupData, error) {
f, err := os.Open(fname)
if err != nil {
return nil, err
}
defer f.Close()
g := new(groupData)
if err := json.NewDecoder(f).Decode(g); err != nil {
return nil, err
}
// On every load, ping for liveness and prune.
//
// Otherwise, we can get into situations where we sometimes
// don't have an accurate record.
ctx := context.Background()
newInstances := make([]string, 0, len(g.Instances))
for _, inst := range g.Instances {
err := doPing(ctx, inst)
if instanceDoesNotExist(err) {
continue
} else if err != nil {
return nil, err
}
newInstances = append(newInstances, inst)
}
g.Instances = newInstances
return g, storeGroup(g)
}
func storeGroup(data *groupData) error {
fname, err := groupFilePath(data.Name)
if err != nil {
return fmt.Errorf("storing group %q: %w", data.Name, err)
}
if err := os.MkdirAll(filepath.Dir(fname), 0755); err != nil {
return fmt.Errorf("storing group %q: %w", data.Name, err)
}
f, err := os.Create(fname)
if err != nil {
return fmt.Errorf("storing group %q: %w", data.Name, err)
}
defer f.Close()
if err := json.NewEncoder(f).Encode(data); err != nil {
return fmt.Errorf("storing group %q: %w", data.Name, err)
}
return nil
}
func deleteGroup(name string) error {
fname, err := groupFilePath(name)
if err != nil {
return fmt.Errorf("deleting group %q: %w", name, err)
}
if err := os.Remove(fname); err != nil {
return fmt.Errorf("deleting group %q: %w", name, err)
}
return nil
}
func groupFilePath(name string) (string, error) {
dir, err := groupDir()
if err != nil {
return "", err
}
return filepath.Join(dir, fmt.Sprintf("%s.json", name)), nil
}
func groupDir() (string, error) {
cfgDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(cfgDir, "gomote", "groups"), nil
}