github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/cmd/migrate.go (about) 1 /* 2 * Copyright 2018-2019 The NATS Authors 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package cmd 17 18 import ( 19 "fmt" 20 "os" 21 "path/filepath" 22 "strings" 23 24 cli "github.com/nats-io/cliprompts/v2" 25 "github.com/nats-io/jwt/v2" 26 "github.com/nats-io/nkeys" 27 "github.com/spf13/cobra" 28 29 "github.com/nats-io/nsc/cmd/store" 30 ) 31 32 func createMigrateCmd() *cobra.Command { 33 var params MigrateCmdParams 34 var cmd = &cobra.Command{ 35 Hidden: true, 36 37 Short: "Migrate an account to the current operator", 38 Example: "migrate --url <path or url to account jwt>", 39 Use: `migrate`, 40 Args: MaxArgs(0), 41 RunE: func(cmd *cobra.Command, args []string) error { 42 if err := RunAction(cmd, args, ¶ms); err != nil { 43 return err 44 } 45 return nil 46 }, 47 } 48 cmd.Flags().StringVarP(¶ms.url, "url", "u", "", "path or url to import jwt from") 49 cmd.Flags().StringVarP(¶ms.storeDir, "operator-dir", "", "", "path to an operator dir - all accounts are migrated") 50 cmd.Flags().BoolVarP(¶ms.overwrite, "force", "F", false, "overwrite accounts with the same name") 51 return cmd 52 } 53 54 func init() { 55 GetRootCmd().AddCommand(createMigrateCmd()) 56 } 57 58 type MigrateCmdParams struct { 59 url string 60 storeDir string 61 overwrite bool 62 Jobs []*MigrateJob 63 } 64 65 func (p *MigrateCmdParams) SetDefaults(ctx ActionCtx) error { 66 if p.url != "" && p.storeDir != "" { 67 return fmt.Errorf("specify one of --url or --store-dir") 68 } 69 return nil 70 } 71 72 func (p *MigrateCmdParams) PreInteractive(ctx ActionCtx) error { 73 ok, err := cli.Confirm("migrate all accounts under a particular operator", true) 74 if err != nil { 75 return err 76 } 77 if ok { 78 p.storeDir, err = cli.Prompt("specify the directory for the operator", "", cli.Val(func(v string) error { 79 _, err := store.LoadStore(v) 80 return err 81 })) 82 if err != nil { 83 return err 84 } 85 } else { 86 p.url, err = cli.Prompt("account jwt url/or path ", p.url, cli.Val(func(v string) error { 87 // we expect either a file or url 88 if IsURL(v) { 89 return nil 90 } 91 v, err := Expand(v) 92 if err != nil { 93 return err 94 } 95 _, err = os.Stat(v) 96 return err 97 })) 98 if err != nil { 99 return err 100 } 101 } 102 return nil 103 } 104 105 func (p *MigrateCmdParams) Load(ctx ActionCtx) error { 106 var err error 107 if p.storeDir != "" { 108 p.storeDir, err = Expand(p.storeDir) 109 if err != nil { 110 return err 111 } 112 113 s, err := store.LoadStore(p.storeDir) 114 if err != nil { 115 return fmt.Errorf("error loading operator %#q: %v", p.storeDir, err) 116 } 117 names, err := s.ListSubContainers(store.Accounts) 118 if err != nil { 119 return fmt.Errorf("error listing accounts in %#q: %v", p.storeDir, err) 120 } 121 for _, n := range names { 122 mj := NewMigrateJob(filepath.Join(p.storeDir, store.Accounts, n, store.JwtName(n)), p.overwrite) 123 p.Jobs = append(p.Jobs, &mj) 124 } 125 } else { 126 mj := NewMigrateJob(p.url, p.overwrite) 127 p.Jobs = append(p.Jobs, &mj) 128 } 129 130 for _, j := range p.Jobs { 131 j.Load(ctx) 132 } 133 return nil 134 } 135 136 func (p *MigrateCmdParams) PostInteractive(ctx ActionCtx) error { 137 for _, j := range p.Jobs { 138 if j.OK() { 139 j.PostInteractive(ctx) 140 } 141 } 142 return nil 143 } 144 145 func (p *MigrateCmdParams) Validate(ctx ActionCtx) error { 146 for _, j := range p.Jobs { 147 if j.OK() { 148 j.Validate(ctx) 149 } 150 } 151 return nil 152 } 153 154 func (p *MigrateCmdParams) Run(ctx ActionCtx) (store.Status, error) { 155 var jobs store.MultiJob 156 for _, j := range p.Jobs { 157 if j.OK() { 158 j.Run(ctx) 159 } 160 jobs = append(jobs, j.status) 161 } 162 m, err := jobs.Summary() 163 if m != "" { 164 ctx.CurrentCmd().Println(m) 165 } 166 return jobs, err 167 } 168 169 type MigrateJob struct { 170 accountToken string 171 claim *jwt.AccountClaims 172 url string 173 isFileImport bool 174 operator string 175 migratedUsers []*jwt.UserClaims 176 overwrite bool 177 178 status store.Status 179 } 180 181 func NewMigrateJob(url string, overwrite bool) MigrateJob { 182 return MigrateJob{url: url, overwrite: overwrite, status: &store.Report{}} 183 } 184 185 func (j *MigrateJob) OK() bool { 186 code := j.status.Code() 187 return code == store.OK || code == store.NONE 188 } 189 190 func (j *MigrateJob) getAccountKeys() []string { 191 var keys []string 192 keys = append(keys, j.claim.Subject) 193 keys = append(keys, j.claim.SigningKeys.Keys()...) 194 return keys 195 } 196 197 func (j *MigrateJob) Load(ctx ActionCtx) { 198 if j.url == "" { 199 j.status = store.ErrorStatus("an url or path to the account jwt is required") 200 return 201 } 202 data, err := LoadFromFileOrURL(j.url) 203 if err != nil { 204 j.status = store.ErrorStatus(fmt.Sprintf("error loading from %#q: %v", j.url, err)) 205 return 206 } 207 j.isFileImport = !IsURL(j.url) 208 209 j.accountToken, err = jwt.ParseDecoratedJWT(data) 210 if err != nil { 211 j.status = store.ErrorStatus(fmt.Sprintf("error parsing JWT: %v", err)) 212 return 213 } 214 j.claim, err = jwt.DecodeAccountClaims(j.accountToken) 215 if err != nil { 216 j.status = store.ErrorStatus(fmt.Sprintf("error decoding JWT: %v", err)) 217 return 218 } 219 } 220 221 func (j *MigrateJob) PostInteractive(ctx ActionCtx) { 222 if ctx.StoreCtx().Store.HasAccount(j.claim.Name) && !j.overwrite { 223 aac, err := ctx.StoreCtx().Store.ReadAccountClaim(j.claim.Name) 224 if err != nil { 225 226 j.status = store.ErrorStatus(fmt.Sprintf("error reading account JWT: %v", err)) 227 return 228 } 229 j.overwrite = aac.Subject == j.claim.Subject 230 if !j.overwrite { 231 j.overwrite, err = cli.Confirm("account %q already exists under the current operator, replace it", false) 232 if err != nil { 233 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 234 return 235 } 236 } 237 } 238 } 239 240 func (j *MigrateJob) Validate(ctx ActionCtx) { 241 if j.isFileImport { 242 parent := ctx.StoreCtx().Store.Dir 243 // it is already determined to be a file 244 fp, err := Expand(j.url) 245 if err != nil { 246 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 247 return 248 } 249 if strings.HasPrefix(fp, parent) { 250 j.status = store.ErrorStatus(fmt.Sprintf("cannot migrate %q onto itself", fp)) 251 return 252 } 253 } 254 255 if !j.overwrite && ctx.StoreCtx().Store.HasAccount(j.claim.Name) { 256 j.status = store.ErrorStatus(fmt.Sprintf("account %q already exists, specify --force to overwrite", j.claim.Name)) 257 return 258 } 259 260 keys := j.getAccountKeys() 261 var hasOne bool 262 for _, k := range keys { 263 kp, _ := ctx.StoreCtx().KeyStore.GetKeyPair(k) 264 if kp != nil { 265 hasOne = true 266 break 267 } 268 } 269 if !hasOne { 270 j.status = store.ErrorStatus(fmt.Sprintf("unable to find an account key for %q - need one of %s", j.claim.Name, strings.Join(keys, ", "))) 271 return 272 } 273 } 274 275 func (j *MigrateJob) Run(ctx ActionCtx) { 276 ctx.CurrentCmd().SilenceUsage = true 277 j.operator = ctx.StoreCtx().Operator.Name 278 279 token, err := jwt.ParseDecoratedJWT([]byte(j.accountToken)) 280 if err != nil { 281 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 282 return 283 } 284 ac, err := jwt.DecodeAccountClaims(token) 285 if err != nil { 286 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 287 return 288 } 289 290 if ctx.StoreCtx().Store.IsManaged() { 291 var keys []string 292 keys = append(keys, ac.Subject) 293 keys = append(keys, ac.SigningKeys.Keys()...) 294 295 // need to sign it with any key we can get 296 var kp nkeys.KeyPair 297 for _, k := range keys { 298 kp, _ = ctx.StoreCtx().KeyStore.GetKeyPair(k) 299 if kp != nil { 300 break 301 } 302 } 303 if kp == nil { 304 j.status = store.ErrorStatus(fmt.Sprintf("unable to find any account keys - need any of %s", strings.Join(keys, ", "))) 305 return 306 } 307 j.accountToken, err = ac.Encode(kp) 308 if err != nil { 309 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 310 return 311 } 312 } 313 314 remote, err := ctx.StoreCtx().Store.StoreClaim([]byte(j.accountToken)) 315 if err != nil { 316 j.status = store.ErrorStatus(fmt.Sprintf("failed to migrate %q: %v", ac.Name, err)) 317 return 318 } 319 320 if j.isFileImport { 321 udir := filepath.Join(filepath.Dir(j.url), store.Users) 322 fi, err := os.Stat(udir) 323 if err == nil && fi.IsDir() { 324 dirEntries, err := os.ReadDir(udir) 325 if err != nil { 326 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 327 return 328 } 329 for _, v := range dirEntries { 330 n := v.Name() 331 if !v.IsDir() && filepath.Ext(n) == ".jwt" { 332 up := filepath.Join(udir, n) 333 d, err := Read(up) 334 if err != nil { 335 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 336 return 337 } 338 s, err := jwt.ParseDecoratedJWT(d) 339 if err != nil { 340 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 341 return 342 } 343 uc, err := jwt.DecodeUserClaims(s) 344 if err != nil { 345 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 346 return 347 } 348 if err := ctx.StoreCtx().Store.StoreRaw([]byte(s)); err != nil { 349 j.status = store.ErrorStatus(fmt.Sprintf("%v", err)) 350 return 351 } 352 j.migratedUsers = append(j.migratedUsers, uc) 353 } 354 } 355 } 356 } 357 358 m := fmt.Sprintf("migrated %q to operator %q", j.claim.Name, j.operator) 359 um := fmt.Sprintf("%d users migrated", len(j.migratedUsers)) 360 if len(j.migratedUsers) == 0 { 361 um = "no users migrated" 362 } 363 if !j.isFileImport { 364 um = "" 365 } 366 367 j.status = store.OKStatus(fmt.Sprintf("%s [%s]", m, um)) 368 if remote != nil { 369 si, ok := j.status.(*store.Report) 370 if ok { 371 si.Details = append(si.Details, remote) 372 } 373 } 374 }