diff --git a/Makefile b/Makefile index df4bd66..1e672db 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ LOCAL_BIN:=$(CURDIR)/bin # Linter config. GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint -GOLANGCI_TAG:=1.53.3 +GOLANGCI_TAG:=1.55.2 .PHONY: all all: deps test build diff --git a/app.go b/app.go index e9a3fc9..a506434 100644 --- a/app.go +++ b/app.go @@ -3,6 +3,7 @@ package launchr import ( "errors" "fmt" + "io/fs" "os" "path/filepath" "reflect" @@ -32,6 +33,7 @@ type appImpl struct { actionMngr action.Manager pluginMngr PluginManager config Config + regFS []fs.FS } // getPluginByType returns specific plugins from the app. @@ -68,6 +70,9 @@ func (app *appImpl) Name() string { return name } func (app *appImpl) GetWD() string { return app.workDir } func (app *appImpl) Streams() cli.Streams { return app.streams } +func (app *appImpl) AddDiscoveryFS(fs fs.FS) { app.regFS = append(app.regFS, fs) } +func (app *appImpl) GetDiscoveryFS() []fs.FS { return app.regFS } + func (app *appImpl) AddService(s Service) { info := s.ServiceInfo() launchr.InitServiceInfo(&info, s) @@ -111,6 +116,8 @@ func (app *appImpl) init() error { if err != nil { return err } + app.regFS = make([]fs.FS, 0, 4) + app.AddDiscoveryFS(os.DirFS(app.workDir)) // Prepare dependencies. app.streams = cli.StandardStreams() app.services = make(map[ServiceInfo]Service) @@ -133,6 +140,19 @@ func (app *appImpl) init() error { } } + // Discover actions. + for _, p := range getPluginByType[ActionDiscoveryPlugin](app) { + for _, fs := range app.GetDiscoveryFS() { + actions, err := p.DiscoverActions(fs) + if err != nil { + return err + } + for _, actConf := range actions { + app.actionMngr.Add(actConf) + } + } + } + return nil } diff --git a/go.mod b/go.mod index 027b022..137ca40 100644 --- a/go.mod +++ b/go.mod @@ -4,46 +4,46 @@ go 1.21 require ( github.com/a8m/envsubst v1.4.2 - github.com/docker/docker v24.0.6+incompatible + github.com/docker/docker v24.0.7+incompatible github.com/golang/mock v1.6.0 - github.com/moby/moby v24.0.6+incompatible + github.com/moby/moby v24.0.7+incompatible github.com/moby/sys/signal v0.7.0 github.com/moby/term v0.5.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 - golang.org/x/sys v0.13.0 + golang.org/x/sys v0.16.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/containerd/containerd v1.7.7 // indirect + github.com/containerd/containerd v1.7.12 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.17.1 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect - github.com/opencontainers/runc v1.1.9 // indirect + github.com/opencontainers/runc v1.1.11 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gotest.tools/v3 v3.4.0 // indirect ) diff --git a/go.sum b/go.sum index fb17995..3fc75a7 100644 --- a/go.sum +++ b/go.sum @@ -10,7 +10,10 @@ github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAAHe15q4= github.com/containerd/containerd v1.7.7/go.mod h1:3c4XZv6VeT9qgf9GMTxNTMFxGJrGpI2vz1yk4ye+YY8= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -23,8 +26,12 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -40,6 +47,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g= github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -49,6 +58,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/moby/moby v24.0.6+incompatible h1:O/XZsZtaOVTYszsJQlr9pN1Zo1aRSH0KCWAIa6Kpm3s= github.com/moby/moby v24.0.6+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/moby v24.0.7+incompatible h1:RrVT5IXBn85mRtFKP+gFwVLCcnNPZIgN3NVRJG9Le+4= +github.com/moby/moby v24.0.7+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= @@ -65,6 +76,8 @@ github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/ github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/opencontainers/runc v1.1.9 h1:XR0VIHTGce5eWPkaPesqTBrhW2yAcaraWfsEalNwQLM= github.com/opencontainers/runc v1.1.9/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= +github.com/opencontainers/runc v1.1.11 h1:9LjxyVlE0BPMRP2wuQDRlHV4941Jp9rc3F0+YKimopA= +github.com/opencontainers/runc v1.1.11/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -80,6 +93,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -97,6 +112,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -104,6 +121,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -122,6 +141,8 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -135,6 +156,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/launchr/types.go b/internal/launchr/types.go index 3226a0f..24cf9f0 100644 --- a/internal/launchr/types.go +++ b/internal/launchr/types.go @@ -2,6 +2,7 @@ package launchr import ( "fmt" + "io/fs" "github.com/spf13/cobra" @@ -25,6 +26,10 @@ type App interface { // GetService retrieves a service of type v and assigns it to v. // Panics if a service is not found. GetService(v interface{}) + // AddDiscoveryFS registers a File System for discovery of actions in launchr. + AddDiscoveryFS(fs fs.FS) + // GetDiscoveryFS returns an array of registered File Systems for action discovery. + GetDiscoveryFS() []fs.FS } // AppVersion stores application version. diff --git a/pkg/action/discover.go b/pkg/action/discover.go index b5d3ad6..f755da6 100644 --- a/pkg/action/discover.go +++ b/pkg/action/discover.go @@ -5,40 +5,47 @@ import ( "fmt" "io/fs" "path/filepath" - "regexp" "sort" "strings" "sync" - "time" "github.com/launchrctl/launchr/internal/launchr" "github.com/launchrctl/launchr/pkg/log" ) -// Discovery finds action files and parses them. -type Discovery interface { - Discover() ([]*Action, error) -} +const actionsDirname = "actions" + +var actionsSubdir = strings.Join([]string{"", actionsDirname, ""}, string(filepath.Separator)) -type yamlDiscovery struct { - fs fs.FS - cwd string - targetRgx *regexp.Regexp +// DiscoveryPlugin is a launchr plugin to discover actions. +type DiscoveryPlugin interface { + launchr.Plugin + DiscoverActions(fs fs.FS) ([]*Action, error) } -var actionYamlRegex = regexp.MustCompile(`^action\.(yaml|yml)$`) +// FileLoadFn is a type for loading a file. +type FileLoadFn func() (fs.File, error) -// NewYamlDiscovery creates an instance of action discovery. -func NewYamlDiscovery(fs fs.FS) Discovery { - cwd := launchr.GetFsAbsPath(fs) - return &yamlDiscovery{fs, cwd, actionYamlRegex} +// DiscoveryStrategy is a way files will be discovered and loaded. +type DiscoveryStrategy interface { + IsValid(name string) bool + Loader(l FileLoadFn, p ...LoadProcessor) Loader } -const actionsDirname = "actions" +// Discovery defines a common functionality for discovering action files. +type Discovery struct { + fs fs.FS + cwd string + s DiscoveryStrategy +} -var actionsSubdir = strings.Join([]string{"", actionsDirname, ""}, string(filepath.Separator)) +// NewDiscovery creates an instance of action discovery. +func NewDiscovery(fs fs.FS, ds DiscoveryStrategy) *Discovery { + cwd := launchr.GetFsAbsPath(fs) + return &Discovery{fs, cwd, ds} +} -func (ad *yamlDiscovery) isValid(path string, d fs.DirEntry) bool { +func (ad *Discovery) isValid(path string, d fs.DirEntry) bool { i := strings.LastIndex(path, actionsSubdir) if d.IsDir() || i == -1 || isHidden(path) { @@ -46,12 +53,12 @@ func (ad *yamlDiscovery) isValid(path string, d fs.DirEntry) bool { } return strings.Count(path[i+len(actionsSubdir):], string(filepath.Separator)) == 1 && // Nested actions are not allowed. - ad.targetRgx.MatchString(d.Name()) + ad.s.IsValid(d.Name()) } // findFiles searches for a filename in a given dir. // Returns an array of relative file paths. -func (ad *yamlDiscovery) findFiles() chan string { +func (ad *Discovery) findFiles() chan string { ch := make(chan string, 10) go func() { err := fs.WalkDir(ad.fs, ".", func(path string, d fs.DirEntry, err error) error { @@ -81,17 +88,10 @@ func (ad *yamlDiscovery) findFiles() chan string { return ch } -// @todo move somewhere -func timeTrack(start time.Time, name string) { - log.Debug("%s took %s", name, time.Since(start)) -} - -// Discover traverses the file structure for a given discovery path dp. -// Returns array of ActionCommand. -// If a command is invalid, it's ignored. -func (ad *yamlDiscovery) Discover() ([]*Action, error) { - defer timeTrack(time.Now(), "launchr.Discover") - +// Discover traverses the file structure for a given discovery path. +// Returns array of Action. +// If an action is invalid, it's ignored. +func (ad *Discovery) Discover() ([]*Action, error) { wg := sync.WaitGroup{} mx := sync.Mutex{} actions := make([]*Action, 0, 32) @@ -101,7 +101,7 @@ func (ad *yamlDiscovery) Discover() ([]*Action, error) { go func(f string) { defer wg.Done() // @todo skip duplicate like action.yaml+action.yml, prefer yaml. - a := ad.parseYamlAction(f) + a := ad.parseFile(f) mx.Lock() defer mx.Unlock() actions = append(actions, a) @@ -117,23 +117,18 @@ func (ad *yamlDiscovery) Discover() ([]*Action, error) { return actions, nil } -// parseYamlAction parses yaml file f and returns available actions. -func (ad *yamlDiscovery) parseYamlAction(f string) *Action { +// parseFile parses file f and returns an action. +func (ad *Discovery) parseFile(f string) *Action { id := getActionID(f) if id == "" { panic(fmt.Errorf("action id cannot be empty, file %q", f)) } a := NewAction(id, ad.cwd, f) - a.Loader = &yamlFileLoader{ - open: func() (fs.File, error) { - return ad.fs.Open(f) - }, - processor: NewPipeProcessor( - escapeYamlTplCommentsProcessor{}, - envProcessor{}, - inputProcessor{}, - ), - } + a.Loader = ad.s.Loader( + func() (fs.File, error) { return ad.fs.Open(f) }, + envProcessor{}, + inputProcessor{}, + ) return a } diff --git a/pkg/action/discover_test.go b/pkg/action/discover_test.go index 8e367fa..f5ad8d9 100644 --- a/pkg/action/discover_test.go +++ b/pkg/action/discover_test.go @@ -94,7 +94,7 @@ func Test_Discover_isValid(t *testing.T) { } // Run tests. - ad := NewYamlDiscovery(fstest.MapFS{}).(*yamlDiscovery) + ad := NewYamlDiscovery(fstest.MapFS{}) for _, tt := range tts { tt := tt t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/action/loader.go b/pkg/action/loader.go index d044940..0f7bf82 100644 --- a/pkg/action/loader.go +++ b/pkg/action/loader.go @@ -1,14 +1,10 @@ package action import ( - "bufio" "bytes" "fmt" - "io" - "io/fs" "regexp" "strings" - "sync" "text/template" "github.com/a8m/envsubst" @@ -29,71 +25,6 @@ type LoadContext struct { Action *Action } -type yamlFileLoader struct { - processor LoadProcessor - raw *Definition - cached []byte - open func() (fs.File, error) - mx sync.Mutex -} - -func (l *yamlFileLoader) Content() ([]byte, error) { - l.mx.Lock() - defer l.mx.Unlock() - // @todo unload unused, maybe manager must do it. - var err error - if l.cached != nil { - return l.cached, nil - } - f, err := l.open() - if err != nil { - return nil, err - } - defer f.Close() - l.cached, err = io.ReadAll(f) - if err != nil { - return nil, err - } - return l.cached, nil -} - -func (l *yamlFileLoader) LoadRaw() (*Definition, error) { - var err error - buf, err := l.Content() - if err != nil { - return nil, err - } - l.mx.Lock() - defer l.mx.Unlock() - if l.raw == nil { - l.raw, err = CreateFromYamlTpl(buf) - if err != nil { - return nil, err - } - } - return l.raw, err -} - -func (l *yamlFileLoader) Load(ctx LoadContext) (res *Definition, err error) { - // Open a file and cache content for future reads. - c, err := l.Content() - if err != nil { - return nil, err - } - buf := make([]byte, len(c)) - copy(buf, c) - buf, err = l.processor.Process(ctx, buf) - if err != nil { - return nil, err - } - r := bytes.NewReader(buf) - res, err = CreateFromYaml(r) - if err != nil { - return nil, err - } - return res, err -} - // LoadProcessor is an interface for processing input on load. type LoadProcessor interface { // Process gets an input action file data and returns a processed result. @@ -120,30 +51,6 @@ func (p *pipeProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { return b, nil } -type escapeYamlTplCommentsProcessor struct{} - -func (p escapeYamlTplCommentsProcessor) Process(_ LoadContext, b []byte) ([]byte, error) { - // Read by line. - scanner := bufio.NewScanner(bytes.NewBuffer(b)) - res := make([]byte, 0, len(b)) - for scanner.Scan() { - l := scanner.Bytes() - if i := bytes.IndexByte(l, '#'); i != -1 { - // Check the comment symbol is not inside a string. - // Multiline strings are not supported for now. - if !(bytes.LastIndexByte(l[:i], '"') != -1 && bytes.IndexByte(l[i:], '"') != -1 || - bytes.LastIndexByte(l[:i], '\'') != -1 && bytes.IndexByte(l[i:], '\'') != -1) { - // Strip data after comment symbol. - l = l[:i] - } - } - // Collect the modified lines. - res = append(res, l...) - res = append(res, '\n') - } - return res, nil -} - type envProcessor struct{} func (p envProcessor) Process(_ LoadContext, b []byte) ([]byte, error) { diff --git a/pkg/action/yaml.go b/pkg/action/yaml.def.go similarity index 100% rename from pkg/action/yaml.go rename to pkg/action/yaml.def.go diff --git a/pkg/action/yaml.discovery.go b/pkg/action/yaml.discovery.go new file mode 100644 index 0000000..d8befd8 --- /dev/null +++ b/pkg/action/yaml.discovery.go @@ -0,0 +1,126 @@ +package action + +import ( + "bufio" + "bytes" + "io" + "io/fs" + "regexp" + "sync" +) + +var rgxYamlFile = regexp.MustCompile(`^action\.(yaml|yml)$`) + +// NewYamlDiscovery is an implementation of discovery for searching yaml files. +func NewYamlDiscovery(fs fs.FS) *Discovery { + return NewDiscovery(fs, YamlDiscoveryStrategy{TargetRgx: rgxYamlFile}) +} + +// YamlDiscoveryStrategy is a yaml discovery strategy. +type YamlDiscoveryStrategy struct { + TargetRgx *regexp.Regexp +} + +// IsValid implements DiscoveryStrategy. +func (y YamlDiscoveryStrategy) IsValid(name string) bool { + return y.TargetRgx.MatchString(name) +} + +// Loader implements DiscoveryStrategy. +func (y YamlDiscoveryStrategy) Loader(l FileLoadFn, p ...LoadProcessor) Loader { + return &yamlFileLoader{ + open: l, + processor: NewPipeProcessor( + append([]LoadProcessor{escapeYamlTplCommentsProcessor{}}, p...)..., + ), + } +} + +type yamlFileLoader struct { + processor LoadProcessor + raw *Definition + cached []byte + open func() (fs.File, error) + mx sync.Mutex +} + +func (l *yamlFileLoader) Content() ([]byte, error) { + l.mx.Lock() + defer l.mx.Unlock() + // @todo unload unused, maybe manager must do it. + var err error + if l.cached != nil { + return l.cached, nil + } + f, err := l.open() + if err != nil { + return nil, err + } + defer f.Close() + l.cached, err = io.ReadAll(f) + if err != nil { + return nil, err + } + return l.cached, nil +} + +func (l *yamlFileLoader) LoadRaw() (*Definition, error) { + var err error + buf, err := l.Content() + if err != nil { + return nil, err + } + l.mx.Lock() + defer l.mx.Unlock() + if l.raw == nil { + l.raw, err = CreateFromYamlTpl(buf) + if err != nil { + return nil, err + } + } + return l.raw, err +} + +func (l *yamlFileLoader) Load(ctx LoadContext) (res *Definition, err error) { + // Open a file and cache content for future reads. + c, err := l.Content() + if err != nil { + return nil, err + } + buf := make([]byte, len(c)) + copy(buf, c) + buf, err = l.processor.Process(ctx, buf) + if err != nil { + return nil, err + } + r := bytes.NewReader(buf) + res, err = CreateFromYaml(r) + if err != nil { + return nil, err + } + return res, err +} + +type escapeYamlTplCommentsProcessor struct{} + +func (p escapeYamlTplCommentsProcessor) Process(_ LoadContext, b []byte) ([]byte, error) { + // Read by line. + scanner := bufio.NewScanner(bytes.NewBuffer(b)) + res := make([]byte, 0, len(b)) + for scanner.Scan() { + l := scanner.Bytes() + if i := bytes.IndexByte(l, '#'); i != -1 { + // Check the comment symbol is not inside a string. + // Multiline strings are not supported for now. + if !(bytes.LastIndexByte(l[:i], '"') != -1 && bytes.IndexByte(l[i:], '"') != -1 || + bytes.LastIndexByte(l[:i], '\'') != -1 && bytes.IndexByte(l[i:], '\'') != -1) { + // Strip data after comment symbol. + l = l[:i] + } + } + // Collect the modified lines. + res = append(res, l...) + res = append(res, '\n') + } + return res, nil +} diff --git a/pkg/log/plain.go b/pkg/log/plain.go index d8292e9..d3ace19 100644 --- a/pkg/log/plain.go +++ b/pkg/log/plain.go @@ -50,18 +50,18 @@ func (l *consoleLogger) Warn(format string, v ...any) { // Err implements Logger.Err. func (l *consoleLogger) Err(format string, v ...any) { if l.Verbosity <= ErrLvl { - l.err.Printf("ERROR: "+format, v...) + l.err.Printf("ERROR: "+format, v...) //nolint goconst } } // Panic implements Logger.Panic. func (l *consoleLogger) Panic(format string, v ...any) { - l.err.Panicf("ERROR: "+format, v...) + l.err.Panicf("ERROR: "+format, v...) //nolint goconst } // Fatal implements Logger.Fatal. func (l *consoleLogger) Fatal(format string, v ...any) { - l.err.Fatalf("ERROR: "+format, v...) + l.err.Fatalf("ERROR: "+format, v...) //nolint goconst } // SetLevel implements Logger.SetLevel. diff --git a/plugins/yamldiscovery/plugin.go b/plugins/yamldiscovery/plugin.go index 3b4a7f3..5ac5819 100644 --- a/plugins/yamldiscovery/plugin.go +++ b/plugins/yamldiscovery/plugin.go @@ -4,7 +4,6 @@ package yamldiscovery import ( "io/fs" - "os" "github.com/spf13/cobra" @@ -30,30 +29,27 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { // OnAppInit implements launchr.Plugin interface to provide discovered actions. func (p *Plugin) OnAppInit(app launchr.App) error { p.app = app - dp := p.app.GetWD() - appFs := os.DirFS(dp) - actions, err := discoverActions(appFs) - if err != nil { - return err - } - var actionMngr action.Manager - app.GetService(&actionMngr) - for _, actConf := range actions { - actionMngr.Add(actConf) - } return nil } +// DiscoverActions implements launchr.ActionDiscoveryPlugin interface. +func (p *Plugin) DiscoverActions(fs fs.FS) ([]*action.Action, error) { + return action.NewYamlDiscovery(fs).Discover() +} + // CobraAddCommands implements launchr.CobraPlugin interface to provide discovered actions. func (p *Plugin) CobraAddCommands(rootCmd *cobra.Command) error { var discoverCmd = &cobra.Command{ Use: "discover", Short: "Discovers available actions in filesystem", RunE: func(cmd *cobra.Command, args []string) error { - dp := p.app.GetWD() - actions, err := discoverActions(os.DirFS(dp)) - if err != nil { - return err + var actions []*action.Action + for _, fs := range p.app.GetDiscoveryFS() { + res, err := p.DiscoverActions(fs) + if err != nil { + return err + } + actions = append(actions, res...) } // @todo cache discovery to read fs only once. @@ -68,7 +64,3 @@ func (p *Plugin) CobraAddCommands(rootCmd *cobra.Command) error { rootCmd.AddCommand(discoverCmd) return nil } - -func discoverActions(fs fs.FS) ([]*action.Action, error) { - return action.NewYamlDiscovery(fs).Discover() -} diff --git a/types.go b/types.go index 9432186..1947d43 100644 --- a/types.go +++ b/types.go @@ -5,6 +5,7 @@ import ( "io/fs" "github.com/launchrctl/launchr/internal/launchr" + "github.com/launchrctl/launchr/pkg/action" ) const ( @@ -31,6 +32,8 @@ type ( Plugin = launchr.Plugin // OnAppInitPlugin is an interface to implement a plugin for app initialisation. OnAppInitPlugin = launchr.OnAppInitPlugin + // ActionDiscoveryPlugin is an interface to implement a plugin to discover actions. + ActionDiscoveryPlugin = action.DiscoveryPlugin // CobraPlugin is an interface to implement a plugin for cobra. CobraPlugin = launchr.CobraPlugin // PluginGeneratedData is a struct containing a result information of plugin generation.