From ff8ad7d4cad94ef856dcdc2dae251b3bed3f60d6 Mon Sep 17 00:00:00 2001 From: Janos Dobronszki Date: Thu, 23 Apr 2020 16:30:43 +0200 Subject: [PATCH] Default runtime now checks out code on demand (#1563) * Default runtime now checks out code on demand * Go mod tidy --- go.mod | 3 +- go.sum | 27 ++- runtime/default.go | 18 ++ runtime/local/git/git.go | 333 ++++++++++++++++++++++++++++++++++ runtime/local/git/git_test.go | 95 ++++++++++ 5 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 runtime/local/git/git.go create mode 100644 runtime/local/git/git_test.go diff --git a/go.mod b/go.mod index 05e82553..fe1ce6b0 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/fsouza/go-dockerclient v1.6.0 github.com/ghodss/yaml v1.0.0 github.com/go-acme/lego/v3 v3.3.0 + github.com/go-git/go-git/v5 v5.0.0 github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee github.com/gobwas/pool v0.2.0 // indirect @@ -61,7 +62,7 @@ require ( go.etcd.io/bbolt v1.3.4 go.uber.org/zap v1.13.0 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 - golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 + golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 google.golang.org/grpc v1.26.0 diff --git a/go.sum b/go.sum index a204a929..c86297fe 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -146,6 +147,14 @@ github.com/go-acme/lego/v3 v3.3.0 h1:6BePZsOiYA4/w+M7QDytxQtMfCipMPGnWAHs9pWks98 github.com/go-acme/lego/v3 v3.3.0/go.mod h1:iGSY2vQrvQs3WezicSB/oVbO2eCrD88dpWPwb1qLqu0= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= +github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= +github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -263,6 +272,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= @@ -317,6 +328,8 @@ github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1t github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249 h1:Pr5gZa2VcmktVwq0lyC39MsN5tz356vC/pQHKvq+QBo= github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw= @@ -385,6 +398,8 @@ github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -456,6 +471,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -500,8 +516,8 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -539,6 +555,8 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPT golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -612,6 +630,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -639,6 +660,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/runtime/default.go b/runtime/default.go index 8ac00bb9..c3c5c1ec 100644 --- a/runtime/default.go +++ b/runtime/default.go @@ -13,6 +13,7 @@ import ( "github.com/hpcloud/tail" "github.com/micro/go-micro/v2/logger" + "github.com/micro/go-micro/v2/runtime/local/git" ) type runtime struct { @@ -52,6 +53,21 @@ func NewRuntime(opts ...Option) Runtime { } } +// @todo move this to runtime default +func (r *runtime) checkoutSourceIfNeeded(s *Service) error { + source, err := git.ParseSourceLocal("", s.Source) + if err != nil { + return err + } + source.Ref = s.Version + err = git.CheckoutSource(os.TempDir(), source) + if err != nil { + return err + } + s.Source = source.FullPath + return nil +} + // Init initializes runtime options func (r *runtime) Init(opts ...Option) error { r.Lock() @@ -193,6 +209,7 @@ func serviceKey(s *Service) string { // Create creates a new service which is then started by runtime func (r *runtime) Create(s *Service, opts ...CreateOption) error { + r.checkoutSourceIfNeeded(s) r.Lock() defer r.Unlock() @@ -336,6 +353,7 @@ func (r *runtime) Read(opts ...ReadOption) ([]*Service, error) { // Update attemps to update the service func (r *runtime) Update(s *Service, opts ...UpdateOption) error { + r.checkoutSourceIfNeeded(s) r.Lock() service, ok := r.services[serviceKey(s)] r.Unlock() diff --git a/runtime/local/git/git.go b/runtime/local/git/git.go new file mode 100644 index 00000000..c23a8080 --- /dev/null +++ b/runtime/local/git/git.go @@ -0,0 +1,333 @@ +package git + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" +) + +type Gitter interface { + Clone(repo string) error + FetchAll(repo string) error + Checkout(repo, branchOrCommit string) error + RepoDir(repo string) string +} + +type libGitter struct { + folder string +} + +func (g libGitter) Clone(repo string) error { + fold := filepath.Join(g.folder, dirifyRepo(repo)) + exists, err := pathExists(fold) + if err != nil { + return err + } + if exists { + return nil + } + _, err = git.PlainClone(fold, false, &git.CloneOptions{ + URL: repo, + Progress: os.Stdout, + }) + return err +} + +func (g libGitter) FetchAll(repo string) error { + repos, err := git.PlainOpen(filepath.Join(g.folder, dirifyRepo(repo))) + if err != nil { + return err + } + remotes, err := repos.Remotes() + if err != nil { + return err + } + + err = remotes[0].Fetch(&git.FetchOptions{ + RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}, + Progress: os.Stdout, + Depth: 1, + }) + if err != nil && err != git.NoErrAlreadyUpToDate { + return err + } + return nil +} + +func (g libGitter) Checkout(repo, branchOrCommit string) error { + if branchOrCommit == "latest" { + branchOrCommit = "master" + } + repos, err := git.PlainOpen(filepath.Join(g.folder, dirifyRepo(repo))) + if err != nil { + return err + } + worktree, err := repos.Worktree() + if err != nil { + return err + } + isCommit := func(s string) bool { + return strings.ContainsAny(s, "0123456789") && len(s) == 40 + } + if isCommit(branchOrCommit) { + err = worktree.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(branchOrCommit), + Force: true, + }) + if err != nil { + return err + } + } else { + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branchOrCommit), + Force: true, + }) + if err != nil { + return err + } + } + return nil +} + +func (g libGitter) RepoDir(repo string) string { + return filepath.Join(g.folder, dirifyRepo(repo)) +} + +type binaryGitter struct { + folder string +} + +func (g binaryGitter) Clone(repo string) error { + fold := filepath.Join(g.folder, dirifyRepo(repo)) + exists, err := pathExists(fold) + if err != nil { + return err + } + if exists { + return nil + } + cmd := exec.Command("git", "clone", repo, ".") + + err = os.MkdirAll(fold, 0777) + if err != nil { + return err + } + cmd.Dir = fold + _, err = cmd.Output() + if err != nil { + return err + } + return err +} + +func (g binaryGitter) FetchAll(repo string) error { + cmd := exec.Command("git", "fetch", "--all") + cmd.Dir = filepath.Join(g.folder, dirifyRepo(repo)) + _, err := cmd.Output() + if err != nil { + return err + } + return err +} + +func (g binaryGitter) Checkout(repo, branchOrCommit string) error { + if branchOrCommit == "latest" { + branchOrCommit = "master" + } + cmd := exec.Command("git", "checkout", "-f", branchOrCommit) + cmd.Dir = filepath.Join(g.folder, dirifyRepo(repo)) + _, err := cmd.Output() + if err != nil { + return err + } + return nil +} + +func (g binaryGitter) RepoDir(repo string) string { + return filepath.Join(g.folder, dirifyRepo(repo)) +} + +func NewGitter(folder string) Gitter { + if commandExists("git") { + return binaryGitter{folder} + } + return libGitter{folder} +} + +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +func dirifyRepo(s string) string { + s = strings.ReplaceAll(s, "https://", "") + s = strings.ReplaceAll(s, "/", "-") + return s +} + +// exists returns whether the given file or directory exists +func pathExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} + +// GetRepoRoot determines the repo root from a full path. +// Returns empty string and no error if not found +func GetRepoRoot(fullPath string) (string, error) { + // traverse parent directories + prev := fullPath + for { + current := prev + exists, err := pathExists(filepath.Join(current, ".git")) + if err != nil { + return "", err + } + if exists { + return current, nil + } + prev = filepath.Dir(current) + // reached top level, see: + // https://play.golang.org/p/rDgVdk3suzb + if current == prev { + break + } + } + return "", nil +} + +const defaultRepo = "github.com/micro/services" + +// Source is not just git related @todo move +type Source struct { + // is it a local folder intended for a local runtime? + Local bool + // absolute path to service folder in local mode + FullPath string + // path of folder to repo root + // be it local or github repo + Folder string + // github ref + Ref string + // for cloning purposes + // blank for local + Repo string + // dir to repo root + // blank for non local + LocalRepoRoot string +} + +// Name to be passed to RPC call runtime.Create Update Delete +// eg: `helloworld/api`, `crufter/myrepo/helloworld/api`, `localfolder` +func (s *Source) RuntimeName() string { + if s.Repo == "github.com/micro/services" || s.Repo == "" { + return s.Folder + } + return fmt.Sprintf("%v/%v", strings.ReplaceAll(s.Repo, "github.com/", ""), s.Folder) +} + +// Source to be passed to RPC call runtime.Create Update Delete +// eg: `helloworld`, `github.com/crufter/myrepo/helloworld`, `/path/to/localrepo/localfolder` +func (s *Source) RuntimeSource() string { + if s.Local { + return s.FullPath + } + if s.Repo == "github.com/micro/services" || s.Repo == "" { + return s.Folder + } + return fmt.Sprintf("%v/%v", s.Repo, s.Folder) +} + +// ParseSource parses a `micro run/update/kill` source. +func ParseSource(source string) (*Source, error) { + // If github is not present, we got a shorthand for `micro/services` + if !strings.Contains(source, "github.com") { + source = "github.com/micro/services/" + source + } + if !strings.Contains(source, "@") { + source += "@latest" + } + ret := &Source{} + refs := strings.Split(source, "@") + ret.Ref = refs[1] + parts := strings.Split(refs[0], "/") + ret.Repo = strings.Join(parts[0:3], "/") + if len(parts) > 1 { + ret.Folder = strings.Join(parts[3:], "/") + } + + return ret, nil +} + +// ParseSourceLocal detects and handles local pathes too +// workdir should be used only from the CLI @todo better interface for this function +func ParseSourceLocal(workDir, source string) (*Source, error) { + var localFullPath string + if len(workDir) > 0 { + localFullPath = filepath.Join(workDir, source) + } else { + localFullPath = source + } + if exists, err := pathExists(localFullPath); err == nil && exists { + localRepoRoot, err := GetRepoRoot(localFullPath) + if err != nil { + return nil, err + } + folder := strings.ReplaceAll(localFullPath, localRepoRoot+string(filepath.Separator), "") + return &Source{ + Local: true, + Folder: folder, + FullPath: localFullPath, + LocalRepoRoot: localRepoRoot, + Ref: "latest", // @todo consider extracting branch from git here + }, nil + } + return ParseSource(source) +} + +// CheckoutSource for the local runtime server +// folder is the folder to check out the source code to +// Modifies source path to set it to checked out repo absolute path locally. +func CheckoutSource(folder string, source *Source) error { + // if it's a local folder, do nothing + if exists, err := pathExists(source.FullPath); err == nil && exists { + return nil + } + gitter := NewGitter(folder) + repo := source.Repo + if !strings.Contains(repo, "https://") { + repo = "https://" + repo + } + // Always clone, it's idempotent and only clones if needed + err := gitter.Clone(repo) + if err != nil { + return err + } + source.FullPath = filepath.Join(gitter.RepoDir(source.Repo), source.Folder) + return gitter.Checkout(repo, source.Ref) +} + +// code below is not used yet + +var nameExtractRegexp = regexp.MustCompile(`((micro|web)\.Name\(")(.*)("\))`) + +func extractServiceName(fileContent []byte) string { + hits := nameExtractRegexp.FindAll(fileContent, 1) + if len(hits) == 0 { + return "" + } + hit := string(hits[0]) + return strings.Split(hit, "\"")[1] +} diff --git a/runtime/local/git/git_test.go b/runtime/local/git/git_test.go new file mode 100644 index 00000000..79561b80 --- /dev/null +++ b/runtime/local/git/git_test.go @@ -0,0 +1,95 @@ +package git + +import ( + "testing" +) + +type parseCase struct { + url string + expected *Source +} + +func TestParseSource(t *testing.T) { + cases := []parseCase{ + { + url: "helloworld", + expected: &Source{ + Repo: "github.com/micro/services", + Folder: "helloworld", + Ref: "latest", + }, + }, + { + url: "github.com/micro/services/helloworld", + expected: &Source{ + Repo: "github.com/micro/services", + Folder: "helloworld", + Ref: "latest", + }, + }, + { + url: "github.com/micro/services/helloworld@v1.12.1", + expected: &Source{ + Repo: "github.com/micro/services", + Folder: "helloworld", + Ref: "v1.12.1", + }, + }, + { + url: "github.com/micro/services/helloworld@branchname", + expected: &Source{ + Repo: "github.com/micro/services", + Folder: "helloworld", + Ref: "branchname", + }, + }, + { + url: "github.com/crufter/reponame/helloworld@branchname", + expected: &Source{ + Repo: "github.com/crufter/reponame", + Folder: "helloworld", + Ref: "branchname", + }, + }, + } + for i, c := range cases { + result, err := ParseSource(c.url) + if err != nil { + t.Fatalf("Failed case %v: %v", i, err) + } + if result.Folder != c.expected.Folder { + t.Fatalf("Folder does not match for '%v', expected '%v', got '%v'", i, c.expected.Folder, result.Folder) + } + if result.Repo != c.expected.Repo { + t.Fatalf("Repo address does not match for '%v', expected '%v', got '%v'", i, c.expected.Repo, result.Repo) + } + if result.Ref != c.expected.Ref { + t.Fatalf("Ref does not match for '%v', expected '%v', got '%v'", i, c.expected.Ref, result.Ref) + } + } +} + +type nameCase struct { + fileContent string + expected string +} + +func TestServiceNameExtract(t *testing.T) { + cases := []nameCase{ + { + fileContent: `func main() { + // New Service + service := micro.NewService( + micro.Name("go.micro.service.helloworld"), + micro.Version("latest"), + )`, + expected: "go.micro.service.helloworld", + }, + } + for i, c := range cases { + result := extractServiceName([]byte(c.fileContent)) + if result != c.expected { + t.Fatalf("Case %v, expected: %v, got: %v", i, c.expected, result) + } + } +}