github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/sync/sync_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package sync_test
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"os"
    14  	"os/exec"
    15  	"path"
    16  	"path/filepath"
    17  
    18  	"github.com/juju/errors"
    19  	jujuhttp "github.com/juju/http/v2"
    20  	jujutesting "github.com/juju/testing"
    21  	jc "github.com/juju/testing/checkers"
    22  	"github.com/juju/utils/v3/tar"
    23  	"github.com/juju/version/v2"
    24  	"go.uber.org/mock/gomock"
    25  	gc "gopkg.in/check.v1"
    26  
    27  	"github.com/juju/juju/core/arch"
    28  	corebase "github.com/juju/juju/core/base"
    29  	coreos "github.com/juju/juju/core/os"
    30  	"github.com/juju/juju/core/os/ostype"
    31  	"github.com/juju/juju/environs/filestorage"
    32  	"github.com/juju/juju/environs/simplestreams"
    33  	"github.com/juju/juju/environs/storage"
    34  	"github.com/juju/juju/environs/sync"
    35  	envtesting "github.com/juju/juju/environs/testing"
    36  	envtools "github.com/juju/juju/environs/tools"
    37  	toolstesting "github.com/juju/juju/environs/tools/testing"
    38  	"github.com/juju/juju/juju/names"
    39  	coretesting "github.com/juju/juju/testing"
    40  	coretools "github.com/juju/juju/tools"
    41  	jujuversion "github.com/juju/juju/version"
    42  )
    43  
    44  type syncSuite struct {
    45  	coretesting.FakeJujuXDGDataHomeSuite
    46  	envtesting.ToolsFixture
    47  	storage      storage.Storage
    48  	localStorage string
    49  }
    50  
    51  var _ = gc.Suite(&syncSuite{})
    52  var _ = gc.Suite(&uploadSuite{})
    53  var _ = gc.Suite(&badBuildSuite{})
    54  
    55  func (s *syncSuite) setUpTest(c *gc.C) {
    56  	s.FakeJujuXDGDataHomeSuite.SetUpTest(c)
    57  	s.ToolsFixture.SetUpTest(c)
    58  
    59  	// It's important that this be v1.8.x to match the test data.
    60  	s.PatchValue(&jujuversion.Current, version.MustParse("1.8.3"))
    61  
    62  	// Create a source storage.
    63  	baseDir := c.MkDir()
    64  	stor, err := filestorage.NewFileStorageWriter(baseDir)
    65  	c.Assert(err, jc.ErrorIsNil)
    66  	s.storage = stor
    67  
    68  	// Create a local tools directory.
    69  	s.localStorage = c.MkDir()
    70  
    71  	// Populate both local and default tools locations with the public tools.
    72  	versionStrings := make([]string, len(vAll))
    73  	for i, vers := range vAll {
    74  		versionStrings[i] = vers.String()
    75  	}
    76  	toolstesting.MakeTools(c, baseDir, "released", versionStrings)
    77  	toolstesting.MakeTools(c, s.localStorage, "released", versionStrings)
    78  
    79  	// Switch the default tools location.
    80  	baseURL, err := s.storage.URL(storage.BaseToolsPath)
    81  	c.Assert(err, jc.ErrorIsNil)
    82  	s.PatchValue(&envtools.DefaultBaseURL, baseURL)
    83  }
    84  
    85  func (s *syncSuite) tearDownTest(c *gc.C) {
    86  	s.ToolsFixture.TearDownTest(c)
    87  	s.FakeJujuXDGDataHomeSuite.TearDownTest(c)
    88  }
    89  
    90  var tests = []struct {
    91  	description string
    92  	ctx         *sync.SyncContext
    93  	source      bool
    94  	tools       []version.Binary
    95  	major       int
    96  	minor       int
    97  }{
    98  	{
    99  		description: "copy newest from the filesystem",
   100  		ctx: &sync.SyncContext{
   101  			ChosenVersion: version.MustParse("1.8.0"),
   102  		},
   103  		source: true,
   104  		tools:  v180all,
   105  	},
   106  	{
   107  		description: "copy newest from the dummy model",
   108  		ctx: &sync.SyncContext{
   109  			ChosenVersion: version.MustParse("1.8.0"),
   110  		},
   111  		tools: v180all,
   112  	},
   113  	{
   114  		description: "copy matching dev from the dummy model",
   115  		ctx: &sync.SyncContext{
   116  			ChosenVersion: version.MustParse("1.9.0"),
   117  		},
   118  		tools: v190all,
   119  	},
   120  	{
   121  		description: "copy matching version from the dummy model",
   122  		ctx: &sync.SyncContext{
   123  			ChosenVersion: version.MustParse("3.2.0"),
   124  		},
   125  		tools: []version.Binary{v320u64},
   126  	},
   127  	{
   128  		description: "copy matching major, minor dev from the dummy model",
   129  		ctx: &sync.SyncContext{
   130  			ChosenVersion: version.MustParse("3.1.0"),
   131  		},
   132  		tools: []version.Binary{v310u64},
   133  	},
   134  }
   135  
   136  func (s *syncSuite) TestSyncing(c *gc.C) {
   137  	for i, test := range tests {
   138  		// Perform all tests in a "clean" environment.
   139  		func() {
   140  			s.setUpTest(c)
   141  			defer s.tearDownTest(c)
   142  
   143  			c.Logf("test %d: %s", i, test.description)
   144  
   145  			if test.source {
   146  				test.ctx.Source = s.localStorage
   147  			}
   148  			if test.ctx.ChosenVersion != version.Zero {
   149  				jujuversion.Current = test.ctx.ChosenVersion
   150  			}
   151  
   152  			uploader := fakeToolsUploader{
   153  				uploaded: make(map[version.Binary]bool),
   154  			}
   155  			test.ctx.TargetToolsFinder = mockToolsFinder{}
   156  			test.ctx.TargetToolsUploader = &uploader
   157  
   158  			err := sync.SyncTools(test.ctx)
   159  			c.Assert(err, jc.ErrorIsNil)
   160  
   161  			ds, err := sync.SelectSourceDatasource(test.ctx)
   162  			c.Assert(err, jc.ErrorIsNil)
   163  
   164  			// This data source does not require to contain signed data.
   165  			// However, it may still contain it.
   166  			// Since we will always try to read signed data first,
   167  			// we want to be able to try to read this signed data
   168  			// with public key with Juju-known public key for tools.
   169  			// Bugs #1542127, #1542131
   170  			c.Assert(ds.PublicSigningKey(), gc.Not(gc.Equals), "")
   171  
   172  			var uploaded []version.Binary
   173  			for v := range uploader.uploaded {
   174  				uploaded = append(uploaded, v)
   175  			}
   176  			c.Assert(uploaded, jc.SameContents, test.tools)
   177  		}()
   178  	}
   179  }
   180  
   181  // regression test for https://pad.lv/2029881
   182  func (s *syncSuite) TestSyncToolsOldPatchVersion(c *gc.C) {
   183  	s.setUpTest(c)
   184  	defer s.tearDownTest(c)
   185  
   186  	// Add some extra tools for the newer patch versions
   187  	toolstesting.MakeTools(c, s.localStorage, "released", []string{"1.8.3-ubuntu-amd64"})
   188  
   189  	err := sync.SyncTools(&sync.SyncContext{
   190  		Source: s.localStorage,
   191  		// Request an older patch version of the current series (1.8.x)
   192  		ChosenVersion: version.MustParse("1.8.0"),
   193  		TargetToolsUploader: &fakeToolsUploader{
   194  			uploaded: make(map[version.Binary]bool),
   195  		},
   196  	})
   197  	c.Assert(err, jc.ErrorIsNil)
   198  }
   199  
   200  type fakeToolsUploader struct {
   201  	uploaded map[version.Binary]bool
   202  }
   203  
   204  func (u *fakeToolsUploader) UploadTools(_, _ string, tools *coretools.Tools, _ []byte) error {
   205  	u.uploaded[tools.Version] = true
   206  	return nil
   207  }
   208  
   209  var (
   210  	v100u64 = version.MustParseBinary("1.0.0-ubuntu-amd64")
   211  	v100u32 = version.MustParseBinary("1.0.0-ubuntu-arm64")
   212  	v100all = []version.Binary{v100u64, v100u32}
   213  	v180u64 = version.MustParseBinary("1.8.0-ubuntu-amd64")
   214  	v180u32 = version.MustParseBinary("1.8.0-ubuntu-arm64")
   215  	v180all = []version.Binary{v180u64, v180u32}
   216  	v190u64 = version.MustParseBinary("1.9.0-ubuntu-amd64")
   217  	v190u32 = version.MustParseBinary("1.9.0-ubuntu-arm64")
   218  	v190all = []version.Binary{v190u64, v190u32}
   219  	v1all   = append(append(v100all, v180all...), v190all...)
   220  	v200u64 = version.MustParseBinary("2.0.0-ubuntu-amd64")
   221  	v310u64 = version.MustParseBinary("3.1.0-ubuntu-amd64")
   222  	v320u64 = version.MustParseBinary("3.2.0-ubuntu-amd64")
   223  	vAll    = append(append(v1all, v200u64), v310u64, v320u64)
   224  )
   225  
   226  type uploadSuite struct {
   227  	coretesting.FakeJujuXDGDataHomeSuite
   228  	envtesting.ToolsFixture
   229  	targetStorage storage.Storage
   230  }
   231  
   232  func (s *uploadSuite) SetUpTest(c *gc.C) {
   233  	s.FakeJujuXDGDataHomeSuite.SetUpTest(c)
   234  	s.ToolsFixture.SetUpTest(c)
   235  	s.PatchValue(&corebase.UbuntuDistroInfo, "/path/notexists")
   236  
   237  	// Create a target storage.
   238  	stor, err := filestorage.NewFileStorageWriter(c.MkDir())
   239  	c.Assert(err, jc.ErrorIsNil)
   240  	s.targetStorage = stor
   241  }
   242  
   243  func (s *uploadSuite) patchBundleTools(c *gc.C, v version.Number) {
   244  	// Mock out building of tools. Sync should not care about the contents
   245  	// of tools archives, other than that they hash correctly.
   246  	s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(v))
   247  }
   248  
   249  func (s *uploadSuite) assertEqualsCurrentVersion(c *gc.C, v version.Binary) {
   250  	c.Assert(v, gc.Equals, coretesting.CurrentVersion())
   251  }
   252  
   253  func (s *uploadSuite) TearDownTest(c *gc.C) {
   254  	s.ToolsFixture.TearDownTest(c)
   255  	s.FakeJujuXDGDataHomeSuite.TearDownTest(c)
   256  }
   257  
   258  func (s *uploadSuite) TestUpload(c *gc.C) {
   259  	ctrl := gomock.NewController(c)
   260  	defer ctrl.Finish()
   261  
   262  	ss := NewMockSimplestreamsFetcher(ctrl)
   263  	ss.EXPECT().GetMetadata(gomock.Any(), gomock.Any()).AnyTimes()
   264  
   265  	forceVersion := jujuversion.Current
   266  	s.patchBundleTools(c, forceVersion)
   267  	t, err := sync.Upload(ss, s.targetStorage, "released",
   268  		func(version.Number) version.Number { return forceVersion },
   269  	)
   270  	c.Assert(err, jc.ErrorIsNil)
   271  	s.assertEqualsCurrentVersion(c, t.Version)
   272  	c.Assert(t.URL, gc.Not(gc.Equals), "")
   273  	hostOSType := coreos.HostOSTypeName()
   274  	s.assertUploadedTools(c, t, []string{hostOSType}, "released")
   275  }
   276  
   277  func (s *uploadSuite) TestUploadAndForceVersion(c *gc.C) {
   278  	ctrl := gomock.NewController(c)
   279  	defer ctrl.Finish()
   280  
   281  	ss := NewMockSimplestreamsFetcher(ctrl)
   282  	ss.EXPECT().GetMetadata(gomock.Any(), gomock.Any()).AnyTimes()
   283  
   284  	forceVersion := jujuversion.Current
   285  	forceVersion.Patch++
   286  	s.patchBundleTools(c, forceVersion)
   287  	t, err := sync.Upload(ss, s.targetStorage, "released",
   288  		func(version.Number) version.Number { return forceVersion },
   289  	)
   290  	c.Assert(err, jc.ErrorIsNil)
   291  	c.Assert(t.Version, gc.Equals, coretesting.CurrentVersion())
   292  }
   293  
   294  func (s *uploadSuite) TestSyncTools(c *gc.C) {
   295  	ctrl := gomock.NewController(c)
   296  	defer ctrl.Finish()
   297  
   298  	ss := NewMockSimplestreamsFetcher(ctrl)
   299  	ss.EXPECT().GetMetadata(gomock.Any(), gomock.Any()).AnyTimes()
   300  
   301  	forceVersion := jujuversion.Current
   302  	forceVersion.Patch++
   303  	s.patchBundleTools(c, forceVersion)
   304  	builtTools, err := sync.BuildAgentTarball(true, "released",
   305  		func(version.Number) version.Number { return forceVersion },
   306  	)
   307  	c.Assert(err, jc.ErrorIsNil)
   308  	t, err := sync.SyncBuiltTools(ss, s.targetStorage, "released", builtTools)
   309  	c.Assert(err, jc.ErrorIsNil)
   310  	s.assertEqualsCurrentVersion(c, t.Version)
   311  	c.Assert(t.URL, gc.Not(gc.Equals), "")
   312  }
   313  
   314  func (s *uploadSuite) TestSyncAndForceVersion(c *gc.C) {
   315  	ctrl := gomock.NewController(c)
   316  	defer ctrl.Finish()
   317  
   318  	ss := NewMockSimplestreamsFetcher(ctrl)
   319  	ss.EXPECT().GetMetadata(gomock.Any(), gomock.Any()).AnyTimes()
   320  
   321  	forceVersion := jujuversion.Current
   322  	forceVersion.Patch++
   323  	s.patchBundleTools(c, forceVersion)
   324  	builtTools, err := sync.BuildAgentTarball(true, "released",
   325  		func(version.Number) version.Number { return forceVersion },
   326  	)
   327  	c.Assert(err, jc.ErrorIsNil)
   328  	t, err := sync.SyncBuiltTools(ss, s.targetStorage, "released", builtTools)
   329  	c.Assert(err, jc.ErrorIsNil)
   330  	// Reported version from build call matches the real jujud version.
   331  	c.Assert(t.Version, gc.Equals, coretesting.CurrentVersion())
   332  }
   333  
   334  func (s *uploadSuite) assertUploadedTools(c *gc.C, t *coretools.Tools, expectOSTypes []string, stream string) {
   335  	ctrl := gomock.NewController(c)
   336  	defer ctrl.Finish()
   337  
   338  	ss := NewMockSimplestreamsFetcher(ctrl)
   339  	ss.EXPECT().GetMetadata(gomock.Any(), gomock.Any()).AnyTimes()
   340  
   341  	s.assertEqualsCurrentVersion(c, t.Version)
   342  	expectRaw := downloadToolsRaw(c, t)
   343  
   344  	list, err := envtools.ReadList(s.targetStorage, stream, jujuversion.Current.Major, jujuversion.Current.Minor)
   345  	c.Assert(err, jc.ErrorIsNil)
   346  	c.Assert(list.AllReleases(), jc.SameContents, expectOSTypes)
   347  	for _, t := range list {
   348  		c.Logf("checking %s", t.URL)
   349  		c.Assert(t.Version.Number, gc.Equals, jujuversion.Current)
   350  		actualRaw := downloadToolsRaw(c, t)
   351  		c.Assert(string(actualRaw), gc.Equals, string(expectRaw))
   352  	}
   353  	metadata, err := envtools.ReadMetadata(ss, s.targetStorage, stream)
   354  	c.Assert(err, jc.ErrorIsNil)
   355  	c.Assert(metadata, gc.HasLen, 0)
   356  }
   357  
   358  // downloadToolsRaw downloads the supplied tools and returns the raw bytes.
   359  func downloadToolsRaw(c *gc.C, t *coretools.Tools) []byte {
   360  	client := jujuhttp.NewClient()
   361  	resp, err := client.Get(context.TODO(), t.URL)
   362  	c.Assert(err, jc.ErrorIsNil)
   363  	defer func() { _ = resp.Body.Close() }()
   364  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   365  	var buf bytes.Buffer
   366  	_, err = io.Copy(&buf, resp.Body)
   367  	c.Assert(err, jc.ErrorIsNil)
   368  	return buf.Bytes()
   369  }
   370  
   371  func bundleTools(c *gc.C) (version.Binary, bool, string, error) {
   372  	f, err := os.CreateTemp("", "juju-tgz")
   373  	c.Assert(err, jc.ErrorIsNil)
   374  	defer func() { _ = f.Close() }()
   375  	defer func() { _ = os.Remove(f.Name()) }()
   376  
   377  	tvers, _, official, sha256hash, err := envtools.BundleTools(true, f,
   378  		func(version.Number) version.Number { return jujuversion.Current },
   379  	)
   380  	return tvers, official, sha256hash, err
   381  }
   382  
   383  type badBuildSuite struct {
   384  	jujutesting.LoggingSuite
   385  	jujutesting.CleanupSuite
   386  	envtesting.ToolsFixture
   387  	jujutesting.PatchExecHelper
   388  }
   389  
   390  var badGo = `
   391  #!/bin/bash --norc
   392  exit 1
   393  `[1:]
   394  
   395  func (s *badBuildSuite) SetUpSuite(c *gc.C) {
   396  	s.CleanupSuite.SetUpSuite(c)
   397  	s.LoggingSuite.SetUpSuite(c)
   398  }
   399  
   400  func (s *badBuildSuite) TearDownSuite(c *gc.C) {
   401  	s.LoggingSuite.TearDownSuite(c)
   402  	s.CleanupSuite.TearDownSuite(c)
   403  }
   404  
   405  func (s *badBuildSuite) SetUpTest(c *gc.C) {
   406  	s.CleanupSuite.SetUpTest(c)
   407  	s.LoggingSuite.SetUpTest(c)
   408  	s.ToolsFixture.SetUpTest(c)
   409  
   410  	// Mock go cmd
   411  	testPath := c.MkDir()
   412  	s.PatchEnvPathPrepend(testPath)
   413  	path := filepath.Join(testPath, "go")
   414  	err := os.WriteFile(path, []byte(badGo), 0755)
   415  	c.Assert(err, jc.ErrorIsNil)
   416  
   417  	// Check mocked go cmd errors
   418  	out, err := exec.Command("go").CombinedOutput()
   419  	c.Assert(err, gc.ErrorMatches, "exit status 1")
   420  	c.Assert(string(out), gc.Equals, "")
   421  }
   422  
   423  func (s *badBuildSuite) TearDownTest(c *gc.C) {
   424  	s.ToolsFixture.TearDownTest(c)
   425  	s.LoggingSuite.TearDownTest(c)
   426  	s.CleanupSuite.TearDownTest(c)
   427  }
   428  
   429  func (s *badBuildSuite) assertEqualsCurrentVersion(c *gc.C, v version.Binary) {
   430  	current := coretesting.CurrentVersion()
   431  	c.Assert(v, gc.Equals, current)
   432  }
   433  
   434  func (s *badBuildSuite) TestBundleToolsBadBuild(c *gc.C) {
   435  	s.patchExecCommand(c)
   436  
   437  	// Test that original bundleTools Func fails as expected
   438  	vers, official, sha256Hash, err := bundleTools(c)
   439  	c.Assert(vers, gc.DeepEquals, version.Binary{})
   440  	c.Assert(official, jc.IsFalse)
   441  	c.Assert(sha256Hash, gc.Equals, "")
   442  	c.Assert(err, gc.ErrorMatches, `(?m)cannot build jujud agent binary from source: .*`)
   443  
   444  	s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(jujuversion.Current))
   445  
   446  	// Test that BundleTools func passes after it is
   447  	// mocked out
   448  	vers, official, sha256Hash, err = bundleTools(c)
   449  	c.Assert(err, jc.ErrorIsNil)
   450  	c.Assert(vers.Number, gc.Equals, jujuversion.Current)
   451  	c.Assert(official, jc.IsFalse)
   452  	c.Assert(sha256Hash, gc.Equals, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
   453  }
   454  
   455  func (s *badBuildSuite) patchExecCommand(c *gc.C) {
   456  	execCommand := s.GetExecCommand(jujutesting.PatchExecConfig{
   457  		Stdout: coretesting.CurrentVersion().String(),
   458  		Args:   make(chan []string, 2),
   459  	})
   460  	s.PatchValue(&envtools.ExecCommand, execCommand)
   461  }
   462  
   463  func (s *badBuildSuite) TestBuildToolsBadBuild(c *gc.C) {
   464  	s.patchExecCommand(c)
   465  
   466  	// Test that original BuildAgentTarball fails
   467  	builtTools, err := sync.BuildAgentTarball(true, "released",
   468  		func(version.Number) version.Number { return version.Zero },
   469  	)
   470  	c.Assert(err, gc.ErrorMatches, `(?m)cannot build jujud agent binary from source: .*`)
   471  	c.Assert(builtTools, gc.IsNil)
   472  
   473  	// Test that BuildAgentTarball func passes after BundleTools func is
   474  	// mocked out
   475  	forceVersion := coretesting.CurrentVersion().Number
   476  	s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(forceVersion))
   477  	builtTools, err = sync.BuildAgentTarball(true, "released",
   478  		func(version.Number) version.Number { return forceVersion },
   479  	)
   480  	s.assertEqualsCurrentVersion(c, builtTools.Version)
   481  	c.Assert(err, jc.ErrorIsNil)
   482  }
   483  
   484  func (s *badBuildSuite) TestBuildToolsNoBinaryAvailable(c *gc.C) {
   485  	s.patchExecCommand(c)
   486  
   487  	builtTools, err := sync.BuildAgentTarball(false, "released",
   488  		func(version.Number) version.Number { return version.Zero },
   489  	)
   490  	c.Assert(err, gc.ErrorMatches, `no prepackaged agent available and no jujud binary can be found`)
   491  	c.Assert(builtTools, gc.IsNil)
   492  }
   493  
   494  func (s *uploadSuite) TestMockBundleTools(c *gc.C) {
   495  	var (
   496  		writer       io.Writer
   497  		forceVersion version.Number
   498  		n            int
   499  		p            bytes.Buffer
   500  	)
   501  	p.WriteString("Hello World")
   502  
   503  	s.PatchValue(&envtools.BundleTools,
   504  		func(
   505  			build bool, writerArg io.Writer,
   506  			getForceVersion func(version.Number) version.Number,
   507  		) (vers version.Binary, fVersion version.Number, official bool, sha256Hash string, err error) {
   508  			c.Assert(build, jc.IsTrue)
   509  			writer = writerArg
   510  			n, err = writer.Write(p.Bytes())
   511  			c.Assert(err, jc.ErrorIsNil)
   512  			forceVersion = getForceVersion(version.Zero)
   513  			fVersion = forceVersion
   514  			vers.Number = jujuversion.Current
   515  			return
   516  		},
   517  	)
   518  
   519  	_, err := sync.BuildAgentTarball(true, "released",
   520  		func(version.Number) version.Number { return jujuversion.Current },
   521  	)
   522  	c.Assert(err, jc.ErrorIsNil)
   523  	c.Assert(forceVersion, gc.Equals, jujuversion.Current)
   524  	c.Assert(writer, gc.NotNil)
   525  	c.Assert(n, gc.Equals, len(p.Bytes()))
   526  }
   527  
   528  func (s *uploadSuite) TestMockBuildTools(c *gc.C) {
   529  	checkTools := func(tools *sync.BuiltAgent, vers version.Binary) {
   530  		c.Check(tools.StorageName, gc.Equals, "name")
   531  		c.Check(tools.Version, jc.DeepEquals, vers)
   532  
   533  		f, err := os.Open(filepath.Join(tools.Dir, "name"))
   534  		c.Assert(err, jc.ErrorIsNil)
   535  		defer f.Close()
   536  
   537  		gzr, err := gzip.NewReader(f)
   538  		c.Assert(err, jc.ErrorIsNil)
   539  
   540  		_, tr, err := tar.FindFile(gzr, names.Jujud)
   541  		c.Assert(err, jc.ErrorIsNil)
   542  
   543  		content, err := io.ReadAll(tr)
   544  		c.Assert(err, jc.ErrorIsNil)
   545  		c.Check(string(content), gc.Equals, fmt.Sprintf("jujud contents %s", vers))
   546  	}
   547  
   548  	current := version.MustParseBinary("1.9.1-ubuntu-amd64")
   549  	s.PatchValue(&jujuversion.Current, current.Number)
   550  	s.PatchValue(&arch.HostArch, func() string { return current.Arch })
   551  	s.PatchValue(&coreos.HostOS, func() ostype.OSType { return ostype.Ubuntu })
   552  	buildToolsFunc := toolstesting.GetMockBuildTools(c)
   553  	builtTools, err := buildToolsFunc(true, "released",
   554  		func(version.Number) version.Number { return jujuversion.Current },
   555  	)
   556  	c.Assert(err, jc.ErrorIsNil)
   557  	checkTools(builtTools, current)
   558  
   559  	vers := version.MustParseBinary("1.5.3-ubuntu-amd64")
   560  	builtTools, err = buildToolsFunc(true, "released",
   561  		func(version.Number) version.Number { return vers.Number },
   562  	)
   563  	c.Assert(err, jc.ErrorIsNil)
   564  	checkTools(builtTools, vers)
   565  }
   566  
   567  func (s *uploadSuite) TestStorageToolsUploaderWriteMirrors(c *gc.C) {
   568  	s.testStorageToolsUploaderWriteMirrors(c, envtools.WriteMirrors)
   569  }
   570  
   571  func (s *uploadSuite) TestStorageToolsUploaderDontWriteMirrors(c *gc.C) {
   572  	s.testStorageToolsUploaderWriteMirrors(c, envtools.DoNotWriteMirrors)
   573  }
   574  
   575  func (s *uploadSuite) testStorageToolsUploaderWriteMirrors(c *gc.C, writeMirrors envtools.ShouldWriteMirrors) {
   576  	ctrl := gomock.NewController(c)
   577  	defer ctrl.Finish()
   578  
   579  	ss := NewMockSimplestreamsFetcher(ctrl)
   580  	ss.EXPECT().GetMetadata(gomock.Any(), gomock.Any()).AnyTimes()
   581  
   582  	storageDir := c.MkDir()
   583  	stor, err := filestorage.NewFileStorageWriter(storageDir)
   584  	c.Assert(err, jc.ErrorIsNil)
   585  
   586  	uploader := &sync.StorageToolsUploader{
   587  		Fetcher:       ss,
   588  		Storage:       stor,
   589  		WriteMetadata: true,
   590  		WriteMirrors:  writeMirrors,
   591  	}
   592  
   593  	err = uploader.UploadTools(
   594  		"released",
   595  		"released",
   596  		&coretools.Tools{
   597  			Version: coretesting.CurrentVersion(),
   598  			Size:    7,
   599  			SHA256:  "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
   600  		}, []byte("content"))
   601  	c.Assert(err, jc.ErrorIsNil)
   602  
   603  	mirrorsPath := simplestreams.MirrorsPath(envtools.StreamsVersionV1) + simplestreams.UnsignedSuffix
   604  	r, err := stor.Get(path.Join(storage.BaseToolsPath, mirrorsPath))
   605  	if writeMirrors == envtools.WriteMirrors {
   606  		c.Assert(err, jc.ErrorIsNil)
   607  		data, err := io.ReadAll(r)
   608  		r.Close()
   609  		c.Assert(err, jc.ErrorIsNil)
   610  		c.Assert(string(data), jc.Contains, `"mirrors":`)
   611  	} else {
   612  		c.Assert(err, jc.Satisfies, errors.IsNotFound)
   613  	}
   614  }
   615  
   616  type mockToolsFinder struct{}
   617  
   618  func (mockToolsFinder) FindTools(major int, stream string) (coretools.List, error) {
   619  	return nil, coretools.ErrNoMatches
   620  }