github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/bundle/bundle.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package bundle 5 6 import ( 7 "bytes" 8 "fmt" 9 "sort" 10 "strconv" 11 "strings" 12 13 "github.com/juju/charm/v12" 14 "github.com/juju/charm/v12/resource" 15 "github.com/juju/collections/set" 16 "github.com/juju/description/v5" 17 "github.com/juju/errors" 18 "github.com/juju/loggo" 19 "github.com/juju/names/v5" 20 "gopkg.in/yaml.v2" 21 22 "github.com/juju/juju/apiserver/common" 23 apiservererrors "github.com/juju/juju/apiserver/errors" 24 "github.com/juju/juju/apiserver/facade" 25 appFacade "github.com/juju/juju/apiserver/facades/client/application" 26 corebase "github.com/juju/juju/core/base" 27 bundlechanges "github.com/juju/juju/core/bundle/changes" 28 corecharm "github.com/juju/juju/core/charm" 29 "github.com/juju/juju/core/constraints" 30 "github.com/juju/juju/core/devices" 31 "github.com/juju/juju/core/network" 32 "github.com/juju/juju/core/network/firewall" 33 "github.com/juju/juju/core/permission" 34 "github.com/juju/juju/environs/config" 35 "github.com/juju/juju/rpc/params" 36 "github.com/juju/juju/state" 37 "github.com/juju/juju/storage" 38 "github.com/juju/juju/version" 39 ) 40 41 // APIv6 provides the Bundle API facade for version 6. It is otherwise 42 // identical to V5 with the exception that the V6 adds the support for 43 // multi-part yaml handling to GetChanges and GetChangesMapArgs. 44 type APIv6 struct { 45 *BundleAPI 46 } 47 48 // BundleAPI implements the Bundle interface and is the concrete implementation 49 // of the API end point. 50 type BundleAPI struct { 51 backend Backend 52 authorizer facade.Authorizer 53 modelTag names.ModelTag 54 } 55 56 // NewFacade provides the required signature for facade registration. 57 func newFacade(ctx facade.Context) (*BundleAPI, error) { 58 authorizer := ctx.Auth() 59 st := ctx.State() 60 61 return NewBundleAPI( 62 NewStateShim(st), 63 authorizer, 64 names.NewModelTag(st.ModelUUID()), 65 ) 66 } 67 68 // NewBundleAPI returns the new Bundle API facade. 69 func NewBundleAPI( 70 st Backend, 71 auth facade.Authorizer, 72 tag names.ModelTag, 73 ) (*BundleAPI, error) { 74 if !auth.AuthClient() { 75 return nil, apiservererrors.ErrPerm 76 } 77 78 return &BundleAPI{ 79 backend: st, 80 authorizer: auth, 81 modelTag: tag, 82 }, nil 83 } 84 85 func (b *BundleAPI) checkCanRead() error { 86 return b.authorizer.HasPermission(permission.ReadAccess, b.modelTag) 87 } 88 89 type validators struct { 90 verifyConstraints func(string) error 91 verifyStorage func(string) error 92 verifyDevices func(string) error 93 } 94 95 // GetChanges returns the list of changes required to deploy the given bundle 96 // data. The changes are sorted by requirements, so that they can be applied in 97 // order. 98 // GetChanges has been superseded in favour of GetChangesMapArgs. It's 99 // preferable to use that new method to add new functionality and move clients 100 // away from this one. 101 func (b *BundleAPI) GetChanges(args params.BundleChangesParams) (params.BundleChangesResults, error) { 102 vs := validators{ 103 verifyConstraints: func(s string) error { 104 _, err := constraints.Parse(s) 105 return err 106 }, 107 verifyStorage: func(s string) error { 108 _, err := storage.ParseConstraints(s) 109 return err 110 }, 111 verifyDevices: func(s string) error { 112 _, err := devices.ParseConstraints(s) 113 return err 114 }, 115 } 116 117 var results params.BundleChangesResults 118 changes, validationErrors, err := b.doGetBundleChanges(args, vs) 119 if err != nil { 120 return results, errors.Trace(err) 121 } 122 if len(validationErrors) > 0 { 123 results.Errors = make([]string, len(validationErrors)) 124 for k, v := range validationErrors { 125 results.Errors[k] = v.Error() 126 } 127 return results, nil 128 } 129 err = mapBundleChanges(changes, &results) 130 return results, errors.Trace(err) 131 132 } 133 134 func mapBundleChanges(changes []bundlechanges.Change, results *params.BundleChangesResults) error { 135 results.Changes = make([]*params.BundleChange, len(changes)) 136 for i, c := range changes { 137 var guiArgs []interface{} 138 switch c := c.(type) { 139 case *bundlechanges.AddApplicationChange: 140 guiArgs = c.GUIArgsWithDevices() 141 default: 142 guiArgs = c.GUIArgs() 143 } 144 results.Changes[i] = ¶ms.BundleChange{ 145 Id: c.Id(), 146 Method: c.Method(), 147 Args: guiArgs, 148 Requires: c.Requires(), 149 } 150 } 151 return nil 152 } 153 154 func (b *BundleAPI) doGetBundleChanges( 155 args params.BundleChangesParams, 156 vs validators, 157 ) ([]bundlechanges.Change, []error, error) { 158 dataSource, _ := charm.StreamBundleDataSource(strings.NewReader(args.BundleDataYAML), args.BundleURL) 159 data, err := charm.ReadAndMergeBundleData(dataSource) 160 if err != nil { 161 return nil, nil, errors.Annotate(err, "cannot read bundle YAML") 162 } 163 if err := data.Verify(vs.verifyConstraints, vs.verifyStorage, vs.verifyDevices); err != nil { 164 if verificationError, ok := err.(*charm.VerificationError); ok { 165 validationErrors := make([]error, len(verificationError.Errors)) 166 for i, e := range verificationError.Errors { 167 validationErrors[i] = e 168 } 169 return nil, validationErrors, nil 170 } 171 // This should never happen as Verify only returns verification errors. 172 return nil, nil, errors.Annotate(err, "cannot verify bundle") 173 } 174 changes, err := bundlechanges.FromData( 175 bundlechanges.ChangesConfig{ 176 Bundle: data, 177 BundleURL: args.BundleURL, 178 Logger: loggo.GetLogger("juju.apiserver.bundlechanges"), 179 }) 180 if err != nil { 181 return nil, nil, errors.Trace(err) 182 } 183 return changes, nil, nil 184 } 185 186 // GetChangesMapArgs returns the list of changes required to deploy the given 187 // bundle data. The changes are sorted by requirements, so that they can be 188 // applied in order. 189 // V4 GetChangesMapArgs is not supported on anything less than v4 190 func (b *BundleAPI) GetChangesMapArgs(args params.BundleChangesParams) (params.BundleChangesMapArgsResults, error) { 191 vs := validators{ 192 verifyConstraints: func(s string) error { 193 _, err := constraints.Parse(s) 194 return err 195 }, 196 verifyStorage: func(s string) error { 197 _, err := storage.ParseConstraints(s) 198 return err 199 }, 200 verifyDevices: func(s string) error { 201 _, err := devices.ParseConstraints(s) 202 return err 203 }, 204 } 205 return b.doGetBundleChangesMapArgs(args, vs, func(changes []bundlechanges.Change, results *params.BundleChangesMapArgsResults) error { 206 results.Changes = make([]*params.BundleChangesMapArgs, len(changes)) 207 results.Errors = make([]string, len(changes)) 208 for i, c := range changes { 209 args, err := c.Args() 210 if err != nil { 211 results.Errors[i] = err.Error() 212 continue 213 } 214 results.Changes[i] = ¶ms.BundleChangesMapArgs{ 215 Id: c.Id(), 216 Method: c.Method(), 217 Args: args, 218 Requires: c.Requires(), 219 } 220 } 221 return nil 222 }) 223 } 224 225 func (b *BundleAPI) doGetBundleChangesMapArgs( 226 args params.BundleChangesParams, 227 vs validators, 228 postProcess func([]bundlechanges.Change, *params.BundleChangesMapArgsResults) error, 229 ) (params.BundleChangesMapArgsResults, error) { 230 var results params.BundleChangesMapArgsResults 231 changes, validationErrors, err := b.doGetBundleChanges(args, vs) 232 if err != nil { 233 return results, errors.Trace(err) 234 } 235 if len(validationErrors) > 0 { 236 results.Errors = make([]string, len(validationErrors)) 237 for k, v := range validationErrors { 238 results.Errors[k] = v.Error() 239 } 240 return results, nil 241 } 242 err = postProcess(changes, &results) 243 return results, err 244 } 245 246 // ExportBundle exports the current model configuration as bundle. 247 func (b *BundleAPI) ExportBundle(arg params.ExportBundleParams) (params.StringResult, error) { 248 fail := func(failErr error) (params.StringResult, error) { 249 return params.StringResult{}, apiservererrors.ServerError(failErr) 250 } 251 252 if err := b.checkCanRead(); err != nil { 253 return fail(err) 254 } 255 256 exportConfig := b.backend.GetExportConfig() 257 model, err := b.backend.ExportPartial(exportConfig) 258 if err != nil { 259 return fail(err) 260 } 261 262 // Fill it in charm.BundleData data structure. 263 bundleData, err := b.fillBundleData(model, arg.IncludeCharmDefaults, arg.IncludeSeries, b.backend) 264 if err != nil { 265 return fail(err) 266 } 267 268 // Split the bundle into a base and overlay bundle and encode as a 269 // yaml multi-doc. 270 base, overlay, err := charm.ExtractBaseAndOverlayParts(bundleData) 271 if err != nil { 272 return fail(err) 273 } 274 275 // First create a bundle output from the bundle data. 276 var buf bytes.Buffer 277 enc := yaml.NewEncoder(&buf) 278 if err != nil { 279 return fail(err) 280 } 281 if err = enc.Encode(bundleOutputFromBundleData(base)); err != nil { 282 return fail(err) 283 } 284 285 // Secondly create an output from the overlay. We do it this way, so we can 286 // insert the correct comments for users. 287 output := buf.String() 288 buf.Reset() 289 if err = enc.Encode(overlay); err != nil { 290 return fail(err) 291 } else if err = enc.Close(); err != nil { 292 return fail(err) 293 } 294 overlayOutput := buf.String() 295 296 // If the overlay part is empty, ignore it; otherwise, inject a 297 // comment to let users know that the second document can be extracted 298 // out and used as a standalone overlay. 299 if !strings.HasPrefix(overlayOutput, "--- {}\n") { 300 // strip off the first three dashes and merge the base bundle and the 301 // overlay. 302 if strings.HasPrefix(overlayOutput, "---") { 303 overlayOutput = strings.Replace(overlayOutput, "---", "--- # overlay.yaml", 1) 304 output += overlayOutput 305 } else { 306 return fail(errors.Errorf("expected yaml encoder to delineate multiple documents with \"---\" separator")) 307 } 308 } 309 310 return params.StringResult{Result: output}, nil 311 } 312 313 // bundleOutput has the same top level keys as the charm.BundleData 314 // but in a more user oriented output order, with the description first, 315 // then the distro base/series, then the apps, machines and releations. 316 type bundleOutput struct { 317 Type string `yaml:"bundle,omitempty"` 318 Description string `yaml:"description,omitempty"` 319 DefaultBase string `yaml:"default-base,omitempty"` 320 Series string `yaml:"series,omitempty"` 321 Saas map[string]*charm.SaasSpec `yaml:"saas,omitempty"` 322 Applications map[string]*charm.ApplicationSpec `yaml:"applications,omitempty"` 323 Machines map[string]*charm.MachineSpec `yaml:"machines,omitempty"` 324 Relations [][]string `yaml:"relations,omitempty"` 325 } 326 327 func bundleOutputFromBundleData(bd *charm.BundleData) *bundleOutput { 328 return &bundleOutput{ 329 Type: bd.Type, 330 Description: bd.Description, 331 DefaultBase: bd.DefaultBase, 332 Series: bd.Series, 333 Saas: bd.Saas, 334 Applications: bd.Applications, 335 Machines: bd.Machines, 336 Relations: bd.Relations, 337 } 338 } 339 340 func (b *BundleAPI) fillBundleData(model description.Model, includeCharmDefaults, includeSeries bool, backend Backend) (*charm.BundleData, error) { 341 cfg, err := config.New(config.NoDefaults, model.Config()) 342 if err != nil { 343 return nil, errors.Trace(err) 344 } 345 var defaultBase corebase.Base 346 value, ok := cfg.DefaultBase() 347 if ok { 348 var err error 349 defaultBase, err = corebase.ParseBaseFromString(value) 350 if err != nil { 351 return nil, err 352 } 353 } else { 354 defaultBase = version.DefaultSupportedLTSBase() 355 } 356 357 data := &charm.BundleData{} 358 359 isCAAS := model.Type() == description.CAAS 360 if isCAAS { 361 data.Type = "kubernetes" 362 } else { 363 data.DefaultBase = defaultBase.String() 364 if includeSeries { 365 defaultSeries, err := corebase.GetSeriesFromBase(defaultBase) 366 if err != nil { 367 return nil, err 368 } 369 data.Series = defaultSeries 370 } 371 } 372 373 if len(model.Applications()) == 0 { 374 return nil, errors.Errorf("nothing to export as there are no applications") 375 } 376 377 // Application bundle data. 378 var ( 379 usedBases set.Strings 380 machineIds set.Strings 381 ) 382 data.Applications, machineIds, usedBases, err = b.bundleDataApplications(model.Applications(), defaultBase, isCAAS, includeCharmDefaults, includeSeries, backend) 383 if err != nil { 384 return nil, err 385 } 386 387 // Machine bundle data. 388 var machineBases set.Strings 389 data.Machines, machineBases, err = b.bundleDataMachines(model.Machines(), machineIds, defaultBase, includeSeries) 390 if err != nil { 391 return nil, err 392 } 393 usedBases = usedBases.Union(machineBases) 394 395 // Remote Application bundle data. 396 data.Saas = bundleDataRemoteApplications(model.RemoteApplications()) 397 398 // Relation bundle data. 399 data.Relations = bundleDataRelations(model.Relations()) 400 401 // If there is only one base used, make it the default and remove 402 // base from all the apps and machines. 403 size := usedBases.Size() 404 switch { 405 case size == 1: 406 used, err := corebase.ParseBaseFromString(usedBases.Values()[0]) 407 if err != nil { 408 return nil, errors.Trace(err) 409 } 410 if used != defaultBase { 411 data.DefaultBase = used.String() 412 if includeSeries { 413 series, err := corebase.GetSeriesFromBase(used) 414 if err != nil { 415 return nil, errors.Trace(err) 416 } 417 data.Series = series 418 } 419 for _, app := range data.Applications { 420 app.Base = "" 421 app.Series = "" 422 } 423 for _, mac := range data.Machines { 424 mac.Base = "" 425 mac.Series = "" 426 } 427 } 428 case size > 1: 429 if !usedBases.Contains(defaultBase.String()) { 430 data.DefaultBase = "" 431 data.Series = "" 432 } 433 } 434 435 if isCAAS { 436 // Kubernetes bundles don't specify series right now. 437 data.DefaultBase = "" 438 data.Series = "" 439 } 440 441 return data, nil 442 } 443 444 func (b *BundleAPI) bundleDataApplications( 445 apps []description.Application, 446 defaultBase corebase.Base, 447 isCAAS, includeCharmDefaults, includeSeries bool, 448 backend Backend, 449 ) (map[string]*charm.ApplicationSpec, set.Strings, set.Strings, error) { 450 451 allSpacesInfoLookup, err := b.backend.AllSpaceInfos() 452 if err != nil { 453 return nil, nil, nil, errors.Annotate(err, "unable to retrieve all space information") 454 } 455 456 applicationData := make(map[string]*charm.ApplicationSpec) 457 machineIds := set.NewStrings() 458 usedBases := set.NewStrings() 459 460 charmConfigCache := make(map[string]*charm.Config) 461 printEndpointBindingSpaceNames := b.printSpaceNamesInEndpointBindings(apps) 462 463 for _, application := range apps { 464 if application.CharmOrigin() == nil || application.CharmOrigin().Platform() == "" { 465 return nil, nil, nil, errors.Errorf("missing charm origin data for %q", application) 466 } 467 var newApplication *charm.ApplicationSpec 468 p, err := corecharm.ParsePlatformNormalize(application.CharmOrigin().Platform()) 469 if err != nil { 470 return nil, nil, nil, fmt.Errorf("extracting charm origin from application description %w", err) 471 } 472 473 appBase, err := corebase.ParseBase(p.OS, p.Channel) 474 if err != nil { 475 return nil, nil, nil, fmt.Errorf("extracting base from application description %w", err) 476 } 477 usedBases.Add(appBase.String()) 478 479 var appSeries string 480 if includeSeries { 481 appSeries, err = corebase.GetSeriesFromBase(appBase) 482 if err != nil { 483 return nil, nil, nil, errors.Trace(err) 484 } 485 } 486 487 endpointsWithSpaceNames, err := b.endpointBindings(application.EndpointBindings(), allSpacesInfoLookup, printEndpointBindingSpaceNames) 488 if err != nil { 489 return nil, nil, nil, errors.Trace(err) 490 } 491 492 // For security purposes we do not allow both the expose flag 493 // and the exposed endpoints fields to be populated in exported 494 // bundles. Otherwise, exporting a bundle from a 2.9 controller 495 // (with per-endpoint expose settings) and deploying it to a 496 // 2.8 controller would result in all application ports to be 497 // made accessible from 0.0.0.0/0. 498 exposedEndpoints, err := mapExposedEndpoints(application.ExposedEndpoints(), allSpacesInfoLookup) 499 if err != nil { 500 return nil, nil, nil, errors.Trace(err) 501 } 502 exposedFlag := application.Exposed() && len(exposedEndpoints) == 0 503 504 // We need to correctly handle charmhub urls. The internal 505 // representation of a charm url is not the same as a external 506 // representation, in that only the application name should be rendered. 507 // For charmhub charms, ensure that the revision is listed separately, 508 // not in the charm url. 509 curl, err := charm.ParseURL(application.CharmURL()) 510 if err != nil { 511 return nil, nil, nil, errors.Trace(err) 512 } 513 var charmURL string 514 var revision *int 515 switch { 516 case charm.CharmHub.Matches(curl.Schema): 517 charmURL = curl.Name 518 if curl.Revision >= 0 { 519 cRev := curl.Revision 520 revision = &cRev 521 } 522 case charm.Local.Matches(curl.Schema): 523 charmURL = fmt.Sprintf("local:%s", curl.Name) 524 if curl.Revision >= 0 { 525 charmURL = fmt.Sprintf("%s-%d", charmURL, curl.Revision) 526 } 527 default: 528 return nil, nil, nil, errors.NotValidf("charm schema %q", curl.Schema) 529 } 530 531 var channel string 532 if origin := application.CharmOrigin(); origin != nil { 533 channel = origin.Channel() 534 } 535 if channel == "" { 536 channel = application.Channel() 537 } 538 539 charmCfg := application.CharmConfig() 540 if includeCharmDefaults { 541 // Augment the user specified config with defaults 542 // from the charm config metadata. 543 cfgInfo, ok := charmConfigCache[charmURL] 544 if !ok { 545 ch, err := backend.Charm(curl.String()) 546 if err != nil { 547 return nil, nil, nil, errors.Trace(err) 548 } 549 cfgInfo = ch.Config() 550 charmConfigCache[charmURL] = cfgInfo 551 } 552 for name, opt := range cfgInfo.Options { 553 if _, ok := charmCfg[name]; ok { 554 continue 555 } 556 charmCfg[name] = opt.Default 557 } 558 } 559 if application.Subordinate() { 560 newApplication = &charm.ApplicationSpec{ 561 Charm: charmURL, 562 Revision: revision, 563 Channel: channel, 564 Expose: exposedFlag, 565 ExposedEndpoints: exposedEndpoints, 566 Options: charmCfg, 567 Annotations: application.Annotations(), 568 EndpointBindings: endpointsWithSpaceNames, 569 } 570 } else { 571 var ( 572 numUnits int 573 scale int 574 placement string 575 ut []string 576 ) 577 if isCAAS { 578 placement = application.Placement() 579 scale = len(application.Units()) 580 } else { 581 numUnits = len(application.Units()) 582 for _, unit := range application.Units() { 583 machineID := unit.Machine().Id() 584 unitMachine := unit.Machine() 585 if names.IsContainerMachine(machineID) { 586 machineIds.Add(unitMachine.Parent().Id()) 587 id := unitMachine.ContainerType() + ":" + unitMachine.Parent().Id() 588 ut = append(ut, id) 589 } else { 590 machineIds.Add(unitMachine.Id()) 591 ut = append(ut, unitMachine.Id()) 592 } 593 } 594 } 595 596 newApplication = &charm.ApplicationSpec{ 597 Charm: charmURL, 598 Revision: revision, 599 Channel: channel, 600 NumUnits: numUnits, 601 Scale_: scale, 602 Placement_: placement, 603 To: ut, 604 Expose: exposedFlag, 605 ExposedEndpoints: exposedEndpoints, 606 Options: charmCfg, 607 Annotations: application.Annotations(), 608 EndpointBindings: endpointsWithSpaceNames, 609 } 610 } 611 612 newApplication.Resources = applicationDataResources(application.Resources()) 613 614 if appBase != defaultBase { 615 newApplication.Base = appBase.String() 616 if includeSeries { 617 newApplication.Series = appSeries 618 } 619 } 620 if result := b.constraints(application.Constraints()); len(result) != 0 { 621 newApplication.Constraints = strings.Join(result, " ") 622 } 623 if cons := application.StorageDirectives(); len(cons) != 0 { 624 newApplication.Storage = make(map[string]string) 625 for name, constr := range cons { 626 if newApplication.Storage[name], err = storage.ToString(storage.Constraints{ 627 Pool: constr.Pool(), 628 Size: constr.Size(), 629 Count: constr.Count(), 630 }); err != nil { 631 return nil, nil, nil, errors.NotValidf("storage %q for %q", name, application.Name()) 632 } 633 } 634 } 635 636 // If this application has been trusted by the operator, set the 637 // Trust field of the ApplicationSpec to true 638 if appConfig := application.ApplicationConfig(); appConfig != nil { 639 newApplication.RequiresTrust = appConfig[appFacade.TrustConfigOptionName] == true 640 } 641 642 // Populate offer list 643 if offerList := application.Offers(); offerList != nil { 644 newApplication.Offers = make(map[string]*charm.OfferSpec) 645 for _, offer := range offerList { 646 endpoints := offer.Endpoints() 647 exposedEndpointNames := make([]string, 0, len(endpoints)) 648 for _, ep := range endpoints { 649 exposedEndpointNames = append(exposedEndpointNames, ep) 650 } 651 sort.Strings(exposedEndpointNames) 652 newApplication.Offers[offer.OfferName()] = &charm.OfferSpec{ 653 Endpoints: exposedEndpointNames, 654 ACL: b.filterOfferACL(offer.ACL()), 655 } 656 } 657 } 658 659 applicationData[application.Name()] = newApplication 660 } 661 return applicationData, machineIds, usedBases, nil 662 } 663 664 func applicationDataResources(resources []description.Resource) map[string]interface{} { 665 var resourceData map[string]interface{} 666 for _, res := range resources { 667 appRev := res.ApplicationRevision() 668 if appRev == nil || appRev.Origin() != resource.OriginStore.String() { 669 continue 670 } 671 if resourceData == nil { 672 resourceData = make(map[string]interface{}) 673 } 674 resourceData[res.Name()] = res.ApplicationRevision().Revision() 675 } 676 return resourceData 677 } 678 679 func (b *BundleAPI) bundleDataMachines(machines []description.Machine, machineIds set.Strings, defaultBase corebase.Base, includeSeries bool) (map[string]*charm.MachineSpec, set.Strings, error) { 680 usedBases := set.NewStrings() 681 machineData := make(map[string]*charm.MachineSpec) 682 for _, machine := range machines { 683 if !machineIds.Contains(machine.Tag().Id()) { 684 continue 685 } 686 macBase, err := corebase.ParseBaseFromString(machine.Base()) 687 if err != nil { 688 return nil, nil, errors.Trace(err) 689 } 690 usedBases.Add(macBase.String()) 691 692 var macSeries string 693 if includeSeries { 694 macSeries, err = corebase.GetSeriesFromBase(macBase) 695 if err != nil { 696 return nil, nil, errors.Trace(err) 697 } 698 } 699 newMachine := &charm.MachineSpec{ 700 Annotations: machine.Annotations(), 701 } 702 if macBase != defaultBase { 703 newMachine.Base = macBase.String() 704 if includeSeries { 705 newMachine.Series = macSeries 706 } 707 } 708 709 if result := b.constraints(machine.Constraints()); len(result) != 0 { 710 newMachine.Constraints = strings.Join(result, " ") 711 } 712 713 machineData[machine.Id()] = newMachine 714 } 715 return machineData, usedBases, nil 716 } 717 718 func bundleDataRemoteApplications(remoteApps []description.RemoteApplication) map[string]*charm.SaasSpec { 719 Saas := make(map[string]*charm.SaasSpec, len(remoteApps)) 720 for _, application := range remoteApps { 721 newSaas := &charm.SaasSpec{ 722 URL: application.URL(), 723 } 724 Saas[application.Name()] = newSaas 725 } 726 return Saas 727 } 728 729 func bundleDataRelations(relations []description.Relation) [][]string { 730 var relationData [][]string 731 for _, relation := range relations { 732 var endpointRelation []string 733 for _, endpoint := range relation.Endpoints() { 734 // skipping the 'peer' role which is not of concern in exporting the current model configuration. 735 if endpoint.Role() == "peer" { 736 continue 737 } 738 endpointRelation = append(endpointRelation, endpoint.ApplicationName()+":"+endpoint.Name()) 739 } 740 if len(endpointRelation) != 0 { 741 relationData = append(relationData, endpointRelation) 742 } 743 } 744 return relationData 745 } 746 747 // mapExposedEndpoints converts the description package representation of the 748 // exposed endpoint settings into a format that can be included in the exported 749 // bundle output. The provided spaceInfos list is used to convert space IDs 750 // into space names. 751 func mapExposedEndpoints(exposedEndpoints map[string]description.ExposedEndpoint, spaceInfos network.SpaceInfos) (map[string]charm.ExposedEndpointSpec, error) { 752 if len(exposedEndpoints) == 0 { 753 return nil, nil 754 } else if allEndpointParams, found := exposedEndpoints[""]; found && len(exposedEndpoints) == 1 { 755 // We have a single entry for the wildcard endpoint; check if 756 // it only includes an expose to all networks CIDR. 757 var allNetworkCIDRCount int 758 for _, cidr := range allEndpointParams.ExposeToCIDRs() { 759 if cidr == firewall.AllNetworksIPV4CIDR || cidr == firewall.AllNetworksIPV6CIDR { 760 allNetworkCIDRCount++ 761 } 762 } 763 764 if len(allEndpointParams.ExposeToSpaceIDs()) == 0 && 765 len(allEndpointParams.ExposeToCIDRs()) == allNetworkCIDRCount { 766 return nil, nil // equivalent to using non-granular expose like pre 2.9 juju 767 } 768 } 769 770 res := make(map[string]charm.ExposedEndpointSpec, len(exposedEndpoints)) 771 for endpointName, exposeDetails := range exposedEndpoints { 772 exposeToCIDRs := exposeDetails.ExposeToCIDRs() 773 exposeToSpaceNames, err := mapSpaceIDsToNames(spaceInfos, exposeDetails.ExposeToSpaceIDs()) 774 if err != nil { 775 return nil, errors.Trace(err) 776 } 777 778 // Ensure consistent ordering of results 779 sort.Strings(exposeToSpaceNames) 780 sort.Strings(exposeToCIDRs) 781 782 res[endpointName] = charm.ExposedEndpointSpec{ 783 ExposeToSpaces: exposeToSpaceNames, 784 ExposeToCIDRs: exposeToCIDRs, 785 } 786 } 787 788 return res, nil 789 } 790 791 func mapSpaceIDsToNames(spaceInfos network.SpaceInfos, spaceIDs []string) ([]string, error) { 792 if len(spaceIDs) == 0 { 793 return nil, nil 794 } 795 796 spaceNames := make([]string, len(spaceIDs)) 797 for i, spaceID := range spaceIDs { 798 sp := spaceInfos.GetByID(spaceID) 799 if sp == nil { 800 return nil, errors.NotFoundf("space with ID %q", spaceID) 801 } 802 803 spaceNames[i] = string(sp.Name) 804 } 805 806 return spaceNames, nil 807 } 808 809 func (b *BundleAPI) printSpaceNamesInEndpointBindings(apps []description.Application) bool { 810 // Assumption: if all endpoint bindings in the bundle are in the 811 // same space, spaces aren't really in use and will "muddy the waters" 812 // for export bundle. 813 spaceName := set.NewStrings() 814 for _, app := range apps { 815 for _, v := range app.EndpointBindings() { 816 spaceName.Add(v) 817 } 818 if spaceName.Size() > 1 { 819 return true 820 } 821 } 822 return false 823 } 824 825 func (b *BundleAPI) endpointBindings(bindings map[string]string, spaceLookup network.SpaceInfos, printValue bool) (map[string]string, error) { 826 if !printValue { 827 return nil, nil 828 } 829 endpointBindings, err := state.NewBindings(b.backend, bindings) 830 if err != nil { 831 return nil, errors.Trace(err) 832 } 833 return endpointBindings.MapWithSpaceNames(spaceLookup) 834 } 835 836 // filterOfferACL prunes the input offer ACL to remove internal juju users that 837 // we shouldn't export as part of the bundle. 838 func (b *BundleAPI) filterOfferACL(in map[string]string) map[string]string { 839 delete(in, common.EveryoneTagName) 840 return in 841 } 842 843 func (b *BundleAPI) constraints(cons description.Constraints) []string { 844 if cons == nil { 845 return []string{} 846 } 847 848 var result []string 849 if arch := cons.Architecture(); arch != "" { 850 result = append(result, "arch="+arch) 851 } 852 if cores := cons.CpuCores(); cores != 0 { 853 result = append(result, "cpu-cores="+strconv.Itoa(int(cores))) 854 } 855 if power := cons.CpuPower(); power != 0 { 856 result = append(result, "cpu-power="+strconv.Itoa(int(power))) 857 } 858 if mem := cons.Memory(); mem != 0 { 859 result = append(result, "mem="+strconv.Itoa(int(mem))) 860 } 861 if disk := cons.RootDisk(); disk != 0 { 862 result = append(result, "root-disk="+strconv.Itoa(int(disk))) 863 } 864 if instType := cons.InstanceType(); instType != "" { 865 result = append(result, "instance-type="+instType) 866 } 867 if imageID := cons.ImageID(); imageID != "" { 868 result = append(result, "image-id="+imageID) 869 } 870 if container := cons.Container(); container != "" { 871 result = append(result, "container="+container) 872 } 873 if virtType := cons.VirtType(); virtType != "" { 874 result = append(result, "virt-type="+virtType) 875 } 876 if tags := cons.Tags(); len(tags) != 0 { 877 result = append(result, "tags="+strings.Join(tags, ",")) 878 } 879 if spaces := cons.Spaces(); len(spaces) != 0 { 880 result = append(result, "spaces="+strings.Join(spaces, ",")) 881 } 882 if zones := cons.Zones(); len(zones) != 0 { 883 result = append(result, "zones="+strings.Join(zones, ",")) 884 } 885 if rootDiskSource := cons.RootDiskSource(); rootDiskSource != "" { 886 result = append(result, "root-disk-source="+rootDiskSource) 887 } 888 return result 889 }