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  }