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  }