github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/actions/lua/lakefs/client.go (about)

     1  package lakefs
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/Shopify/go-lua"
    15  	"github.com/go-chi/chi/v5"
    16  	"github.com/treeverse/lakefs/pkg/actions/lua/util"
    17  	"github.com/treeverse/lakefs/pkg/api/apiutil"
    18  	"github.com/treeverse/lakefs/pkg/auth"
    19  	"github.com/treeverse/lakefs/pkg/auth/model"
    20  	"github.com/treeverse/lakefs/pkg/version"
    21  )
    22  
    23  // LuaClientUserAgent is the default user agent that will be sent to the lakeFS server instance
    24  var LuaClientUserAgent = "lakefs-lua/" + version.Version
    25  
    26  func check(l *lua.State, err error) {
    27  	if err != nil {
    28  		lua.Errorf(l, "%s", err.Error())
    29  		panic("unreachable")
    30  	}
    31  }
    32  
    33  func newLakeFSRequest(ctx context.Context, user *model.User, method, reqURL string, data []byte) (*http.Request, error) {
    34  	if !strings.HasPrefix(reqURL, "/api/") {
    35  		var err error
    36  		reqURL, err = url.JoinPath(apiutil.BaseURL, reqURL)
    37  		if err != nil {
    38  			return nil, err
    39  		}
    40  	}
    41  
    42  	var body io.Reader
    43  	if data == nil {
    44  		body = bytes.NewReader(data)
    45  	}
    46  
    47  	// Chi stores its routing information on the request context which breaks this sub-request's routing.
    48  	// We explicitly nullify any existing routing information before creating the new request
    49  	ctx = context.WithValue(ctx, chi.RouteCtxKey, nil)
    50  	// Add user to the request context
    51  	ctx = auth.WithUser(ctx, user)
    52  	req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	req.Header.Set("User-Agent", LuaClientUserAgent)
    57  	return req, nil
    58  }
    59  
    60  func newLakeFSJSONRequest(ctx context.Context, user *model.User, method, reqURL string, data []byte) (*http.Request, error) {
    61  	req, err := newLakeFSRequest(ctx, user, method, reqURL, data)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	req.Header.Set("Content-Type", "application/json")
    66  	return req, nil
    67  }
    68  
    69  func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Request) int {
    70  	rr := httptest.NewRecorder()
    71  	server.Handler.ServeHTTP(rr, request)
    72  	l.PushInteger(rr.Code)
    73  
    74  	var output interface{}
    75  	check(l, json.Unmarshal(rr.Body.Bytes(), &output))
    76  	return 1 + util.DeepPush(l, output)
    77  }
    78  
    79  func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
    80  	clientOpen := func(l *lua.State) int {
    81  		lua.NewLibrary(l, []lua.RegistryFunction{
    82  			{Name: "create_tag", Function: func(state *lua.State) int {
    83  				repo := lua.CheckString(l, 1)
    84  				data, err := json.Marshal(map[string]string{
    85  					"ref": lua.CheckString(l, 2),
    86  					"id":  lua.CheckString(l, 3),
    87  				})
    88  				if err != nil {
    89  					check(l, err)
    90  				}
    91  				reqURL, err := url.JoinPath("/repositories", repo, "tags")
    92  				if err != nil {
    93  					check(l, err)
    94  				}
    95  				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
    96  				if err != nil {
    97  					check(l, err)
    98  				}
    99  				return getLakeFSJSONResponse(l, server, req)
   100  			}},
   101  			{Name: "diff_refs", Function: func(state *lua.State) int {
   102  				repo := lua.CheckString(l, 1)
   103  				leftRef := lua.CheckString(l, 2)
   104  				rightRef := lua.CheckString(l, 3)
   105  				reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
   106  				if err != nil {
   107  					check(l, err)
   108  				}
   109  				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
   110  				if err != nil {
   111  					check(l, err)
   112  				}
   113  				// query params
   114  				q := req.URL.Query()
   115  				if !l.IsNone(4) {
   116  					q.Add("after", lua.CheckString(l, 4))
   117  				}
   118  				if !l.IsNone(5) {
   119  					q.Add("prefix", lua.CheckString(l, 5))
   120  				}
   121  				if !l.IsNone(6) {
   122  					q.Add("delimiter", lua.CheckString(l, 6))
   123  				}
   124  				if !l.IsNone(7) {
   125  					q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
   126  				}
   127  				req.URL.RawQuery = q.Encode()
   128  				return getLakeFSJSONResponse(l, server, req)
   129  			}},
   130  			{Name: "list_objects", Function: func(state *lua.State) int {
   131  				repo := lua.CheckString(l, 1)
   132  				ref := lua.CheckString(l, 2)
   133  				reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
   134  				if err != nil {
   135  					check(l, err)
   136  				}
   137  				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
   138  				if err != nil {
   139  					check(l, err)
   140  				}
   141  				// query params
   142  				q := req.URL.Query()
   143  				if !l.IsNone(3) {
   144  					q.Add("after", lua.CheckString(l, 3))
   145  				}
   146  				if !l.IsNone(4) {
   147  					q.Add("prefix", lua.CheckString(l, 4))
   148  				}
   149  				if !l.IsNone(5) {
   150  					q.Add("delimiter", lua.CheckString(l, 5))
   151  				}
   152  				if !l.IsNone(6) {
   153  					q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
   154  				}
   155  				if !l.IsNone(7) {
   156  					withUserMetadata := "false"
   157  					if l.ToBoolean(7) {
   158  						withUserMetadata = "true"
   159  					}
   160  					q.Add("user_metadata", withUserMetadata)
   161  				}
   162  				req.URL.RawQuery = q.Encode()
   163  				return getLakeFSJSONResponse(l, server, req)
   164  			}},
   165  			{Name: "get_object", Function: func(state *lua.State) int {
   166  				repo := lua.CheckString(l, 1)
   167  				ref := lua.CheckString(l, 2)
   168  				reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
   169  				if err != nil {
   170  					check(l, err)
   171  				}
   172  				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
   173  				if err != nil {
   174  					check(l, err)
   175  				}
   176  				// query params
   177  				q := req.URL.Query()
   178  				q.Add("path", lua.CheckString(l, 3))
   179  				req.URL.RawQuery = q.Encode()
   180  				rr := httptest.NewRecorder()
   181  				server.Handler.ServeHTTP(rr, req)
   182  				l.PushInteger(rr.Code)
   183  				l.PushString(rr.Body.String())
   184  				return 2
   185  			}},
   186  			{Name: "stat_object", Function: func(state *lua.State) int {
   187  				repo := lua.CheckString(l, 1)
   188  				ref := lua.CheckString(l, 2)
   189  				reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
   190  				if err != nil {
   191  					check(l, err)
   192  				}
   193  				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
   194  				if err != nil {
   195  					check(l, err)
   196  				}
   197  				// query params
   198  				q := req.URL.Query()
   199  				q.Add("path", lua.CheckString(l, 3))
   200  				req.URL.RawQuery = q.Encode()
   201  				rr := httptest.NewRecorder()
   202  				server.Handler.ServeHTTP(rr, req)
   203  				l.PushInteger(rr.Code)
   204  				l.PushString(rr.Body.String())
   205  				return 2
   206  			}},
   207  			{Name: "diff_branch", Function: func(state *lua.State) int {
   208  				repo := lua.CheckString(l, 1)
   209  				branch := lua.CheckString(l, 2)
   210  				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
   211  				if err != nil {
   212  					check(l, err)
   213  				}
   214  				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
   215  				if err != nil {
   216  					check(l, err)
   217  				}
   218  				// query params
   219  				q := req.URL.Query()
   220  				if !l.IsNone(3) {
   221  					q.Add("after", lua.CheckString(l, 3))
   222  				}
   223  				if !l.IsNone(4) {
   224  					q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
   225  				}
   226  				if !l.IsNone(5) {
   227  					q.Add("prefix", lua.CheckString(l, 5))
   228  				}
   229  				if !l.IsNone(6) {
   230  					q.Add("delimiter", lua.CheckString(l, 6))
   231  				}
   232  				req.URL.RawQuery = q.Encode()
   233  				return getLakeFSJSONResponse(l, server, req)
   234  			}},
   235  		})
   236  		return 1
   237  	}
   238  	lua.Require(l, "lakefs", clientOpen, false)
   239  	l.Pop(1)
   240  }