github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/cloudconfig/cloudinit/cloudinit_test.go (about) 1 // Copyright 2011, 2012, 2013, 2015 Canonical Ltd. 2 // Copyright 2015 Cloudbase Solutions SRL 3 // Licensed under the AGPLv3, see LICENCE file for details. 4 5 package cloudinit_test 6 7 import ( 8 "fmt" 9 10 "github.com/juju/packaging/v3" 11 jc "github.com/juju/testing/checkers" 12 sshtesting "github.com/juju/utils/v3/ssh/testing" 13 "go.uber.org/mock/gomock" 14 "golang.org/x/crypto/ssh" 15 gc "gopkg.in/check.v1" 16 "gopkg.in/yaml.v3" 17 18 "github.com/juju/juju/cloudconfig/cloudinit" 19 coretesting "github.com/juju/juju/testing" 20 ) 21 22 // TODO integration tests, but how? 23 24 type S struct { 25 coretesting.BaseSuite 26 } 27 28 var _ = gc.Suite(S{}) 29 30 var ctests = []struct { 31 name string 32 expect map[string]any 33 setOption func(cfg cloudinit.CloudConfig) error 34 }{{ 35 "PackageUpgrade", 36 map[string]any{ 37 "package_upgrade": true, 38 }, 39 func(cfg cloudinit.CloudConfig) error { 40 cfg.SetSystemUpgrade(true) 41 return nil 42 }, 43 }, { 44 "PackageUpdate", 45 map[string]any{ 46 "package_update": true, 47 }, 48 func(cfg cloudinit.CloudConfig) error { 49 cfg.SetSystemUpdate(true) 50 return nil 51 }, 52 }, { 53 "PackageProxy", 54 map[string]any{ 55 "apt_proxy": "http://foo.com", 56 }, 57 func(cfg cloudinit.CloudConfig) error { 58 cfg.SetPackageProxy("http://foo.com") 59 return nil 60 }, 61 }, { 62 "PackageMirror", 63 map[string]any{ 64 "apt_mirror": "http://foo.com", 65 }, 66 func(cfg cloudinit.CloudConfig) error { 67 cfg.SetPackageMirror("http://foo.com") 68 return nil 69 }, 70 }, { 71 "DisableEC2Metadata", 72 map[string]any{ 73 "disable_ec2_metadata": true, 74 }, 75 func(cfg cloudinit.CloudConfig) error { 76 cfg.SetDisableEC2Metadata(true) 77 return nil 78 }, 79 }, { 80 "FinalMessage", 81 map[string]any{ 82 "final_message": "goodbye", 83 }, 84 func(cfg cloudinit.CloudConfig) error { 85 cfg.SetFinalMessage("goodbye") 86 return nil 87 }, 88 }, { 89 "Locale", 90 map[string]any{ 91 "locale": "en_us", 92 }, 93 func(cfg cloudinit.CloudConfig) error { 94 cfg.SetLocale("en_us") 95 return nil 96 }, 97 }, { 98 "DisableRoot", 99 map[string]any{ 100 "disable_root": false, 101 }, 102 func(cfg cloudinit.CloudConfig) error { 103 cfg.SetDisableRoot(false) 104 return nil 105 }, 106 }, { 107 "SetSSHAuthorizedKeys with two keys", 108 map[string]any{ 109 "ssh_authorized_keys": []string{ 110 fmt.Sprintf("%s Juju:user@host", sshtesting.ValidKeyOne.Key), 111 fmt.Sprintf("%s Juju:another@host", sshtesting.ValidKeyTwo.Key), 112 }, 113 }, 114 func(cfg cloudinit.CloudConfig) error { 115 cfg.SetSSHAuthorizedKeys( 116 sshtesting.ValidKeyOne.Key + " Juju:user@host\n" + 117 sshtesting.ValidKeyTwo.Key + " another@host") 118 return nil 119 }, 120 }, { 121 "SetSSHAuthorizedKeys with comments in keys", 122 map[string]any{ 123 "ssh_authorized_keys": []string{ 124 fmt.Sprintf("%s Juju:sshkey", sshtesting.ValidKeyOne.Key), 125 fmt.Sprintf("%s Juju:user@host", sshtesting.ValidKeyTwo.Key), 126 fmt.Sprintf("%s Juju:another@host", sshtesting.ValidKeyThree.Key), 127 }, 128 }, 129 func(cfg cloudinit.CloudConfig) error { 130 cfg.SetSSHAuthorizedKeys( 131 "#command\n" + sshtesting.ValidKeyOne.Key + "\n" + 132 sshtesting.ValidKeyTwo.Key + " user@host\n" + 133 "# comment\n\n" + 134 sshtesting.ValidKeyThree.Key + " another@host") 135 return nil 136 }, 137 }, { 138 "SetSSHAuthorizedKeys unsets keys", 139 nil, 140 func(cfg cloudinit.CloudConfig) error { 141 cfg.SetSSHAuthorizedKeys(sshtesting.ValidKeyOne.Key) 142 cfg.SetSSHAuthorizedKeys("") 143 return nil 144 }, 145 }, { 146 "AddUser with keys", 147 map[string]any{ 148 "users": []any{ 149 map[string]any{ 150 "name": "auser", 151 "lock_passwd": true, 152 "ssh_authorized_keys": []string{ 153 fmt.Sprintf("%s Juju:user@host", sshtesting.ValidKeyOne.Key), 154 fmt.Sprintf("%s Juju:another@host", sshtesting.ValidKeyTwo.Key), 155 }, 156 }, 157 }, 158 }, 159 func(cfg cloudinit.CloudConfig) error { 160 keys := (sshtesting.ValidKeyOne.Key + " Juju:user@host\n" + 161 sshtesting.ValidKeyTwo.Key + " another@host") 162 cfg.AddUser(&cloudinit.User{ 163 Name: "auser", 164 SSHAuthorizedKeys: keys, 165 }) 166 return nil 167 }, 168 }, { 169 "AddUser with groups", 170 map[string]any{ 171 "users": []any{ 172 map[string]any{ 173 "name": "auser", 174 "lock_passwd": true, 175 "groups": []string{"agroup", "bgroup"}, 176 }, 177 }, 178 }, 179 func(cfg cloudinit.CloudConfig) error { 180 cfg.AddUser(&cloudinit.User{ 181 Name: "auser", 182 Groups: []string{"agroup", "bgroup"}, 183 }) 184 return nil 185 }, 186 }, { 187 "AddUser with everything", 188 map[string]any{ 189 "users": []any{ 190 map[string]any{ 191 "name": "auser", 192 "lock_passwd": true, 193 "groups": []string{"agroup", "bgroup"}, 194 "shell": "/bin/sh", 195 "ssh_authorized_keys": []string{ 196 sshtesting.ValidKeyOne.Key + " Juju:sshkey", 197 }, 198 "sudo": "ALL=(ALL) ALL", 199 }, 200 }, 201 }, 202 func(cfg cloudinit.CloudConfig) error { 203 cfg.AddUser(&cloudinit.User{ 204 Name: "auser", 205 Groups: []string{"agroup", "bgroup"}, 206 Shell: "/bin/sh", 207 SSHAuthorizedKeys: sshtesting.ValidKeyOne.Key + "\n", 208 Sudo: "ALL=(ALL) ALL", 209 }) 210 return nil 211 }, 212 }, { 213 "AddUser with only name", 214 map[string]any{ 215 "users": []any{ 216 map[string]any{ 217 "name": "auser", 218 "lock_passwd": true, 219 }, 220 }, 221 }, 222 func(cfg cloudinit.CloudConfig) error { 223 cfg.AddUser(&cloudinit.User{ 224 Name: "auser", 225 }) 226 return nil 227 }, 228 }, { 229 "Output", 230 map[string]any{ 231 "output": map[string]any{ 232 "all": []string{">foo", "|bar"}, 233 }, 234 }, 235 func(cfg cloudinit.CloudConfig) error { 236 cfg.SetOutput("all", ">foo", "|bar") 237 return nil 238 }, 239 }, { 240 "Output", 241 map[string]any{ 242 "output": map[string]any{ 243 "all": ">foo", 244 }, 245 }, 246 func(cfg cloudinit.CloudConfig) error { 247 cfg.SetOutput(cloudinit.OutAll, ">foo", "") 248 return nil 249 }, 250 }, { 251 "PackageSources", 252 map[string]any{ 253 "apt_sources": []map[string]any{ 254 { 255 "source": "keyName", 256 "key": "someKey", 257 }, 258 }, 259 }, 260 func(cfg cloudinit.CloudConfig) error { 261 cfg.AddPackageSource(packaging.PackageSource{URL: "keyName", Key: "someKey"}) 262 return nil 263 }, 264 }, { 265 "PackageSources with preferences", 266 map[string]any{ 267 "apt_sources": []map[string]any{ 268 { 269 "source": "keyName", 270 "key": "someKey", 271 }, 272 }, 273 "bootcmd": []string{ 274 "install -D -m 644 /dev/null '/some/path'", 275 "echo 'Explanation: test\n" + 276 "Package: *\n" + 277 "Pin: release n=series\n" + 278 "Pin-Priority: 123\n" + 279 "' > '/some/path'", 280 }, 281 }, 282 func(cfg cloudinit.CloudConfig) error { 283 prefs := packaging.PackagePreferences{ 284 Path: "/some/path", 285 Explanation: "test", 286 Package: "*", 287 Pin: "release n=series", 288 Priority: 123, 289 } 290 cfg.AddPackageSource(packaging.PackageSource{URL: "keyName", Key: "someKey"}) 291 cfg.AddPackagePreferences(prefs) 292 return nil 293 }, 294 }, { 295 "Packages", 296 map[string]any{ 297 "packages": []string{ 298 "juju", 299 "ubuntu", 300 }, 301 }, 302 func(cfg cloudinit.CloudConfig) error { 303 cfg.AddPackage("juju") 304 cfg.AddPackage("ubuntu") 305 return nil 306 }, 307 }, { 308 "BootCmd", 309 map[string]any{ 310 "bootcmd": []string{ 311 "ls > /dev", 312 "ls >with space", 313 }, 314 }, 315 func(cfg cloudinit.CloudConfig) error { 316 cfg.AddBootCmd("ls > /dev") 317 cfg.AddBootCmd("ls >with space") 318 return nil 319 }, 320 }, { 321 "Mounts", 322 map[string]any{ 323 "mounts": [][]string{ 324 {"x", "y"}, 325 {"z", "w"}, 326 }, 327 }, 328 func(cfg cloudinit.CloudConfig) error { 329 cfg.AddMount("x", "y") 330 cfg.AddMount("z", "w") 331 return nil 332 }, 333 }, { 334 "Attr", 335 map[string]any{ 336 "arbitraryAttr": "someValue"}, 337 func(cfg cloudinit.CloudConfig) error { 338 cfg.SetAttr("arbitraryAttr", "someValue") 339 return nil 340 }, 341 }, { 342 "RunCmd", 343 map[string]any{ 344 "runcmd": []string{ 345 "ifconfig", 346 }, 347 }, 348 func(cfg cloudinit.CloudConfig) error { 349 cfg.AddRunCmd("ifconfig") 350 return nil 351 }, 352 }, { 353 "PrependRunCmd", 354 map[string]any{ 355 "runcmd": []string{ 356 "echo 'Hello World'", 357 "ifconfig", 358 }, 359 }, 360 func(cfg cloudinit.CloudConfig) error { 361 cfg.AddRunCmd("ifconfig") 362 cfg.PrependRunCmd( 363 "echo 'Hello World'", 364 ) 365 return nil 366 }, 367 }, { 368 "AddScripts", 369 map[string]any{ 370 "runcmd": []string{ 371 "echo 'Hello World'", 372 "ifconfig", 373 }, 374 }, 375 func(cfg cloudinit.CloudConfig) error { 376 cfg.AddScripts( 377 "echo 'Hello World'", 378 "ifconfig", 379 ) 380 return nil 381 }, 382 }, { 383 "AddTextFile", 384 map[string]any{ 385 "runcmd": []string{ 386 "install -D -m 644 /dev/null '/etc/apt/apt.conf.d/99proxy'", 387 "echo '\"Acquire::http::Proxy \"http://10.0.3.1:3142\";' > '/etc/apt/apt.conf.d/99proxy'", 388 }, 389 }, 390 func(cfg cloudinit.CloudConfig) error { 391 cfg.AddRunTextFile( 392 "/etc/apt/apt.conf.d/99proxy", 393 `"Acquire::http::Proxy "http://10.0.3.1:3142";`, 394 0644, 395 ) 396 return nil 397 }, 398 }, { 399 "AddBinaryFile", 400 map[string]any{ 401 "runcmd": []string{ 402 "install -D -m 644 /dev/null '/dev/nonsense'", 403 "echo -n AAECAw== | base64 -d > '/dev/nonsense'", 404 }, 405 }, 406 func(cfg cloudinit.CloudConfig) error { 407 cfg.AddRunBinaryFile( 408 "/dev/nonsense", 409 []byte{0, 1, 2, 3}, 410 0644, 411 ) 412 return nil 413 }, 414 }, { 415 "AddBootTextFile", 416 map[string]any{ 417 "bootcmd": []string{ 418 "install -D -m 644 /dev/null '/etc/apt/apt.conf.d/99proxy'", 419 "echo '\"Acquire::http::Proxy \"http://10.0.3.1:3142\";' > '/etc/apt/apt.conf.d/99proxy'", 420 }, 421 }, 422 func(cfg cloudinit.CloudConfig) error { 423 cfg.AddBootTextFile( 424 "/etc/apt/apt.conf.d/99proxy", 425 `"Acquire::http::Proxy "http://10.0.3.1:3142";`, 426 0644, 427 ) 428 return nil 429 }, 430 }, { 431 "ManageEtcHosts", 432 map[string]any{ 433 "manage_etc_hosts": true}, 434 func(cfg cloudinit.CloudConfig) error { 435 cfg.ManageEtcHosts(true) 436 return nil 437 }, 438 }, { 439 "SetSSHKeys", 440 map[string]any{ 441 "ssh_keys": map[string]any{ 442 "rsa_private": "private", 443 "rsa_public": "public", 444 }, 445 }, 446 func(cfg cloudinit.CloudConfig) error { 447 return cfg.SetSSHKeys(cloudinit.SSHKeys{{ 448 Private: "private", 449 Public: "public", 450 PublicKeyAlgorithm: ssh.KeyAlgoRSA, 451 }, 452 }) 453 }, 454 }, { 455 "SetSSHKeys unsets keys", 456 nil, 457 func(cfg cloudinit.CloudConfig) error { 458 err := cfg.SetSSHKeys(cloudinit.SSHKeys{{ 459 Private: "private", 460 Public: "public", 461 PublicKeyAlgorithm: ssh.KeyAlgoRSA, 462 }, 463 }) 464 if err != nil { 465 return err 466 } 467 return cfg.SetSSHKeys(cloudinit.SSHKeys{}) 468 }, 469 }, { 470 "SetSSHKeysMultiple", 471 map[string]any{ 472 "ssh_keys": map[string]any{ 473 "rsa_private": "private-rsa", 474 "rsa_public": "public-rsa", 475 "ecdsa_private": "private-ecdsa", 476 "ecdsa_public": "public-ecdsa", 477 "ed25519_private": "private-ed25519", 478 "ed25519_public": "public-ed25519", 479 }, 480 }, 481 func(cfg cloudinit.CloudConfig) error { 482 return cfg.SetSSHKeys(cloudinit.SSHKeys{ 483 { 484 Private: "private-rsa", 485 Public: "public-rsa", 486 PublicKeyAlgorithm: ssh.KeyAlgoRSA, 487 }, { 488 Private: "private-ecdsa", 489 Public: "public-ecdsa", 490 PublicKeyAlgorithm: ssh.KeyAlgoECDSA256, 491 }, { 492 Private: "private-ed25519", 493 Public: "public-ed25519", 494 PublicKeyAlgorithm: ssh.KeyAlgoED25519, 495 }, 496 }) 497 }, 498 }, 499 } 500 501 func (S) TestOutput(c *gc.C) { 502 for i, t := range ctests { 503 c.Logf("test %d: %s", i, t.name) 504 cfg, err := cloudinit.New("ubuntu") 505 c.Assert(err, jc.ErrorIsNil) 506 err = t.setOption(cfg) 507 c.Assert(err, jc.ErrorIsNil) 508 data, err := cfg.RenderYAML() 509 c.Assert(err, jc.ErrorIsNil) 510 c.Assert(data, gc.NotNil) 511 c.Assert(string(data), jc.YAMLEquals, t.expect) 512 data, err = cfg.RenderYAML() 513 c.Assert(err, jc.ErrorIsNil) 514 c.Assert(data, gc.NotNil) 515 c.Assert(string(data), jc.YAMLEquals, t.expect) 516 } 517 } 518 519 func (S) TestRunCmds(c *gc.C) { 520 cfg, err := cloudinit.New("ubuntu") 521 c.Assert(err, jc.ErrorIsNil) 522 c.Assert(cfg.RunCmds(), gc.HasLen, 0) 523 cfg.AddScripts("a", "b") 524 cfg.AddRunCmd("e") 525 c.Assert(cfg.RunCmds(), gc.DeepEquals, []string{ 526 "a", "b", "e", 527 }) 528 } 529 530 func (S) TestPackages(c *gc.C) { 531 cfg, err := cloudinit.New("ubuntu") 532 c.Assert(err, jc.ErrorIsNil) 533 c.Assert(cfg.Packages(), gc.HasLen, 0) 534 cfg.AddPackage("a b c") 535 cfg.AddPackage("d!") 536 expectedPackages := []string{"a b c", "d!"} 537 c.Assert(cfg.Packages(), gc.DeepEquals, expectedPackages) 538 } 539 540 func (S) TestSetOutput(c *gc.C) { 541 type test struct { 542 kind cloudinit.OutputKind 543 stdout string 544 stderr string 545 } 546 tests := []test{{ 547 cloudinit.OutAll, "a", "", 548 }, { 549 cloudinit.OutAll, "", "b", 550 }, { 551 cloudinit.OutInit, "a", "b", 552 }, { 553 cloudinit.OutAll, "a", "b", 554 }, { 555 cloudinit.OutAll, "", "", 556 }, 557 } 558 559 cfg, err := cloudinit.New("ubuntu") 560 c.Assert(err, jc.ErrorIsNil) 561 stdout, stderr := cfg.Output(cloudinit.OutAll) 562 c.Assert(stdout, gc.Equals, "") 563 c.Assert(stderr, gc.Equals, "") 564 for i, t := range tests { 565 c.Logf("test %d: %+v", i, t) 566 cfg.SetOutput(t.kind, t.stdout, t.stderr) 567 stdout, stderr = cfg.Output(t.kind) 568 c.Assert(stdout, gc.Equals, t.stdout) 569 c.Assert(stderr, gc.Equals, t.stderr) 570 } 571 } 572 573 func (S) TestFileTransporter(c *gc.C) { 574 ctrl := gomock.NewController(c) 575 defer ctrl.Finish() 576 577 ft := NewMockFileTransporter(ctrl) 578 ft.EXPECT().SendBytes("/dev/nonsense", []byte{0, 1, 2, 3}).Return("/tmp/dev-nonsense") 579 580 cfg, err := cloudinit.New("ubuntu") 581 c.Assert(err, jc.ErrorIsNil) 582 cfg.SetFileTransporter(ft) 583 584 cfg.AddRunBinaryFile( 585 "/dev/nonsense", 586 []byte{0, 1, 2, 3}, 587 0644, 588 ) 589 590 out, err := cfg.RenderYAML() 591 c.Assert(err, jc.ErrorIsNil) 592 593 unmarshalled := map[string]any{} 594 err = yaml.Unmarshal(out, unmarshalled) 595 c.Assert(err, jc.ErrorIsNil) 596 597 c.Assert(unmarshalled, gc.DeepEquals, map[string]any{ 598 "runcmd": []any{ 599 "install -D -m 644 /dev/null '/dev/nonsense'", 600 "cat '/tmp/dev-nonsense' > '/dev/nonsense'", 601 }, 602 }) 603 }