github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ext/etl/api.go (about)

     1  // Package etl provides utilities to initialize and use transformation pods.
     2  /*
     3   * Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package etl
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"sort"
    11  	"time"
    12  
    13  	"github.com/NVIDIA/aistore/cmn"
    14  	"github.com/NVIDIA/aistore/cmn/cos"
    15  	"github.com/NVIDIA/aistore/cmn/feat"
    16  	"github.com/NVIDIA/aistore/cmn/k8s"
    17  	"github.com/NVIDIA/aistore/ext/etl/runtime"
    18  	jsoniter "github.com/json-iterator/go"
    19  	corev1 "k8s.io/api/core/v1"
    20  	"k8s.io/client-go/kubernetes/scheme"
    21  )
    22  
    23  const PrefixXactID = "etl-"
    24  
    25  const (
    26  	Spec = "spec"
    27  	Code = "code"
    28  )
    29  
    30  // consistent with rfc2396.txt "Uniform Resource Identifiers (URI): Generic Syntax"
    31  const CommTypeSeparator = "://"
    32  
    33  const DefaultTimeout = 45 * time.Second
    34  
    35  // enum communication types (`commTypes`)
    36  const (
    37  	// ETL container receives POST request from target with the data. It
    38  	// must read the data and return response to the target which then will be
    39  	// transferred to the client.
    40  	Hpush = "hpush://"
    41  	// Target redirects the GET request to the ETL container. Then ETL container
    42  	// contacts the target via `AIS_TARGET_URL` env variable to get the data.
    43  	// The data is then transformed and returned to the client.
    44  	Hpull = "hpull://"
    45  	// Similar to redirection strategy but with usage of reverse proxy.
    46  	Hrev = "hrev://"
    47  	// Stdin/stdout communication.
    48  	HpushStdin = "io://"
    49  )
    50  
    51  // enum arg types (`argTypes`)
    52  const (
    53  	ArgTypeDefault = ""
    54  	ArgTypeURL     = "url"
    55  	ArgTypeFQN     = "fqn"
    56  )
    57  
    58  type (
    59  	InitMsg interface {
    60  		Name() string
    61  		MsgType() string // Code or Spec
    62  		CommType() string
    63  		ArgType() string
    64  		Validate() error
    65  		String() string
    66  	}
    67  
    68  	// and implementations
    69  	InitMsgBase struct {
    70  		IDX       string       `json:"id"`            // etlName (not to be confused)
    71  		CommTypeX string       `json:"communication"` // enum commTypes
    72  		ArgTypeX  string       `json:"argument"`      // enum argTypes
    73  		Timeout   cos.Duration `json:"timeout"`
    74  	}
    75  	InitSpecMsg struct {
    76  		InitMsgBase
    77  		Spec []byte `json:"spec"`
    78  	}
    79  
    80  	InitCodeMsg struct {
    81  		InitMsgBase
    82  		Code    []byte `json:"code"`
    83  		Deps    []byte `json:"dependencies"`
    84  		Runtime string `json:"runtime"`
    85  		// ========================================================================================
    86  		// InitCodeMsg carries the name of the transforming function;
    87  		// the `Transform` function is mandatory and cannot be "" (empty) - it _will_ be called
    88  		// by the `Runtime` container (see etl/runtime/all.go for all supported pre-built runtimes);
    89  		// =========================================================================================
    90  		Funcs struct {
    91  			Transform string `json:"transform"` // cannot be omitted
    92  		}
    93  		// 0 (zero) - read the entire payload in memory and then transform it in one shot;
    94  		// > 0 - use chunk-size buffering and transform incrementally, one chunk at a time
    95  		ChunkSize int64 `json:"chunk_size"`
    96  		// bitwise flags: (streaming | debug | strict | ...) future enhancements
    97  		Flags int64 `json:"flags"`
    98  	}
    99  )
   100  
   101  type (
   102  	InfoList []Info
   103  	Info     struct {
   104  		Name     string `json:"id"`
   105  		XactID   string `json:"xaction_id"`
   106  		ObjCount int64  `json:"obj_count"`
   107  		InBytes  int64  `json:"in_bytes"`
   108  		OutBytes int64  `json:"out_bytes"`
   109  	}
   110  
   111  	LogsByTarget []Logs
   112  	Logs         struct {
   113  		TargetID string `json:"target_id"`
   114  		Logs     []byte `json:"logs"`
   115  	}
   116  
   117  	HealthByTarget []*HealthStatus
   118  	HealthStatus   struct {
   119  		TargetID string `json:"target_id"`
   120  		Status   string `json:"health_status"` // enum { HealthStatusRunning, ... } above
   121  	}
   122  
   123  	CPUMemByTarget []*CPUMemUsed
   124  	CPUMemUsed     struct {
   125  		TargetID string  `json:"target_id"`
   126  		CPU      float64 `json:"cpu"`
   127  		Mem      int64   `json:"mem"`
   128  	}
   129  )
   130  
   131  var (
   132  	commTypes = []string{Hpush, Hpull, Hrev, HpushStdin}         // NOTE: must contain all
   133  	argTypes  = []string{ArgTypeDefault, ArgTypeURL, ArgTypeFQN} // ditto
   134  )
   135  
   136  ////////////////
   137  // InitMsg*** //
   138  ////////////////
   139  
   140  // interface guard
   141  var (
   142  	_ InitMsg = (*InitCodeMsg)(nil)
   143  	_ InitMsg = (*InitSpecMsg)(nil)
   144  )
   145  
   146  func (m InitMsgBase) CommType() string { return m.CommTypeX }
   147  func (m InitMsgBase) ArgType() string  { return m.ArgTypeX }
   148  func (m InitMsgBase) Name() string     { return m.IDX }
   149  func (*InitCodeMsg) MsgType() string   { return Code }
   150  func (*InitSpecMsg) MsgType() string   { return Spec }
   151  
   152  func (m *InitCodeMsg) String() string {
   153  	return fmt.Sprintf("init-%s[%s-%s-%s-%s]", Code, m.IDX, m.CommTypeX, m.ArgTypeX, m.Runtime)
   154  }
   155  
   156  func (m *InitSpecMsg) String() string {
   157  	return fmt.Sprintf("init-%s[%s-%s-%s]", Spec, m.IDX, m.CommTypeX, m.ArgTypeX)
   158  }
   159  
   160  // TODO: double-take, unmarshaling-wise. To avoid, include (`Spec`, `Code`) in API calls
   161  func UnmarshalInitMsg(b []byte) (msg InitMsg, err error) {
   162  	var msgInf map[string]json.RawMessage
   163  	if err = jsoniter.Unmarshal(b, &msgInf); err != nil {
   164  		return
   165  	}
   166  	if _, ok := msgInf[Code]; ok {
   167  		msg = &InitCodeMsg{}
   168  		err = jsoniter.Unmarshal(b, msg)
   169  		return
   170  	}
   171  	if _, ok := msgInf[Spec]; ok {
   172  		msg = &InitSpecMsg{}
   173  		err = jsoniter.Unmarshal(b, msg)
   174  		return
   175  	}
   176  	err = fmt.Errorf("invalid etl.InitMsg: %+v", msgInf)
   177  	return
   178  }
   179  
   180  func (m *InitMsgBase) validate(detail string) error {
   181  	if err := k8s.ValidateEtlName(m.IDX); err != nil {
   182  		return fmt.Errorf("%v [%s]", err, detail)
   183  	}
   184  
   185  	errCtx := &cmn.ETLErrCtx{ETLName: m.Name()}
   186  	if m.CommTypeX != "" && !cos.StringInSlice(m.CommTypeX, commTypes) {
   187  		err := fmt.Errorf("unknown comm-type %q", m.CommTypeX)
   188  		return cmn.NewErrETL(errCtx, "%v [%s]", err, detail)
   189  	}
   190  
   191  	if !cos.StringInSlice(m.ArgTypeX, argTypes) {
   192  		err := fmt.Errorf("unsupported arg-type %q", m.ArgTypeX)
   193  		return cmn.NewErrETL(errCtx, "%v [%s]", err, detail)
   194  	}
   195  
   196  	//
   197  	// not-implemented-yet type limitations:
   198  	//
   199  	if m.ArgTypeX == ArgTypeURL && m.CommTypeX != Hpull {
   200  		err := fmt.Errorf("arg-type %q requires comm-type %q (%q is not supported yet)", m.ArgTypeX, Hpull, m.CommTypeX)
   201  		return cmn.NewErrETL(errCtx, "%v [%s]", err, detail)
   202  	}
   203  	if m.ArgTypeX == ArgTypeFQN && !(m.CommTypeX == Hpull || m.CommTypeX == Hpush) {
   204  		err := fmt.Errorf("arg-type %q requires comm-type (%q or %q) - %q is not supported yet",
   205  			m.ArgTypeX, Hpull, Hpush, m.CommTypeX)
   206  		return cmn.NewErrETL(errCtx, "%v [%s]", err, detail)
   207  	}
   208  
   209  	//
   210  	// ArgTypeFQN ("fqn") can also be globally disallowed
   211  	//
   212  	if m.ArgTypeX == ArgTypeFQN && cmn.Rom.Features().IsSet(feat.DontAllowPassingFQNtoETL) {
   213  		err := fmt.Errorf("arg-type %q is not permitted by the configured feature flags (%s)",
   214  			m.ArgTypeX, cmn.Rom.Features().String())
   215  		return cmn.NewErrETL(errCtx, "%v [%s]", err, detail)
   216  	}
   217  
   218  	// NOTE: default comm-type
   219  	if m.CommType() == "" {
   220  		cos.Infof("Warning: empty comm-type, defaulting to %q", Hpush)
   221  		m.CommTypeX = Hpush
   222  	}
   223  	// NOTE: default timeout
   224  	if m.Timeout == 0 {
   225  		m.Timeout = cos.Duration(DefaultTimeout)
   226  	}
   227  	return nil
   228  }
   229  
   230  func (m *InitCodeMsg) Validate() error {
   231  	if err := m.InitMsgBase.validate(m.String()); err != nil {
   232  		return err
   233  	}
   234  
   235  	if len(m.Code) == 0 {
   236  		return fmt.Errorf("source code is empty (%q)", m.Runtime)
   237  	}
   238  	if m.Runtime == "" {
   239  		return fmt.Errorf("runtime is not specified (comm-type %q)", m.CommTypeX)
   240  	}
   241  	if _, ok := runtime.Get(m.Runtime); !ok {
   242  		return fmt.Errorf("unsupported runtime %q (supported: %v)", m.Runtime, runtime.GetNames())
   243  	}
   244  
   245  	if m.Funcs.Transform == "" {
   246  		return fmt.Errorf("transform function cannot be empty (comm-type %q, funcs %+v)", m.CommTypeX, m.Funcs)
   247  	}
   248  	if m.ChunkSize < 0 || m.ChunkSize > cos.MiB {
   249  		return fmt.Errorf("chunk-size %d is invalid, expecting 0 <= chunk-size <= MiB (%q, comm-type %q)",
   250  			m.ChunkSize, m.CommTypeX, m.Runtime)
   251  	}
   252  	return nil
   253  }
   254  
   255  func (m *InitSpecMsg) Validate() (err error) {
   256  	if err := m.InitMsgBase.validate(m.String()); err != nil {
   257  		return err
   258  	}
   259  
   260  	errCtx := &cmn.ETLErrCtx{ETLName: m.Name()}
   261  
   262  	// Check pod specification constraints.
   263  	pod, err := ParsePodSpec(errCtx, m.Spec)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	if len(pod.Spec.Containers) != 1 {
   268  		err = cmn.NewErrETL(errCtx, "unsupported number of containers (%d), expected: 1", len(pod.Spec.Containers))
   269  		return
   270  	}
   271  	container := pod.Spec.Containers[0]
   272  	if len(container.Ports) != 1 {
   273  		return cmn.NewErrETL(errCtx, "unsupported number of container ports (%d), expected: 1", len(container.Ports))
   274  	}
   275  	if container.Ports[0].Name != k8s.Default {
   276  		return cmn.NewErrETL(errCtx, "expected port name: %q, got: %q", k8s.Default, container.Ports[0].Name)
   277  	}
   278  
   279  	// Validate that user container supports health check.
   280  	// Currently we need the `default` port (on which the application runs) to
   281  	// be same as the `readiness` probe port.
   282  	if container.ReadinessProbe == nil {
   283  		return cmn.NewErrETL(errCtx, "readinessProbe section is required in a container spec")
   284  	}
   285  	// TODO: Add support for other health checks.
   286  	if container.ReadinessProbe.HTTPGet == nil {
   287  		return cmn.NewErrETL(errCtx, "httpGet missing in the readinessProbe")
   288  	}
   289  	if container.ReadinessProbe.HTTPGet.Path == "" {
   290  		return cmn.NewErrETL(errCtx, "expected non-empty path for readinessProbe")
   291  	}
   292  	// Currently we need the `default` port (on which the application runs)
   293  	// to be same as the `readiness` probe port in the pod spec.
   294  	if container.ReadinessProbe.HTTPGet.Port.StrVal != k8s.Default {
   295  		return cmn.NewErrETL(errCtx, "readinessProbe port must be the %q port", k8s.Default)
   296  	}
   297  	return nil
   298  }
   299  
   300  func ParsePodSpec(errCtx *cmn.ETLErrCtx, spec []byte) (*corev1.Pod, error) {
   301  	obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(spec, nil, nil)
   302  	if err != nil {
   303  		return nil, cmn.NewErrETL(errCtx, "failed to parse pod spec: %v\n%q", err, string(spec))
   304  	}
   305  	pod, ok := obj.(*corev1.Pod)
   306  	if !ok {
   307  		kind := obj.GetObjectKind().GroupVersionKind().Kind
   308  		return nil, cmn.NewErrETL(errCtx, "expected pod spec, got: %s", kind)
   309  	}
   310  	return pod, nil
   311  }
   312  
   313  //////////////
   314  // InfoList //
   315  //////////////
   316  
   317  var _ sort.Interface = (*InfoList)(nil)
   318  
   319  func (il InfoList) Len() int           { return len(il) }
   320  func (il InfoList) Less(i, j int) bool { return il[i].Name < il[j].Name }
   321  func (il InfoList) Swap(i, j int)      { il[i], il[j] = il[j], il[i] }