github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/cluster/calcium/build.go (about)

     1  package calcium
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/cockroachdb/errors"
    13  	enginetypes "github.com/projecteru2/core/engine/types"
    14  	"github.com/projecteru2/core/log"
    15  	"github.com/projecteru2/core/types"
    16  	"github.com/projecteru2/core/utils"
    17  )
    18  
    19  // BuildImage will build image
    20  func (c *Calcium) BuildImage(ctx context.Context, opts *types.BuildOptions) (ch chan *types.BuildImageMessage, err error) {
    21  	logger := log.WithFunc("calcium.BuildImage").WithField("opts", opts)
    22  	// Disable build API if scm not set
    23  	if c.source == nil {
    24  		return nil, types.ErrNoSCMSetting
    25  	}
    26  	// select nodes
    27  	node, err := c.selectBuildNode(ctx)
    28  	if err != nil {
    29  		logger.Error(ctx, err)
    30  		return nil, err
    31  	}
    32  
    33  	logger.Infof(ctx, "Building image at pod %s node %s", node.Podname, node.Name)
    34  
    35  	var (
    36  		refs []string
    37  		resp io.ReadCloser
    38  	)
    39  	switch opts.BuildMethod {
    40  	case types.BuildFromSCM:
    41  		refs, resp, err = c.buildFromSCM(ctx, node, opts)
    42  	case types.BuildFromRaw:
    43  		refs, resp, err = c.buildFromContent(ctx, node, opts)
    44  	case types.BuildFromExist:
    45  		refs, node, resp, err = c.buildFromExist(ctx, opts)
    46  	default:
    47  		return nil, types.ErrInvaildBuildType
    48  	}
    49  	if err != nil {
    50  		logger.Error(ctx, err)
    51  		return nil, err
    52  	}
    53  	ch, err = c.pushImageAndClean(ctx, resp, node, refs)
    54  	logger.Error(ctx, err)
    55  	return ch, err
    56  }
    57  
    58  func (c *Calcium) selectBuildNode(ctx context.Context) (*types.Node, error) {
    59  	// get pod from config
    60  	// TODO can choose multiple pod here for other engine support
    61  	if c.config.Docker.BuildPod == "" {
    62  		return nil, types.ErrNoBuildPod
    63  	}
    64  
    65  	// get nodes
    66  	nodes, err := c.store.GetNodesByPod(ctx, &types.NodeFilter{Podname: c.config.Docker.BuildPod})
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	if len(nodes) == 0 {
    72  		return nil, types.ErrInsufficientCapacity
    73  	}
    74  	// get idle max node
    75  	return c.getMostIdleNode(ctx, nodes)
    76  }
    77  
    78  func (c *Calcium) getMostIdleNode(ctx context.Context, nodes []*types.Node) (*types.Node, error) {
    79  	nodenames := []string{}
    80  	nodeMap := map[string]*types.Node{}
    81  	for _, node := range nodes {
    82  		nodenames = append(nodenames, node.Name)
    83  		nodeMap[node.Name] = node
    84  	}
    85  
    86  	mostIdleNode, err := c.rmgr.GetMostIdleNode(ctx, nodenames)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	return nodeMap[mostIdleNode], nil
    91  }
    92  
    93  func (c *Calcium) buildFromSCM(ctx context.Context, node *types.Node, opts *types.BuildOptions) ([]string, io.ReadCloser, error) {
    94  	buildContentOpts := &enginetypes.BuildContentOptions{
    95  		User:   opts.User,
    96  		UID:    opts.UID,
    97  		Builds: opts.Builds,
    98  	}
    99  	path, content, err := node.Engine.BuildContent(ctx, c.source, buildContentOpts)
   100  	defer os.RemoveAll(path)
   101  	if err != nil {
   102  		return nil, nil, err
   103  	}
   104  	opts.Tar = content
   105  	return c.buildFromContent(ctx, node, opts)
   106  }
   107  
   108  func (c *Calcium) buildFromContent(ctx context.Context, node *types.Node, opts *types.BuildOptions) ([]string, io.ReadCloser, error) {
   109  	refs := node.Engine.BuildRefs(ctx, toBuildRefOptions(opts))
   110  	resp, err := node.Engine.ImageBuild(ctx, opts.Tar, refs, opts.Platform)
   111  	return refs, resp, err
   112  }
   113  
   114  func (c *Calcium) buildFromExist(ctx context.Context, opts *types.BuildOptions) (refs []string, node *types.Node, resp io.ReadCloser, err error) {
   115  	if node, err = c.getWorkloadNode(ctx, opts.ExistID); err != nil {
   116  		return nil, nil, nil, err
   117  	}
   118  
   119  	refs = node.Engine.BuildRefs(ctx, toBuildRefOptions(opts))
   120  	imgID, err := node.Engine.ImageBuildFromExist(ctx, opts.ExistID, refs, opts.User)
   121  	if err != nil {
   122  		return nil, nil, nil, err
   123  	}
   124  
   125  	buildMsg, err := json.Marshal(types.BuildImageMessage{ID: imgID})
   126  	if err != nil {
   127  		return nil, nil, nil, err
   128  	}
   129  
   130  	return refs, node, io.NopCloser(bytes.NewReader(buildMsg)), nil
   131  }
   132  
   133  func (c *Calcium) pushImageAndClean(ctx context.Context, resp io.ReadCloser, node *types.Node, tags []string) (chan *types.BuildImageMessage, error) { //nolint:unparam
   134  	logger := log.WithFunc("calcium.pushImageAndClean").WithField("node", node).WithField("tags", tags)
   135  	logger.Infof(ctx, "Pushing image at pod %s node %s", node.Podname, node.Name)
   136  	return c.withImageBuiltChannel(func(ch chan *types.BuildImageMessage) {
   137  		defer resp.Close()
   138  		decoder := json.NewDecoder(resp)
   139  		lastMessage := &types.BuildImageMessage{}
   140  		for {
   141  			message := &types.BuildImageMessage{}
   142  			if err := decoder.Decode(message); err != nil {
   143  				if err == io.EOF {
   144  					break
   145  				}
   146  				if err == context.Canceled || err == context.DeadlineExceeded {
   147  					logger.Error(ctx, err, "context timeout")
   148  					lastMessage.ErrorDetail.Code = -1
   149  					lastMessage.ErrorDetail.Message = err.Error()
   150  					lastMessage.Error = err.Error()
   151  					break
   152  				}
   153  				malformed, _ := io.ReadAll(decoder.Buffered()) // TODO err check
   154  				logger.Errorf(ctx, err, "Decode build image message failed, buffered: %+v", malformed)
   155  				return
   156  			}
   157  			ch <- message
   158  			lastMessage = message
   159  		}
   160  
   161  		if lastMessage.Error != "" {
   162  			logger.Errorf(ctx, errors.New(lastMessage.Error), "Build image failed %+v", lastMessage.ErrorDetail.Message)
   163  			return
   164  		}
   165  
   166  		// push and clean
   167  		for i := range tags {
   168  			tag := tags[i]
   169  			logger.Infof(ctx, "Push image %s", tag)
   170  			rc, err := node.Engine.ImagePush(ctx, tag)
   171  			if err != nil {
   172  				logger.Error(ctx, err)
   173  				ch <- &types.BuildImageMessage{Error: err.Error()}
   174  				continue
   175  			}
   176  
   177  			for message := range c.processBuildImageStream(ctx, rc) {
   178  				ch <- message
   179  			}
   180  
   181  			ch <- &types.BuildImageMessage{Stream: fmt.Sprintf("finished %s\n", tag), Status: "finished", Progress: tag}
   182  		}
   183  		// 无论如何都删掉build机器的
   184  		// 事实上他不会跟cached pod一样
   185  		// 一样就砍死
   186  		_ = c.pool.Invoke(func() {
   187  			cleanupNodeImages(ctx, node, tags, c.config.GlobalTimeout)
   188  		})
   189  	}), nil
   190  
   191  }
   192  
   193  func (c *Calcium) getWorkloadNode(ctx context.Context, ID string) (*types.Node, error) {
   194  	w, err := c.store.GetWorkload(ctx, ID)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	node, err := c.store.GetNode(ctx, w.Nodename)
   199  	return node, err
   200  }
   201  
   202  func (c *Calcium) withImageBuiltChannel(f func(chan *types.BuildImageMessage)) chan *types.BuildImageMessage {
   203  	ch := make(chan *types.BuildImageMessage)
   204  	_ = c.pool.Invoke(func() {
   205  		defer close(ch)
   206  		f(ch)
   207  	})
   208  	return ch
   209  }
   210  
   211  func cleanupNodeImages(ctx context.Context, node *types.Node, IDs []string, ttl time.Duration) {
   212  	logger := log.WithFunc("calcium.cleanupNodeImages").WithField("node", node).WithField("IDs", IDs).WithField("ttl", ttl)
   213  	ctx, cancel := context.WithTimeout(utils.NewInheritCtx(ctx), ttl)
   214  	defer cancel()
   215  	for _, ID := range IDs {
   216  		if _, err := node.Engine.ImageRemove(ctx, ID, false, true); err != nil {
   217  			logger.Error(ctx, err, "Remove image error")
   218  		}
   219  	}
   220  	if spaceReclaimed, err := node.Engine.ImageBuildCachePrune(ctx, true); err != nil {
   221  		logger.Error(ctx, err, "Remove build image cache error")
   222  	} else {
   223  		logger.Infof(ctx, "Clean cached image and release space %d", spaceReclaimed)
   224  	}
   225  }
   226  
   227  func toBuildRefOptions(opts *types.BuildOptions) *enginetypes.BuildRefOptions {
   228  	return &enginetypes.BuildRefOptions{
   229  		Name: opts.Name,
   230  		Tags: opts.Tags,
   231  		User: opts.User,
   232  	}
   233  }