github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/uniter/charm/manifest_deployer_test.go (about)

     1  // Copyright 2012-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm_test
     5  
     6  import (
     7  	"fmt"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/juju/clock/testclock"
    12  	"github.com/juju/collections/set"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/loggo"
    15  	jc "github.com/juju/testing/checkers"
    16  	ft "github.com/juju/testing/filetesting"
    17  	"go.uber.org/mock/gomock"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/testing"
    21  	"github.com/juju/juju/worker/uniter/charm"
    22  	"github.com/juju/juju/worker/uniter/charm/mocks"
    23  )
    24  
    25  type ManifestDeployerSuite struct {
    26  	testing.BaseSuite
    27  	bundles    *bundleReader
    28  	targetPath string
    29  	deployer   charm.Deployer
    30  }
    31  
    32  var _ = gc.Suite(&ManifestDeployerSuite{})
    33  
    34  // because we generally use real charm bundles for testing, and charm bundling
    35  // sets every file mode to 0755 or 0644, all our input data uses those modes as
    36  // well.
    37  
    38  func (s *ManifestDeployerSuite) SetUpTest(c *gc.C) {
    39  	s.BaseSuite.SetUpTest(c)
    40  	s.bundles = &bundleReader{}
    41  	s.targetPath = filepath.Join(c.MkDir(), "target")
    42  	deployerPath := filepath.Join(c.MkDir(), "deployer")
    43  	s.deployer = charm.NewManifestDeployer(s.targetPath, deployerPath, s.bundles, loggo.GetLogger("test"))
    44  }
    45  
    46  func (s *ManifestDeployerSuite) addMockCharm(revision int, bundle charm.Bundle) charm.BundleInfo {
    47  	return s.bundles.AddBundle(charmURL(revision), bundle)
    48  }
    49  
    50  func (s *ManifestDeployerSuite) addCharm(c *gc.C, revision int, content ...ft.Entry) charm.BundleInfo {
    51  	return s.bundles.AddCustomBundle(c, charmURL(revision), func(path string) {
    52  		ft.Entries(content).Create(c, path)
    53  	})
    54  }
    55  
    56  func (s *ManifestDeployerSuite) deployCharm(c *gc.C, revision int, content ...ft.Entry) charm.BundleInfo {
    57  	info := s.addCharm(c, revision, content...)
    58  	err := s.deployer.Stage(info, nil)
    59  	c.Assert(err, jc.ErrorIsNil)
    60  	err = s.deployer.Deploy()
    61  	c.Assert(err, jc.ErrorIsNil)
    62  	s.assertCharm(c, revision, content...)
    63  	return info
    64  }
    65  
    66  func (s *ManifestDeployerSuite) assertCharm(c *gc.C, revision int, content ...ft.Entry) {
    67  	url, err := charm.ReadCharmURL(filepath.Join(s.targetPath, ".juju-charm"))
    68  	c.Assert(err, jc.ErrorIsNil)
    69  	c.Assert(url, gc.Equals, charmURL(revision).String())
    70  	ft.Entries(content).Check(c, s.targetPath)
    71  }
    72  
    73  func (s *ManifestDeployerSuite) TestAbortStageWhenClosed(c *gc.C) {
    74  	info := s.addMockCharm(1, mockBundle{})
    75  	abort := make(chan struct{})
    76  	errors := make(chan error)
    77  	s.bundles.EnableWaitForAbort()
    78  	go func() {
    79  		errors <- s.deployer.Stage(info, abort)
    80  	}()
    81  	close(abort)
    82  	err := <-errors
    83  	c.Assert(err, gc.ErrorMatches, "charm read aborted")
    84  }
    85  
    86  func (s *ManifestDeployerSuite) TestDontAbortStageWhenNotClosed(c *gc.C) {
    87  	info := s.addMockCharm(1, mockBundle{})
    88  	abort := make(chan struct{})
    89  	errors := make(chan error)
    90  	stopWaiting := s.bundles.EnableWaitForAbort()
    91  	go func() {
    92  		errors <- s.deployer.Stage(info, abort)
    93  	}()
    94  	close(stopWaiting)
    95  	err := <-errors
    96  	c.Assert(err, jc.ErrorIsNil)
    97  }
    98  
    99  func (s *ManifestDeployerSuite) TestDeployWithoutStage(c *gc.C) {
   100  	err := s.deployer.Deploy()
   101  	c.Assert(err, gc.ErrorMatches, "charm deployment failed: no charm set")
   102  }
   103  
   104  func (s *ManifestDeployerSuite) TestInstall(c *gc.C) {
   105  	s.deployCharm(c, 1,
   106  		ft.File{"some-file", "hello", 0644},
   107  		ft.Dir{"some-dir", 0755},
   108  		ft.Symlink{"some-dir/some-link", "../some-file"},
   109  	)
   110  }
   111  
   112  func (s *ManifestDeployerSuite) TestUpgradeOverwrite(c *gc.C) {
   113  	s.deployCharm(c, 1,
   114  		ft.File{"some-file", "hello", 0644},
   115  		ft.Dir{"some-dir", 0755},
   116  		ft.File{"some-dir/another-file", "to be removed", 0755},
   117  		ft.Dir{"another-dir", 0755},
   118  		ft.Symlink{"another-dir/some-link", "../some-file"},
   119  	)
   120  	// Replace each of file, dir, and symlink with a different entry; in
   121  	// the case of dir, checking that contained files are also removed.
   122  	s.deployCharm(c, 2,
   123  		ft.Symlink{"some-file", "no-longer-a-file"},
   124  		ft.File{"some-dir", "no-longer-a-dir", 0644},
   125  		ft.Dir{"another-dir", 0755},
   126  		ft.Dir{"another-dir/some-link", 0755},
   127  	)
   128  }
   129  
   130  func (s *ManifestDeployerSuite) TestUpgradePreserveUserFiles(c *gc.C) {
   131  	originalCharmContent := ft.Entries{
   132  		ft.File{"charm-file", "to-be-removed", 0644},
   133  		ft.Dir{"charm-dir", 0755},
   134  	}
   135  	s.deployCharm(c, 1, originalCharmContent...)
   136  
   137  	// Add user files we expect to keep to the target dir.
   138  	preserveUserContent := ft.Entries{
   139  		ft.File{"user-file", "to-be-preserved", 0644},
   140  		ft.Dir{"user-dir", 0755},
   141  		ft.File{"user-dir/user-file", "also-preserved", 0644},
   142  	}.Create(c, s.targetPath)
   143  
   144  	// Add some user files we expect to be removed.
   145  	removeUserContent := ft.Entries{
   146  		ft.File{"charm-dir/user-file", "whoops-removed", 0755},
   147  	}.Create(c, s.targetPath)
   148  
   149  	// Add some user files we expect to be replaced.
   150  	ft.Entries{
   151  		ft.File{"replace-file", "original", 0644},
   152  		ft.Dir{"replace-dir", 0755},
   153  		ft.Symlink{"replace-symlink", "replace-file"},
   154  	}.Create(c, s.targetPath)
   155  
   156  	// Deploy an upgrade; all new content overwrites the old...
   157  	s.deployCharm(c, 2,
   158  		ft.File{"replace-file", "updated", 0644},
   159  		ft.Dir{"replace-dir", 0755},
   160  		ft.Symlink{"replace-symlink", "replace-dir"},
   161  	)
   162  
   163  	// ...and other files are preserved or removed according to
   164  	// source and location.
   165  	preserveUserContent.Check(c, s.targetPath)
   166  	removeUserContent.AsRemoveds().Check(c, s.targetPath)
   167  	originalCharmContent.AsRemoveds().Check(c, s.targetPath)
   168  }
   169  
   170  func (s *ManifestDeployerSuite) TestUpgradeConflictResolveRetrySameCharm(c *gc.C) {
   171  	// Create base install.
   172  	s.deployCharm(c, 1,
   173  		ft.File{"shared-file", "old", 0755},
   174  		ft.File{"old-file", "old", 0644},
   175  	)
   176  
   177  	// Create mock upgrade charm that can (claim to) fail to expand...
   178  	failDeploy := true
   179  	upgradeContent := ft.Entries{
   180  		ft.File{"shared-file", "new", 0755},
   181  		ft.File{"new-file", "new", 0644},
   182  	}
   183  	mockCharm := mockBundle{
   184  		paths: set.NewStrings(upgradeContent.Paths()...),
   185  		expand: func(targetPath string) error {
   186  			upgradeContent.Create(c, targetPath)
   187  			if failDeploy {
   188  				return fmt.Errorf("oh noes")
   189  			}
   190  			return nil
   191  		},
   192  	}
   193  	info := s.addMockCharm(2, mockCharm)
   194  	err := s.deployer.Stage(info, nil)
   195  	c.Assert(err, jc.ErrorIsNil)
   196  
   197  	// ...and see it fail to expand. We're not too bothered about the actual
   198  	// content of the target dir at this stage, but we do want to check it's
   199  	// still marked as based on the original charm...
   200  	err = s.deployer.Deploy()
   201  	c.Assert(err, gc.Equals, charm.ErrConflict)
   202  	s.assertCharm(c, 1)
   203  
   204  	// ...and we want to verify that if we "fix the errors" and redeploy the
   205  	// same charm...
   206  	failDeploy = false
   207  	err = s.deployer.Deploy()
   208  	c.Assert(err, jc.ErrorIsNil)
   209  
   210  	// ...we end up with the right stuff in play.
   211  	s.assertCharm(c, 2, upgradeContent...)
   212  	ft.Removed{"old-file"}.Check(c, s.targetPath)
   213  }
   214  
   215  func (s *ManifestDeployerSuite) TestUpgradeConflictRevertRetryDifferentCharm(c *gc.C) {
   216  	// Create base install and add a user file.
   217  	s.deployCharm(c, 1,
   218  		ft.File{"shared-file", "old", 0755},
   219  		ft.File{"old-file", "old", 0644},
   220  	)
   221  	userFile := ft.File{"user-file", "user", 0644}.Create(c, s.targetPath)
   222  
   223  	// Create a charm upgrade that never works (but still writes a bunch of files),
   224  	// and deploy it.
   225  	badUpgradeContent := ft.Entries{
   226  		ft.File{"shared-file", "bad", 0644},
   227  		ft.File{"bad-file", "bad", 0644},
   228  	}
   229  	badCharm := mockBundle{
   230  		paths: set.NewStrings(badUpgradeContent.Paths()...),
   231  		expand: func(targetPath string) error {
   232  			badUpgradeContent.Create(c, targetPath)
   233  			return fmt.Errorf("oh noes")
   234  		},
   235  	}
   236  	badInfo := s.addMockCharm(2, badCharm)
   237  	err := s.deployer.Stage(badInfo, nil)
   238  	c.Assert(err, jc.ErrorIsNil)
   239  	err = s.deployer.Deploy()
   240  	c.Assert(err, gc.Equals, charm.ErrConflict)
   241  
   242  	// Create a charm upgrade that creates a bunch of different files, without
   243  	// error, and deploy it; check user files are preserved, and nothing from
   244  	// charm 1 or 2 is.
   245  	s.deployCharm(c, 3,
   246  		ft.File{"shared-file", "new", 0755},
   247  		ft.File{"new-file", "new", 0644},
   248  	)
   249  	userFile.Check(c, s.targetPath)
   250  	ft.Removed{"old-file"}.Check(c, s.targetPath)
   251  	ft.Removed{"bad-file"}.Check(c, s.targetPath)
   252  }
   253  
   254  var _ = gc.Suite(&RetryingBundleReaderSuite{})
   255  
   256  type RetryingBundleReaderSuite struct {
   257  	bundleReader *mocks.MockBundleReader
   258  	bundleInfo   *mocks.MockBundleInfo
   259  	bundle       *mocks.MockBundle
   260  	clock        *testclock.Clock
   261  	rbr          charm.RetryingBundleReader
   262  }
   263  
   264  func (s *RetryingBundleReaderSuite) TestReadBundleMaxAttemptsExceeded(c *gc.C) {
   265  	defer s.setupMocks(c).Finish()
   266  
   267  	s.bundleInfo.EXPECT().URL().Return("ch:focal/dummy-1").AnyTimes()
   268  	s.bundleReader.EXPECT().Read(gomock.Any(), gomock.Any()).Return(nil, errors.NotYetAvailablef("still in the oven")).AnyTimes()
   269  
   270  	go func() {
   271  		// We retry 10 times in total so we need to advance the clock 9
   272  		// times to exceed the max retry attempts (the first attempt
   273  		// does not use the clock).
   274  		for i := 0; i < 9; i++ {
   275  			c.Assert(s.clock.WaitAdvance(10*time.Second, time.Second, 1), jc.ErrorIsNil)
   276  		}
   277  	}()
   278  
   279  	_, err := s.rbr.Read(s.bundleInfo, nil)
   280  	c.Assert(errors.Is(err, errors.NotFound), jc.IsTrue)
   281  }
   282  
   283  func (s *RetryingBundleReaderSuite) TestReadBundleEventuallySucceeds(c *gc.C) {
   284  	defer s.setupMocks(c).Finish()
   285  
   286  	s.bundleInfo.EXPECT().URL().Return("ch:focal/dummy-1").AnyTimes()
   287  	gomock.InOrder(
   288  		s.bundleReader.EXPECT().Read(gomock.Any(), gomock.Any()).Return(nil, errors.NotYetAvailablef("still in the oven")),
   289  		s.bundleReader.EXPECT().Read(gomock.Any(), gomock.Any()).Return(s.bundle, nil),
   290  	)
   291  
   292  	go func() {
   293  		// The first attempt should fail; advance the clock to trigger
   294  		// another attempt which should succeed.
   295  		c.Assert(s.clock.WaitAdvance(10*time.Second, time.Second, 1), jc.ErrorIsNil)
   296  	}()
   297  
   298  	got, err := s.rbr.Read(s.bundleInfo, nil)
   299  	c.Assert(err, jc.ErrorIsNil)
   300  	c.Assert(got, gc.Equals, s.bundle)
   301  }
   302  
   303  func (s *RetryingBundleReaderSuite) setupMocks(c *gc.C) *gomock.Controller {
   304  	ctrl := gomock.NewController(c)
   305  	s.bundleReader = mocks.NewMockBundleReader(ctrl)
   306  	s.bundleInfo = mocks.NewMockBundleInfo(ctrl)
   307  	s.bundle = mocks.NewMockBundle(ctrl)
   308  	s.clock = testclock.NewClock(time.Now())
   309  	s.rbr = charm.RetryingBundleReader{
   310  		BundleReader: s.bundleReader,
   311  		Clock:        s.clock,
   312  		Logger:       loggo.GetLogger("test"),
   313  	}
   314  
   315  	return ctrl
   316  }