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 }