github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/tools/tools_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package tools_test 5 6 import ( 7 stdcontext "context" 8 "os" 9 "path/filepath" 10 11 "github.com/juju/errors" 12 "github.com/juju/loggo" 13 jc "github.com/juju/testing/checkers" 14 "github.com/juju/utils/v3" 15 "github.com/juju/version/v2" 16 gc "gopkg.in/check.v1" 17 18 corebase "github.com/juju/juju/core/base" 19 "github.com/juju/juju/environs" 20 "github.com/juju/juju/environs/bootstrap" 21 "github.com/juju/juju/environs/simplestreams" 22 sstesting "github.com/juju/juju/environs/simplestreams/testing" 23 envtesting "github.com/juju/juju/environs/testing" 24 envtools "github.com/juju/juju/environs/tools" 25 toolstesting "github.com/juju/juju/environs/tools/testing" 26 "github.com/juju/juju/juju/keys" 27 "github.com/juju/juju/jujuclient" 28 "github.com/juju/juju/provider/dummy" 29 coretesting "github.com/juju/juju/testing" 30 coretools "github.com/juju/juju/tools" 31 jujuversion "github.com/juju/juju/version" 32 ) 33 34 type SimpleStreamsToolsSuite struct { 35 env environs.Environ 36 coretesting.BaseSuite 37 envtesting.ToolsFixture 38 origCurrentVersion version.Number 39 customToolsDir string 40 publicToolsDir string 41 } 42 43 func setupToolsTests() { 44 gc.Suite(&SimpleStreamsToolsSuite{}) 45 gc.Suite(&ToolsListSuite{}) 46 } 47 48 func (s *SimpleStreamsToolsSuite) SetUpSuite(c *gc.C) { 49 s.BaseSuite.SetUpSuite(c) 50 s.customToolsDir = c.MkDir() 51 s.publicToolsDir = c.MkDir() 52 s.PatchValue(&keys.JujuPublicKey, sstesting.SignedMetadataPublicKey) 53 s.PatchValue(&corebase.UbuntuDistroInfo, "/path/notexists") 54 } 55 56 func (s *SimpleStreamsToolsSuite) SetUpTest(c *gc.C) { 57 s.ToolsFixture.DefaultBaseURL = utils.MakeFileURL(s.publicToolsDir) 58 s.BaseSuite.SetUpTest(c) 59 s.ToolsFixture.SetUpTest(c) 60 s.origCurrentVersion = jujuversion.Current 61 s.reset(c, nil) 62 } 63 64 func (s *SimpleStreamsToolsSuite) TearDownTest(c *gc.C) { 65 dummy.Reset(c) 66 jujuversion.Current = s.origCurrentVersion 67 s.ToolsFixture.TearDownTest(c) 68 s.BaseSuite.TearDownTest(c) 69 } 70 71 func (s *SimpleStreamsToolsSuite) reset(c *gc.C, attrs map[string]interface{}) { 72 final := map[string]interface{}{ 73 "agent-metadata-url": utils.MakeFileURL(s.customToolsDir), 74 "agent-stream": "proposed", 75 } 76 for k, v := range attrs { 77 final[k] = v 78 } 79 s.resetEnv(c, final) 80 } 81 82 func (s *SimpleStreamsToolsSuite) removeTools(c *gc.C) { 83 for _, dir := range []string{s.customToolsDir, s.publicToolsDir} { 84 files, err := os.ReadDir(dir) 85 c.Assert(err, jc.ErrorIsNil) 86 for _, f := range files { 87 err := os.RemoveAll(filepath.Join(dir, f.Name())) 88 c.Assert(err, jc.ErrorIsNil) 89 } 90 } 91 } 92 93 func (s *SimpleStreamsToolsSuite) uploadCustom(c *gc.C, verses ...version.Binary) map[version.Binary]string { 94 return toolstesting.UploadToDirectory(c, s.customToolsDir, toolstesting.StreamVersions{"proposed": verses})["proposed"] 95 } 96 97 func (s *SimpleStreamsToolsSuite) uploadPublic(c *gc.C, verses ...version.Binary) map[version.Binary]string { 98 return toolstesting.UploadToDirectory(c, s.publicToolsDir, toolstesting.StreamVersions{"proposed": verses})["proposed"] 99 } 100 101 func (s *SimpleStreamsToolsSuite) uploadStreams(c *gc.C, versions toolstesting.StreamVersions) map[string]map[version.Binary]string { 102 return toolstesting.UploadToDirectory(c, s.publicToolsDir, versions) 103 } 104 105 func (s *SimpleStreamsToolsSuite) resetEnv(c *gc.C, attrs map[string]interface{}) { 106 jujuversion.Current = s.origCurrentVersion 107 dummy.Reset(c) 108 attrs = dummy.SampleConfig().Merge(attrs) 109 env, err := bootstrap.PrepareController(false, envtesting.BootstrapContext(stdcontext.TODO(), c), 110 jujuclient.NewMemStore(), 111 bootstrap.PrepareParams{ 112 ControllerConfig: coretesting.FakeControllerConfig(), 113 ControllerName: attrs["name"].(string), 114 ModelConfig: attrs, 115 Cloud: dummy.SampleCloudSpec(), 116 AdminSecret: "admin-secret", 117 }, 118 ) 119 c.Assert(err, jc.ErrorIsNil) 120 s.env = env.(environs.Environ) 121 s.removeTools(c) 122 } 123 124 var findToolsTests = []struct { 125 info string 126 major int 127 minor int 128 custom []version.Binary 129 public []version.Binary 130 expect []version.Binary 131 err error 132 }{{ 133 info: "none available anywhere", 134 major: 1, 135 err: envtools.ErrNoTools, 136 }, { 137 info: "custom/private tools only, none matching", 138 major: 1, 139 minor: 2, 140 custom: envtesting.V220all, 141 err: coretools.ErrNoMatches, 142 }, { 143 info: "custom tools found", 144 major: 1, 145 minor: 2, 146 custom: envtesting.VAll, 147 expect: envtesting.V120all, 148 }, { 149 info: "public tools found", 150 major: 1, 151 minor: 1, 152 public: envtesting.VAll, 153 expect: envtesting.V110all, 154 }, { 155 info: "public and custom tools found, only taken from custom", 156 major: 1, 157 minor: 1, 158 custom: envtesting.V110p, 159 public: envtesting.VAll, 160 expect: envtesting.V110p, 161 }, { 162 info: "custom tools completely block public ones", 163 major: 1, 164 minor: -1, 165 custom: envtesting.V220all, 166 public: envtesting.VAll, 167 expect: envtesting.V1all, 168 }, { 169 info: "tools matching major version only", 170 major: 1, 171 minor: -1, 172 public: envtesting.VAll, 173 expect: envtesting.V1all, 174 }} 175 176 func (s *SimpleStreamsToolsSuite) TestFindTools(c *gc.C) { 177 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 178 for i, test := range findToolsTests { 179 c.Logf("\ntest %d: %s", i, test.info) 180 s.reset(c, nil) 181 custom := s.uploadCustom(c, test.custom...) 182 public := s.uploadPublic(c, test.public...) 183 streams := envtools.PreferredStreams(&jujuversion.Current, s.env.Config().Development(), s.env.Config().AgentStream()) 184 actual, err := envtools.FindTools(ss, s.env, test.major, test.minor, streams, coretools.Filter{}) 185 if test.err != nil { 186 if len(actual) > 0 { 187 c.Logf(actual.String()) 188 } 189 c.Check(err, jc.Satisfies, errors.IsNotFound) 190 continue 191 } 192 expect := map[version.Binary][]string{} 193 for _, expected := range test.expect { 194 // If the tools exist in custom, that's preferred. 195 url, ok := custom[expected] 196 if !ok { 197 url = public[expected] 198 } 199 expect[expected] = append(expect[expected], url) 200 } 201 c.Check(actual.URLs(), gc.DeepEquals, expect) 202 } 203 } 204 205 func (s *SimpleStreamsToolsSuite) TestFindToolsFiltering(c *gc.C) { 206 var tw loggo.TestWriter 207 c.Assert(loggo.RegisterWriter("filter-tester", &tw), gc.IsNil) 208 defer loggo.RemoveWriter("filter-tester") 209 logger := loggo.GetLogger("juju.environs") 210 defer logger.SetLogLevel(logger.LogLevel()) 211 logger.SetLogLevel(loggo.TRACE) 212 213 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 214 _, err := envtools.FindTools(ss, 215 s.env, 1, -1, []string{"released"}, coretools.Filter{Number: version.Number{Major: 1, Minor: 2, Patch: 3}}) 216 c.Assert(err, jc.Satisfies, errors.IsNotFound) 217 // This is slightly overly prescriptive, but feel free to change or add 218 // messages. This still helps to ensure that all log messages are 219 // properly formed. 220 messages := []jc.SimpleMessage{ 221 {loggo.DEBUG, "reading agent binaries with major version 1"}, 222 {loggo.DEBUG, "filtering agent binaries by version: \\d+\\.\\d+\\.\\d+"}, 223 {loggo.TRACE, "no architecture specified when finding agent binaries, looking for "}, 224 {loggo.TRACE, "no os type specified when finding agent binaries, looking for \\[.*\\]"}, 225 } 226 sources, err := envtools.GetMetadataSources(s.env, ss) 227 c.Assert(err, jc.ErrorIsNil) 228 for i := 0; i < len(sources); i++ { 229 messages = append(messages, 230 jc.SimpleMessage{loggo.TRACE, `fetchData failed for .*`}, 231 jc.SimpleMessage{loggo.DEBUG, `cannot load index .*`}) 232 } 233 c.Check(tw.Log(), jc.LogMatches, messages) 234 } 235 236 var findExactToolsTests = []struct { 237 info string 238 // These are the contents of the proposed streams in each source. 239 custom []version.Binary 240 public []version.Binary 241 seek version.Binary 242 err error 243 }{{ 244 info: "nothing available", 245 seek: envtesting.V100u64, 246 err: envtools.ErrNoTools, 247 }, { 248 info: "only non-matches available in custom", 249 custom: append(envtesting.V110all, envtesting.V100u32, envtesting.V1001u64), 250 seek: envtesting.V100u64, 251 err: coretools.ErrNoMatches, 252 }, { 253 info: "exact match available in custom", 254 custom: []version.Binary{envtesting.V100u64}, 255 seek: envtesting.V100u64, 256 }, { 257 info: "only non-matches available in public", 258 custom: append(envtesting.V110all, envtesting.V100u32, envtesting.V1001u64), 259 seek: envtesting.V100u64, 260 err: coretools.ErrNoMatches, 261 }, { 262 info: "exact match available in public", 263 public: []version.Binary{envtesting.V100u64}, 264 seek: envtesting.V100u64, 265 }, { 266 info: "exact match in public not blocked by custom", 267 custom: envtesting.V110all, 268 public: []version.Binary{envtesting.V100u64}, 269 seek: envtesting.V100u64, 270 }} 271 272 func (s *SimpleStreamsToolsSuite) TestFindExactTools(c *gc.C) { 273 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 274 for i, test := range findExactToolsTests { 275 c.Logf("\ntest %d: %s", i, test.info) 276 s.reset(c, nil) 277 custom := s.uploadCustom(c, test.custom...) 278 public := s.uploadPublic(c, test.public...) 279 actual, err := envtools.FindExactTools(ss, s.env, test.seek.Number, test.seek.Release, test.seek.Arch) 280 if test.err == nil { 281 if !c.Check(err, jc.ErrorIsNil) { 282 continue 283 } 284 c.Check(actual.Version, gc.Equals, test.seek) 285 if _, ok := custom[actual.Version]; ok { 286 c.Check(actual.URL, gc.DeepEquals, custom[actual.Version]) 287 } else { 288 c.Check(actual.URL, gc.DeepEquals, public[actual.Version]) 289 } 290 } else { 291 c.Check(err, jc.Satisfies, errors.IsNotFound) 292 } 293 } 294 } 295 296 func copyAndAppend(vs []version.Binary, more ...[]version.Binary) []version.Binary { 297 // TODO(babbageclunk): I think the append(someversions, 298 // moreversions...) technique used in environs/testing/tools.go 299 // might be wrong because it can mutate someversions if there's 300 // enough capacity. Use this there. 301 // https://medium.com/@Jarema./golang-slice-append-gotcha-e9020ff37374 302 result := make([]version.Binary, len(vs)) 303 copy(result, vs) 304 for _, items := range more { 305 result = append(result, items...) 306 } 307 return result 308 } 309 310 var findToolsFallbackTests = []struct { 311 info string 312 major int 313 minor int 314 streams []string 315 devel []version.Binary 316 proposed []version.Binary 317 released []version.Binary 318 expect []version.Binary 319 err error 320 }{{ 321 info: "nothing available", 322 major: 1, 323 streams: []string{"released"}, 324 err: envtools.ErrNoTools, 325 }, { 326 info: "only available in non-selected stream", 327 major: 1, 328 minor: 2, 329 streams: []string{"released"}, 330 devel: envtesting.VAll, 331 err: coretools.ErrNoMatches, 332 }, { 333 info: "finds things in devel and released, ignores proposed", 334 major: 1, 335 minor: -1, 336 streams: []string{"devel", "released"}, 337 devel: envtesting.V120all, 338 proposed: envtesting.V110all, 339 released: envtesting.V100all, 340 expect: copyAndAppend(envtesting.V120all, envtesting.V100all), 341 }, { 342 info: "finds matching things everywhere", 343 major: 1, 344 minor: 2, 345 streams: []string{"devel", "proposed", "released"}, 346 devel: []version.Binary{}, 347 proposed: []version.Binary{envtesting.V110u64, envtesting.V120u64}, 348 released: []version.Binary{envtesting.V100u64}, 349 expect: []version.Binary{envtesting.V120u64}, 350 }} 351 352 func (s *SimpleStreamsToolsSuite) TestFindToolsWithStreamFallback(c *gc.C) { 353 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 354 for i, test := range findToolsFallbackTests { 355 c.Logf("\ntest %d: %s", i, test.info) 356 s.reset(c, nil) 357 streams := s.uploadStreams(c, toolstesting.StreamVersions{ 358 "devel": test.devel, 359 "proposed": test.proposed, 360 "released": test.released, 361 }) 362 actual, err := envtools.FindTools(ss, 363 s.env, test.major, test.minor, test.streams, coretools.Filter{}) 364 if test.err != nil { 365 if len(actual) > 0 { 366 c.Logf(actual.String()) 367 } 368 c.Check(err, jc.Satisfies, errors.IsNotFound) 369 continue 370 } 371 expect := map[version.Binary][]string{} 372 for _, expected := range test.expect { 373 for _, stream := range []string{"devel", "proposed", "released"} { 374 if url, ok := streams[stream][expected]; ok { 375 expect[expected] = []string{url} 376 break 377 } 378 } 379 } 380 c.Check(actual.URLs(), gc.DeepEquals, expect) 381 } 382 } 383 384 var preferredStreamTests = []struct { 385 explicitVers string 386 currentVers string 387 forceDevel bool 388 streamInConfig string 389 expected []string 390 }{{ 391 currentVers: "1.22.0", 392 streamInConfig: "released", 393 expected: []string{"released"}, 394 }, { 395 currentVers: "1.22.0", 396 streamInConfig: "proposed", 397 expected: []string{"proposed", "released"}, 398 }, { 399 currentVers: "1.22.0", 400 streamInConfig: "devel", 401 expected: []string{"devel", "proposed", "released"}, 402 }, { 403 currentVers: "1.22.0", 404 streamInConfig: "testing", 405 expected: []string{"testing", "devel", "proposed", "released"}, 406 }, { 407 currentVers: "1.22.0", 408 expected: []string{"released"}, 409 }, { 410 currentVers: "1.22-beta1", 411 expected: []string{"devel", "proposed", "released"}, 412 }, { 413 currentVers: "1.22-beta1", 414 streamInConfig: "released", 415 expected: []string{"devel", "proposed", "released"}, 416 }, { 417 currentVers: "1.22-beta1", 418 streamInConfig: "devel", 419 expected: []string{"devel", "proposed", "released"}, 420 }, { 421 currentVers: "1.22.0", 422 forceDevel: true, 423 expected: []string{"devel", "proposed", "released"}, 424 }, { 425 currentVers: "1.22.0", 426 explicitVers: "1.22-beta1", 427 expected: []string{"devel", "proposed", "released"}, 428 }, { 429 currentVers: "1.22-bta1", 430 explicitVers: "1.22.0", 431 expected: []string{"released"}, 432 }} 433 434 func (s *SimpleStreamsToolsSuite) TestPreferredStreams(c *gc.C) { 435 for i, test := range preferredStreamTests { 436 c.Logf("\ntest %d", i) 437 s.PatchValue(&jujuversion.Current, version.MustParse(test.currentVers)) 438 var vers *version.Number 439 if test.explicitVers != "" { 440 v := version.MustParse(test.explicitVers) 441 vers = &v 442 } 443 obtained := envtools.PreferredStreams(vers, test.forceDevel, test.streamInConfig) 444 c.Check(obtained, gc.DeepEquals, test.expected) 445 } 446 } 447 448 // fakeToolsForRelease fakes a Tools object with just enough information for 449 // testing the handling its OS type. 450 func fakeToolsForRelease(osType string) *coretools.Tools { 451 return &coretools.Tools{Version: version.Binary{Release: osType}} 452 } 453 454 // fakeToolsList fakes a envtools.List containing Tools objects for the given 455 // respective os types, in the same number and order. 456 func fakeToolsList(releases ...string) coretools.List { 457 list := coretools.List{} 458 for _, name := range releases { 459 list = append(list, fakeToolsForRelease(name)) 460 } 461 return list 462 } 463 464 type ToolsListSuite struct{} 465 466 func (s *ToolsListSuite) TestCheckToolsReleaseRequiresTools(c *gc.C) { 467 err := envtools.CheckToolsReleases(fakeToolsList(), "ubuntu") 468 c.Assert(err, gc.NotNil) 469 c.Check(err, gc.ErrorMatches, "expected single os type, got \\[\\]") 470 } 471 472 func (s *ToolsListSuite) TestCheckToolsReleaseAcceptsOneSetOfTools(c *gc.C) { 473 names := []string{"ubuntu", "windows"} 474 for _, release := range names { 475 list := fakeToolsList(release) 476 err := envtools.CheckToolsReleases(list, release) 477 c.Check(err, jc.ErrorIsNil) 478 } 479 } 480 481 func (s *ToolsListSuite) TestCheckToolsReleaseAcceptsMultipleForSameOSType(c *gc.C) { 482 osType := "ubuntu" 483 list := fakeToolsList(osType, osType, osType) 484 err := envtools.CheckToolsReleases(list, osType) 485 c.Check(err, jc.ErrorIsNil) 486 } 487 488 func (s *ToolsListSuite) TestCheckToolsReleaseRejectsToolsForOthers(c *gc.C) { 489 list := fakeToolsList("windows") 490 err := envtools.CheckToolsReleases(list, "ubuntu") 491 c.Assert(err, gc.NotNil) 492 c.Check(err, gc.ErrorMatches, "agent binary mismatch: expected os type ubuntu, got windows") 493 } 494 495 func (s *ToolsListSuite) TestCheckToolsReleaseRejectsToolsForMixed(c *gc.C) { 496 list := fakeToolsList("ubuntu", "windows") 497 err := envtools.CheckToolsReleases(list, "ubuntu") 498 c.Assert(err, gc.NotNil) 499 c.Check(err, gc.ErrorMatches, "expected single os type, got .*") 500 }