github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/prxbck.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  	"errors"
     9  	"net/http"
    10  	"net/url"
    11  	"sync"
    12  
    13  	"github.com/NVIDIA/aistore/api/apc"
    14  	"github.com/NVIDIA/aistore/cmn"
    15  	"github.com/NVIDIA/aistore/cmn/cos"
    16  	"github.com/NVIDIA/aistore/cmn/debug"
    17  	"github.com/NVIDIA/aistore/cmn/nlog"
    18  	"github.com/NVIDIA/aistore/core/meta"
    19  	"github.com/NVIDIA/aistore/xact"
    20  )
    21  
    22  type bctx struct {
    23  	w http.ResponseWriter
    24  	r *http.Request
    25  
    26  	p *proxy
    27  
    28  	bck *meta.Bck
    29  	msg *apc.ActMsg
    30  
    31  	// URL query: the conventional/slow and
    32  	// the fast alternative tailored exclusively for the datapath
    33  	query url.Values
    34  	dpq   *dpq
    35  
    36  	origURLBck string
    37  
    38  	reqBody []byte          // request body of original request
    39  	perms   apc.AccessAttrs // apc.AceGET, apc.AcePATCH etc.
    40  
    41  	// 5 user or caller-provided control flags followed by
    42  	// 3 result flags
    43  	skipBackend    bool // initialize bucket via `bck.InitNoBackend`
    44  	createAIS      bool // create ais bucket on the fly
    45  	dontAddRemote  bool // do not create (ie., add -> BMD) remote bucket on the fly
    46  	dontHeadRemote bool // do not HEAD remote bucket (to find out whether it exists and/or get properties)
    47  	tryHeadRemote  bool // when listing objects anonymously (via ListObjsMsg.Flags LsTryHeadRemote)
    48  	isPresent      bool // the bucket is confirmed to be present (in the cluster's BMD)
    49  	exists         bool // remote bucket is confirmed to exist
    50  	modified       bool // bucket-defining control structure got modified
    51  }
    52  
    53  ////////////////
    54  // ibargsPool //
    55  ////////////////
    56  
    57  var (
    58  	ibargsPool sync.Pool
    59  	ib0        bctx
    60  )
    61  
    62  func allocBctx() (a *bctx) {
    63  	if v := ibargsPool.Get(); v != nil {
    64  		a = v.(*bctx)
    65  		return
    66  	}
    67  	return &bctx{}
    68  }
    69  
    70  func freeBctx(a *bctx) {
    71  	*a = ib0
    72  	ibargsPool.Put(a)
    73  }
    74  
    75  //
    76  // lookup and add buckets on the fly
    77  //
    78  
    79  func (p *proxy) a2u(aliasOrUUID string) string {
    80  	p.remais.mu.RLock()
    81  	for _, remais := range p.remais.A {
    82  		if aliasOrUUID == remais.Alias || aliasOrUUID == remais.UUID {
    83  			p.remais.mu.RUnlock()
    84  			return remais.UUID
    85  		}
    86  	}
    87  	l := len(p.remais.old)
    88  	for i := l - 1; i >= 0; i-- {
    89  		remais := p.remais.old[i]
    90  		if aliasOrUUID == remais.Alias || aliasOrUUID == remais.UUID {
    91  			p.remais.mu.RUnlock()
    92  			return remais.UUID
    93  		}
    94  	}
    95  	p.remais.mu.RUnlock()
    96  	return aliasOrUUID
    97  }
    98  
    99  // initialize bucket and check access permissions
   100  func (bctx *bctx) init() (ecode int, err error) {
   101  	debug.Assert(bctx.bck != nil)
   102  
   103  	bck := bctx.bck
   104  
   105  	// remote ais aliasing
   106  	if bck.IsRemoteAIS() {
   107  		if uuid := bctx.p.a2u(bck.Ns.UUID); uuid != bck.Ns.UUID {
   108  			bctx.modified = true
   109  			// care of targets
   110  			query := bctx.query
   111  			if query == nil {
   112  				query = bctx.r.URL.Query()
   113  			}
   114  			bck.Ns.UUID = uuid
   115  			query.Set(apc.QparamNamespace, bck.Ns.Uname())
   116  			bctx.r.URL.RawQuery = query.Encode()
   117  		}
   118  	}
   119  
   120  	if err = bctx.accessSupported(); err != nil {
   121  		ecode = http.StatusMethodNotAllowed
   122  		return
   123  	}
   124  	if bctx.skipBackend {
   125  		err = bck.InitNoBackend(bctx.p.owner.bmd)
   126  	} else {
   127  		err = bck.Init(bctx.p.owner.bmd)
   128  	}
   129  	if err != nil {
   130  		ecode = http.StatusBadRequest
   131  		if cmn.IsErrBucketNought(err) {
   132  			ecode = http.StatusNotFound
   133  		}
   134  		return
   135  	}
   136  
   137  	bctx.isPresent = true
   138  
   139  	// if permissions are not explicitly specified check the default (msg.Action => permissions)
   140  	if bctx.perms == 0 && bctx.msg != nil {
   141  		dtor, ok := xact.Table[bctx.msg.Action]
   142  		if !ok || dtor.Access == 0 {
   143  			return
   144  		}
   145  		bctx.perms = dtor.Access
   146  	}
   147  	ecode, err = bctx.accessAllowed(bck)
   148  	return
   149  }
   150  
   151  // returns true when operation requires the 'perm' type access
   152  func (bctx *bctx) _perm(perm apc.AccessAttrs) bool { return (bctx.perms & perm) == perm }
   153  
   154  // (compare w/ accessAllowed)
   155  func (bctx *bctx) accessSupported() error {
   156  	if !bctx.bck.IsRemote() {
   157  		return nil
   158  	}
   159  
   160  	var op string
   161  	if bctx._perm(apc.AceMoveBucket) {
   162  		op = "rename/move remote bucket"
   163  		goto rerr
   164  	}
   165  	// HTTP buckets are not writeable
   166  	if bctx.bck.IsHTTP() && bctx._perm(apc.AcePUT) {
   167  		op = "write to HTTP bucket"
   168  		goto rerr
   169  	}
   170  	// Cloud bucket: destroy op. not allowed, and not supported yet
   171  	// (have no separate perm for eviction, that's why an extra check)
   172  	if rmb := bctx.bck.IsCloud() && bctx._perm(apc.AceDestroyBucket) && bctx.msg.Action == apc.ActDestroyBck; !rmb {
   173  		return nil
   174  	}
   175  	op = "destroy cloud bucket"
   176  rerr:
   177  	return cmn.NewErrUnsupp(op, bctx.bck.Cname(""))
   178  }
   179  
   180  // (compare w/ accessSupported)
   181  func (bctx *bctx) accessAllowed(bck *meta.Bck) (ecode int, err error) {
   182  	err = bctx.p.access(bctx.r.Header, bck, bctx.perms)
   183  	ecode = aceErrToCode(err)
   184  	return ecode, err
   185  }
   186  
   187  // initAndTry initializes the bucket (proxy-only, as the filename implies).
   188  // The method _may_ try to add it to the BMD if the bucket doesn't exist.
   189  // NOTE:
   190  // - on error it calls `p.writeErr` and friends, so make sure _not_ to do the same in the caller
   191  // - for remais buckets: user-provided alias(***)
   192  func (bctx *bctx) initAndTry() (bck *meta.Bck, err error) {
   193  	var ecode int
   194  
   195  	// 1. init bucket
   196  	bck = bctx.bck
   197  	if ecode, err = bctx.init(); err == nil {
   198  		return
   199  	}
   200  	if ecode != http.StatusNotFound {
   201  		bctx.p.writeErr(bctx.w, bctx.r, err, ecode)
   202  		return
   203  	}
   204  	// 2. handle two specific errors
   205  	switch {
   206  	case cmn.IsErrBckNotFound(err):
   207  		debug.Assert(bck.IsAIS())
   208  		if !bctx.createAIS {
   209  			if bctx.perms == apc.AceBckHEAD {
   210  				bctx.p.writeErr(bctx.w, bctx.r, err, ecode, Silent)
   211  			} else {
   212  				bctx.p.writeErr(bctx.w, bctx.r, err, ecode)
   213  			}
   214  			return
   215  		}
   216  	case cmn.IsErrRemoteBckNotFound(err):
   217  		debug.Assert(bck.IsRemote())
   218  		// when remote-bucket lookup is not permitted
   219  		if bctx.dontHeadRemote {
   220  			bctx.p.writeErr(bctx.w, bctx.r, err, ecode, Silent)
   221  			return
   222  		}
   223  	default:
   224  		debug.Assertf(false, "%q: unexpected %v(%d)", bctx.bck, err, ecode)
   225  		bctx.p.writeErr(bctx.w, bctx.r, err, ecode)
   226  		return
   227  	}
   228  
   229  	// 3. create ais bucket _or_ lookup and, *if* confirmed, add remote bucket to the BMD
   230  	// (see also: "on the fly")
   231  	return bctx.try()
   232  }
   233  
   234  func (bctx *bctx) try() (bck *meta.Bck, err error) {
   235  	bck, ecode, err := bctx._try()
   236  	switch {
   237  	case err == nil || err == errForwarded:
   238  		return bck, err
   239  	case cmn.IsErrBucketAlreadyExists(err):
   240  		// e.g., when (re)setting backend two times in a row
   241  		nlog.Infoln(bctx.p.String()+":", err, " - nothing to do")
   242  		return bck, nil
   243  	default:
   244  		if bctx.perms == apc.AceBckHEAD {
   245  			bctx.p.writeErr(bctx.w, bctx.r, err, ecode, Silent)
   246  		} else {
   247  			bctx.p.writeErr(bctx.w, bctx.r, err, ecode)
   248  		}
   249  		return bck, err
   250  	}
   251  }
   252  
   253  //
   254  // methods that are internal to this source
   255  //
   256  
   257  func (bctx *bctx) _try() (bck *meta.Bck, ecode int, err error) {
   258  	if err = bctx.bck.Validate(); err != nil {
   259  		ecode = http.StatusBadRequest
   260  		return
   261  	}
   262  
   263  	if bctx.p.forwardCP(bctx.w, bctx.r, bctx.msg, "add-bucket", bctx.reqBody) {
   264  		err = errForwarded
   265  		return
   266  	}
   267  
   268  	// am primary from this point on
   269  	bck = bctx.bck
   270  	var (
   271  		action    = apc.ActCreateBck
   272  		remoteHdr http.Header
   273  	)
   274  	if backend := bck.Backend(); backend != nil {
   275  		bck = backend
   276  	}
   277  	if bck.IsAIS() {
   278  		if err = bctx.p.access(bctx.r.Header, nil /*bck*/, apc.AceCreateBucket); err != nil {
   279  			ecode = aceErrToCode(err)
   280  			return
   281  		}
   282  		nlog.Warningf("%s: %q doesn't exist, proceeding to create", bctx.p, bctx.bck)
   283  		goto creadd
   284  	}
   285  	action = apc.ActAddRemoteBck // only if requested via bctx
   286  
   287  	// lookup remote
   288  	if remoteHdr, ecode, err = bctx.lookup(bck); err != nil {
   289  		bck = nil
   290  		return
   291  	}
   292  
   293  	// orig-url for the ht:// bucket
   294  	if bck.IsHTTP() {
   295  		if bctx.origURLBck != "" {
   296  			remoteHdr.Set(apc.HdrOrigURLBck, bctx.origURLBck)
   297  		} else {
   298  			var (
   299  				hbo     *cmn.HTTPBckObj
   300  				origURL = bctx.getOrigURL()
   301  			)
   302  			if origURL == "" {
   303  				err = cmn.NewErrFailedTo(bctx.p, "initialize", bctx.bck, errors.New("missing HTTP URL"))
   304  				return
   305  			}
   306  			if hbo, err = cmn.NewHTTPObjPath(origURL); err != nil {
   307  				return
   308  			}
   309  			remoteHdr.Set(apc.HdrOrigURLBck, hbo.OrigURLBck)
   310  		}
   311  	}
   312  
   313  	// when explicitly asked _not to_
   314  	bctx.exists = true
   315  	if bctx.dontAddRemote {
   316  		if bck.IsRemoteAIS() {
   317  			bck.Props, err = remoteBckProps(bckPropsArgs{bck: bck, hdr: remoteHdr})
   318  		} else {
   319  			// Background (#18995):
   320  			//
   321  			// The bucket is not in the BMD - has no local representation. The best we could do
   322  			// is return remote metadata as is. But there's no control structure for that
   323  			// other than (AIS-only) `BucketProps`.
   324  			// Therefore: return the result of merging cluster defaults with the remote header
   325  			// resulting from the backend.Head(bucket) call and containing actual
   326  			// values (e.g. versioning).
   327  			// The returned bucket props will have its BID == 0 (zero), which also means:
   328  			// this bucket is not initialized and/or not present in BMD.
   329  
   330  			bck.Props = defaultBckProps(bckPropsArgs{bck: bck, hdr: remoteHdr})
   331  		}
   332  		return
   333  	}
   334  
   335  	// add/create
   336  creadd:
   337  	if err = bctx.p.createBucket(&apc.ActMsg{Action: action}, bck, remoteHdr); err != nil {
   338  		ecode = crerrStatus(err)
   339  		return
   340  	}
   341  	// finally, initialize the newly added/created
   342  	if err = bck.Init(bctx.p.owner.bmd); err != nil {
   343  		debug.AssertNoErr(err)
   344  		ecode = http.StatusInternalServerError
   345  		err = cmn.NewErrFailedTo(bctx.p, "post create-bucket init", bck, err, ecode)
   346  	}
   347  	bck = bctx.bck
   348  	return
   349  }
   350  
   351  func (bctx *bctx) getOrigURL() (ourl string) {
   352  	if bctx.query != nil {
   353  		debug.Assert(bctx.dpq == nil)
   354  		ourl = bctx.query.Get(apc.QparamOrigURL)
   355  	} else {
   356  		ourl = bctx.dpq.origURL
   357  	}
   358  	return
   359  }
   360  
   361  func (bctx *bctx) lookup(bck *meta.Bck) (hdr http.Header, code int, err error) {
   362  	var (
   363  		q       = url.Values{}
   364  		retried bool
   365  	)
   366  	if bck.IsHTTP() {
   367  		origURL := bctx.getOrigURL()
   368  		q.Set(apc.QparamOrigURL, origURL)
   369  	}
   370  	if bctx.tryHeadRemote {
   371  		q.Set(apc.QparamSilent, "true")
   372  	}
   373  retry:
   374  	hdr, code, err = bctx.p.headRemoteBck(bck.Bucket(), q)
   375  
   376  	if (code == http.StatusUnauthorized || code == http.StatusForbidden) && bctx.tryHeadRemote {
   377  		if bctx.dontAddRemote {
   378  			return
   379  		}
   380  		// NOTE: assuming OK
   381  		nlog.Warningf("Proceeding to add remote bucket %s to the BMD after getting err: %v(%d)", bck, err, code)
   382  		nlog.Warningf("Using all cluster defaults for %s property values", bck)
   383  		hdr = make(http.Header, 2)
   384  		hdr.Set(apc.HdrBackendProvider, bck.Provider)
   385  		hdr.Set(apc.HdrBucketVerEnabled, "false")
   386  		err = nil
   387  		return
   388  	}
   389  	// NOTE: retrying once (via random target)
   390  	if err != nil && !retried && cos.IsErrClientURLTimeout(err) {
   391  		nlog.Warningf("%s: HEAD(%s) timeout %q - retrying...", bctx.p, bck, errors.Unwrap(err))
   392  		retried = true
   393  		goto retry
   394  	}
   395  	return
   396  }