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  }