github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/common/crossmodel/crossmodel.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package crossmodel
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/names/v5"
    13  
    14  	"github.com/juju/juju/apiserver/authentication"
    15  	"github.com/juju/juju/apiserver/common"
    16  	apiservererrors "github.com/juju/juju/apiserver/errors"
    17  	"github.com/juju/juju/core/crossmodel"
    18  	"github.com/juju/juju/core/life"
    19  	corelogger "github.com/juju/juju/core/logger"
    20  	"github.com/juju/juju/core/migration"
    21  	"github.com/juju/juju/core/permission"
    22  	"github.com/juju/juju/core/status"
    23  	"github.com/juju/juju/network"
    24  	"github.com/juju/juju/rpc/params"
    25  )
    26  
    27  var (
    28  	logger     = loggo.GetLoggerWithLabels("juju.apiserver.common.crossmodel", corelogger.CMR)
    29  	authlogger = loggo.GetLoggerWithLabels("juju.apiserver.common.crossmodelauth", corelogger.CMR_AUTH)
    30  )
    31  
    32  // PublishRelationChange applies the relation change event to the specified backend.
    33  func PublishRelationChange(auth authoriser, backend Backend, relationTag, applicationTag names.Tag, change params.RemoteRelationChangeEvent) error {
    34  	logger.Debugf("publish into model %v change for %v on %v: %#v", backend.ModelUUID(), relationTag, applicationTag, &change)
    35  
    36  	dyingOrDead := change.Life != "" && change.Life != life.Alive
    37  	// Ensure the relation exists.
    38  	rel, err := backend.KeyRelation(relationTag.Id())
    39  	if errors.IsNotFound(err) {
    40  		if dyingOrDead {
    41  			return nil
    42  		}
    43  	}
    44  	if err != nil {
    45  		return errors.Trace(err)
    46  	}
    47  
    48  	if err := handleSuspendedRelation(auth, backend, change, rel, dyingOrDead); err != nil {
    49  		return errors.Trace(err)
    50  	}
    51  
    52  	// If the remote model has destroyed the relation, do it here also.
    53  	forceCleanUp := change.ForceCleanup != nil && *change.ForceCleanup
    54  	if dyingOrDead {
    55  		logger.Debugf("remote consuming side of %v died", relationTag)
    56  		if forceCleanUp && applicationTag != nil {
    57  			logger.Debugf("forcing cleanup of units for %v", applicationTag.Id())
    58  			remoteUnits, err := rel.AllRemoteUnits(applicationTag.Id())
    59  			if err != nil {
    60  				return errors.Trace(err)
    61  			}
    62  			logger.Debugf("got %v relation units to clean", len(remoteUnits))
    63  			for _, ru := range remoteUnits {
    64  				if err := ru.LeaveScope(); err != nil {
    65  					return errors.Trace(err)
    66  				}
    67  			}
    68  		}
    69  
    70  		if forceCleanUp {
    71  			oppErrs, err := rel.DestroyWithForce(true, 0)
    72  			if len(oppErrs) > 0 {
    73  				logger.Warningf("errors forcing cleanup of %v: %v", rel.Tag().Id(), oppErrs)
    74  			}
    75  			// If we are forcing cleanup, we can exit early here.
    76  			return errors.Trace(err)
    77  		}
    78  		if err := rel.Destroy(); err != nil {
    79  			return errors.Trace(err)
    80  		}
    81  	}
    82  
    83  	// TODO(wallyworld) - deal with remote application being removed
    84  	if applicationTag == nil {
    85  		logger.Infof("no remote application found for %v", relationTag.Id())
    86  		return nil
    87  	}
    88  	logger.Debugf("remote application for changed relation %v is %v in model %v",
    89  		relationTag.Id(), applicationTag.Id(), backend.ModelUUID())
    90  
    91  	// Allow sending an empty non-nil map to clear all the settings.
    92  	if change.ApplicationSettings != nil {
    93  		logger.Debugf("remote application %v in %v settings changed to %v",
    94  			applicationTag.Id(), relationTag.Id(), change.ApplicationSettings)
    95  		err := rel.ReplaceApplicationSettings(applicationTag.Id(), change.ApplicationSettings)
    96  		if err != nil {
    97  			return errors.Trace(err)
    98  		}
    99  	}
   100  
   101  	if err := handleDepartedUnits(backend, change, applicationTag, rel); err != nil {
   102  		return errors.Trace(err)
   103  	}
   104  
   105  	return errors.Trace(handleChangedUnits(change, applicationTag, rel))
   106  }
   107  
   108  type authoriser interface {
   109  	EntityHasPermission(entity names.Tag, operation permission.Access, target names.Tag) error
   110  }
   111  
   112  type offerBackend interface {
   113  	ApplicationOfferForUUID(offerUUID string) (*crossmodel.ApplicationOffer, error)
   114  }
   115  
   116  // CheckCanConsume checks consume permission for a user on an offer connection.
   117  func CheckCanConsume(auth authoriser, backend offerBackend, controllerTag, modelTag names.Tag, oc OfferConnection) (bool, error) {
   118  	user := names.NewUserTag(oc.UserName())
   119  	err := auth.EntityHasPermission(user, permission.SuperuserAccess, controllerTag)
   120  	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   121  		return false, errors.Trace(err)
   122  	} else if err == nil {
   123  		return true, nil
   124  	}
   125  
   126  	err = auth.EntityHasPermission(user, permission.AdminAccess, modelTag)
   127  	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   128  		return false, errors.Trace(err)
   129  	} else if err == nil {
   130  		return true, nil
   131  	}
   132  	err = auth.EntityHasPermission(user, permission.ConsumeAccess, names.NewApplicationOfferTag(oc.OfferUUID()))
   133  	return err == nil, err
   134  }
   135  
   136  func handleSuspendedRelation(auth authoriser, backend Backend, change params.RemoteRelationChangeEvent, rel Relation, dyingOrDead bool) error {
   137  	// Update the relation suspended status.
   138  	currentStatus := rel.Suspended()
   139  	if !dyingOrDead && change.Suspended != nil && currentStatus != *change.Suspended {
   140  		var (
   141  			newStatus status.Status
   142  			message   string
   143  		)
   144  		if *change.Suspended {
   145  			newStatus = status.Suspending
   146  			message = change.SuspendedReason
   147  			if message == "" {
   148  				message = "suspending after update from remote model"
   149  			}
   150  		} else {
   151  			oc, err := backend.OfferConnectionForRelation(rel.Tag().Id())
   152  			if err != nil && !errors.Is(err, errors.NotFound) {
   153  				return errors.Trace(err)
   154  			}
   155  			if err == nil {
   156  				ok, err := CheckCanConsume(auth, backend, backend.ControllerTag(), backend.ModelTag(), oc)
   157  				if err != nil {
   158  					return errors.Trace(err)
   159  				}
   160  				if !ok {
   161  					return apiservererrors.ErrPerm
   162  				}
   163  			}
   164  		}
   165  		if err := rel.SetSuspended(*change.Suspended, message); err != nil {
   166  			return errors.Trace(err)
   167  		}
   168  		if !*change.Suspended {
   169  			newStatus = status.Joining
   170  			message = ""
   171  		}
   172  		if err := rel.SetStatus(status.StatusInfo{
   173  			Status:  newStatus,
   174  			Message: message,
   175  		}); err != nil && !errors.IsNotValid(err) {
   176  			return errors.Trace(err)
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  func handleDepartedUnits(backend Backend, change params.RemoteRelationChangeEvent, applicationTag names.Tag, rel Relation) error {
   183  	for _, id := range change.DepartedUnits {
   184  		unitTag := names.NewUnitTag(fmt.Sprintf("%s/%v", applicationTag.Id(), id))
   185  		logger.Debugf("unit %v has departed relation %v", unitTag.Id(), rel.Tag().Id())
   186  		ru, err := rel.RemoteUnit(unitTag.Id())
   187  		if err != nil {
   188  			return errors.Trace(err)
   189  		}
   190  		logger.Debugf("%s leaving scope", unitTag.Id())
   191  		if err := ru.LeaveScope(); err != nil {
   192  			return errors.Trace(err)
   193  		}
   194  		if err := backend.RemoveSecretConsumer(unitTag); err != nil {
   195  			return errors.Trace(err)
   196  		}
   197  	}
   198  	return nil
   199  }
   200  
   201  func handleChangedUnits(change params.RemoteRelationChangeEvent, applicationTag names.Tag, rel Relation) error {
   202  	for _, change := range change.ChangedUnits {
   203  		unitTag := names.NewUnitTag(fmt.Sprintf("%s/%v", applicationTag.Id(), change.UnitId))
   204  		logger.Debugf("changed unit tag for unit id %v is %v", change.UnitId, unitTag)
   205  		ru, err := rel.RemoteUnit(unitTag.Id())
   206  		if err != nil {
   207  			return errors.Trace(err)
   208  		}
   209  		inScope, err := ru.InScope()
   210  		if err != nil {
   211  			return errors.Trace(err)
   212  		}
   213  		settings := make(map[string]interface{})
   214  		for k, v := range change.Settings {
   215  			settings[k] = v
   216  		}
   217  		if !inScope {
   218  			logger.Debugf("%s entering scope (%v)", unitTag.Id(), settings)
   219  			err = ru.EnterScope(settings)
   220  		} else {
   221  			logger.Debugf("%s updated settings (%v)", unitTag.Id(), settings)
   222  			err = ru.ReplaceSettings(settings)
   223  		}
   224  		if err != nil {
   225  			return errors.Trace(err)
   226  		}
   227  	}
   228  	return nil
   229  }
   230  
   231  // GetOfferingRelationTokens returns the tokens for the relation and the offer
   232  // of the passed in relation tag.
   233  func GetOfferingRelationTokens(backend Backend, tag names.RelationTag) (string, string, error) {
   234  	offerUUID, err := backend.OfferUUIDForRelation(tag.Id())
   235  	if err != nil {
   236  		return "", "", errors.Annotatef(err, "getting offer for relation %q", tag.Id())
   237  	}
   238  	relationToken, err := backend.GetToken(tag)
   239  	if err != nil {
   240  		return "", "", errors.Annotatef(err, "getting token for relation %q", tag.Id())
   241  	}
   242  	appToken, err := backend.GetToken(names.NewApplicationOfferTag(offerUUID))
   243  	if err != nil {
   244  		return "", "", errors.Annotatef(err, "getting token for application offer %q", offerUUID)
   245  	}
   246  	return relationToken, appToken, nil
   247  }
   248  
   249  // GetConsumingRelationTokens returns the tokens for the relation and the local
   250  // application of the passed in relation tag.
   251  func GetConsumingRelationTokens(backend Backend, tag names.RelationTag) (string, string, error) {
   252  	relation, err := backend.KeyRelation(tag.Id())
   253  	if err != nil {
   254  		return "", "", errors.Annotatef(err, "getting relation for %q", tag.Id())
   255  	}
   256  	localAppName, err := getLocalApplicationName(backend, relation)
   257  	if err != nil {
   258  		return "", "", errors.Annotatef(err, "getting local application for relation %q", tag.Id())
   259  	}
   260  	relationToken, err := backend.GetToken(tag)
   261  	if err != nil {
   262  		return "", "", errors.Annotatef(err, "getting consuming token for relation %q", tag.Id())
   263  	}
   264  	appToken, err := backend.GetToken(names.NewApplicationTag(localAppName))
   265  	if err != nil {
   266  		return "", "", errors.Annotatef(err, "getting consuming token for application %q", localAppName)
   267  	}
   268  	return relationToken, appToken, nil
   269  }
   270  
   271  func getLocalApplicationName(backend Backend, relation Relation) (string, error) {
   272  	for _, ep := range relation.Endpoints() {
   273  		_, err := backend.Application(ep.ApplicationName)
   274  		if errors.IsNotFound(err) {
   275  			// Not found, so it's the remote application. Try the next endpoint.
   276  			continue
   277  		} else if err != nil {
   278  			return "", errors.Trace(err)
   279  		}
   280  		return ep.ApplicationName, nil
   281  	}
   282  	return "", errors.NotFoundf("local application for %s", names.ReadableString(relation.Tag()))
   283  }
   284  
   285  // WatchRelationUnits returns a watcher for changes to the units on the specified relation.
   286  func WatchRelationUnits(backend Backend, tag names.RelationTag) (common.RelationUnitsWatcher, error) {
   287  	relation, err := backend.KeyRelation(tag.Id())
   288  	if err != nil {
   289  		return nil, errors.Annotatef(err, "getting relation for %q", tag.Id())
   290  	}
   291  	localAppName, err := getLocalApplicationName(backend, relation)
   292  	if err != nil {
   293  		return nil, errors.Annotatef(err, "getting local application for relation %q", tag.Id())
   294  	}
   295  	w, err := relation.WatchUnits(localAppName)
   296  	if err != nil {
   297  		return nil, errors.Annotatef(err, "watching units for %q", localAppName)
   298  	}
   299  	wrapped, err := common.RelationUnitsWatcherFromState(w)
   300  	if err != nil {
   301  		return nil, errors.Annotatef(err, "getting relation units watcher for %q", tag.Id())
   302  	}
   303  	return wrapped, nil
   304  }
   305  
   306  // ExpandChange converts a params.RelationUnitsChange into a
   307  // params.RemoteRelationChangeEvent by filling out the extra
   308  // information from the passed backend. This takes relation and
   309  // application token so that it can still return sensible results if
   310  // the relation has been removed (just departing units).
   311  func ExpandChange(
   312  	backend Backend,
   313  	relationToken string,
   314  	appToken string,
   315  	change params.RelationUnitsChange,
   316  ) (params.RemoteRelationChangeEvent, error) {
   317  	var empty params.RemoteRelationChangeEvent
   318  
   319  	var departed []int
   320  	for _, unitName := range change.Departed {
   321  		num, err := names.UnitNumber(unitName)
   322  		if err != nil {
   323  			return empty, errors.Trace(err)
   324  		}
   325  		departed = append(departed, num)
   326  	}
   327  
   328  	relationTag, err := backend.GetRemoteEntity(relationToken)
   329  	if errors.IsNotFound(err) {
   330  		// This can happen when the last unit leaves scope on a dying
   331  		// relation and the relation is removed. In that case there
   332  		// aren't any application- or unit-level settings to send; we
   333  		// just send the departed units so they can leave scope on
   334  		// the other side of a cross-model relation.
   335  		return params.RemoteRelationChangeEvent{
   336  			RelationToken:    relationToken,
   337  			ApplicationToken: appToken,
   338  			DepartedUnits:    departed,
   339  		}, nil
   340  
   341  	} else if err != nil {
   342  		return empty, errors.Trace(err)
   343  	}
   344  
   345  	relation, err := backend.KeyRelation(relationTag.Id())
   346  	if err != nil {
   347  		return empty, errors.Trace(err)
   348  	}
   349  	localAppName, err := getLocalApplicationName(backend, relation)
   350  	if err != nil {
   351  		return empty, errors.Trace(err)
   352  	}
   353  
   354  	var appSettings map[string]interface{}
   355  	if len(change.AppChanged) > 0 {
   356  		appSettings, err = relation.ApplicationSettings(localAppName)
   357  		if err != nil {
   358  			return empty, errors.Trace(err)
   359  		}
   360  	}
   361  
   362  	var unitChanges []params.RemoteRelationUnitChange
   363  	for unitName := range change.Changed {
   364  		relUnit, err := relation.Unit(unitName)
   365  		if err != nil {
   366  			return empty, errors.Annotatef(err, "getting unit %q in %q", unitName, relationTag.Id())
   367  		}
   368  		unitSettings, err := relUnit.Settings()
   369  		if err != nil {
   370  			return empty, errors.Annotatef(err, "getting settings for %q in %q", unitName, relationTag.Id())
   371  		}
   372  		num, err := names.UnitNumber(unitName)
   373  		if err != nil {
   374  			return empty, errors.Trace(err)
   375  		}
   376  		unitChanges = append(unitChanges, params.RemoteRelationUnitChange{
   377  			UnitId:   num,
   378  			Settings: unitSettings,
   379  		})
   380  	}
   381  
   382  	uc := relation.UnitCount()
   383  	result := params.RemoteRelationChangeEvent{
   384  		RelationToken:       relationToken,
   385  		ApplicationToken:    appToken,
   386  		ApplicationSettings: appSettings,
   387  		ChangedUnits:        unitChanges,
   388  		DepartedUnits:       departed,
   389  		UnitCount:           &uc,
   390  	}
   391  
   392  	return result, nil
   393  }
   394  
   395  // WrappedUnitsWatcher is a relation units watcher that remembers
   396  // details about the relation it came from so changes can be expanded
   397  // for sending outside this model.
   398  type WrappedUnitsWatcher struct {
   399  	common.RelationUnitsWatcher
   400  	RelationToken    string
   401  	ApplicationToken string
   402  }
   403  
   404  // RelationUnitSettings returns the unit settings for the specified relation unit.
   405  func RelationUnitSettings(backend Backend, ru params.RelationUnit) (params.Settings, error) {
   406  	relationTag, err := names.ParseRelationTag(ru.Relation)
   407  	if err != nil {
   408  		return nil, errors.Trace(err)
   409  	}
   410  	rel, err := backend.KeyRelation(relationTag.Id())
   411  	if err != nil {
   412  		return nil, errors.Trace(err)
   413  	}
   414  	unitTag, err := names.ParseUnitTag(ru.Unit)
   415  	if err != nil {
   416  		return nil, errors.Trace(err)
   417  	}
   418  	unit, err := rel.Unit(unitTag.Id())
   419  	if err != nil {
   420  		return nil, errors.Trace(err)
   421  	}
   422  	settings, err := unit.Settings()
   423  	if err != nil {
   424  		return nil, errors.Trace(err)
   425  	}
   426  	paramsSettings := make(params.Settings)
   427  	for k, v := range settings {
   428  		vString, ok := v.(string)
   429  		if !ok {
   430  			return nil, errors.Errorf(
   431  				"invalid relation setting %q: expected string, got %T", k, v,
   432  			)
   433  		}
   434  		paramsSettings[k] = vString
   435  	}
   436  	return paramsSettings, nil
   437  }
   438  
   439  // PublishIngressNetworkChange saves the specified ingress networks for a relation.
   440  func PublishIngressNetworkChange(backend Backend, relationTag names.Tag, change params.IngressNetworksChangeEvent) error {
   441  	logger.Debugf("publish into model %v network change for %v: %#v", backend.ModelUUID(), relationTag, &change)
   442  
   443  	// Ensure the relation exists.
   444  	rel, err := backend.KeyRelation(relationTag.Id())
   445  	if errors.IsNotFound(err) {
   446  		return nil
   447  	}
   448  	if err != nil {
   449  		return errors.Trace(err)
   450  	}
   451  
   452  	logger.Debugf("relation %v requires ingress networks %v", rel, change.Networks)
   453  	if err := validateIngressNetworks(backend, change.Networks); err != nil {
   454  		return errors.Trace(err)
   455  	}
   456  
   457  	_, err = backend.SaveIngressNetworks(rel.Tag().Id(), change.Networks)
   458  	return err
   459  }
   460  
   461  func validateIngressNetworks(backend Backend, networks []string) error {
   462  	if len(networks) == 0 {
   463  		return nil
   464  	}
   465  
   466  	// Check that the required ingress is allowed.
   467  	cfg, err := backend.ModelConfig()
   468  	if err != nil {
   469  		return errors.Trace(err)
   470  	}
   471  
   472  	var whitelistCIDRs, requestedCIDRs []*net.IPNet
   473  	if err := parseCIDRs(&whitelistCIDRs, cfg.SAASIngressAllow()); err != nil {
   474  		return errors.Trace(err)
   475  	}
   476  	if err := parseCIDRs(&requestedCIDRs, networks); err != nil {
   477  		return errors.Trace(err)
   478  	}
   479  	if len(whitelistCIDRs) > 0 {
   480  		for _, n := range requestedCIDRs {
   481  			if !network.SubnetInAnyRange(whitelistCIDRs, n) {
   482  				return &params.Error{
   483  					Code:    params.CodeForbidden,
   484  					Message: fmt.Sprintf("subnet %v not in firewall whitelist", n),
   485  				}
   486  			}
   487  		}
   488  	}
   489  	return nil
   490  }
   491  
   492  func parseCIDRs(cidrs *[]*net.IPNet, values []string) error {
   493  	for _, cidrStr := range values {
   494  		if _, ipNet, err := net.ParseCIDR(cidrStr); err != nil {
   495  			return err
   496  		} else {
   497  			*cidrs = append(*cidrs, ipNet)
   498  		}
   499  	}
   500  	return nil
   501  }
   502  
   503  type relationGetter interface {
   504  	// KeyRelation returns the relation identified by the input key.
   505  	KeyRelation(string) (Relation, error)
   506  	// IsMigrationActive returns true if the current model is
   507  	// in the process of being migrated to another controller.
   508  	IsMigrationActive() (bool, error)
   509  }
   510  
   511  // GetRelationLifeSuspendedStatusChange returns a life/suspended status change
   512  // struct for a specified relation key.
   513  func GetRelationLifeSuspendedStatusChange(
   514  	st relationGetter, key string,
   515  ) (*params.RelationLifeSuspendedStatusChange, error) {
   516  	rel, err := st.KeyRelation(key)
   517  	if errors.IsNotFound(err) {
   518  		// If the relation is not found we represent it as dead,
   519  		// but *only* if we are not currently migrating.
   520  		// If we are migrating, we do not want to inform remote watchers that
   521  		// the relation is dead before they have had a chance to be redirected
   522  		// to the new controller.
   523  		if migrating, mErr := st.IsMigrationActive(); mErr == nil && !migrating {
   524  			return &params.RelationLifeSuspendedStatusChange{
   525  				Key:  key,
   526  				Life: life.Dead,
   527  			}, nil
   528  		} else if mErr != nil {
   529  			err = mErr
   530  		}
   531  	}
   532  	if err != nil {
   533  		return nil, errors.Trace(err)
   534  	}
   535  	return &params.RelationLifeSuspendedStatusChange{
   536  		Key:             key,
   537  		Life:            life.Value(rel.Life().String()),
   538  		Suspended:       rel.Suspended(),
   539  		SuspendedReason: rel.SuspendedReason(),
   540  	}, nil
   541  }
   542  
   543  type offerGetter interface {
   544  	ApplicationOfferForUUID(string) (*crossmodel.ApplicationOffer, error)
   545  	Application(string) (Application, error)
   546  
   547  	// IsMigrationActive returns true if the current model is
   548  	// in the process of being migrated to another controller.
   549  	IsMigrationActive() (bool, error)
   550  }
   551  
   552  // GetOfferStatusChange returns a status change struct for the input offer name.
   553  // If the offer or application are not found during a migration, a specific
   554  // error to indicate the migration-in-progress is returned.
   555  // This is interpreted upstream as a watcher error and propagated to the
   556  // remote CMR consumer.
   557  func GetOfferStatusChange(st offerGetter, offerUUID, offerName string) (*params.OfferStatusChange, error) {
   558  	migrating, err := st.IsMigrationActive()
   559  	if err != nil {
   560  		return nil, errors.Trace(err)
   561  	}
   562  
   563  	offer, err := st.ApplicationOfferForUUID(offerUUID)
   564  	if errors.IsNotFound(err) {
   565  		if migrating {
   566  			return nil, migration.ErrMigrating
   567  		}
   568  		return &params.OfferStatusChange{
   569  			OfferName: offerName,
   570  			Status: params.EntityStatus{
   571  				Status: status.Terminated,
   572  				Info:   "offer has been removed",
   573  			},
   574  		}, nil
   575  	} else if err != nil {
   576  		return nil, errors.Trace(err)
   577  	}
   578  
   579  	app, err := st.Application(offer.ApplicationName)
   580  	if errors.IsNotFound(err) {
   581  		if migrating {
   582  			return nil, migration.ErrMigrating
   583  		}
   584  		return &params.OfferStatusChange{
   585  			OfferName: offerName,
   586  			Status: params.EntityStatus{
   587  				Status: status.Terminated,
   588  				Info:   "application has been removed",
   589  			},
   590  		}, nil
   591  	} else if err != nil {
   592  		return nil, errors.Trace(err)
   593  	}
   594  
   595  	sts := status.StatusInfo{
   596  		Status: status.Unknown,
   597  	}
   598  
   599  	if appStatus, err := app.Status(); err == nil {
   600  		// If the status is set to unset, then we need to query all the
   601  		// units of the application to work out the correct series.
   602  		if appStatus.Status == status.Unset {
   603  			derived, err := getDerivedUnitsStatus(app)
   604  			if err == nil {
   605  				sts = derived
   606  			}
   607  		} else {
   608  			sts = appStatus
   609  		}
   610  	}
   611  
   612  	return &params.OfferStatusChange{
   613  		OfferName: offerName,
   614  		Status: params.EntityStatus{
   615  			Status: sts.Status,
   616  			Info:   sts.Message,
   617  			Data:   sts.Data,
   618  			Since:  sts.Since,
   619  		},
   620  	}, nil
   621  }
   622  
   623  func getDerivedUnitsStatus(app Application) (status.StatusInfo, error) {
   624  	units, err := app.AllUnits()
   625  	if err != nil {
   626  		return status.StatusInfo{}, errors.Trace(err)
   627  	}
   628  
   629  	statuses := make([]status.StatusInfo, len(units))
   630  	for _, unit := range units {
   631  		st, err := unit.Status()
   632  		if err != nil {
   633  			return status.StatusInfo{}, errors.Trace(err)
   634  		}
   635  
   636  		statuses = append(statuses, st)
   637  	}
   638  	derived := status.DeriveStatus(statuses)
   639  	return derived, nil
   640  }