github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/sync/sync.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package sync 5 6 import ( 7 "bytes" 8 "context" 9 "io" 10 "os" 11 "path/filepath" 12 13 "github.com/juju/errors" 14 "github.com/juju/http/v2" 15 "github.com/juju/loggo" 16 "github.com/juju/utils/v3" 17 "github.com/juju/version/v2" 18 19 "github.com/juju/juju/environs/filestorage" 20 "github.com/juju/juju/environs/simplestreams" 21 "github.com/juju/juju/environs/storage" 22 envtools "github.com/juju/juju/environs/tools" 23 "github.com/juju/juju/juju/keys" 24 coretools "github.com/juju/juju/tools" 25 jujuversion "github.com/juju/juju/version" 26 ) 27 28 var logger = loggo.GetLogger("juju.environs.sync") 29 30 // SyncContext describes the context for tool synchronization. 31 type SyncContext struct { 32 // TargetToolsFinder is a ToolsFinder provided to find existing 33 // tools in the target destination. 34 TargetToolsFinder ToolsFinder 35 36 // TargetToolsUploader is a ToolsUploader provided to upload 37 // tools to the target destination. 38 TargetToolsUploader ToolsUploader 39 40 // AllVersions controls the copy of all versions, not only the latest. 41 // TODO: remove here because it's only used for tests! 42 AllVersions bool 43 44 // DryRun controls that nothing is copied. Instead it's logged 45 // what would be coppied. 46 DryRun bool 47 48 // Stream specifies the simplestreams stream to use (defaults to "Released"). 49 Stream string 50 51 // Source, if non-empty, specifies a directory in the local file system 52 // to use as a source. 53 Source string 54 55 // ChosenVersion is the requested version to upload. 56 ChosenVersion version.Number 57 } 58 59 // ToolsFinder provides an interface for finding tools of a specified version. 60 type ToolsFinder interface { 61 // FindTools returns a list of tools with the specified major version in the specified stream. 62 FindTools(major int, stream string) (coretools.List, error) 63 } 64 65 // ToolsUploader provides an interface for uploading tools and associated 66 // metadata. 67 type ToolsUploader interface { 68 // UploadTools uploads the tools with the specified version and tarball contents. 69 UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error 70 } 71 72 // SyncTools copies the Juju tools tarball from the official bucket 73 // or a specified source directory into the user's environment. 74 func SyncTools(syncContext *SyncContext) error { 75 sourceDataSource, err := selectSourceDatasource(syncContext) 76 if err != nil { 77 return errors.Trace(err) 78 } 79 80 logger.Infof("listing available agent binaries") 81 if syncContext.ChosenVersion.Major == 0 && syncContext.ChosenVersion.Minor == 0 { 82 syncContext.ChosenVersion.Major = jujuversion.Current.Major 83 syncContext.ChosenVersion.Minor = -1 84 if !syncContext.AllVersions { 85 syncContext.ChosenVersion.Minor = jujuversion.Current.Minor 86 } 87 } 88 89 toolsDir := syncContext.Stream 90 // If no stream has been specified, assume "released" for non-devel versions of Juju. 91 if syncContext.Stream == "" { 92 // We now store the tools in a directory named after their stream, but the 93 // legacy behaviour is to store all tools in a single "releases" directory. 94 toolsDir = envtools.ReleasedStream 95 // Always use the primary stream here - the user can specify 96 // to override that decision. 97 syncContext.Stream = envtools.PreferredStreams(&jujuversion.Current, false, "")[0] 98 } 99 // TODO (stickupkid): We should lift this simplestreams constructor out of 100 // this function. 101 ss := simplestreams.NewSimpleStreams(simplestreams.DefaultDataSourceFactory()) 102 // For backwards compatibility with cloud storage, if there are no tools in the specified stream, 103 // double check the release stream. 104 // TODO - remove this when we no longer need to support cloud storage upgrades. 105 streams := []string{syncContext.Stream, envtools.ReleasedStream} 106 sourceTools, err := envtools.FindToolsForCloud( 107 ss, 108 []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, 109 streams, syncContext.ChosenVersion.Major, syncContext.ChosenVersion.Minor, coretools.Filter{}) 110 if err != nil { 111 return errors.Trace(err) 112 } 113 114 logger.Infof("found %d agent binaries", len(sourceTools)) 115 for _, tool := range sourceTools { 116 logger.Debugf("found source agent binary: %v", tool) 117 } 118 119 logger.Infof("listing target agent binaries storage") 120 121 result, err := sourceTools.Match(coretools.Filter{Number: syncContext.ChosenVersion}) 122 logger.Tracef("syncContext.ChosenVersion %s, result %s", syncContext.ChosenVersion, result) 123 if err != nil { 124 return errors.Wrap(err, errors.NotFoundf("%q", syncContext.ChosenVersion)) 125 } 126 _, chosenList := result.Newest() 127 logger.Tracef("syncContext.ChosenVersion %s, chosenList %s", syncContext.ChosenVersion, chosenList) 128 if syncContext.TargetToolsFinder != nil { 129 targetTools, err := syncContext.TargetToolsFinder.FindTools( 130 syncContext.ChosenVersion.Major, syncContext.Stream, 131 ) 132 switch err { 133 case nil, coretools.ErrNoMatches, envtools.ErrNoTools: 134 default: 135 return errors.Trace(err) 136 } 137 for _, tool := range targetTools { 138 logger.Debugf("found target agent binary: %v", tool) 139 } 140 if targetTools.Exclude(chosenList).Len() != targetTools.Len() { 141 // already in target. 142 return nil 143 } 144 } 145 146 if syncContext.DryRun { 147 for _, tools := range chosenList { 148 logger.Infof("copying %s from %s", tools.Version, tools.URL) 149 } 150 return nil 151 } 152 153 err = copyTools(toolsDir, syncContext.Stream, chosenList, syncContext.TargetToolsUploader) 154 if err != nil { 155 return err 156 } 157 logger.Infof("copied %d agent binaries", len(chosenList)) 158 return nil 159 } 160 161 // selectSourceDatasource returns a storage reader based on the source setting. 162 func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) { 163 source := syncContext.Source 164 if source == "" { 165 source = envtools.DefaultBaseURL 166 } 167 sourceURL, err := envtools.ToolsURL(source) 168 if err != nil { 169 return nil, err 170 } 171 logger.Infof("source for sync of agent binaries: %v", sourceURL) 172 config := simplestreams.Config{ 173 Description: "sync agent binaries source", 174 BaseURL: sourceURL, 175 PublicSigningKey: keys.JujuPublicKey, 176 HostnameVerification: true, 177 Priority: simplestreams.CUSTOM_CLOUD_DATA, 178 } 179 if err := config.Validate(); err != nil { 180 return nil, errors.Annotate(err, "simplestreams config validation failed") 181 } 182 return simplestreams.NewDataSource(config), nil 183 } 184 185 // copyTools copies a set of tools from the source to the target. 186 func copyTools(toolsDir, stream string, tools []*coretools.Tools, u ToolsUploader) error { 187 for _, tool := range tools { 188 logger.Infof("copying %s from %s", tool.Version, tool.URL) 189 if err := copyOneToolsPackage(toolsDir, stream, tool, u); err != nil { 190 return err 191 } 192 } 193 return nil 194 } 195 196 // copyOneToolsPackage copies one tool from the source to the target. 197 func copyOneToolsPackage(toolsDir, stream string, tools *coretools.Tools, u ToolsUploader) error { 198 toolsName := envtools.StorageName(tools.Version, toolsDir) 199 logger.Infof("downloading %q %v (%v)", stream, toolsName, tools.URL) 200 client := http.NewClient() 201 resp, err := client.Get(context.TODO(), tools.URL) 202 if err != nil { 203 return err 204 } 205 defer func() { _ = resp.Body.Close() }() 206 // Verify SHA-256 hash. 207 var buf bytes.Buffer 208 sha256, size, err := utils.ReadSHA256(io.TeeReader(resp.Body, &buf)) 209 if err != nil { 210 return err 211 } 212 if tools.SHA256 == "" { 213 logger.Errorf("no SHA-256 hash for %v", tools.SHA256) // TODO(dfc) can you spot the bug ? 214 } else if sha256 != tools.SHA256 { 215 return errors.Errorf("SHA-256 hash mismatch (%v/%v)", sha256, tools.SHA256) 216 } 217 sizeInKB := (size + 512) / 1024 218 logger.Infof("uploading %v (%dkB) to model", toolsName, sizeInKB) 219 return u.UploadTools(toolsDir, stream, tools, buf.Bytes()) 220 } 221 222 // UploadFunc is the type of Upload, which may be 223 // reassigned to control the behaviour of tools 224 // uploading. 225 type UploadFunc func( 226 envtools.SimplestreamsFetcher, storage.Storage, string, 227 func(vers version.Number) version.Number, 228 ) (*coretools.Tools, error) 229 230 // Upload is exported for testing. 231 var Upload UploadFunc = upload 232 233 // upload builds whatever version of github.com/juju/juju is in $GOPATH, 234 // uploads it to the given storage, and returns a Tools instance describing 235 // them. If forceVersion is not nil, the uploaded tools bundle will report 236 // the given version number. 237 func upload( 238 ss envtools.SimplestreamsFetcher, store storage.Storage, stream string, 239 f func(vers version.Number) version.Number, 240 ) (*coretools.Tools, error) { 241 if f == nil { 242 f = func(vers version.Number) version.Number { return vers } 243 } 244 builtTools, err := BuildAgentTarball(true, stream, f) 245 if err != nil { 246 return nil, err 247 } 248 defer os.RemoveAll(builtTools.Dir) 249 return syncBuiltTools(ss, store, stream, builtTools) 250 } 251 252 // generateAgentMetadata copies the built tools tarball into a tarball for the specified 253 // stream and series and generates corresponding metadata. 254 func generateAgentMetadata(ss envtools.SimplestreamsFetcher, toolsInfo *BuiltAgent, stream string) error { 255 // Copy the tools to the target storage, recording a Tools struct for each one. 256 var targetTools coretools.List 257 targetTools = append(targetTools, &coretools.Tools{ 258 Version: toolsInfo.Version, 259 Size: toolsInfo.Size, 260 SHA256: toolsInfo.Sha256Hash, 261 }) 262 // The tools have been copied to a temp location from which they will be uploaded, 263 // now write out the matching simplestreams metadata so that SyncTools can find them. 264 metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir) 265 if err != nil { 266 return err 267 } 268 logger.Debugf("generating agent metadata") 269 return envtools.MergeAndWriteMetadata(ss, metadataStore, stream, stream, targetTools, false) 270 } 271 272 // BuiltAgent contains metadata for a tools tarball resulting from 273 // a call to BundleTools. 274 type BuiltAgent struct { 275 Version version.Binary 276 Official bool 277 Dir string 278 StorageName string 279 Sha256Hash string 280 Size int64 281 } 282 283 // BuildAgentTarballFunc is a function which can build an agent tarball. 284 type BuildAgentTarballFunc func( 285 build bool, stream string, getForceVersion func(version.Number) version.Number, 286 ) (*BuiltAgent, error) 287 288 // Override for testing. 289 var BuildAgentTarball BuildAgentTarballFunc = buildAgentTarball 290 291 // BuildAgentTarball bundles an agent tarball and places it in a temp directory in 292 // the expected agent path. 293 func buildAgentTarball( 294 build bool, stream string, 295 getForceVersion func(version.Number) version.Number, 296 ) (_ *BuiltAgent, err error) { 297 // TODO(rog) find binaries from $PATH when not using a development 298 // version of juju within a $GOPATH. 299 300 logger.Debugf("Making agent binary tarball") 301 // We create the entire archive before asking the environment to 302 // start uploading so that we can be sure we have archived 303 // correctly. 304 f, err := os.CreateTemp("", "juju-tgz") 305 if err != nil { 306 return nil, err 307 } 308 defer func() { 309 _ = f.Close() 310 _ = os.Remove(f.Name()) 311 }() 312 313 toolsVersion, forceVersion, official, sha256Hash, err := envtools.BundleTools(build, f, getForceVersion) 314 if err != nil { 315 return nil, err 316 } 317 // Built agent version needs to match the client used to bootstrap. 318 builtVersion := toolsVersion 319 builtVersion.Build = 0 320 clientVersion := jujuversion.Current 321 clientVersion.Build = 0 322 if builtVersion.Number.Compare(clientVersion) != 0 { 323 return nil, errors.Errorf( 324 "agent binary %v not compatible with bootstrap client %v", 325 toolsVersion.Number, jujuversion.Current, 326 ) 327 } 328 fileInfo, err := f.Stat() 329 if err != nil { 330 return nil, errors.Errorf("cannot stat newly made agent binary archive: %v", err) 331 } 332 size := fileInfo.Size() 333 agentBinary := "agent binary" 334 if official { 335 agentBinary = "official agent binary" 336 } 337 logger.Infof("using %s %v aliased to %v (%dkB)", agentBinary, toolsVersion, forceVersion, (size+512)/1024) 338 339 baseToolsDir, err := os.MkdirTemp("", "juju-tools") 340 if err != nil { 341 return nil, err 342 } 343 344 // If we exit with an error, clean up the built tools directory. 345 defer func() { 346 if err != nil { 347 os.RemoveAll(baseToolsDir) 348 } 349 }() 350 351 err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, stream), 0755) 352 if err != nil { 353 return nil, err 354 } 355 storageName := envtools.StorageName(toolsVersion, stream) 356 err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name()) 357 if err != nil { 358 return nil, err 359 } 360 return &BuiltAgent{ 361 Version: toolsVersion, 362 Official: official, 363 Dir: baseToolsDir, 364 StorageName: storageName, 365 Size: size, 366 Sha256Hash: sha256Hash, 367 }, nil 368 } 369 370 // syncBuiltTools copies to storage a tools tarball and cloned copies for each series. 371 func syncBuiltTools(ss envtools.SimplestreamsFetcher, store storage.Storage, stream string, builtTools *BuiltAgent) (*coretools.Tools, error) { 372 if err := generateAgentMetadata(ss, builtTools, stream); err != nil { 373 return nil, err 374 } 375 syncContext := &SyncContext{ 376 Source: builtTools.Dir, 377 TargetToolsFinder: StorageToolsFinder{store}, 378 TargetToolsUploader: StorageToolsUploader{ 379 Fetcher: ss, 380 Storage: store, 381 WriteMetadata: false, 382 WriteMirrors: false, 383 }, 384 AllVersions: true, 385 Stream: stream, 386 ChosenVersion: builtTools.Version.Number, 387 } 388 logger.Debugf("uploading agent binaries to cloud storage") 389 err := SyncTools(syncContext) 390 if err != nil { 391 return nil, err 392 } 393 url, err := store.URL(builtTools.StorageName) 394 if err != nil { 395 return nil, err 396 } 397 return &coretools.Tools{ 398 Version: builtTools.Version, 399 URL: url, 400 Size: builtTools.Size, 401 SHA256: builtTools.Sha256Hash, 402 }, nil 403 } 404 405 // StorageToolsFinder is an implementation of ToolsFinder 406 // that searches for tools in the specified storage. 407 type StorageToolsFinder struct { 408 Storage storage.StorageReader 409 } 410 411 func (f StorageToolsFinder) FindTools(major int, stream string) (coretools.List, error) { 412 return envtools.ReadList(f.Storage, stream, major, -1) 413 } 414 415 // StorageToolsUploader is an implementation of ToolsUploader that 416 // writes tools to the provided storage and then writes merged 417 // metadata, optionally with mirrors. 418 type StorageToolsUploader struct { 419 Fetcher envtools.SimplestreamsFetcher 420 Storage storage.Storage 421 WriteMetadata bool 422 WriteMirrors envtools.ShouldWriteMirrors 423 } 424 425 func (u StorageToolsUploader) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error { 426 toolsName := envtools.StorageName(tools.Version, toolsDir) 427 if err := u.Storage.Put(toolsName, bytes.NewReader(data), int64(len(data))); err != nil { 428 return err 429 } 430 if !u.WriteMetadata { 431 return nil 432 } 433 err := envtools.MergeAndWriteMetadata(u.Fetcher, u.Storage, toolsDir, stream, coretools.List{tools}, u.WriteMirrors) 434 if err != nil { 435 logger.Errorf("error writing agent metadata: %v", err) 436 return err 437 } 438 return nil 439 }