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 }