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 }