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 }