github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/bundlediff_test.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application_test 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "path/filepath" 10 "reflect" 11 "strings" 12 13 "github.com/juju/cmd" 14 "github.com/juju/cmd/cmdtesting" 15 "github.com/juju/errors" 16 jujutesting "github.com/juju/testing" 17 jc "github.com/juju/testing/checkers" 18 gc "gopkg.in/check.v1" 19 "gopkg.in/juju/charm.v6" 20 csparams "gopkg.in/juju/charmrepo.v3/csclient/params" 21 22 "github.com/juju/juju/api/base" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/cmd/juju/application" 25 "github.com/juju/juju/core/constraints" 26 "github.com/juju/juju/core/model" 27 "github.com/juju/juju/jujuclient" 28 "github.com/juju/juju/jujuclient/jujuclienttesting" 29 "github.com/juju/juju/testing" 30 ) 31 32 type diffSuite struct { 33 jujutesting.IsolationSuite 34 apiRoot *mockAPIRoot 35 charmStore *mockCharmStore 36 dir string 37 } 38 39 var _ = gc.Suite(&diffSuite{}) 40 41 func (s *diffSuite) SetUpTest(c *gc.C) { 42 s.IsolationSuite.SetUpTest(c) 43 s.apiRoot = &mockAPIRoot{responses: makeAPIResponses()} 44 s.charmStore = &mockCharmStore{} 45 s.dir = c.MkDir() 46 } 47 48 func (s *diffSuite) runDiffBundle(c *gc.C, args ...string) (*cmd.Context, error) { 49 store := jujuclienttesting.MinimalStore() 50 store.Models["enz"] = &jujuclient.ControllerModels{ 51 CurrentModel: "golden/horse", 52 Models: map[string]jujuclient.ModelDetails{"golden/horse": { 53 ModelType: model.IAAS, 54 }}, 55 } 56 command := application.NewBundleDiffCommandForTest(s.apiRoot, s.charmStore, store) 57 return cmdtesting.RunCommandInDir(c, command, args, s.dir) 58 } 59 60 func (s *diffSuite) TestNoArgs(c *gc.C) { 61 _, err := s.runDiffBundle(c) 62 c.Assert(err, gc.ErrorMatches, "no bundle specified") 63 } 64 65 func (s *diffSuite) TestTooManyArgs(c *gc.C) { 66 _, err := s.runDiffBundle(c, "bundle", "somethingelse") 67 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["somethingelse"\]`) 68 } 69 70 func (s *diffSuite) TestVerifiesBundle(c *gc.C) { 71 _, err := s.runDiffBundle(c, s.writeLocalBundle(c, invalidBundle)) 72 c.Assert(err, gc.ErrorMatches, "(?s)the provided bundle has the following errors:.*") 73 } 74 75 func (s *diffSuite) TestNotABundle(c *gc.C) { 76 s.charmStore.url = &charm.URL{ 77 Schema: "cs", 78 Name: "prometheus", 79 Revision: 23, 80 Series: "xenial", 81 } 82 s.apiRoot.responses["ModelConfig.ModelGet"] = params.ModelConfigResults{ 83 Config: map[string]params.ConfigValue{ 84 "uuid": {Value: testing.ModelTag.Id()}, 85 "type": {Value: "iaas"}, 86 "name": {Value: "horse"}, 87 "default-series": {Value: "xenial"}, 88 }, 89 } 90 _, err := s.runDiffBundle(c, "prometheus") 91 c.Logf(errors.ErrorStack(err)) 92 // Fails because the series that comes back from the charm store 93 // is xenial rather than "bundle" (and there's no local bundle). 94 c.Assert(err, gc.ErrorMatches, `couldn't interpret "prometheus" as a local or charmstore bundle`) 95 } 96 97 func (s *diffSuite) TestLocalBundle(c *gc.C) { 98 ctx, err := s.runDiffBundle(c, s.writeLocalBundle(c, testBundle)) 99 c.Assert(err, jc.ErrorIsNil) 100 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, ` 101 applications: 102 grafana: 103 missing: bundle 104 prometheus: 105 options: 106 ontology: 107 bundle: anselm 108 model: kant 109 constraints: 110 bundle: cores=4 111 model: cores=3 112 machines: 113 "1": 114 missing: bundle 115 `[1:]) 116 } 117 118 func (s *diffSuite) TestIncludeAnnotations(c *gc.C) { 119 ctx, err := s.runDiffBundle(c, "--annotations", s.writeLocalBundle(c, testBundle)) 120 c.Assert(err, jc.ErrorIsNil) 121 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, ` 122 applications: 123 grafana: 124 missing: bundle 125 prometheus: 126 options: 127 ontology: 128 bundle: anselm 129 model: kant 130 annotations: 131 aspect: 132 bundle: west 133 model: north 134 constraints: 135 bundle: cores=4 136 model: cores=3 137 machines: 138 "1": 139 missing: bundle 140 `[1:]) 141 } 142 143 func (s *diffSuite) TestHandlesIncludes(c *gc.C) { 144 s.writeFile(c, "include.yaml", "hume") 145 ctx, err := s.runDiffBundle(c, s.writeLocalBundle(c, withInclude)) 146 c.Assert(err, jc.ErrorIsNil) 147 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, ` 148 applications: 149 grafana: 150 missing: bundle 151 prometheus: 152 options: 153 ontology: 154 bundle: hume 155 model: kant 156 constraints: 157 bundle: cores=4 158 model: cores=3 159 machines: 160 "1": 161 missing: bundle 162 `[1:]) 163 } 164 165 func (s *diffSuite) TestHandlesOverlays(c *gc.C) { 166 path1 := s.writeFile(c, "overlay1.yaml", overlay1) 167 path2 := s.writeFile(c, "overlay2.yaml", overlay2) 168 ctx, err := s.runDiffBundle(c, 169 "--overlay", path1, 170 "--overlay", path2, 171 s.writeLocalBundle(c, testBundle)) 172 c.Assert(err, jc.ErrorIsNil) 173 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, ` 174 applications: 175 grafana: 176 missing: bundle 177 prometheus: 178 options: 179 admin-user: 180 bundle: lovecraft 181 model: null 182 ontology: 183 bundle: anselm 184 model: kant 185 constraints: 186 bundle: cores=4 187 model: cores=3 188 telegraf: 189 missing: model 190 machines: 191 "1": 192 missing: bundle 193 relations: 194 bundle-additions: 195 - - prometheus:juju-info 196 - telegraf:info 197 `[1:]) 198 } 199 200 func (s *diffSuite) TestCharmStoreBundle(c *gc.C) { 201 bundleData, err := charm.ReadBundleData(strings.NewReader(testBundle)) 202 c.Assert(err, jc.ErrorIsNil) 203 s.charmStore.url = &charm.URL{ 204 Schema: "cs", 205 Name: "my-bundle", 206 Series: "bundle", 207 } 208 s.charmStore.bundle = &mockBundle{data: bundleData} 209 210 ctx, err := s.runDiffBundle(c, "my-bundle") 211 c.Assert(err, jc.ErrorIsNil) 212 213 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, ` 214 applications: 215 grafana: 216 missing: bundle 217 prometheus: 218 options: 219 ontology: 220 bundle: anselm 221 model: kant 222 constraints: 223 bundle: cores=4 224 model: cores=3 225 machines: 226 "1": 227 missing: bundle 228 `[1:]) 229 } 230 231 func (s *diffSuite) TestBundleNotFound(c *gc.C) { 232 s.charmStore.stub.SetErrors(errors.NotFoundf(`cannot resolve URL "cs:my-bundle": charm or bundle`)) 233 _, err := s.runDiffBundle(c, "cs:my-bundle") 234 c.Assert(err, gc.ErrorMatches, `cannot resolve URL "cs:my-bundle": charm or bundle not found`) 235 } 236 237 func (s *diffSuite) TestMachineMap(c *gc.C) { 238 ctx, err := s.runDiffBundle(c, 239 "--map-machines", "0=1", 240 s.writeLocalBundle(c, testBundle)) 241 c.Assert(err, jc.ErrorIsNil) 242 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, ` 243 applications: 244 grafana: 245 missing: bundle 246 prometheus: 247 options: 248 ontology: 249 bundle: anselm 250 model: kant 251 constraints: 252 bundle: cores=4 253 model: cores=3 254 machines: 255 "0": 256 missing: bundle 257 "1": 258 series: 259 bundle: xenial 260 model: bionic 261 `[1:]) 262 } 263 264 func (s *diffSuite) writeLocalBundle(c *gc.C, content string) string { 265 return s.writeFile(c, "bundle.yaml", content) 266 } 267 268 func (s *diffSuite) writeFile(c *gc.C, name, content string) string { 269 path := filepath.Join(s.dir, name) 270 err := ioutil.WriteFile(path, []byte(content), 0666) 271 c.Assert(err, jc.ErrorIsNil) 272 return path 273 } 274 275 func makeAPIResponses() map[string]interface{} { 276 var cores uint64 = 3 277 return map[string]interface{}{ 278 "ModelConfig.ModelGet": params.ModelConfigResults{ 279 Config: map[string]params.ConfigValue{ 280 "uuid": {Value: testing.ModelTag.Id()}, 281 "type": {Value: "iaas"}, 282 "name": {Value: "horse"}, 283 "default-series": {Value: "xenial"}, 284 }, 285 }, 286 "Client.FullStatus": params.FullStatus{ 287 Applications: map[string]params.ApplicationStatus{ 288 "prometheus": { 289 Charm: "cs:prometheus2-7", 290 Series: "xenial", 291 Life: "alive", 292 Units: map[string]params.UnitStatus{ 293 "prometheus/0": {Machine: "0"}, 294 }, 295 }, 296 "grafana": { 297 Charm: "cs:grafana-19", 298 Series: "bionic", 299 Life: "alive", 300 Units: map[string]params.UnitStatus{ 301 "grafana/0": {Machine: "1"}, 302 }, 303 }, 304 }, 305 Machines: map[string]params.MachineStatus{ 306 "0": {Series: "xenial"}, 307 "1": {Series: "bionic"}, 308 }, 309 }, 310 "Annotations.Get": params.AnnotationsGetResults{ 311 Results: []params.AnnotationsGetResult{{ 312 EntityTag: "application-prometheus", 313 Annotations: map[string]string{ 314 "aspect": "north", 315 }, 316 }}, 317 }, 318 "ModelConfig.Sequences": params.ModelSequencesResult{}, 319 "Application.CharmConfig": params.ApplicationGetConfigResults{ 320 // Included twice since we can't predict which app will be 321 // requested first. 322 Results: []params.ConfigResult{{ 323 Config: map[string]interface{}{"ontology": map[string]interface{}{ 324 "value": "kant", 325 "source": "user", 326 }}, 327 }, { 328 Config: map[string]interface{}{"ontology": map[string]interface{}{ 329 "value": "kant", 330 "source": "user", 331 }}, 332 }}, 333 }, 334 "Application.GetConstraints": params.ApplicationGetConstraintsResults{ 335 Results: []params.ApplicationConstraint{{ 336 Constraints: constraints.Value{CpuCores: &cores}, 337 }, { 338 Constraints: constraints.Value{CpuCores: &cores}, 339 }}, 340 }, 341 } 342 } 343 344 type mockCharmStore struct { 345 stub jujutesting.Stub 346 url *charm.URL 347 channel csparams.Channel 348 series []string 349 bundle *mockBundle 350 } 351 352 func (s *mockCharmStore) ResolveWithChannel(url *charm.URL) (*charm.URL, csparams.Channel, []string, error) { 353 s.stub.AddCall("ResolveWithChannel", url) 354 return s.url, s.channel, s.series, s.stub.NextErr() 355 } 356 357 func (s *mockCharmStore) GetBundle(url *charm.URL) (charm.Bundle, error) { 358 s.stub.AddCall("GetBundle", url) 359 return s.bundle, s.stub.NextErr() 360 } 361 362 type mockBundle struct { 363 data *charm.BundleData 364 } 365 366 func (b *mockBundle) Data() *charm.BundleData { return b.data } 367 func (b *mockBundle) ReadMe() string { return "" } 368 369 type mockAPIRoot struct { 370 base.APICallCloser 371 372 stub jujutesting.Stub 373 responses map[string]interface{} 374 } 375 376 func (r *mockAPIRoot) BestFacadeVersion(name string) int { 377 r.stub.AddCall("BestFacadeVersion", name) 378 return 42 379 } 380 381 func (r *mockAPIRoot) APICall(objType string, version int, id, request string, params, response interface{}) error { 382 call := objType + "." + request 383 r.stub.AddCall(call, version, params) 384 value := r.responses[call] 385 rv := reflect.ValueOf(response) 386 if value == nil { 387 panic(fmt.Sprintf("nil response for %s call", call)) 388 } 389 if reflect.TypeOf(value).AssignableTo(rv.Type().Elem()) { 390 rv.Elem().Set(reflect.ValueOf(value)) 391 } else { 392 panic(fmt.Sprintf("%s: can't assign value %v to %T", call, value, response)) 393 } 394 return r.stub.NextErr() 395 } 396 397 func (r *mockAPIRoot) Close() error { 398 r.stub.AddCall("Close") 399 return r.stub.NextErr() 400 } 401 402 const ( 403 testBundle = ` 404 applications: 405 prometheus: 406 charm: 'cs:prometheus2-7' 407 num_units: 1 408 series: xenial 409 options: 410 ontology: anselm 411 annotations: 412 aspect: west 413 constraints: 'cores=4' 414 to: 415 - 0 416 machines: 417 '0': 418 series: xenial 419 ` 420 withInclude = ` 421 applications: 422 prometheus: 423 charm: 'cs:prometheus2-7' 424 num_units: 1 425 series: xenial 426 options: 427 ontology: include-file://include.yaml 428 annotations: 429 aspect: west 430 constraints: 'cores=4' 431 to: 432 - 0 433 machines: 434 '0': 435 series: xenial 436 ` 437 invalidBundle = ` 438 machines: 439 0: 440 ` 441 overlay1 = ` 442 applications: 443 prometheus: 444 options: 445 admin-user: lovecraft 446 ` 447 448 overlay2 = ` 449 applications: 450 telegraf: 451 charm: 'cs:telegraf-3' 452 relations: 453 - - telegraf:info 454 - prometheus:juju-info 455 ` 456 )