github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/store/etcdv3/workload.go (about) 1 package etcdv3 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "path/filepath" 8 9 "github.com/projecteru2/core/log" 10 "github.com/projecteru2/core/types" 11 "github.com/projecteru2/core/utils" 12 13 "go.etcd.io/etcd/api/v3/mvccpb" 14 clientv3 "go.etcd.io/etcd/client/v3" 15 ) 16 17 // AddWorkload add a workload 18 // mainly record its relationship on pod and node 19 // actually if we already know its node, we will know its pod 20 // but we still store it 21 // storage path in etcd is `/workload/:workloadid` 22 func (m Mercury) AddWorkload(ctx context.Context, workload *types.Workload, processing *types.Processing) error { 23 return m.doOpsWorkload(ctx, workload, processing, true) 24 } 25 26 // UpdateWorkload update a workload 27 func (m *Mercury) UpdateWorkload(ctx context.Context, workload *types.Workload) error { 28 return m.doOpsWorkload(ctx, workload, nil, false) 29 } 30 31 // RemoveWorkload remove a workload 32 // workload ID must be in full length 33 func (m *Mercury) RemoveWorkload(ctx context.Context, workload *types.Workload) error { 34 return m.cleanWorkloadData(ctx, workload) 35 } 36 37 // GetWorkload get a workload 38 // workload if must be in full length, or we can't find it in etcd 39 // storage path in etcd is `/workload/:workloadid` 40 func (m *Mercury) GetWorkload(ctx context.Context, ID string) (*types.Workload, error) { 41 workloads, err := m.GetWorkloads(ctx, []string{ID}) 42 if err != nil { 43 return nil, err 44 } 45 return workloads[0], nil 46 } 47 48 // GetWorkloads get many workloads 49 func (m *Mercury) GetWorkloads(ctx context.Context, IDs []string) (workloads []*types.Workload, err error) { 50 keys := []string{} 51 for _, ID := range IDs { 52 keys = append(keys, fmt.Sprintf(workloadInfoKey, ID)) 53 } 54 55 return m.doGetWorkloads(ctx, keys) 56 } 57 58 // GetWorkloadStatus get workload status 59 func (m *Mercury) GetWorkloadStatus(ctx context.Context, ID string) (*types.StatusMeta, error) { 60 workload, err := m.GetWorkload(ctx, ID) 61 if err != nil { 62 return nil, err 63 } 64 return workload.StatusMeta, nil 65 } 66 67 // SetWorkloadStatus set workload status 68 func (m *Mercury) SetWorkloadStatus(ctx context.Context, status *types.StatusMeta, ttl int64) error { 69 if status.Appname == "" || status.Entrypoint == "" || status.Nodename == "" { 70 return types.ErrInvaildWorkloadStatus 71 } 72 73 data, err := json.Marshal(status) 74 if err != nil { 75 return err 76 } 77 statusVal := string(data) 78 statusKey := filepath.Join(workloadStatusPrefix, status.Appname, status.Entrypoint, status.Nodename, status.ID) 79 workloadKey := fmt.Sprintf(workloadInfoKey, status.ID) 80 return m.BindStatus(ctx, workloadKey, statusKey, statusVal, ttl) 81 } 82 83 // ListWorkloads list workloads 84 func (m *Mercury) ListWorkloads(ctx context.Context, appname, entrypoint, nodename string, limit int64, labels map[string]string) ([]*types.Workload, error) { 85 if appname == "" { 86 entrypoint = "" 87 } 88 if entrypoint == "" { 89 nodename = "" 90 } 91 // 这里显式加个 / 来保证 prefix 是唯一的 92 key := filepath.Join(workloadDeployPrefix, appname, entrypoint, nodename) + "/" 93 resp, err := m.Get(ctx, key, clientv3.WithPrefix(), clientv3.WithLimit(limit)) 94 if err != nil { 95 return nil, err 96 } 97 98 workloads := []*types.Workload{} 99 for _, ev := range resp.Kvs { 100 workload := &types.Workload{} 101 if err := json.Unmarshal(ev.Value, workload); err != nil { 102 return nil, err 103 } 104 if utils.LabelsFilter(workload.Labels, labels) { 105 workloads = append(workloads, workload) 106 } 107 } 108 109 return m.bindWorkloadsAdditions(ctx, workloads) 110 } 111 112 // ListNodeWorkloads list workloads belong to one node 113 func (m *Mercury) ListNodeWorkloads(ctx context.Context, nodename string, labels map[string]string) ([]*types.Workload, error) { 114 key := fmt.Sprintf(nodeWorkloadsKey, nodename, "") 115 resp, err := m.Get(ctx, key, clientv3.WithPrefix()) 116 if err != nil { 117 return nil, err 118 } 119 120 workloads := []*types.Workload{} 121 for _, ev := range resp.Kvs { 122 workload := &types.Workload{} 123 if err := json.Unmarshal(ev.Value, workload); err != nil { 124 return nil, err 125 } 126 if utils.LabelsFilter(workload.Labels, labels) { 127 workloads = append(workloads, workload) 128 } 129 } 130 131 return m.bindWorkloadsAdditions(ctx, workloads) 132 } 133 134 // WorkloadStatusStream watch deployed status 135 func (m *Mercury) WorkloadStatusStream(ctx context.Context, appname, entrypoint, nodename string, labels map[string]string) chan *types.WorkloadStatus { 136 if appname == "" { 137 entrypoint = "" 138 } 139 if entrypoint == "" { 140 nodename = "" 141 } 142 // 显式加个 / 保证 prefix 唯一 143 statusKey := filepath.Join(workloadStatusPrefix, appname, entrypoint, nodename) + "/" 144 ch := make(chan *types.WorkloadStatus) 145 logger := log.WithFunc("store.etcdv3.WorkloadStatusStream") 146 _ = m.pool.Invoke(func() { 147 defer func() { 148 logger.Info(ctx, "close WorkloadStatus channel") 149 close(ch) 150 }() 151 152 logger.Infof(ctx, "watch on %s", statusKey) 153 for resp := range m.Watch(ctx, statusKey, clientv3.WithPrefix()) { 154 if resp.Err() != nil { 155 if !resp.Canceled { 156 logger.Error(ctx, resp.Err(), "watch failed") 157 } 158 return 159 } 160 for _, ev := range resp.Events { 161 _, _, _, ID := parseStatusKey(string(ev.Kv.Key)) 162 msg := &types.WorkloadStatus{ID: ID, Delete: ev.Type == clientv3.EventTypeDelete} 163 workload, err := m.GetWorkload(ctx, ID) 164 switch { 165 case err != nil: 166 msg.Error = err 167 case utils.LabelsFilter(workload.Labels, labels): 168 logger.Debugf(ctx, "workload %s status changed", workload.ID) 169 msg.Workload = workload 170 default: 171 continue 172 } 173 ch <- msg 174 } 175 } 176 }) 177 return ch 178 } 179 180 func (m *Mercury) cleanWorkloadData(ctx context.Context, workload *types.Workload) error { 181 appname, entrypoint, _, err := utils.ParseWorkloadName(workload.Name) 182 if err != nil { 183 return err 184 } 185 186 keys := []string{ 187 filepath.Join(workloadStatusPrefix, appname, entrypoint, workload.Nodename, workload.ID), // workload deploy status 188 filepath.Join(workloadDeployPrefix, appname, entrypoint, workload.Nodename, workload.ID), // workload deploy status 189 fmt.Sprintf(workloadInfoKey, workload.ID), // workload info 190 fmt.Sprintf(nodeWorkloadsKey, workload.Nodename, workload.ID), // node workloads 191 } 192 _, err = m.BatchDelete(ctx, keys) 193 return err 194 } 195 196 func (m *Mercury) doGetWorkloads(ctx context.Context, keys []string) (workloads []*types.Workload, err error) { 197 var kvs []*mvccpb.KeyValue 198 if kvs, err = m.GetMulti(ctx, keys); err != nil { 199 return 200 } 201 202 for _, kv := range kvs { 203 workload := &types.Workload{} 204 if err = json.Unmarshal(kv.Value, workload); err != nil { 205 log.WithFunc("store.etcdv3.doGetWorkloads").Errorf(ctx, err, "failed to unmarshal %+v", string(kv.Key)) 206 return 207 } 208 workloads = append(workloads, workload) 209 } 210 211 return m.bindWorkloadsAdditions(ctx, workloads) 212 } 213 214 func (m *Mercury) bindWorkloadsAdditions(ctx context.Context, workloads []*types.Workload) ([]*types.Workload, error) { 215 nodes := map[string]*types.Node{} 216 nodenames := []string{} 217 nodenameCache := map[string]struct{}{} 218 statusKeys := map[string]string{} 219 logger := log.WithFunc("store.etcdv3.bindWorkloadsAdditions") 220 for _, workload := range workloads { 221 appname, entrypoint, _, err := utils.ParseWorkloadName(workload.Name) 222 if err != nil { 223 return nil, err 224 } 225 statusKeys[workload.ID] = filepath.Join(workloadStatusPrefix, appname, entrypoint, workload.Nodename, workload.ID) 226 if _, ok := nodenameCache[workload.Nodename]; !ok { 227 nodenameCache[workload.Nodename] = struct{}{} 228 nodenames = append(nodenames, workload.Nodename) 229 } 230 } 231 ns, err := m.GetNodes(ctx, nodenames) 232 if err != nil { 233 return nil, err 234 } 235 for _, node := range ns { 236 nodes[node.Name] = node 237 } 238 239 for index, workload := range workloads { 240 if _, ok := nodes[workload.Nodename]; !ok { 241 return nil, types.ErrInvaildWorkloadMeta 242 } 243 workloads[index].Engine = nodes[workload.Nodename].Engine 244 if _, ok := statusKeys[workload.ID]; !ok { 245 continue 246 } 247 kv, err := m.GetOne(ctx, statusKeys[workload.ID]) 248 if err != nil { 249 continue 250 } 251 status := &types.StatusMeta{} 252 if err := json.Unmarshal(kv.Value, &status); err != nil { 253 logger.Warnf(ctx, "unmarshal %s status data failed %+v", workload.ID, err) 254 logger.Errorf(ctx, err, "status raw: %s", kv.Value) 255 continue 256 } 257 workloads[index].StatusMeta = status 258 } 259 return workloads, nil 260 } 261 262 func (m *Mercury) doOpsWorkload(ctx context.Context, workload *types.Workload, processing *types.Processing, create bool) error { 263 var err error 264 appname, entrypoint, _, err := utils.ParseWorkloadName(workload.Name) 265 if err != nil { 266 return err 267 } 268 269 // now everything is ok 270 // we use full length ID instead 271 bytes, err := json.Marshal(workload) 272 if err != nil { 273 return err 274 } 275 workloadData := string(bytes) 276 277 data := map[string]string{ 278 fmt.Sprintf(workloadInfoKey, workload.ID): workloadData, 279 fmt.Sprintf(nodeWorkloadsKey, workload.Nodename, workload.ID): workloadData, 280 filepath.Join(workloadDeployPrefix, appname, entrypoint, workload.Nodename, workload.ID): workloadData, 281 } 282 283 var resp *clientv3.TxnResponse 284 if create { 285 if processing != nil { 286 processingKey := m.getProcessingKey(processing) 287 err = m.BatchCreateAndDecr(ctx, data, processingKey) 288 } else { 289 resp, err = m.BatchCreate(ctx, data) 290 } 291 } else { 292 resp, err = m.BatchUpdate(ctx, data) 293 } 294 if err != nil { 295 return err 296 } 297 if resp != nil && !resp.Succeeded { 298 return types.ErrTxnConditionFailed 299 } 300 return nil 301 }