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  }