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  }