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] }