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 }