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

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package common
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  	"github.com/juju/version/v2"
    13  
    14  	apiservererrors "github.com/juju/juju/apiserver/errors"
    15  	"github.com/juju/juju/controller"
    16  	"github.com/juju/juju/core/network"
    17  	"github.com/juju/juju/environs"
    18  	"github.com/juju/juju/environs/simplestreams"
    19  	envtools "github.com/juju/juju/environs/tools"
    20  	"github.com/juju/juju/rpc/params"
    21  	"github.com/juju/juju/state"
    22  	"github.com/juju/juju/state/binarystorage"
    23  	coretools "github.com/juju/juju/tools"
    24  )
    25  
    26  var envtoolsFindTools = envtools.FindTools
    27  
    28  type ToolsFindEntity interface {
    29  	FindEntity(tag names.Tag) (state.Entity, error)
    30  }
    31  
    32  // ToolsURLGetter is an interface providing the ToolsURL method.
    33  type ToolsURLGetter interface {
    34  	// ToolsURLs returns URLs for the tools with
    35  	// the specified binary version.
    36  	ToolsURLs(v version.Binary) ([]string, error)
    37  }
    38  
    39  // APIHostPortsForAgentsGetter is an interface providing
    40  // the APIHostPortsForAgents method.
    41  type APIHostPortsForAgentsGetter interface {
    42  	// APIHostPortsForAgents returns the HostPorts for each API server that
    43  	// are suitable for agent-to-controller API communication based on the
    44  	// configured (if any) controller management space.
    45  	APIHostPortsForAgents() ([]network.SpaceHostPorts, error)
    46  }
    47  
    48  // ToolsStorageGetter is an interface providing the ToolsStorage method.
    49  type ToolsStorageGetter interface {
    50  	// ToolsStorage returns a binarystorage.StorageCloser.
    51  	ToolsStorage() (binarystorage.StorageCloser, error)
    52  }
    53  
    54  // AgentTooler is implemented by entities
    55  // that have associated agent tools.
    56  type AgentTooler interface {
    57  	AgentTools() (*coretools.Tools, error)
    58  	SetAgentVersion(version.Binary) error
    59  
    60  	// Tag is included in this interface only so the generated mock of
    61  	// AgentTooler implements state.Entity, returned by FindEntity
    62  	Tag() names.Tag
    63  }
    64  
    65  // ToolsGetter implements a common Tools method for use by various
    66  // facades.
    67  type ToolsGetter struct {
    68  	entityFinder       ToolsFindEntity
    69  	configGetter       environs.EnvironConfigGetter
    70  	toolsStorageGetter ToolsStorageGetter
    71  	toolsFinder        ToolsFinder
    72  	urlGetter          ToolsURLGetter
    73  	getCanRead         GetAuthFunc
    74  }
    75  
    76  // NewToolsGetter returns a new ToolsGetter. The GetAuthFunc will be
    77  // used on each invocation of Tools to determine current permissions.
    78  func NewToolsGetter(
    79  	entityFinder ToolsFindEntity,
    80  	configGetter environs.EnvironConfigGetter,
    81  	toolsStorageGetter ToolsStorageGetter,
    82  	urlGetter ToolsURLGetter,
    83  	toolsFinder ToolsFinder,
    84  	getCanRead GetAuthFunc,
    85  ) *ToolsGetter {
    86  	return &ToolsGetter{
    87  		entityFinder:       entityFinder,
    88  		configGetter:       configGetter,
    89  		toolsStorageGetter: toolsStorageGetter,
    90  		urlGetter:          urlGetter,
    91  		toolsFinder:        toolsFinder,
    92  		getCanRead:         getCanRead,
    93  	}
    94  }
    95  
    96  // Tools finds the tools necessary for the given agents.
    97  func (t *ToolsGetter) Tools(args params.Entities) (params.ToolsResults, error) {
    98  	result := params.ToolsResults{
    99  		Results: make([]params.ToolsResult, len(args.Entities)),
   100  	}
   101  	canRead, err := t.getCanRead()
   102  	if err != nil {
   103  		return result, err
   104  	}
   105  	agentVersion, err := t.getGlobalAgentVersion()
   106  	if err != nil {
   107  		return result, err
   108  	}
   109  
   110  	for i, entity := range args.Entities {
   111  		tag, err := names.ParseTag(entity.Tag)
   112  		if err != nil {
   113  			result.Results[i].Error = apiservererrors.ServerError(apiservererrors.ErrPerm)
   114  			continue
   115  		}
   116  		agentToolsList, err := t.oneAgentTools(canRead, tag, agentVersion)
   117  		if err == nil {
   118  			result.Results[i].ToolsList = agentToolsList
   119  		}
   120  		result.Results[i].Error = apiservererrors.ServerError(err)
   121  	}
   122  	return result, nil
   123  }
   124  
   125  func (t *ToolsGetter) getGlobalAgentVersion() (version.Number, error) {
   126  	// Get the Agent Version requested in the Model Config
   127  	nothing := version.Number{}
   128  	cfg, err := t.configGetter.ModelConfig()
   129  	if err != nil {
   130  		return nothing, err
   131  	}
   132  	agentVersion, ok := cfg.AgentVersion()
   133  	if !ok {
   134  		return nothing, errors.New("agent version not set in model config")
   135  	}
   136  	return agentVersion, nil
   137  }
   138  
   139  func (t *ToolsGetter) oneAgentTools(canRead AuthFunc, tag names.Tag, agentVersion version.Number) (coretools.List, error) {
   140  	if !canRead(tag) {
   141  		return nil, apiservererrors.ErrPerm
   142  	}
   143  	entity, err := t.entityFinder.FindEntity(tag)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	tooler, ok := entity.(AgentTooler)
   148  	if !ok {
   149  		return nil, apiservererrors.NotSupportedError(tag, "agent binaries")
   150  	}
   151  	existingTools, err := tooler.AgentTools()
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	findParams := FindAgentsParams{
   157  		Number: agentVersion,
   158  		OSType: existingTools.Version.Release,
   159  		Arch:   existingTools.Version.Arch,
   160  	}
   161  
   162  	return t.toolsFinder.FindAgents(findParams)
   163  }
   164  
   165  // ToolsSetter implements a common Tools method for use by various
   166  // facades.
   167  type ToolsSetter struct {
   168  	st          ToolsFindEntity
   169  	getCanWrite GetAuthFunc
   170  }
   171  
   172  // NewToolsSetter returns a new ToolsGetter. The GetAuthFunc will be
   173  // used on each invocation of Tools to determine current permissions.
   174  func NewToolsSetter(st ToolsFindEntity, getCanWrite GetAuthFunc) *ToolsSetter {
   175  	return &ToolsSetter{
   176  		st:          st,
   177  		getCanWrite: getCanWrite,
   178  	}
   179  }
   180  
   181  // SetTools updates the recorded tools version for the agents.
   182  func (t *ToolsSetter) SetTools(args params.EntitiesVersion) (params.ErrorResults, error) {
   183  	results := params.ErrorResults{
   184  		Results: make([]params.ErrorResult, len(args.AgentTools)),
   185  	}
   186  	canWrite, err := t.getCanWrite()
   187  	if err != nil {
   188  		return results, errors.Trace(err)
   189  	}
   190  	for i, agentTools := range args.AgentTools {
   191  		tag, err := names.ParseTag(agentTools.Tag)
   192  		if err != nil {
   193  			results.Results[i].Error = apiservererrors.ServerError(apiservererrors.ErrPerm)
   194  			continue
   195  		}
   196  		err = t.setOneAgentVersion(tag, agentTools.Tools.Version, canWrite)
   197  		results.Results[i].Error = apiservererrors.ServerError(err)
   198  	}
   199  	return results, nil
   200  }
   201  
   202  func (t *ToolsSetter) setOneAgentVersion(tag names.Tag, vers version.Binary, canWrite AuthFunc) error {
   203  	if !canWrite(tag) {
   204  		return apiservererrors.ErrPerm
   205  	}
   206  	entity0, err := t.st.FindEntity(tag)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	entity, ok := entity0.(AgentTooler)
   211  	if !ok {
   212  		return apiservererrors.NotSupportedError(tag, "agent binaries")
   213  	}
   214  	return entity.SetAgentVersion(vers)
   215  }
   216  
   217  // FindAgentsParams defines parameters for the FindAgents method.
   218  type FindAgentsParams struct {
   219  	// ControllerCfg is the controller config.
   220  	ControllerCfg controller.Config
   221  
   222  	// ModelType is the type of the model.
   223  	ModelType state.ModelType
   224  
   225  	// Number will be used to match tools versions exactly if non-zero.
   226  	Number version.Number
   227  
   228  	// MajorVersion will be used to match the major version if non-zero.
   229  	MajorVersion int
   230  
   231  	// MinorVersion will be used to match the minor version if non-zero.
   232  	MinorVersion int
   233  
   234  	// Arch will be used to match tools by architecture if non-empty.
   235  	Arch string
   236  
   237  	// OSType will be used to match tools by os type if non-empty.
   238  	OSType string
   239  
   240  	// AgentStream will be used to set agent stream to search
   241  	AgentStream string
   242  }
   243  
   244  // ToolsFinder defines methods for finding tools.
   245  type ToolsFinder interface {
   246  	FindAgents(args FindAgentsParams) (coretools.List, error)
   247  }
   248  
   249  type toolsFinder struct {
   250  	configGetter       environs.EnvironConfigGetter
   251  	toolsStorageGetter ToolsStorageGetter
   252  	urlGetter          ToolsURLGetter
   253  	newEnviron         NewEnvironFunc
   254  }
   255  
   256  // NewToolsFinder returns a new ToolsFinder, returning tools
   257  // with their URLs pointing at the API server.
   258  func NewToolsFinder(
   259  	configGetter environs.EnvironConfigGetter,
   260  	toolsStorageGetter ToolsStorageGetter,
   261  	urlGetter ToolsURLGetter,
   262  	newEnviron NewEnvironFunc,
   263  ) *toolsFinder {
   264  	return &toolsFinder{configGetter, toolsStorageGetter, urlGetter, newEnviron}
   265  }
   266  
   267  // FindAgents calls findMatchingTools and then rewrites the URLs
   268  // using the provided ToolsURLGetter.
   269  func (f *toolsFinder) FindAgents(args FindAgentsParams) (coretools.List, error) {
   270  	list, err := f.findMatchingAgents(args)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	// Rewrite the URLs so they point at the API servers. If the
   276  	// tools are not in tools storage, then the API server will
   277  	// download and cache them if the client requests that version.
   278  	var fullList coretools.List
   279  	for _, baseTools := range list {
   280  		urls, err := f.urlGetter.ToolsURLs(baseTools.Version)
   281  		if err != nil {
   282  			return nil, err
   283  		}
   284  		for _, url := range urls {
   285  			tools := *baseTools
   286  			tools.URL = url
   287  			fullList = append(fullList, &tools)
   288  		}
   289  	}
   290  	return fullList, nil
   291  }
   292  
   293  // findMatchingAgents searches agent storage and simplestreams for agents
   294  // matching the given parameters.
   295  // If an exact match is specified (number, ostype and arch) and is found in
   296  // agent storage, then simplestreams will not be searched.
   297  func (f *toolsFinder) findMatchingAgents(args FindAgentsParams) (result coretools.List, _ error) {
   298  	exactMatch := args.Number != version.Zero && args.OSType != "" && args.Arch != ""
   299  
   300  	storageList, err := f.matchingStorageAgent(args)
   301  	if err != nil && err != coretools.ErrNoMatches {
   302  		return nil, err
   303  	}
   304  	if len(storageList) > 0 && exactMatch {
   305  		return storageList, nil
   306  	}
   307  
   308  	// Look for tools in simplestreams too, but don't replace
   309  	// any versions found in storage.
   310  	env, err := f.newEnviron()
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  	filter := toolsFilter(args)
   315  	cfg := env.Config()
   316  	requestedStream := cfg.AgentStream()
   317  	if args.AgentStream != "" {
   318  		requestedStream = args.AgentStream
   319  	}
   320  
   321  	streams := envtools.PreferredStreams(&args.Number, cfg.Development(), requestedStream)
   322  	ss := simplestreams.NewSimpleStreams(simplestreams.DefaultDataSourceFactory())
   323  	majorVersion := args.Number.Major
   324  	minorVersion := args.Number.Minor
   325  	if args.Number == version.Zero {
   326  		majorVersion = args.MajorVersion
   327  		minorVersion = args.MinorVersion
   328  	}
   329  	simplestreamsList, err := envtoolsFindTools(ss,
   330  		env, majorVersion, minorVersion, streams, filter,
   331  	)
   332  	if len(storageList) == 0 && err != nil {
   333  		return nil, err
   334  	}
   335  
   336  	list := storageList
   337  	found := make(map[version.Binary]bool)
   338  	for _, tools := range storageList {
   339  		found[tools.Version] = true
   340  	}
   341  	for _, tools := range simplestreamsList {
   342  		if !found[tools.Version] {
   343  			list = append(list, tools)
   344  		}
   345  	}
   346  	sort.Sort(list)
   347  	return list, nil
   348  }
   349  
   350  // matchingStorageAgent returns a coretools.List, with an entry for each
   351  // metadata entry in the agent storage that matches the given parameters.
   352  func (f *toolsFinder) matchingStorageAgent(args FindAgentsParams) (coretools.List, error) {
   353  	storage, err := f.toolsStorageGetter.ToolsStorage()
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  	defer func() { _ = storage.Close() }()
   358  
   359  	allMetadata, err := storage.AllMetadata()
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	list := make(coretools.List, len(allMetadata))
   364  	for i, m := range allMetadata {
   365  		vers, err := version.ParseBinary(m.Version)
   366  		if err != nil {
   367  			return nil, errors.Annotatef(err, "unexpected bad version %q of agent binary in storage", m.Version)
   368  		}
   369  		list[i] = &coretools.Tools{
   370  			Version: vers,
   371  			Size:    m.Size,
   372  			SHA256:  m.SHA256,
   373  		}
   374  	}
   375  	list, err = list.Match(toolsFilter(args))
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  	// Return early if we are doing an exact match.
   380  	if args.Number != version.Zero {
   381  		if len(list) == 0 {
   382  			return nil, coretools.ErrNoMatches
   383  		}
   384  		return list, nil
   385  	}
   386  	// At this point, we are matching just on major or minor version
   387  	// rather than an exact match.
   388  	var matching coretools.List
   389  	for _, tools := range list {
   390  		if tools.Version.Major != args.MajorVersion {
   391  			continue
   392  		}
   393  		if args.MinorVersion > 0 && tools.Version.Minor != args.MinorVersion {
   394  			continue
   395  		}
   396  		matching = append(matching, tools)
   397  	}
   398  	if len(matching) == 0 {
   399  		return nil, coretools.ErrNoMatches
   400  	}
   401  	return matching, nil
   402  }
   403  
   404  func toolsFilter(args FindAgentsParams) coretools.Filter {
   405  	return coretools.Filter{
   406  		Number: args.Number,
   407  		Arch:   args.Arch,
   408  		OSType: args.OSType,
   409  	}
   410  }
   411  
   412  type toolsURLGetter struct {
   413  	modelUUID          string
   414  	apiHostPortsGetter APIHostPortsForAgentsGetter
   415  }
   416  
   417  // NewToolsURLGetter creates a new ToolsURLGetter that
   418  // returns tools URLs pointing at an API server.
   419  func NewToolsURLGetter(modelUUID string, a APIHostPortsForAgentsGetter) *toolsURLGetter {
   420  	return &toolsURLGetter{modelUUID, a}
   421  }
   422  
   423  func (t *toolsURLGetter) ToolsURLs(v version.Binary) ([]string, error) {
   424  	addrs, err := apiAddresses(t.apiHostPortsGetter)
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  	if len(addrs) == 0 {
   429  		return nil, errors.Errorf("no suitable API server address to pick from")
   430  	}
   431  	var urls []string
   432  	for _, addr := range addrs {
   433  		serverRoot := fmt.Sprintf("https://%s/model/%s", addr, t.modelUUID)
   434  		url := ToolsURL(serverRoot, v)
   435  		urls = append(urls, url)
   436  	}
   437  	return urls, nil
   438  }
   439  
   440  // ToolsURL returns a tools URL pointing the API server
   441  // specified by the "serverRoot".
   442  func ToolsURL(serverRoot string, v version.Binary) string {
   443  	return fmt.Sprintf("%s/tools/%s", serverRoot, v.String())
   444  }