github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/tgtetl.go (about) 1 // Package ais provides core functionality for the AIStore object storage. 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package ais 6 7 import ( 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "strconv" 13 14 "github.com/NVIDIA/aistore/api/apc" 15 "github.com/NVIDIA/aistore/cmn" 16 "github.com/NVIDIA/aistore/cmn/cos" 17 "github.com/NVIDIA/aistore/cmn/debug" 18 "github.com/NVIDIA/aistore/cmn/k8s" 19 "github.com/NVIDIA/aistore/cmn/nlog" 20 "github.com/NVIDIA/aistore/core" 21 "github.com/NVIDIA/aistore/core/meta" 22 "github.com/NVIDIA/aistore/ext/etl" 23 "github.com/NVIDIA/aistore/fs" 24 ) 25 26 // [METHOD] /v1/etl 27 func (t *target) etlHandler(w http.ResponseWriter, r *http.Request) { 28 if !k8s.IsK8s() { 29 t.writeErr(w, r, k8s.ErrK8sRequired, 0, Silent) 30 return 31 } 32 switch { 33 case r.Method == http.MethodPut: 34 t.handleETLPut(w, r) 35 case r.Method == http.MethodPost: 36 t.handleETLPost(w, r) 37 case r.Method == http.MethodGet: 38 t.handleETLGet(w, r) 39 case r.Method == http.MethodHead: 40 t.headObjectETL(w, r) 41 default: 42 cmn.WriteErr405(w, r, http.MethodGet, http.MethodHead, http.MethodPost) 43 } 44 } 45 46 // PUT /v1/etl 47 // start ETL spec/code 48 func (t *target) handleETLPut(w http.ResponseWriter, r *http.Request) { 49 // disallow to run when above high wm (let alone OOS) 50 cs := fs.Cap() 51 if err := cs.Err(); err != nil { 52 t.writeErr(w, r, err, http.StatusInsufficientStorage) 53 return 54 } 55 if _, err := t.parseURL(w, r, apc.URLPathETL.L, 0, false); err != nil { 56 return 57 } 58 59 b, err := io.ReadAll(r.Body) 60 if err != nil { 61 t.writeErr(w, r, err) 62 return 63 } 64 r.Body.Close() 65 66 initMsg, err := etl.UnmarshalInitMsg(b) 67 if err != nil { 68 t.writeErr(w, r, err) 69 return 70 } 71 xid := r.URL.Query().Get(apc.QparamUUID) 72 73 switch msg := initMsg.(type) { 74 case *etl.InitSpecMsg: 75 err = etl.InitSpec(msg, xid, etl.StartOpts{}) 76 case *etl.InitCodeMsg: 77 err = etl.InitCode(msg, xid) 78 default: 79 debug.Assert(false, initMsg.String()) 80 } 81 if err != nil { 82 t.writeErr(w, r, err) 83 return 84 } 85 if cmn.Rom.FastV(4, cos.SmoduleETL) { 86 nlog.Infoln(t.String() + ": " + initMsg.String()) 87 } 88 } 89 90 func (t *target) handleETLGet(w http.ResponseWriter, r *http.Request) { 91 apiItems, err := t.parseURL(w, r, apc.URLPathETL.L, 0, true) 92 if err != nil { 93 return 94 } 95 96 // /v1/etl 97 if len(apiItems) == 0 { 98 t.writeJSON(w, r, etl.List(), "list-etl") 99 return 100 } 101 102 // /v1/etl/_objects/<secret>/<uname> 103 if apiItems[0] == apc.ETLObject { 104 t.getObjectETL(w, r) 105 return 106 } 107 108 // /v1/etl/<etl-name> 109 if len(apiItems) == 1 { 110 t.writeErr(w, r, fmt.Errorf("GET(ETL[%s] info) not implemented yet", apiItems[0]), http.StatusNotImplemented) 111 return 112 } 113 114 // /v1/etl/<etl-name>/logs or /v1/etl/<etl-name>/health or /v1/etl/<etl-name>/metrics 115 switch apiItems[1] { 116 case apc.ETLLogs: 117 t.logsETL(w, r, apiItems[0]) 118 case apc.ETLHealth: 119 t.healthETL(w, r, apiItems[0]) 120 case apc.ETLMetrics: 121 k8s.InitMetricsClient() 122 t.metricsETL(w, r, apiItems[0]) 123 default: 124 t.writeErrURL(w, r) 125 } 126 } 127 128 // POST /v1/etl/<etl-name>/stop (or) TODO: /v1/etl/<etl-name>/start 129 // 130 // Handles starting/stopping ETL pods 131 func (t *target) handleETLPost(w http.ResponseWriter, r *http.Request) { 132 apiItems, err := t.parseURL(w, r, apc.URLPathETL.L, 2, true) 133 if err != nil { 134 return 135 } 136 if apiItems[1] == apc.ETLStop { 137 t.stopETL(w, r, apiItems[0]) 138 return 139 } 140 // TODO: Implement ETLStart to start inactive ETLs 141 t.writeErrURL(w, r) 142 } 143 144 func (t *target) stopETL(w http.ResponseWriter, r *http.Request, etlName string) { 145 if err := etl.Stop(etlName, cmn.ErrXactUserAbort); err != nil { 146 statusCode := http.StatusBadRequest 147 if cos.IsErrNotFound(err) { 148 statusCode = http.StatusNotFound 149 } 150 t.writeErr(w, r, err, statusCode) 151 return 152 } 153 } 154 155 func (t *target) getETL(w http.ResponseWriter, r *http.Request, etlName string, bck *meta.Bck, objName string) { 156 var ( 157 comm etl.Communicator 158 err error 159 ) 160 comm, err = etl.GetCommunicator(etlName) 161 if err != nil { 162 if cos.IsErrNotFound(err) { 163 smap := t.owner.smap.Get() 164 errV := fmt.Errorf("%v - try starting new ETL with \"%s/v1/etl/init\" endpoint", 165 err, smap.Primary.URL(cmn.NetPublic)) 166 t.writeErr(w, r, errV, http.StatusNotFound) 167 return 168 } 169 t.writeErr(w, r, err) 170 return 171 } 172 if err := comm.InlineTransform(w, r, bck, objName); err != nil { 173 errV := cmn.NewErrETL(&cmn.ETLErrCtx{ETLName: etlName, PodName: comm.PodName(), SvcName: comm.SvcName()}, 174 err.Error()) 175 xetl := comm.Xact() 176 xetl.AddErr(errV) 177 t.writeErr(w, r, errV) 178 } 179 } 180 181 func (t *target) logsETL(w http.ResponseWriter, r *http.Request, etlName string) { 182 logs, err := etl.PodLogs(etlName) 183 if err != nil { 184 t.writeErr(w, r, err) 185 return 186 } 187 t.writeJSON(w, r, logs, "logs-etl") 188 } 189 190 func (t *target) healthETL(w http.ResponseWriter, r *http.Request, etlName string) { 191 health, err := etl.PodHealth(etlName) 192 if err != nil { 193 if cos.IsErrNotFound(err) { 194 t.writeErr(w, r, err, http.StatusNotFound, Silent) 195 } else { 196 t.writeErr(w, r, err) 197 } 198 return 199 } 200 w.Header().Set(cos.HdrContentLength, strconv.Itoa(len(health))) 201 w.Write([]byte(health)) 202 } 203 204 func (t *target) metricsETL(w http.ResponseWriter, r *http.Request, etlName string) { 205 metricMsg, err := etl.PodMetrics(etlName) 206 if err != nil { 207 if cos.IsErrNotFound(err) { 208 t.writeErr(w, r, err, http.StatusNotFound, Silent) 209 } else { 210 t.writeErr(w, r, err) 211 } 212 return 213 } 214 t.writeJSON(w, r, metricMsg, "metrics-etl") 215 } 216 217 func etlParseObjectReq(_ http.ResponseWriter, r *http.Request) (secret string, bck *meta.Bck, objName string, err error) { 218 var items []string 219 items, err = cmn.ParseURL(r.URL.EscapedPath(), apc.URLPathETLObject.L, 2, false) 220 if err != nil { 221 return 222 } 223 secret = items[0] 224 // Encoding is done in `transformerPath`. 225 var uname string 226 uname, err = url.PathUnescape(items[1]) 227 if err != nil { 228 return 229 } 230 var b cmn.Bck 231 b, objName = cmn.ParseUname(uname) 232 if err = b.Validate(); err != nil { 233 err = fmt.Errorf("%v, uname=%q", err, uname) 234 return 235 } 236 if objName == "" { 237 err = fmt.Errorf("object name is missing (bucket=%s, uname=%q)", b, uname) 238 return 239 } 240 bck = meta.CloneBck(&b) 241 return 242 } 243 244 // GET /v1/etl/_objects/<secret>/<uname> 245 // Handles GET requests from ETL containers (K8s Pods). 246 // Validates the secret that was injected into a Pod during its initialization 247 // (see boot.go `_setPodEnv`). 248 // 249 // NOTE: this is an internal URL with "_objects" in its path intended to avoid 250 // conflicts with ETL name in `/v1/elts/<etl-name>/...` 251 func (t *target) getObjectETL(w http.ResponseWriter, r *http.Request) { 252 secret, bck, objName, err := etlParseObjectReq(w, r) 253 if err != nil { 254 t.writeErr(w, r, err) 255 return 256 } 257 if err := etl.CheckSecret(secret); err != nil { 258 t.writeErr(w, r, err) 259 return 260 } 261 dpq := dpqAlloc() 262 if err := dpq.parse(r.URL.RawQuery); err != nil { 263 dpqFree(dpq) 264 t.writeErr(w, r, err) 265 return 266 } 267 lom := core.AllocLOM(objName) 268 lom, err = t.getObject(w, r, dpq, bck, lom) 269 core.FreeLOM(lom) 270 271 if err != nil { 272 t._erris(w, r, dpq.silent, err, 0) 273 } 274 dpqFree(dpq) 275 } 276 277 // HEAD /v1/etl/objects/<secret>/<uname> 278 // 279 // Handles HEAD requests from ETL containers (K8s Pods). 280 // Validates the secret that was injected into a Pod during its initialization. 281 func (t *target) headObjectETL(w http.ResponseWriter, r *http.Request) { 282 secret, bck, objName, err := etlParseObjectReq(w, r) 283 if err != nil { 284 t.writeErr(w, r, err) 285 return 286 } 287 if err := etl.CheckSecret(secret); err != nil { 288 t.writeErr(w, r, err) 289 return 290 } 291 292 lom := core.AllocLOM(objName) 293 ecode, err := t.objHead(w.Header(), r.URL.Query(), bck, lom) 294 core.FreeLOM(lom) 295 if err != nil { 296 // always silent (compare w/ httpobjhead) 297 t.writeErr(w, r, err, ecode, Silent) 298 } 299 }