github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/modelgeneration/modelgeneration.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package modelgeneration
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/collections/set"
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  
    13  	"github.com/juju/juju/apiserver/authentication"
    14  	apiservererrors "github.com/juju/juju/apiserver/errors"
    15  	"github.com/juju/juju/apiserver/facade"
    16  	"github.com/juju/juju/core/model"
    17  	"github.com/juju/juju/core/permission"
    18  	"github.com/juju/juju/rpc/params"
    19  )
    20  
    21  // API is the concrete implementation of the API endpoint.
    22  type API struct {
    23  	authorizer facade.Authorizer
    24  	apiUser    names.UserTag
    25  	st         State
    26  	model      Model
    27  	modelCache ModelCache
    28  }
    29  
    30  // NewModelGenerationAPI creates a new API endpoint for dealing with model generations.
    31  func NewModelGenerationAPI(
    32  	st State,
    33  	authorizer facade.Authorizer,
    34  	m Model,
    35  	mc ModelCache,
    36  ) (*API, error) {
    37  	if !authorizer.AuthClient() {
    38  		return nil, apiservererrors.ErrPerm
    39  	}
    40  	// Since we know this is a user tag (because AuthClient is true),
    41  	// we just do the type assertion to the UserTag.
    42  	apiUser, _ := authorizer.GetAuthTag().(names.UserTag)
    43  
    44  	return &API{
    45  		authorizer: authorizer,
    46  		apiUser:    apiUser,
    47  		st:         st,
    48  		model:      m,
    49  		modelCache: mc,
    50  	}, nil
    51  }
    52  
    53  func (api *API) hasAdminAccess() error {
    54  	// We used to cache the result on the api object if the user was a superuser.
    55  	// We don't do that anymore as permission caching could become invalid
    56  	// for long lived connections.
    57  	err := api.authorizer.HasPermission(permission.SuperuserAccess, api.st.ControllerTag())
    58  	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
    59  		return err
    60  	}
    61  
    62  	if err == nil {
    63  		return nil
    64  	}
    65  
    66  	return api.authorizer.HasPermission(permission.AdminAccess, api.model.ModelTag())
    67  }
    68  
    69  // AddBranch adds a new branch with the input name to the model.
    70  func (api *API) AddBranch(arg params.BranchArg) (params.ErrorResult, error) {
    71  	result := params.ErrorResult{}
    72  	if err := api.hasAdminAccess(); err != nil {
    73  		return result, err
    74  	}
    75  
    76  	if err := model.ValidateBranchName(arg.BranchName); err != nil {
    77  		result.Error = apiservererrors.ServerError(err)
    78  	} else {
    79  		result.Error = apiservererrors.ServerError(api.model.AddBranch(arg.BranchName, api.apiUser.Name()))
    80  	}
    81  	return result, nil
    82  }
    83  
    84  // TrackBranch marks the input units and/or applications as tracking the input
    85  // branch, causing them to realise changes made under that branch.
    86  func (api *API) TrackBranch(arg params.BranchTrackArg) (params.ErrorResults, error) {
    87  	if err := api.hasAdminAccess(); err != nil {
    88  		return params.ErrorResults{}, err
    89  	}
    90  
    91  	// Ensure we guard against the numUnits being greater than 0 and the number
    92  	// units/applications greater than 1. This is because we don't know how to
    93  	// topographically distribute between all the applications and units,
    94  	// especially if an error occurs whilst assigning the units.
    95  	if arg.NumUnits > 0 && len(arg.Entities) > 1 {
    96  		return params.ErrorResults{}, errors.Errorf("number of units and unit IDs can not be specified at the same time")
    97  	}
    98  
    99  	branch, err := api.model.Branch(arg.BranchName)
   100  	if err != nil {
   101  		return params.ErrorResults{}, errors.Trace(err)
   102  	}
   103  
   104  	result := params.ErrorResults{
   105  		Results: make([]params.ErrorResult, len(arg.Entities)),
   106  	}
   107  	for i, entity := range arg.Entities {
   108  		tag, err := names.ParseTag(entity.Tag)
   109  		if err != nil {
   110  			result.Results[i].Error = apiservererrors.ServerError(err)
   111  			continue
   112  		}
   113  		switch tag.Kind() {
   114  		case names.ApplicationTagKind:
   115  			result.Results[i].Error = apiservererrors.ServerError(branch.AssignUnits(tag.Id(), arg.NumUnits))
   116  		case names.UnitTagKind:
   117  			result.Results[i].Error = apiservererrors.ServerError(branch.AssignUnit(tag.Id()))
   118  		default:
   119  			result.Results[i].Error = apiservererrors.ServerError(
   120  				errors.Errorf("expected names.UnitTag or names.ApplicationTag, got %T", tag))
   121  		}
   122  	}
   123  	return result, nil
   124  }
   125  
   126  // CommitBranch commits the input branch, making its changes applicable to
   127  // the whole model and marking it complete.
   128  func (api *API) CommitBranch(arg params.BranchArg) (params.IntResult, error) {
   129  	result := params.IntResult{}
   130  
   131  	if err := api.hasAdminAccess(); err != nil {
   132  		return result, err
   133  	}
   134  
   135  	branch, err := api.model.Branch(arg.BranchName)
   136  	if err != nil {
   137  		return intResultsError(err)
   138  	}
   139  
   140  	if genId, err := branch.Commit(api.apiUser.Name()); err != nil {
   141  		result.Error = apiservererrors.ServerError(err)
   142  	} else {
   143  		result.Result = genId
   144  	}
   145  	return result, nil
   146  }
   147  
   148  // AbortBranch aborts the input branch, marking it complete.  However no
   149  // changes are made applicable to the whole model.  No units may be assigned
   150  // to the branch when aborting.
   151  func (api *API) AbortBranch(arg params.BranchArg) (params.ErrorResult, error) {
   152  	result := params.ErrorResult{}
   153  
   154  	if err := api.hasAdminAccess(); err != nil {
   155  		return result, err
   156  	}
   157  
   158  	branch, err := api.model.Branch(arg.BranchName)
   159  	if err != nil {
   160  		result.Error = apiservererrors.ServerError(err)
   161  		return result, nil
   162  	}
   163  
   164  	if err := branch.Abort(api.apiUser.Name()); err != nil {
   165  		result.Error = apiservererrors.ServerError(err)
   166  	}
   167  	return result, nil
   168  }
   169  
   170  // BranchInfo will return details of branch identified by the input argument,
   171  // including units on the branch and the configuration disjoint with the
   172  // master generation.
   173  // An error is returned if no in-flight branch matching in input is found.
   174  func (api *API) BranchInfo(
   175  	args params.BranchInfoArgs) (params.BranchResults, error) {
   176  	result := params.BranchResults{}
   177  
   178  	if err := api.hasAdminAccess(); err != nil {
   179  		return result, err
   180  	}
   181  
   182  	// From clients, we expect a single branch name or none,
   183  	// but we accommodate any number - they all must exist to avoid an error.
   184  	// If no branch is supplied, get them all.
   185  	var err error
   186  	var branches []Generation
   187  	if len(args.BranchNames) > 0 {
   188  		branches = make([]Generation, len(args.BranchNames))
   189  		for i, name := range args.BranchNames {
   190  			if branches[i], err = api.model.Branch(name); err != nil {
   191  				return branchResultsError(err)
   192  			}
   193  		}
   194  	} else {
   195  		if branches, err = api.model.Branches(); err != nil {
   196  			return branchResultsError(err)
   197  		}
   198  	}
   199  
   200  	results := make([]params.Generation, len(branches))
   201  	for i, b := range branches {
   202  		if results[i], err = api.oneBranchInfo(b, args.Detailed); err != nil {
   203  			return branchResultsError(err)
   204  		}
   205  	}
   206  	result.Generations = results
   207  	return result, nil
   208  }
   209  
   210  // ShowCommit will return details a commit given by its generationId
   211  // An error is returned if either no branch can be found corresponding to the generation id.
   212  // Or the generation id given is below 1.
   213  func (api *API) ShowCommit(arg params.GenerationId) (params.GenerationResult, error) {
   214  	result := params.GenerationResult{}
   215  
   216  	if err := api.hasAdminAccess(); err != nil {
   217  		return result, err
   218  	}
   219  
   220  	if arg.GenerationId < 1 {
   221  		err := errors.Errorf("supplied generation id has to be higher than 0")
   222  		return generationResultError(err)
   223  	}
   224  
   225  	branch, err := api.model.Generation(arg.GenerationId)
   226  	if err != nil {
   227  		result.Error = apiservererrors.ServerError(err)
   228  		return result, nil
   229  	}
   230  
   231  	generationCommit, err := api.getGenerationCommit(branch)
   232  	if err != nil {
   233  		return generationResultError(err)
   234  	}
   235  
   236  	result.Generation = generationCommit
   237  
   238  	return result, nil
   239  }
   240  
   241  // ListCommits will return the commits, hence only branches with generation_id higher than 0
   242  func (api *API) ListCommits() (params.BranchResults, error) {
   243  	var result params.BranchResults
   244  
   245  	if err := api.hasAdminAccess(); err != nil {
   246  		return result, err
   247  	}
   248  
   249  	var err error
   250  	var branches []Generation
   251  	if branches, err = api.model.Generations(); err != nil {
   252  		return branchResultsError(err)
   253  	}
   254  
   255  	results := make([]params.Generation, len(branches))
   256  	for i, b := range branches {
   257  		gen := params.Generation{
   258  			BranchName:   b.BranchName(),
   259  			Completed:    b.Completed(),
   260  			CompletedBy:  b.CompletedBy(),
   261  			GenerationId: b.GenerationId(),
   262  		}
   263  		results[i] = gen
   264  	}
   265  
   266  	result.Generations = results
   267  	return result, nil
   268  }
   269  
   270  func (api *API) oneBranchInfo(branch Generation, detailed bool) (params.Generation, error) {
   271  	deltas := branch.Config()
   272  
   273  	var apps []params.GenerationApplication
   274  	for appName, tracking := range branch.AssignedUnits() {
   275  		app, err := api.st.Application(appName)
   276  		if err != nil {
   277  			return params.Generation{}, errors.Trace(err)
   278  		}
   279  		allUnits, err := app.UnitNames()
   280  		if err != nil {
   281  			return params.Generation{}, errors.Trace(err)
   282  		}
   283  
   284  		branchApp := params.GenerationApplication{
   285  			ApplicationName: appName,
   286  			UnitProgress:    fmt.Sprintf("%d/%d", len(tracking), len(allUnits)),
   287  		}
   288  
   289  		// Determine the effective charm configuration changes.
   290  		defaults, err := app.DefaultCharmConfig()
   291  		if err != nil {
   292  			return params.Generation{}, errors.Trace(err)
   293  		}
   294  		branchApp.ConfigChanges = deltas[appName].EffectiveChanges(defaults)
   295  
   296  		// TODO (manadart 2019-04-12): Charm URL.
   297  
   298  		// TODO (manadart 2019-04-12): Resources.
   299  
   300  		// Only include unit names if detailed info was requested.
   301  		if detailed {
   302  			trackingSet := set.NewStrings(tracking...)
   303  			branchApp.UnitsTracking = trackingSet.SortedValues()
   304  			branchApp.UnitsPending = set.NewStrings(allUnits...).Difference(trackingSet).SortedValues()
   305  		}
   306  
   307  		apps = append(apps, branchApp)
   308  	}
   309  
   310  	return params.Generation{
   311  		BranchName:   branch.BranchName(),
   312  		Created:      branch.Created(),
   313  		CreatedBy:    branch.CreatedBy(),
   314  		Applications: apps,
   315  	}, nil
   316  }
   317  
   318  func (api *API) getGenerationCommit(branch Generation) (params.Generation, error) {
   319  	generation, err := api.oneBranchInfo(branch, true)
   320  	if err != nil {
   321  		return params.Generation{}, errors.Trace(err)
   322  	}
   323  	return params.Generation{
   324  		BranchName:   branch.BranchName(),
   325  		Completed:    branch.Completed(),
   326  		CompletedBy:  branch.CompletedBy(),
   327  		GenerationId: branch.GenerationId(),
   328  		Created:      branch.Created(),
   329  		CreatedBy:    branch.CreatedBy(),
   330  		Applications: generation.Applications,
   331  	}, nil
   332  }
   333  
   334  // HasActiveBranch returns a true result if the input model has an "in-flight"
   335  // branch matching the input name.
   336  func (api *API) HasActiveBranch(arg params.BranchArg) (params.BoolResult, error) {
   337  	result := params.BoolResult{}
   338  	if err := api.hasAdminAccess(); err != nil {
   339  		return result, err
   340  	}
   341  
   342  	if _, err := api.modelCache.Branch(arg.BranchName); err != nil {
   343  		if errors.IsNotFound(err) {
   344  			result.Result = false
   345  		} else {
   346  			result.Error = apiservererrors.ServerError(err)
   347  		}
   348  	} else {
   349  		result.Result = true
   350  	}
   351  	return result, nil
   352  }
   353  
   354  func branchResultsError(err error) (params.BranchResults, error) {
   355  	return params.BranchResults{Error: apiservererrors.ServerError(err)}, nil
   356  }
   357  
   358  func generationResultError(err error) (params.GenerationResult, error) {
   359  	return params.GenerationResult{Error: apiservererrors.ServerError(err)}, nil
   360  }
   361  
   362  func intResultsError(err error) (params.IntResult, error) {
   363  	return params.IntResult{Error: apiservererrors.ServerError(err)}, nil
   364  }