github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/imagedistributor/p2p_distributor.go (about) 1 // Copyright © 2023 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package imagedistributor 16 17 import ( 18 "archive/tar" 19 "compress/gzip" 20 "context" 21 "crypto/sha256" 22 b64 "encoding/base64" 23 "encoding/hex" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "os" 29 "path/filepath" 30 "strings" 31 "time" 32 33 sealerConfig "github.com/sealerio/sealer/pkg/config" 34 "github.com/sealerio/sealer/pkg/env" 35 "github.com/sealerio/sealer/pkg/infradriver" 36 v1 "github.com/sealerio/sealer/types/api/v1" 37 osi "github.com/sealerio/sealer/utils/os" 38 39 config "github.com/ipfs/go-ipfs-config" 40 files "github.com/ipfs/go-ipfs-files" 41 "github.com/ipfs/go-ipfs/core" 42 "github.com/ipfs/go-ipfs/core/coreapi" 43 "github.com/ipfs/go-ipfs/core/node/libp2p" 44 "github.com/ipfs/go-ipfs/plugin/loader" 45 "github.com/ipfs/go-ipfs/repo/fsrepo" 46 icore "github.com/ipfs/interface-go-ipfs-core" 47 "github.com/ipfs/interface-go-ipfs-core/options" 48 "github.com/ipfs/interface-go-ipfs-core/path" 49 lp2p "github.com/libp2p/go-libp2p" 50 "github.com/libp2p/go-libp2p-core/host" 51 "github.com/libp2p/go-libp2p-core/peer" 52 "github.com/libp2p/go-libp2p-core/peerstore" 53 "github.com/sirupsen/logrus" 54 "golang.org/x/sync/errgroup" 55 ) 56 57 type p2pDistributor struct { 58 ipfsNode icore.CoreAPI 59 ipfsAPI *core.IpfsNode 60 ipfsCancel context.CancelFunc 61 ipfsContext context.Context 62 sshInfraDriver infradriver.InfraDriver 63 imageMountInfo []ClusterImageMountInfo 64 registryCacheDir string 65 rootfsCacheDir string 66 configs []v1.Config 67 options DistributeOption 68 } 69 70 func (p *p2pDistributor) DistributeRegistry(deployHosts []net.IP, dataDir string) error { 71 for _, info := range p.imageMountInfo { 72 if !osi.IsFileExist(filepath.Join(info.MountDir, RegistryDirName)) { 73 continue 74 } 75 76 logrus.Infof("Distributing %s", info.MountDir) 77 78 if err := p.distributeImpl(deployHosts, info.MountDir, dataDir); err != nil { 79 return fmt.Errorf("failed to distribute: %s", err) 80 } 81 } 82 83 return nil 84 } 85 86 func (p *p2pDistributor) Distribute(hosts []net.IP, dest string) error { 87 for _, info := range p.imageMountInfo { 88 logrus.Infof("Distributing %s", info.MountDir) 89 if err := p.dumpConfigToRootfs(info.MountDir); err != nil { 90 return err 91 } 92 93 if err := p.renderRootfs(info.MountDir); err != nil { 94 return err 95 } 96 97 if err := p.distributeImpl(hosts, info.MountDir, dest); err != nil { 98 return fmt.Errorf("failed to distribute: %s", err) 99 } 100 } 101 102 return nil 103 } 104 105 func (p *p2pDistributor) Restore(targetDir string, hosts []net.IP) error { 106 if !p.options.Prune { 107 return nil 108 } 109 110 rmRootfsCMD := fmt.Sprintf("rm -rf %s", targetDir) 111 112 eg, _ := errgroup.WithContext(context.Background()) 113 for _, ip := range hosts { 114 host := ip 115 eg.Go(func() error { 116 err := p.sshInfraDriver.CmdAsync(host, nil, rmRootfsCMD) 117 if err != nil { 118 return fmt.Errorf("faild to delete rootfs on host [%s]: %v", host.String(), err) 119 } 120 return nil 121 }) 122 } 123 124 if err := eg.Wait(); err != nil { 125 return err 126 } 127 128 return nil 129 } 130 131 func (p *p2pDistributor) distributeImpl(deployHosts []net.IP, dir string, dest string) error { 132 name, err := tarGzDirectory(dir) 133 if err != nil { 134 return err 135 } 136 logrus.Infof("Compressed %s", dir) 137 138 ipfsDirectory, err := getUnixfsNode(name) 139 if err != nil { 140 return fmt.Errorf("failed to prepare rootfs %s: %s", dir, err) 141 } 142 143 cid, err := p.ipfsNode.Unixfs().Add(p.ipfsContext, ipfsDirectory, func(uas *options.UnixfsAddSettings) error { 144 uas.Chunker = "size-1048576" 145 uas.FsCache = true 146 return nil 147 }) 148 if err != nil { 149 return fmt.Errorf("failed to add rootfs %s to IPFS network: %s", dir, err) 150 } 151 logrus.Infof("Loaded %s", dir) 152 153 cidString := cidToString(cid) 154 155 eg, _ := errgroup.WithContext(context.Background()) 156 157 for _, ip := range deployHosts { 158 host := ip 159 160 eg.Go(func() error { 161 logrus.Infof("%s start", host) 162 163 localIP, err := p.getRealIP(host) 164 if err != nil { 165 return fmt.Errorf("failed to distribute to host %s: %s", host, err) 166 } 167 168 localID := p.ipfsAPI.Identity.String() 169 localHost := fmt.Sprintf("/ip4/%s/tcp/40011/p2p/%s", localIP, localID) 170 171 command := fmt.Sprintf("/usr/bin/dist-receiver -bootstrap %s -cid %s -filename %s -target %s", localHost, cidString, name, dest) 172 if _, err = p.sshInfraDriver.Cmd(host, nil, command); err != nil { 173 return fmt.Errorf("failed to distribute to host %s: %s", host, err) 174 } 175 176 logrus.Infof("%s done. ", host) 177 178 return nil 179 }) 180 } 181 182 if err := waitForState(deployHosts, "1"); err != nil { 183 return err 184 } 185 186 remote := "" 187 known, err := p.ipfsNode.Swarm().KnownAddrs(p.ipfsContext) 188 if err != nil { 189 return err 190 } 191 192 for _, host := range known { 193 remote = remote + fmt.Sprintf("%s,", host) 194 } 195 196 encoded := b64.StdEncoding.EncodeToString([]byte(remote)) 197 198 for _, ip := range deployHosts { 199 host := ip 200 go func() { 201 _, _ = http.Get(fmt.Sprintf("http://%s:4002/connect?target=%s", host, encoded)) 202 _, _ = http.Get(fmt.Sprintf("http://%s:4002/next", host)) 203 }() 204 } 205 206 if err := waitForState(deployHosts, "2"); err != nil { 207 return err 208 } 209 210 goNext(deployHosts) 211 212 if err := eg.Wait(); err != nil { 213 return err 214 } 215 216 if err := os.Remove(name); err != nil { 217 logrus.Warnf("Failed to delete intermediate file %s: %s", name, err) 218 } 219 220 return nil 221 } 222 223 func (p *p2pDistributor) getRealIP(host net.IP) (string, error) { 224 localIPBytes, err := p.sshInfraDriver.Cmd(host, nil, "echo $SSH_CLIENT | awk '{print $1}'") 225 if err != nil { 226 return "", fmt.Errorf("failed to distribute to host %s: %s", host, err) 227 } 228 229 localIP := string(localIPBytes[:]) 230 localIP = strings.TrimSpace(localIP) 231 232 return localIP, nil 233 } 234 235 func cidToString(cid path.Resolved) string { 236 fullCid := cid.String() 237 cidParts := strings.Split(fullCid, "/") 238 var cidString string 239 for _, s := range cidParts { 240 cidString = s 241 } 242 return cidString 243 } 244 245 func NewP2PDistributor( 246 imageMountInfo []ClusterImageMountInfo, 247 driver infradriver.InfraDriver, 248 configs []v1.Config, 249 options DistributeOption, 250 ) (Distributor, error) { 251 ctx, cancel := context.WithCancel(context.Background()) 252 253 node, api, err := spawnNode(ctx) 254 if err != nil { 255 cancel() 256 return nil, fmt.Errorf("failed to create IPFS node: %s", err) 257 } 258 259 return &p2pDistributor{ 260 ipfsNode: node, 261 ipfsAPI: api, 262 ipfsCancel: cancel, 263 ipfsContext: ctx, 264 sshInfraDriver: driver, 265 imageMountInfo: imageMountInfo, 266 registryCacheDir: filepath.Join(driver.GetClusterRootfsPath(), "cache", RegistryCacheDirName), 267 rootfsCacheDir: filepath.Join(driver.GetClusterRootfsPath(), "cache", RootfsCacheDirName), 268 configs: configs, 269 options: options, 270 }, nil 271 } 272 273 func spawnNode(ctx context.Context) (icore.CoreAPI, *core.IpfsNode, error) { 274 // Create a Temporary Repo 275 repoPath, err := createTempRepo() 276 if err != nil { 277 return nil, nil, fmt.Errorf("failed to create temp repo: %s", err) 278 } 279 280 node, err := createNode(ctx, repoPath) 281 if err != nil { 282 return nil, nil, err 283 } 284 285 api, err := coreapi.NewCoreAPI(node) 286 287 return api, node, err 288 } 289 290 func createTempRepo() (string, error) { 291 err := setupPlugins("") 292 if err != nil { 293 return "", err 294 } 295 296 repoPath, err := os.MkdirTemp("", "ipfs-shell") 297 if err != nil { 298 return "", fmt.Errorf("failed to get temp dir: %s", err) 299 } 300 301 // Create a config with default options and a 2048 bit key 302 cfg, err := config.Init(io.Discard, 2048) 303 if err != nil { 304 return "", err 305 } 306 307 var bs []string 308 cfg.Bootstrap = bs 309 310 var listen []string 311 listen = append(listen, "/ip4/0.0.0.0/tcp/40011") 312 cfg.Addresses.Swarm = listen 313 314 // Create the repo with the config 315 if err := fsrepo.Init(repoPath, cfg); err != nil { 316 return "", fmt.Errorf("failed to init ephemeral node: %s", err) 317 } 318 319 return repoPath, nil 320 } 321 322 func createNode(ctx context.Context, repoPath string) (*core.IpfsNode, error) { 323 // Open the repo 324 repo, err := fsrepo.Open(repoPath) 325 if err != nil { 326 return nil, err 327 } 328 329 // Construct the node 330 nodeOptions := &core.BuildCfg{ 331 Online: true, 332 Routing: libp2p.DHTOption, // This option sets the node to be a full DHT node (both fetching and storing DHT Records) 333 // Routing: libp2p.DHTClientOption, // This option sets the node to be a client DHT node (only fetching records) 334 Host: constructPeerHost, 335 Repo: repo, 336 } 337 338 return core.NewNode(ctx, nodeOptions) 339 } 340 341 func constructPeerHost(id peer.ID, ps peerstore.Peerstore, options ...lp2p.Option) (host.Host, error) { 342 pkey := ps.PrivKey(id) 343 if pkey == nil { 344 return nil, fmt.Errorf("missing private key for node ID: %s", id.Pretty()) 345 } 346 options = append([]lp2p.Option{lp2p.Identity(pkey), lp2p.Peerstore(ps)}, options...) 347 return lp2p.New(options...) 348 } 349 350 func setupPlugins(externalPluginsPath string) error { 351 // Load any external plugins if available on externalPluginsPath 352 plugins, err := loader.NewPluginLoader(filepath.Join(externalPluginsPath, "plugins")) 353 if err != nil { 354 return fmt.Errorf("error loading plugins: %s", err) 355 } 356 357 // Load preloaded and external plugins 358 if err := plugins.Initialize(); err != nil { 359 return fmt.Errorf("error initializing plugins: %s", err) 360 } 361 362 if err := plugins.Inject(); err != nil { 363 return fmt.Errorf("error injecting plugins: %s", err) 364 } 365 366 return nil 367 } 368 369 func (p *p2pDistributor) dumpConfigToRootfs(mountDir string) error { 370 return sealerConfig.NewConfiguration(mountDir).Dump(p.configs) 371 } 372 373 func (p *p2pDistributor) renderRootfs(mountDir string) error { 374 var ( 375 renderEtc = filepath.Join(mountDir, "etc") 376 renderChart = filepath.Join(mountDir, "charts") 377 renderManifests = filepath.Join(mountDir, "manifests") 378 renderData = p.sshInfraDriver.GetClusterEnv() 379 ) 380 381 for _, dir := range []string{renderEtc, renderChart, renderManifests} { 382 if osi.IsFileExist(dir) { 383 err := env.RenderTemplate(dir, renderData) 384 if err != nil { 385 return err 386 } 387 } 388 } 389 390 return nil 391 } 392 393 func getUnixfsNode(path string) (files.Node, error) { 394 st, err := os.Stat(path) 395 if err != nil { 396 return nil, err 397 } 398 399 f, err := files.NewSerialFile(path, false, st) 400 if err != nil { 401 return nil, err 402 } 403 404 return f, nil 405 } 406 407 func tarGzDirectory(sourceDir string) (string, error) { 408 h := sha256.New() 409 h.Write([]byte(sourceDir)) 410 name := hex.EncodeToString(h.Sum(nil)) 411 412 filename := fmt.Sprintf("%s.tar.gz", name) 413 414 if err := createTarGz(sourceDir, filename); err != nil { 415 return "", fmt.Errorf("failed to uncompress resource file: %s", err) 416 } 417 418 return filename, nil 419 } 420 421 func waitForState(hosts []net.IP, expectedStage string) error { 422 for { 423 good := 0 424 eg, _ := errgroup.WithContext(context.Background()) 425 426 for _, ip := range hosts { 427 host := ip 428 429 eg.Go(func() error { 430 resp, err := http.Get(fmt.Sprintf("http://%s:4002/stage", host)) 431 if err != nil { 432 return err 433 } 434 435 if contentBytes, err := io.ReadAll(resp.Body); err != nil { 436 return err 437 } else if string(contentBytes) == expectedStage { 438 good++ 439 } 440 441 return nil 442 }) 443 } 444 445 if err := eg.Wait(); err != nil { 446 continue 447 } 448 449 if good == len(hosts) { 450 return nil 451 } 452 453 time.Sleep(time.Second) 454 } 455 } 456 457 func goNext(hosts []net.IP) { 458 for _, ip := range hosts { 459 host := ip 460 go func() { 461 _, _ = http.Get(fmt.Sprintf("http://%s:4002/next", host)) 462 }() 463 } 464 } 465 466 func createTarGz(sourceDir, filename string) error { 467 tarFile, err := os.Create(filepath.Clean(filename)) 468 if err != nil { 469 return err 470 } 471 defer tarFile.Close() 472 473 gzWriter := gzip.NewWriter(tarFile) 474 defer gzWriter.Close() 475 476 tarWriter := tar.NewWriter(gzWriter) 477 defer tarWriter.Close() 478 479 return filepath.Walk(sourceDir, func(filePath string, info os.FileInfo, err error) error { 480 if err != nil { 481 return err 482 } 483 484 header, err := tar.FileInfoHeader(info, "") 485 if err != nil { 486 return err 487 } 488 header.Name = filePath 489 490 if err := tarWriter.WriteHeader(header); err != nil { 491 return err 492 } 493 494 if !info.IsDir() { 495 file, err := os.Open(filepath.Clean(filePath)) 496 if err != nil { 497 return err 498 } 499 defer file.Close() 500 501 if _, err := io.Copy(tarWriter, file); err != nil { 502 return err 503 } 504 } 505 506 return nil 507 }) 508 }