github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/config/command.go (about)

     1  // Copyright 2021 Daniel Erat.
     2  // All rights reserved.
     3  
     4  package config
     5  
     6  import (
     7  	"context"
     8  	"flag"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  
    14  	"cloud.google.com/go/datastore"
    15  
    16  	"github.com/derat/nup/cmd/nup/client"
    17  	srvconfig "github.com/derat/nup/server/config"
    18  	"github.com/google/subcommands"
    19  
    20  	"golang.org/x/oauth2/google"
    21  
    22  	"google.golang.org/api/appengine/v1"
    23  	"google.golang.org/api/option"
    24  )
    25  
    26  type Command struct {
    27  	Cfg *client.Config
    28  
    29  	deleteInstances bool   // delete instances after set
    30  	setPath         string // path of config file to set
    31  	service         string // service name whose instances should be deleted
    32  }
    33  
    34  func (*Command) Name() string     { return "config" }
    35  func (*Command) Synopsis() string { return "manage server configuration" }
    36  func (*Command) Usage() string {
    37  	return `config <flags>:
    38  	Manage the App Engine server's configuration in Datastore.
    39  	By default, prints the existing JSON-marshaled configuration.
    40  
    41  `
    42  }
    43  
    44  func (cmd *Command) SetFlags(f *flag.FlagSet) {
    45  	f.BoolVar(&cmd.deleteInstances, "delete-instances", false, "Delete running instances after setting config")
    46  	f.StringVar(&cmd.setPath, "set", "", "Path of updated JSON config file to save to Datastore")
    47  	f.StringVar(&cmd.service, "service", "default", "Service name for -delete-instances")
    48  }
    49  
    50  func (cmd *Command) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
    51  	projectID, err := cmd.Cfg.ProjectID()
    52  	if err != nil {
    53  		fmt.Fprintln(os.Stderr, "Failed getting project ID:", err)
    54  		return subcommands.ExitFailure
    55  	}
    56  	creds, err := google.FindDefaultCredentials(ctx,
    57  		"https://www.googleapis.com/auth/datastore",
    58  		"https://www.googleapis.com/auth/appengine.admin",
    59  	)
    60  	if err != nil {
    61  		fmt.Fprintln(os.Stderr, "Failed finding credentials:", err)
    62  		return subcommands.ExitFailure
    63  	}
    64  	cl, err := datastore.NewClient(ctx, projectID, option.WithCredentials(creds))
    65  	if err != nil {
    66  		fmt.Fprintln(os.Stderr, "Failed creating client:", err)
    67  		return subcommands.ExitFailure
    68  	}
    69  	defer cl.Close()
    70  
    71  	key := &datastore.Key{
    72  		Kind: srvconfig.DatastoreKind,
    73  		Name: srvconfig.DatastoreKeyName,
    74  	}
    75  
    76  	// Just fetch and print the active config if requested.
    77  	if cmd.setPath == "" {
    78  		var cfg srvconfig.SavedConfig
    79  		if err := cl.Get(ctx, key, &cfg); err != nil {
    80  			fmt.Fprintln(os.Stderr, "Failed getting config:", err)
    81  			return subcommands.ExitFailure
    82  		}
    83  		fmt.Print(cfg.JSON)
    84  		return subcommands.ExitSuccess
    85  	}
    86  
    87  	// Check that the server code will be happy with the new config.
    88  	// The Parse function also assign defaults to unspecified fields.
    89  	data, err := ioutil.ReadFile(cmd.setPath)
    90  	if err != nil {
    91  		fmt.Fprintln(os.Stderr, "Failed reading config:", err)
    92  		return subcommands.ExitFailure
    93  	}
    94  	cfg, err := srvconfig.Parse(data)
    95  	if err != nil {
    96  		fmt.Fprintln(os.Stderr, "Bad config:", err)
    97  		return subcommands.ExitFailure
    98  	}
    99  
   100  	// Check that the 'nup' command will still be able to access the server with the new config.
   101  	var foundUser bool
   102  	for _, u := range cfg.Users {
   103  		if u.Username == cmd.Cfg.Username {
   104  			if u.Password != cmd.Cfg.Password {
   105  				fmt.Fprintf(os.Stderr, "Password for user %q doesn't match client config\n", u.Username)
   106  				return subcommands.ExitFailure
   107  			} else if !u.Admin {
   108  				fmt.Fprintf(os.Stderr, "User %q is not an admin\n", u.Username)
   109  				return subcommands.ExitFailure
   110  			}
   111  			foundUser = true
   112  			break
   113  		}
   114  	}
   115  	if !foundUser {
   116  		fmt.Fprintf(os.Stderr, "Config doesn't contain admin user %q for 'nup' command\n", cmd.Cfg.Username)
   117  		return subcommands.ExitFailure
   118  	}
   119  
   120  	// Save the config to Datastore.
   121  	if _, err := cl.Put(ctx, key, &srvconfig.SavedConfig{JSON: string(data)}); err != nil {
   122  		fmt.Fprintln(os.Stderr, "Failed saving config:", err)
   123  		return subcommands.ExitFailure
   124  	}
   125  
   126  	if cmd.deleteInstances {
   127  		if err := deleteInstances(ctx, projectID, cmd.service, creds); err != nil {
   128  			fmt.Fprintln(os.Stderr, "Failed deleting instances:", err)
   129  			return subcommands.ExitFailure
   130  		}
   131  	}
   132  
   133  	return subcommands.ExitSuccess
   134  }
   135  
   136  // deleteInstances deletes all App Engine instances of service in projectID.
   137  func deleteInstances(ctx context.Context, projectID, service string, creds *google.Credentials) error {
   138  	asrv, err := appengine.NewService(ctx, option.WithCredentials(creds))
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	vsrv := appengine.NewAppsServicesVersionsService(asrv)
   144  	isrv := appengine.NewAppsServicesVersionsInstancesService(asrv)
   145  
   146  	resp, err := vsrv.List(projectID, service).Do()
   147  	if err != nil {
   148  		return fmt.Errorf("list versions: %v", err)
   149  	}
   150  	for _, ver := range resp.Versions {
   151  		resp, err := isrv.List(projectID, service, ver.Id).Do()
   152  		if err != nil {
   153  			return fmt.Errorf("list instances: %v", err)
   154  		}
   155  		for _, inst := range resp.Instances {
   156  			log.Println("Deleting instance", inst.Name)
   157  			if _, err := isrv.Delete(projectID, service, ver.Id, inst.Id).Do(); err != nil {
   158  				return fmt.Errorf("delete instance: %v", err)
   159  			}
   160  		}
   161  	}
   162  	return nil
   163  }