github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/controller/controller.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // The controller package defines an API end point for functions dealing
     5  // with controllers as a whole.
     6  package controller
     7  
     8  import (
     9  	"encoding/json"
    10  	"sort"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"github.com/juju/txn"
    15  	"github.com/juju/utils/set"
    16  	"gopkg.in/juju/names.v2"
    17  	"gopkg.in/macaroon.v1"
    18  
    19  	"github.com/juju/juju/api"
    20  	"github.com/juju/juju/api/migrationtarget"
    21  	"github.com/juju/juju/apiserver/common"
    22  	"github.com/juju/juju/apiserver/common/cloudspec"
    23  	"github.com/juju/juju/apiserver/facade"
    24  	"github.com/juju/juju/apiserver/params"
    25  	coremigration "github.com/juju/juju/core/migration"
    26  	"github.com/juju/juju/migration"
    27  	"github.com/juju/juju/permission"
    28  	"github.com/juju/juju/state"
    29  	"github.com/juju/juju/state/stateenvirons"
    30  )
    31  
    32  var logger = loggo.GetLogger("juju.apiserver.controller")
    33  
    34  func init() {
    35  	common.RegisterStandardFacade("Controller", 3, NewControllerAPI)
    36  }
    37  
    38  // Controller defines the methods on the controller API end point.
    39  type Controller interface {
    40  	AllModels() (params.UserModelList, error)
    41  	DestroyController(args params.DestroyControllerArgs) error
    42  	ModelConfig() (params.ModelConfigResults, error)
    43  	HostedModelConfigs() (params.HostedModelConfigsResults, error)
    44  	GetControllerAccess(params.Entities) (params.UserAccessResults, error)
    45  	ControllerConfig() (params.ControllerConfigResult, error)
    46  	ListBlockedModels() (params.ModelBlockInfoList, error)
    47  	RemoveBlocks(args params.RemoveBlocksArgs) error
    48  	WatchAllModels() (params.AllWatcherId, error)
    49  	ModelStatus(params.Entities) (params.ModelStatusResults, error)
    50  	InitiateMigration(params.InitiateMigrationArgs) (params.InitiateMigrationResults, error)
    51  	ModifyControllerAccess(params.ModifyControllerAccessRequest) (params.ErrorResults, error)
    52  }
    53  
    54  // ControllerAPI implements the environment manager interface and is
    55  // the concrete implementation of the api end point.
    56  type ControllerAPI struct {
    57  	*common.ControllerConfigAPI
    58  	*common.ModelStatusAPI
    59  	cloudspec.CloudSpecAPI
    60  
    61  	state      *state.State
    62  	authorizer facade.Authorizer
    63  	apiUser    names.UserTag
    64  	resources  facade.Resources
    65  }
    66  
    67  var _ Controller = (*ControllerAPI)(nil)
    68  
    69  // NewControllerAPI creates a new api server endpoint for managing
    70  // environments.
    71  func NewControllerAPI(
    72  	st *state.State,
    73  	resources facade.Resources,
    74  	authorizer facade.Authorizer,
    75  ) (*ControllerAPI, error) {
    76  	if !authorizer.AuthClient() {
    77  		return nil, errors.Trace(common.ErrPerm)
    78  	}
    79  
    80  	// Since we know this is a user tag (because AuthClient is true),
    81  	// we just do the type assertion to the UserTag.
    82  	apiUser, _ := authorizer.GetAuthTag().(names.UserTag)
    83  
    84  	environConfigGetter := stateenvirons.EnvironConfigGetter{st}
    85  	return &ControllerAPI{
    86  		ControllerConfigAPI: common.NewControllerConfig(st),
    87  		ModelStatusAPI:      common.NewModelStatusAPI(common.NewModelManagerBackend(st), authorizer, apiUser),
    88  		CloudSpecAPI:        cloudspec.NewCloudSpec(environConfigGetter.CloudSpec, common.AuthFuncForTag(st.ModelTag())),
    89  		state:               st,
    90  		authorizer:          authorizer,
    91  		apiUser:             apiUser,
    92  		resources:           resources,
    93  	}, nil
    94  }
    95  
    96  func (s *ControllerAPI) checkHasAdmin() error {
    97  	isAdmin, err := s.authorizer.HasPermission(permission.SuperuserAccess, s.state.ControllerTag())
    98  	if err != nil {
    99  		return errors.Trace(err)
   100  	}
   101  	if !isAdmin {
   102  		return common.ServerError(common.ErrPerm)
   103  	}
   104  	return nil
   105  }
   106  
   107  // AllModels allows controller administrators to get the list of all the
   108  // environments in the controller.
   109  func (s *ControllerAPI) AllModels() (params.UserModelList, error) {
   110  	result := params.UserModelList{}
   111  	if err := s.checkHasAdmin(); err != nil {
   112  		return result, errors.Trace(err)
   113  	}
   114  
   115  	// Get all the environments that the authenticated user can see, and
   116  	// supplement that with the other environments that exist that the user
   117  	// cannot see. The reason we do this is to get the LastConnection time for
   118  	// the environments that the user is able to see, so we have consistent
   119  	// output when listing with or without --all when an admin user.
   120  	environments, err := s.state.ModelsForUser(s.apiUser)
   121  	if err != nil {
   122  		return result, errors.Trace(err)
   123  	}
   124  	visibleEnvironments := set.NewStrings()
   125  	for _, env := range environments {
   126  		lastConn, err := env.LastConnection()
   127  		if err != nil && !state.IsNeverConnectedError(err) {
   128  			return result, errors.Trace(err)
   129  		}
   130  		visibleEnvironments.Add(env.UUID())
   131  		result.UserModels = append(result.UserModels, params.UserModel{
   132  			Model: params.Model{
   133  				Name:     env.Name(),
   134  				UUID:     env.UUID(),
   135  				OwnerTag: env.Owner().String(),
   136  			},
   137  			LastConnection: &lastConn,
   138  		})
   139  	}
   140  
   141  	allEnvs, err := s.state.AllModels()
   142  	if err != nil {
   143  		return result, errors.Trace(err)
   144  	}
   145  
   146  	for _, env := range allEnvs {
   147  		if !visibleEnvironments.Contains(env.UUID()) {
   148  			result.UserModels = append(result.UserModels, params.UserModel{
   149  				Model: params.Model{
   150  					Name:     env.Name(),
   151  					UUID:     env.UUID(),
   152  					OwnerTag: env.Owner().String(),
   153  				},
   154  				// No LastConnection as this user hasn't.
   155  			})
   156  		}
   157  	}
   158  
   159  	// Sort the resulting sequence by environment name, then owner.
   160  	sort.Sort(orderedUserModels(result.UserModels))
   161  
   162  	return result, nil
   163  }
   164  
   165  // ListBlockedModels returns a list of all environments on the controller
   166  // which have a block in place.  The resulting slice is sorted by environment
   167  // name, then owner. Callers must be controller administrators to retrieve the
   168  // list.
   169  func (s *ControllerAPI) ListBlockedModels() (params.ModelBlockInfoList, error) {
   170  	results := params.ModelBlockInfoList{}
   171  	if err := s.checkHasAdmin(); err != nil {
   172  		return results, errors.Trace(err)
   173  	}
   174  	blocks, err := s.state.AllBlocksForController()
   175  	if err != nil {
   176  		return results, errors.Trace(err)
   177  	}
   178  
   179  	envBlocks := make(map[string][]string)
   180  	for _, block := range blocks {
   181  		uuid := block.ModelUUID()
   182  		types, ok := envBlocks[uuid]
   183  		if !ok {
   184  			types = []string{block.Type().String()}
   185  		} else {
   186  			types = append(types, block.Type().String())
   187  		}
   188  		envBlocks[uuid] = types
   189  	}
   190  
   191  	for uuid, blocks := range envBlocks {
   192  		envInfo, err := s.state.GetModel(names.NewModelTag(uuid))
   193  		if err != nil {
   194  			logger.Debugf("Unable to get name for model: %s", uuid)
   195  			continue
   196  		}
   197  		results.Models = append(results.Models, params.ModelBlockInfo{
   198  			UUID:     envInfo.UUID(),
   199  			Name:     envInfo.Name(),
   200  			OwnerTag: envInfo.Owner().String(),
   201  			Blocks:   blocks,
   202  		})
   203  	}
   204  
   205  	// Sort the resulting sequence by environment name, then owner.
   206  	sort.Sort(orderedBlockInfo(results.Models))
   207  
   208  	return results, nil
   209  }
   210  
   211  // ModelConfig returns the environment config for the controller
   212  // environment.  For information on the current environment, use
   213  // client.ModelGet
   214  func (s *ControllerAPI) ModelConfig() (params.ModelConfigResults, error) {
   215  	result := params.ModelConfigResults{}
   216  	if err := s.checkHasAdmin(); err != nil {
   217  		return result, errors.Trace(err)
   218  	}
   219  
   220  	controllerModel, err := s.state.ControllerModel()
   221  	if err != nil {
   222  		return result, errors.Trace(err)
   223  	}
   224  
   225  	cfg, err := controllerModel.Config()
   226  	if err != nil {
   227  		return result, errors.Trace(err)
   228  	}
   229  
   230  	result.Config = make(map[string]params.ConfigValue)
   231  	for name, val := range cfg.AllAttrs() {
   232  		result.Config[name] = params.ConfigValue{
   233  			Value: val,
   234  		}
   235  	}
   236  	return result, nil
   237  }
   238  
   239  // HostedModelConfigs returns all the information that the client needs in
   240  // order to connect directly with the host model's provider and destroy it
   241  // directly.
   242  func (s *ControllerAPI) HostedModelConfigs() (params.HostedModelConfigsResults, error) {
   243  	result := params.HostedModelConfigsResults{}
   244  	if err := s.checkHasAdmin(); err != nil {
   245  		return result, errors.Trace(err)
   246  	}
   247  
   248  	controllerModel, err := s.state.ControllerModel()
   249  	if err != nil {
   250  		return result, errors.Trace(err)
   251  	}
   252  
   253  	allModels, err := s.state.AllModels()
   254  	if err != nil {
   255  		return result, errors.Trace(err)
   256  	}
   257  
   258  	for _, model := range allModels {
   259  		if model.UUID() != controllerModel.UUID() {
   260  			config := params.HostedModelConfig{
   261  				Name:     model.Name(),
   262  				OwnerTag: model.Owner().String(),
   263  			}
   264  			modelConf, err := model.Config()
   265  			if err != nil {
   266  				config.Error = common.ServerError(err)
   267  			} else {
   268  				config.Config = modelConf.AllAttrs()
   269  			}
   270  			cloudSpec := s.GetCloudSpec(model.ModelTag())
   271  			if config.Error == nil {
   272  				config.CloudSpec = cloudSpec.Result
   273  				config.Error = cloudSpec.Error
   274  			}
   275  			result.Models = append(result.Models, config)
   276  		}
   277  	}
   278  
   279  	return result, nil
   280  }
   281  
   282  // RemoveBlocks removes all the blocks in the controller.
   283  func (s *ControllerAPI) RemoveBlocks(args params.RemoveBlocksArgs) error {
   284  	if err := s.checkHasAdmin(); err != nil {
   285  		return errors.Trace(err)
   286  	}
   287  
   288  	if !args.All {
   289  		return errors.New("not supported")
   290  	}
   291  	return errors.Trace(s.state.RemoveAllBlocksForController())
   292  }
   293  
   294  // WatchAllModels starts watching events for all models in the
   295  // controller. The returned AllWatcherId should be used with Next on the
   296  // AllModelWatcher endpoint to receive deltas.
   297  func (c *ControllerAPI) WatchAllModels() (params.AllWatcherId, error) {
   298  	if err := c.checkHasAdmin(); err != nil {
   299  		return params.AllWatcherId{}, errors.Trace(err)
   300  	}
   301  	w := c.state.WatchAllModels()
   302  	return params.AllWatcherId{
   303  		AllWatcherId: c.resources.Register(w),
   304  	}, nil
   305  }
   306  
   307  type orderedBlockInfo []params.ModelBlockInfo
   308  
   309  func (o orderedBlockInfo) Len() int {
   310  	return len(o)
   311  }
   312  
   313  func (o orderedBlockInfo) Less(i, j int) bool {
   314  	if o[i].Name < o[j].Name {
   315  		return true
   316  	}
   317  	if o[i].Name > o[j].Name {
   318  		return false
   319  	}
   320  
   321  	if o[i].OwnerTag < o[j].OwnerTag {
   322  		return true
   323  	}
   324  	if o[i].OwnerTag > o[j].OwnerTag {
   325  		return false
   326  	}
   327  
   328  	// Unreachable based on the rules of there not being duplicate
   329  	// environments of the same name for the same owner, but return false
   330  	// instead of panicing.
   331  	return false
   332  }
   333  
   334  // GetControllerAccess returns the level of access the specifed users
   335  // have on the controller.
   336  func (c *ControllerAPI) GetControllerAccess(req params.Entities) (params.UserAccessResults, error) {
   337  	results := params.UserAccessResults{}
   338  	isAdmin, err := c.authorizer.HasPermission(permission.SuperuserAccess, c.state.ControllerTag())
   339  	if err != nil {
   340  		return results, errors.Trace(err)
   341  	}
   342  
   343  	users := req.Entities
   344  	results.Results = make([]params.UserAccessResult, len(users))
   345  	for i, user := range users {
   346  		userTag, err := names.ParseUserTag(user.Tag)
   347  		if err != nil {
   348  			results.Results[i].Error = common.ServerError(err)
   349  			continue
   350  		}
   351  		if !isAdmin && !c.authorizer.AuthOwner(userTag) {
   352  			results.Results[i].Error = common.ServerError(common.ErrPerm)
   353  			continue
   354  		}
   355  		accessInfo, err := c.state.UserAccess(userTag, c.state.ControllerTag())
   356  		if err != nil {
   357  			results.Results[i].Error = common.ServerError(err)
   358  			continue
   359  		}
   360  		results.Results[i].Result = &params.UserAccess{
   361  			Access:  string(accessInfo.Access),
   362  			UserTag: userTag.String()}
   363  	}
   364  	return results, nil
   365  }
   366  
   367  // InitiateMigration attempts to begin the migration of one or
   368  // more models to other controllers.
   369  func (c *ControllerAPI) InitiateMigration(reqArgs params.InitiateMigrationArgs) (
   370  	params.InitiateMigrationResults, error,
   371  ) {
   372  	out := params.InitiateMigrationResults{
   373  		Results: make([]params.InitiateMigrationResult, len(reqArgs.Specs)),
   374  	}
   375  	if err := c.checkHasAdmin(); err != nil {
   376  		return out, errors.Trace(err)
   377  	}
   378  
   379  	for i, spec := range reqArgs.Specs {
   380  		result := &out.Results[i]
   381  		result.ModelTag = spec.ModelTag
   382  		id, err := c.initiateOneMigration(spec)
   383  		if err != nil {
   384  			result.Error = common.ServerError(err)
   385  		} else {
   386  			result.MigrationId = id
   387  		}
   388  	}
   389  	return out, nil
   390  }
   391  
   392  func (c *ControllerAPI) initiateOneMigration(spec params.MigrationSpec) (string, error) {
   393  	modelTag, err := names.ParseModelTag(spec.ModelTag)
   394  	if err != nil {
   395  		return "", errors.Annotate(err, "model tag")
   396  	}
   397  
   398  	// Ensure the model exists.
   399  	if _, err := c.state.GetModel(modelTag); err != nil {
   400  		return "", errors.Annotate(err, "unable to read model")
   401  	}
   402  
   403  	hostedState, err := c.state.ForModel(modelTag)
   404  	if err != nil {
   405  		return "", errors.Trace(err)
   406  	}
   407  	defer hostedState.Close()
   408  
   409  	// Construct target info.
   410  	specTarget := spec.TargetInfo
   411  	controllerTag, err := names.ParseControllerTag(specTarget.ControllerTag)
   412  	if err != nil {
   413  		return "", errors.Annotate(err, "controller tag")
   414  	}
   415  	authTag, err := names.ParseUserTag(specTarget.AuthTag)
   416  	if err != nil {
   417  		return "", errors.Annotate(err, "auth tag")
   418  	}
   419  	var macs []macaroon.Slice
   420  	if specTarget.Macaroons != "" {
   421  		if err := json.Unmarshal([]byte(specTarget.Macaroons), &macs); err != nil {
   422  			return "", errors.Annotate(err, "invalid macaroons")
   423  		}
   424  	}
   425  	targetInfo := coremigration.TargetInfo{
   426  		ControllerTag: controllerTag,
   427  		Addrs:         specTarget.Addrs,
   428  		CACert:        specTarget.CACert,
   429  		AuthTag:       authTag,
   430  		Password:      specTarget.Password,
   431  		Macaroons:     macs,
   432  	}
   433  
   434  	// Check if the migration is likely to succeed.
   435  	if !(spec.ExternalControl && spec.SkipInitialPrechecks) {
   436  		if err := runMigrationPrechecks(hostedState, targetInfo); err != nil {
   437  			return "", errors.Trace(err)
   438  		}
   439  	}
   440  
   441  	// Trigger the migration.
   442  	mig, err := hostedState.CreateMigration(state.MigrationSpec{
   443  		InitiatedBy:     c.apiUser,
   444  		TargetInfo:      targetInfo,
   445  		ExternalControl: spec.ExternalControl,
   446  	})
   447  	if err != nil {
   448  		return "", errors.Trace(err)
   449  	}
   450  	return mig.Id(), nil
   451  }
   452  
   453  // ModifyControllerAccess changes the model access granted to users.
   454  func (c *ControllerAPI) ModifyControllerAccess(args params.ModifyControllerAccessRequest) (params.ErrorResults, error) {
   455  	result := params.ErrorResults{
   456  		Results: make([]params.ErrorResult, len(args.Changes)),
   457  	}
   458  	if len(args.Changes) == 0 {
   459  		return result, nil
   460  	}
   461  
   462  	hasPermission, err := c.authorizer.HasPermission(permission.SuperuserAccess, c.state.ControllerTag())
   463  	if err != nil {
   464  		return result, errors.Trace(err)
   465  	}
   466  
   467  	for i, arg := range args.Changes {
   468  		if !hasPermission {
   469  			result.Results[i].Error = common.ServerError(common.ErrPerm)
   470  			continue
   471  		}
   472  
   473  		controllerAccess := permission.Access(arg.Access)
   474  		if err := permission.ValidateControllerAccess(controllerAccess); err != nil {
   475  			result.Results[i].Error = common.ServerError(err)
   476  			continue
   477  		}
   478  
   479  		targetUserTag, err := names.ParseUserTag(arg.UserTag)
   480  		if err != nil {
   481  			result.Results[i].Error = common.ServerError(errors.Annotate(err, "could not modify controller access"))
   482  			continue
   483  		}
   484  
   485  		result.Results[i].Error = common.ServerError(
   486  			ChangeControllerAccess(c.state, c.apiUser, targetUserTag, arg.Action, controllerAccess))
   487  	}
   488  	return result, nil
   489  }
   490  
   491  var runMigrationPrechecks = func(st *state.State, targetInfo coremigration.TargetInfo) error {
   492  	// Check model and source controller.
   493  	if err := migration.SourcePrecheck(migration.PrecheckShim(st)); err != nil {
   494  		return errors.Annotate(err, "source prechecks failed")
   495  	}
   496  
   497  	// Check target controller.
   498  	conn, err := api.Open(targetToAPIInfo(targetInfo), migration.ControllerDialOpts())
   499  	if err != nil {
   500  		return errors.Annotate(err, "connect to target controller")
   501  	}
   502  	defer conn.Close()
   503  	modelInfo, err := makeModelInfo(st)
   504  	if err != nil {
   505  		return errors.Trace(err)
   506  	}
   507  	err = migrationtarget.NewClient(conn).Prechecks(modelInfo)
   508  	return errors.Annotate(err, "target prechecks failed")
   509  }
   510  
   511  func makeModelInfo(st *state.State) (coremigration.ModelInfo, error) {
   512  	var empty coremigration.ModelInfo
   513  
   514  	model, err := st.Model()
   515  	if err != nil {
   516  		return empty, errors.Trace(err)
   517  	}
   518  	conf, err := st.ModelConfig()
   519  	if err != nil {
   520  		return empty, errors.Trace(err)
   521  	}
   522  	agentVersion, _ := conf.AgentVersion()
   523  	return coremigration.ModelInfo{
   524  		UUID:         model.UUID(),
   525  		Name:         model.Name(),
   526  		Owner:        model.Owner(),
   527  		AgentVersion: agentVersion,
   528  	}, nil
   529  }
   530  
   531  func targetToAPIInfo(ti coremigration.TargetInfo) *api.Info {
   532  	return &api.Info{
   533  		Addrs:     ti.Addrs,
   534  		CACert:    ti.CACert,
   535  		Tag:       ti.AuthTag,
   536  		Password:  ti.Password,
   537  		Macaroons: ti.Macaroons,
   538  	}
   539  }
   540  
   541  func grantControllerAccess(accessor *state.State, targetUserTag, apiUser names.UserTag, access permission.Access) error {
   542  	_, err := accessor.AddControllerUser(state.UserAccessSpec{User: targetUserTag, CreatedBy: apiUser, Access: access})
   543  	if errors.IsAlreadyExists(err) {
   544  		controllerTag := accessor.ControllerTag()
   545  		controllerUser, err := accessor.UserAccess(targetUserTag, controllerTag)
   546  		if errors.IsNotFound(err) {
   547  			// Conflicts with prior check, must be inconsistent state.
   548  			err = txn.ErrExcessiveContention
   549  		}
   550  		if err != nil {
   551  			return errors.Annotate(err, "could not look up controller access for user")
   552  		}
   553  
   554  		// Only set access if greater access is being granted.
   555  		if controllerUser.Access.EqualOrGreaterControllerAccessThan(access) {
   556  			return errors.Errorf("user already has %q access or greater", access)
   557  		}
   558  		if _, err = accessor.SetUserAccess(controllerUser.UserTag, controllerUser.Object, access); err != nil {
   559  			return errors.Annotate(err, "could not set controller access for user")
   560  		}
   561  		return nil
   562  
   563  	}
   564  	if err != nil {
   565  		return errors.Trace(err)
   566  	}
   567  	return nil
   568  }
   569  
   570  func revokeControllerAccess(accessor *state.State, targetUserTag, apiUser names.UserTag, access permission.Access) error {
   571  	controllerTag := accessor.ControllerTag()
   572  	switch access {
   573  	case permission.LoginAccess:
   574  		// Revoking login access removes all access.
   575  		err := accessor.RemoveUserAccess(targetUserTag, controllerTag)
   576  		return errors.Annotate(err, "could not revoke controller access")
   577  	case permission.AddModelAccess:
   578  		// Revoking add-model access sets login.
   579  		controllerUser, err := accessor.UserAccess(targetUserTag, controllerTag)
   580  		if err != nil {
   581  			return errors.Annotate(err, "could not look up controller access for user")
   582  		}
   583  		_, err = accessor.SetUserAccess(controllerUser.UserTag, controllerUser.Object, permission.LoginAccess)
   584  		return errors.Annotate(err, "could not set controller access to read-only")
   585  	case permission.SuperuserAccess:
   586  		// Revoking superuser sets add-model.
   587  		controllerUser, err := accessor.UserAccess(targetUserTag, controllerTag)
   588  		if err != nil {
   589  			return errors.Annotate(err, "could not look up controller access for user")
   590  		}
   591  		_, err = accessor.SetUserAccess(controllerUser.UserTag, controllerUser.Object, permission.AddModelAccess)
   592  		return errors.Annotate(err, "could not set controller access to add-model")
   593  
   594  	default:
   595  		return errors.Errorf("don't know how to revoke %q access", access)
   596  	}
   597  
   598  }
   599  
   600  // ChangeControllerAccess performs the requested access grant or revoke action for the
   601  // specified user on the controller.
   602  func ChangeControllerAccess(accessor *state.State, apiUser, targetUserTag names.UserTag, action params.ControllerAction, access permission.Access) error {
   603  	switch action {
   604  	case params.GrantControllerAccess:
   605  		err := grantControllerAccess(accessor, targetUserTag, apiUser, access)
   606  		if err != nil {
   607  			return errors.Annotate(err, "could not grant controller access")
   608  		}
   609  		return nil
   610  	case params.RevokeControllerAccess:
   611  		return revokeControllerAccess(accessor, targetUserTag, apiUser, access)
   612  	default:
   613  		return errors.Errorf("unknown action %q", action)
   614  	}
   615  }
   616  
   617  func (o orderedBlockInfo) Swap(i, j int) {
   618  	o[i], o[j] = o[j], o[i]
   619  }
   620  
   621  type orderedUserModels []params.UserModel
   622  
   623  func (o orderedUserModels) Len() int {
   624  	return len(o)
   625  }
   626  
   627  func (o orderedUserModels) Less(i, j int) bool {
   628  	if o[i].Name < o[j].Name {
   629  		return true
   630  	}
   631  	if o[i].Name > o[j].Name {
   632  		return false
   633  	}
   634  
   635  	if o[i].OwnerTag < o[j].OwnerTag {
   636  		return true
   637  	}
   638  	if o[i].OwnerTag > o[j].OwnerTag {
   639  		return false
   640  	}
   641  
   642  	// Unreachable based on the rules of there not being duplicate
   643  	// environments of the same name for the same owner, but return false
   644  	// instead of panicing.
   645  	return false
   646  }
   647  
   648  func (o orderedUserModels) Swap(i, j int) {
   649  	o[i], o[j] = o[j], o[i]
   650  }