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  }