cmr: Capture remote space info when creating a remote application #7340

Merged
merged 1 commit into from May 16, 2017
Jump to file or symbol
Failed to load files and symbols.
+474 −109
Split
@@ -23,11 +23,13 @@ import (
"github.com/juju/juju/apiserver/facade"
"github.com/juju/juju/apiserver/params"
jujucrossmodel "github.com/juju/juju/core/crossmodel"
+ "github.com/juju/juju/environs"
"github.com/juju/juju/feature"
"github.com/juju/juju/instance"
jjj "github.com/juju/juju/juju"
"github.com/juju/juju/permission"
"github.com/juju/juju/state"
+ "github.com/juju/juju/state/stateenvirons"
)
var logger = loggo.GetLogger("juju.apiserver.application")
@@ -49,6 +51,7 @@ type API struct {
stateCharm func(Charm) *state.Charm
deployApplicationFunc func(backend Backend, args jjj.DeployApplicationParams) error
+ getEnviron stateenvirons.NewEnvironFunc
}
// DeployApplication is a wrapper around juju.DeployApplication, to
@@ -71,6 +74,7 @@ func NewFacade(ctx facade.Context) (*API, error) {
blockChecker,
stateCharm,
DeployApplication,
+ stateenvirons.GetNewEnvironFunc(environs.New),
)
}
@@ -83,6 +87,7 @@ func NewAPI(
blockChecker BlockChecker,
stateCharm func(Charm) *state.Charm,
deployApplication func(Backend, jjj.DeployApplicationParams) error,
+ getEnviron stateenvirons.NewEnvironFunc,
) (*API, error) {
if !authorizer.AuthClient() {
return nil, common.ErrPerm
@@ -96,6 +101,7 @@ func NewAPI(
statePool: statePool,
dataDir: dataDir.String(),
deployApplicationFunc: deployApplication,
+ getEnviron: getEnviron,
}, nil
}
@@ -946,7 +952,7 @@ func (api *API) sameControllerSourceModel(userName, modelName string) (names.Mod
// of the application and endpoint. These details are saved to the state model so relations to
// the remote application can be created.
func (api *API) processRemoteApplication(url *jujucrossmodel.ApplicationURL, alias string) (*state.RemoteApplication, error) {
- offer, sourceModelTag, err := api.offeredApplicationDetails(url, permission.ConsumeAccess)
+ offer, err := api.offeredApplicationDetails(url, permission.ConsumeAccess)
if err != nil {
return nil, errors.Trace(err)
}
@@ -955,28 +961,18 @@ func (api *API) processRemoteApplication(url *jujucrossmodel.ApplicationURL, ali
if appName == "" {
appName = url.ApplicationName
}
- remoteApp, err := api.saveRemoteApplication(sourceModelTag, appName, url.ApplicationName, url.String(), offer.Endpoints)
+ remoteApp, err := api.saveRemoteApplication(url.String(), appName, offer)
return remoteApp, err
}
// offeredApplicationDetails returns details of the application offered at the specified URL.
// The user is required to have the specified permission on the offer.
func (api *API) offeredApplicationDetails(url *jujucrossmodel.ApplicationURL, perm permission.Access) (
- offer *params.ApplicationOffer,
- sourceModelTag names.ModelTag,
- err error,
+ *params.ApplicationOffer, error,
) {
- fail := func(err error) (
- *params.ApplicationOffer,
- names.ModelTag,
- error,
- ) {
- return nil, sourceModelTag, err
- }
-
// We require the hosting model to be specified.
if url.ModelName == "" {
- return fail(errors.Errorf("missing model name in URL %q", url.String()))
+ return nil, errors.Errorf("missing model name in URL %q", url.String())
}
// The user name is either specified in URL, or else we default to
@@ -987,38 +983,27 @@ func (api *API) offeredApplicationDetails(url *jujucrossmodel.ApplicationURL, pe
}
// Get the hosting model from the name.
- sourceModelTag, err = api.sameControllerSourceModel(userName, url.ModelName)
+ sourceModelTag, err := api.sameControllerSourceModel(userName, url.ModelName)
if err == nil {
- app, releaser, err := api.sameControllerOfferedApplication(sourceModelTag, url.ApplicationName, perm)
+ offerParams, err := api.sameControllerOfferedApplication(sourceModelTag, url.ApplicationName, perm)
if err != nil {
- return fail(errors.Trace(err))
+ return nil, errors.Trace(err)
}
- defer releaser()
- offer, err := api.makeOfferParamsFromApplication(url.ApplicationName, app)
- return offer, sourceModelTag, err
+ return offerParams, err
}
- if !errors.IsNotFound(err) {
- return fail(errors.Trace(err))
+ if errors.IsNotFound(err) {
+ return api.differentControllerOfferedApplication(userName, url.ModelName, url.ApplicationName)
}
- return api.differentControllerOfferedApplication(userName, url.ModelName, url.ApplicationName)
+ return nil, errors.Trace(err)
}
func (api *API) differentControllerOfferedApplication(userName, modelName, offerName string) (
*params.ApplicationOffer,
- names.ModelTag,
error,
) {
- fail := func(err error) (
- *params.ApplicationOffer,
- names.ModelTag,
- error,
- ) {
- return nil, names.ModelTag{}, err
- }
-
// TODO(wallyworld) - we need a way to pass in the JEM api Info
// For now act as if the offer is not found.
- return fail(errors.NotFoundf("application offer at %s/%s.%s", userName, modelName, offerName))
+ return nil, errors.NotFoundf("application offer at %s/%s.%s", userName, modelName, offerName)
//dialOpts := jujuapi.DefaultDialOpts()
//conn, err := jujuapi.Open(nil, dialOpts)
@@ -1049,54 +1034,120 @@ func (api *API) differentControllerOfferedApplication(userName, modelName, offer
//return &offers[0], sourceModelTag, nil
}
-func (api *API) makeOfferParamsFromApplication(offerName string, app *state.Application) (*params.ApplicationOffer, error) {
- ch, _, err := app.Charm()
+func (api *API) makeOfferParams(st *state.State, offer *jujucrossmodel.ApplicationOffer) (
+ *params.ApplicationOffer, error,
+) {
+ app, err := st.Application(offer.ApplicationName)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ appBindings, err := app.EndpointBindings()
if err != nil {
return nil, errors.Trace(err)
}
result := params.ApplicationOffer{
- SourceModelTag: api.backend.ModelTag().String(),
- OfferName: offerName,
- ApplicationDescription: ch.Meta().Description,
+ OfferName: offer.OfferName,
+ ApplicationDescription: offer.ApplicationDescription,
}
- eps, err := app.Endpoints()
- for _, ep := range eps {
+
+ spaceNames := set.NewStrings()
+ for _, ep := range offer.Endpoints {
result.Endpoints = append(result.Endpoints, params.RemoteEndpoint{
Name: ep.Name,
Interface: ep.Interface,
Role: ep.Role,
Scope: ep.Scope,
Limit: ep.Limit,
})
+ spaceName, ok := appBindings[ep.Name]
+ if !ok {
+ // There should always be some binding (even if it's to
+ // the default space).
+ return nil, errors.Errorf("no binding for %q endpoint", ep.Name)
+ }
+ spaceNames.Add(spaceName)
+ }
+
+ spaces, err := api.collectRemoteSpaces(st, spaceNames.SortedValues())
+ if errors.IsNotSupported(err) {
+ // Provider doesn't support ProviderSpaceInfo; continue
+ // without any space information, we shouldn't short-circuit
+ // cross-model connections.
+ return &result, nil
+ }
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ // Ensure bindings only contains entries for which we have spaces.
+ result.Bindings = make(map[string]string)
+ for epName, spaceName := range appBindings {
+ space, ok := spaces[spaceName]
+ if !ok {
+ continue
+ }
+ result.Bindings[epName] = spaceName
+ result.Spaces = append(result.Spaces, space)
}
return &result, nil
}
-func (api *API) sameControllerOfferedApplication(sourceModelTag names.ModelTag, offerName string, perm permission.Access) (
- _ *state.Application,
- releaser func(),
- err error,
-) {
- defer func() {
- if err != nil && releaser != nil {
- releaser()
- }
- }()
+// collectRemoteSpaces gets provider information about the spaces from
+// the state passed in. (This state will be for a different model than
+// this API instance, which is why the results are *remote* spaces.)
+// These can be used by the provider later on to decide whether a
+// connection can be made via cloud-local addresses. If the provider
+// doesn't support getting ProviderSpaceInfo the NotSupported error
+// will be returned.
+func (api *API) collectRemoteSpaces(st *state.State, spaceNames []string) (map[string]params.RemoteSpace, error) {
+ env, err := api.getEnviron(st)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ netEnv, ok := environs.SupportsNetworking(env)
+ if !ok {
+ logger.Debugf("cloud provider doesn't support networking, not getting space info")
+ return nil, nil
+ }
- fail := func(err error) (
- *state.Application,
- func(),
- error,
- ) {
- return nil, releaser, err
+ results := make(map[string]params.RemoteSpace)
+ for _, name := range spaceNames {
+ space := environs.DefaultSpaceInfo
+ if name != environs.DefaultSpaceName {
+ dbSpace, err := st.Space(name)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ space, err = spaceInfoFromState(dbSpace)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ }
+ providerSpace, err := netEnv.ProviderSpaceInfo(space)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+ if providerSpace == nil {
+ logger.Errorf("nil provider space info for %q", name)
+ continue
+ }
+ remoteSpace := paramsFromProviderSpaceInfo(providerSpace)
+ // Use the name from state in case provider and state disagree.
+ remoteSpace.Name = name
+ results[name] = remoteSpace
}
+ return results, nil
+}
+func (api *API) sameControllerOfferedApplication(sourceModelTag names.ModelTag, offerName string, perm permission.Access) (
+ *params.ApplicationOffer, error,
+) {
// Get the backend state for the source model so we can lookup the application.
- var st *state.State
- st, releaser, err = api.statePool.Get(sourceModelTag.Id())
+ st, releaser, err := api.statePool.Get(sourceModelTag.Id())
if err != nil {
- return fail(errors.Trace(err))
+ return nil, errors.Trace(err)
}
+ defer releaser()
// For now, offer URL is matched against the specified application
// name as seen from the consuming model.
@@ -1107,16 +1158,16 @@ func (api *API) sameControllerOfferedApplication(sourceModelTag names.ModelTag,
},
)
if err != nil {
- return fail(errors.Trace(err))
+ return nil, errors.Trace(err)
}
// The offers query succeeded but there were no offers matching the required offer name.
if len(offers) == 0 {
- return fail(errors.NotFoundf("application offer %q", offerName))
+ return nil, errors.NotFoundf("application offer %q", offerName)
}
// Sanity check - this should never happen.
if len(offers) > 1 {
- return fail(errors.Errorf("unexpected: %d matching offers for %q", len(offers), offerName))
+ return nil, errors.Errorf("unexpected: %d matching offers for %q", len(offers), offerName)
}
// Check the permissions - a user can access the offer if they are an admin
@@ -1127,7 +1178,7 @@ func (api *API) sameControllerOfferedApplication(sourceModelTag names.ModelTag,
err = api.checkPermission(sourceModelTag, permission.AdminAccess)
}
if err != nil && err != common.ErrPerm {
- return fail(errors.Trace(err))
+ return nil, errors.Trace(err)
}
isAdmin = err == nil
@@ -1138,26 +1189,27 @@ func (api *API) sameControllerOfferedApplication(sourceModelTag names.ModelTag,
apiUser := api.authorizer.GetAuthTag().(names.UserTag)
access, err := st.GetOfferAccess(names.NewApplicationOfferTag(offer.OfferName), apiUser)
if err != nil && !errors.IsNotFound(err) {
- return fail(errors.Trace(err))
+ return nil, errors.Trace(err)
}
if !access.EqualOrGreaterOfferAccessThan(perm) {
- return fail(common.ErrPerm)
+ return nil, common.ErrPerm
}
}
- app, err := st.Application(offer.ApplicationName)
+ offerParams, err := api.makeOfferParams(st, &offer)
if err != nil {
- return fail(errors.Trace(err))
+ return nil, errors.Trace(err)
}
- return app, releaser, err
+ offerParams.SourceModelTag = sourceModelTag.String()
+ return offerParams, nil
}
// saveRemoteApplication saves the details of the specified remote application and its endpoints
// to the state model so relations to the remote application can be created.
-func (api *API) saveRemoteApplication(
- sourceModelTag names.ModelTag, applicationName, offerName, url string, endpoints []params.RemoteEndpoint,
-) (*state.RemoteApplication, error) {
- remoteEps := make([]charm.Relation, len(endpoints))
- for j, ep := range endpoints {
+func (api *API) saveRemoteApplication(url, applicationName string, offer *params.ApplicationOffer) (
+ *state.RemoteApplication, error,
+) {
+ remoteEps := make([]charm.Relation, len(offer.Endpoints))
+ for j, ep := range offer.Endpoints {
remoteEps[j] = charm.Relation{
Name: ep.Name,
Role: ep.Role,
@@ -1167,8 +1219,20 @@ func (api *API) saveRemoteApplication(
}
}
+ remoteSpaces := make([]*environs.ProviderSpaceInfo, len(offer.Spaces))
+ for i, space := range offer.Spaces {
+ remoteSpaces[i] = providerSpaceInfoFromParams(space)
+ }
+
// If the a remote application with the same name and endpoints from the same
// source model already exists, we will use that one.
+ sourceModelTag, err := names.ParseModelTag(offer.SourceModelTag)
+ if err != nil {
+ return nil, errors.Trace(err)
+ }
+
+ // TODO(babbageclunk): how should we handle changed spaces and/or
+ // bindings as well here?
remoteApp, err := api.maybeUpdateExistingApplicationEndpoints(applicationName, sourceModelTag, remoteEps)
if err == nil {
return remoteApp, nil
@@ -1178,10 +1242,12 @@ func (api *API) saveRemoteApplication(
return api.backend.AddRemoteApplication(state.AddRemoteApplicationParams{
Name: applicationName,
- OfferName: offerName,
+ OfferName: offer.OfferName,
URL: url,
SourceModel: sourceModelTag,
Endpoints: remoteEps,
+ Bindings: offer.Bindings,
+ Spaces: remoteSpaces,
})
}
@@ -1263,13 +1329,13 @@ func (api *API) oneRemoteApplicationInfo(urlStr string) (*params.RemoteApplicati
}
// We need at least read access to the model to see the application details.
- offer, sourceModelTag, err := api.offeredApplicationDetails(url, permission.ReadAccess)
+ offer, err := api.offeredApplicationDetails(url, permission.ReadAccess)
if err != nil {
return nil, errors.Trace(err)
}
return &params.RemoteApplicationInfo{
- ModelTag: sourceModelTag.String(),
+ ModelTag: offer.SourceModelTag,
Name: url.ApplicationName,
Description: offer.ApplicationDescription,
ApplicationURL: url.String(),
Oops, something went wrong.