diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5aa5958..f57dc07 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,76 +2,32 @@ name: release on: push: - # Sequence of patterns matched against refs/tags tags: - - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + - '*' + +permissions: + contents: write jobs: - build: - name: Release - permissions: write-all + goreleaser: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - + name: Checkout + uses: actions/checkout@v2 with: - go-version: '>=1.18.0' - - - run: | - rm -f ./toolbx || true - env GOOS=linux GOARCH=amd64 go build ./cmd/toolbx - tar -czvf toolbx-${{github.ref_name}}-linux-64bit.tar.gz b./toolbx - - - run: | - rm -f ./toolbx || true - env GOOS=darwin GOARCH=amd64 go build ./cmd/toolbx - tar -czvf toolbx-${{github.ref_name}}-macos-64bit.tar.gz b./toolbx - - - run: | - rm -f ./toolbx || true - env GOOS=windows GOARCH=amd64 go build ./cmd/toolbx - zip toolbx-${{github.ref_name}}-win-64bit.zip ./toolbx - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v2 with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref_name }} - draft: true - prerelease: false - - - name: Upload Linux archive - id: upload-linux-archive - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./toolbx-commands-${{github.ref_name}}-linux-64bit.tar.gz - asset_name: toolbx-commands-${{github.ref_name}}-linux-64bit.tar.gz - asset_content_type: application/x-gzip - - - name: Upload MacOS archive - id: upload-macos-archive - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + go-version: 1.18 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./toolbx-commands-${{github.ref_name}}-macos-64bit.tar.gz - asset_name: toolbx-commands-${{github.ref_name}}-macos-64bit.tar.gz - asset_content_type: application/x-gzip - - - name: Upload Win archive - id: upload-macos-archive - uses: actions/upload-release-asset@v1 + distribution: goreleaser + version: latest + args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./toolbx-commands-${{github.ref_name}}-win-64bit.zip - asset_name: toolbx-commands-${{github.ref_name}}-win-64bit.zip - asset_content_type: application/zip + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..2b8d349 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,34 @@ +before: + hooks: + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm + - arm64 + + ldflags: + - -X 'main.version={{.Version}}' + +archives: + - replacements: + darwin: macOS + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' + - '^refactor:' + +release: + name_template: "{{.ProjectName}}-v{{.Version}}" \ No newline at end of file diff --git a/api/command_test.go b/api/command_test.go deleted file mode 100644 index 4961986..0000000 --- a/api/command_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package api - -import ( - "testing" -) - -func Test_InstallationID(t *testing.T) { - - // Scenario: resolve installation ID hierarchically by joining all parents - // names and name of the current name. - // - // It's very same logic the Git is using e.g. for subcommand 'one two three' - // the binary will be 'one-two-three'. - t.Run("binaryAsChain", func(t *testing.T) { - level1Cmd := &Command{ - Name: "a", - Metadata: &Metadata{}, - } - - level2Cmd := &Command{ - Name: "b", - Parent: level1Cmd, - Metadata: &Metadata{}, - } - - level3Cmd := &Command{ - Name: "c", - Parent: level2Cmd, - Metadata: &Metadata{}, - } - - binary := level3Cmd.GetInstallationID() - if binary != "a-b-c" { - t.FailNow() - } - }) - - // Scenario: when root parent have empty name - t.Run("rootWithoutName", func(t *testing.T) { - level1Cmd := &Command{ - Name: "", - } - - level2Cmd := &Command{ - Name: "hello", - Parent: level1Cmd, - } - - level3Cmd := &Command{ - Name: "subcommand", - Parent: level2Cmd, - } - - binary := level3Cmd.GetInstallationID() - if binary != "hello-subcommand" { - t.FailNow() - } - }) -} diff --git a/config.go b/config.go deleted file mode 100644 index 1898d7c..0000000 --- a/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package toolbx - -type Config struct { - // path to all installed binaries e.g. $TOOLBX/installations - InstallationsPath string `yaml:"installationsPath",omitempty` - - // path to all command definitions, it's synced with Git repository (e.g. $TOOLBX/commands) - CommandsPath string `yaml:"commandsPath",omitempty` - - // URL to Git repository with commands you want to sync with (e.g. https://gitlab.myorg.com/toolbx-commands.git - Repository string `yaml:"repository",omitempty` - - // What to say, branch that will be used for sync - Branch string `yaml:"branch",omitempty` - - // path to sync file. It's empty file that serve for ensuring - // sync is executed only once per day ( e.g. $TOOLBX/sync ) - SyncFile string `yaml:"syncFile",omitempty` -} diff --git a/errors.go b/errors.go deleted file mode 100644 index b219e81..0000000 --- a/errors.go +++ /dev/null @@ -1,21 +0,0 @@ -package toolbx - -import "errors" - -var ( - // NoChildError signalising there is no child command of parent - // command - NoChildError = errors.New("no child subcommands") - - // NoMetadataError indicates the command.yaml is missing in the subcommand - // folder - NoMetadataError = errors.New("no metadata file") - - // MissingRepoError indicates you have probably not configured toolbx. You should - // create configuration file and define Git repository where are commands - // defined: - // - // echo "repository: https://github.com/sn3d/toolbx-demo.git" > ~/.toolbx.yaml - // - MissingRepoError = errors.New("no repository with commands defined") -) diff --git a/go.mod b/go.mod index d663844..ad7241f 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,20 @@ -module toolbx +module github.com/sn3d/toolbx go 1.18 +require ( + github.com/fatih/color v1.13.0 + github.com/go-git/go-git/v5 v5.4.2 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +) + require ( github.com/Microsoft/go-winio v0.4.16 // indirect github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/emirpasic/gods v1.12.0 // indirect - github.com/fatih/color v1.13.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.3.1 // indirect - github.com/go-git/go-git/v5 v5.4.2 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect @@ -23,5 +27,4 @@ require ( golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 24d3545..98fc3f0 100644 --- a/go.sum +++ b/go.sum @@ -5,25 +5,31 @@ github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C6 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 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.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -34,10 +40,13 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgy github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -48,7 +57,9 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= @@ -57,6 +68,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= @@ -76,21 +88,24 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= diff --git a/install/installer.go b/install/installer.go deleted file mode 100644 index 75f0e8c..0000000 --- a/install/installer.go +++ /dev/null @@ -1,7 +0,0 @@ -package install - -import "net/url" - -type Installer interface { - Install(uri url.URL, destDir string) error -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..16e3a88 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/sn3d/toolbx/sdk" + "os" +) + +// version is set by goreleaser, via -ldflags="-X 'main.version=...'". +var version = "development" + +func main() { + + err := sdk.RunToolbx( + sdk.WithBrandLabel("toolbx"), + sdk.WithGitlab(os.Getenv("GITLAB_TOKEN")), + sdk.WithXdgData(), + sdk.WithXdgConfig(), + ) + + if err != nil { + fmt.Errorf("error %v", err) + os.Exit(1) + } +} diff --git a/options.go b/options.go deleted file mode 100644 index 3025615..0000000 --- a/options.go +++ /dev/null @@ -1,125 +0,0 @@ -package toolbx - -import ( - "gopkg.in/yaml.v3" - "io/ioutil" - "os" - "path/filepath" -) - -type ToolbxOption func(instance *Toolbx) - -func defaultValues(instance *Toolbx) { - instance.syncRepoBranch = "main" - - WithNameFromArgs() - WithToolbxPath(os.Getenv("TOOLBXPATH"))(instance) - WithGitlab(os.Getenv("GITLAB_TOKEN"))(instance) -} - -func WithToolbxPath(toolbxpath string) ToolbxOption { - toolbxpath = toolbxPath(toolbxpath) - homeDir, _ := os.UserHomeDir() - return func(instance *Toolbx) { - instance.syncFile = filepath.Join(toolbxpath, "sync") - WithInstallationsDir(filepath.Join(toolbxpath, "installed"))(instance) - WithCommandsDir(filepath.Join(toolbxpath, "commands"))(instance) - - // The configuration is loaded in following order: - // - ~/.toolbx.yaml - // - $TOOLBX/toolbx.yaml - WithConfigFile(filepath.Join(homeDir, ".toolbx.yaml"))(instance) - WithConfigFile(filepath.Join(toolbxpath, "toolbx.yaml"))(instance) - } -} - -func WithConfigFile(path string) ToolbxOption { - return func(instance *Toolbx) { - if _, err := os.Stat(path); os.IsNotExist(err) { - return - } - - yamlFile, err := ioutil.ReadFile(path) - if err != nil { - return - } - - var config Config - err = yaml.Unmarshal(yamlFile, &config) - if err != nil { - return - } - - if config.InstallationsPath != "" { - instance.installationsDir = config.InstallationsPath - } - - if config.CommandsPath != "" { - instance.commandsDir = config.CommandsPath - } - - if config.Repository != "" { - instance.syncRepo = config.Repository - } - - if config.Branch != "" { - instance.syncRepoBranch = config.Branch - } - - if config.SyncFile != "" { - instance.syncFile = config.SyncFile - } - } -} - -func WithGitlab(personalAccessToken string) ToolbxOption { - return func(instance *Toolbx) { - instance.gitlabToken = personalAccessToken - } -} - -func WithSyncRepo(repo string, branch string) ToolbxOption { - return func(instance *Toolbx) { - instance.syncRepo = repo - if branch != "" { - instance.syncRepoBranch = branch - } - } -} - -func WithInstallationsDir(dir string) ToolbxOption { - return func(instance *Toolbx) { - if dir != "" { - instance.installationsDir = dir - } - } -} - -func WithCommandsDir(dir string) ToolbxOption { - return func(instance *Toolbx) { - if dir != "" { - instance.commandsDir = dir - } - } -} - -func WithName(name string) ToolbxOption { - return func(instance *Toolbx) { - instance.name = name - } -} - -func WithNameFromArgs() ToolbxOption { - base := filepath.Base(os.Args[0]) - return func(instance *Toolbx) { - instance.name = base - } -} - -func toolbxPath(path string) string { - if path == "" { - home, _ := os.UserHomeDir() - return filepath.Join(home, ".toolbx") - } - return path -} diff --git a/pkg/cli/configure.go b/pkg/cli/configure.go new file mode 100644 index 0000000..f54b881 --- /dev/null +++ b/pkg/cli/configure.go @@ -0,0 +1,45 @@ +package cli + +import ( + "flag" + "fmt" + "github.com/sn3d/toolbx/pkg/config" + "github.com/sn3d/toolbx/pkg/dir" + + "gopkg.in/yaml.v3" + "io/ioutil" + "path" +) + +// ConfigureCmd is one of the dot commands, and it's executed when you +// type `toolbx .configure` +// This command run configuration wizard that helps you setup +// new installation of toolbx +func ConfigureCmd(args []string) { + config := &config.Configuration{} + + fs := flag.NewFlagSet("configure", flag.ContinueOnError) + fs.StringVar(&config.CommandsRepository, "repository", "", "URL to repository with commands definitions (e.g. https://github.com/sn3d/toolbx-demo.git)") + fs.StringVar(&config.CommandsBranch, "branch", "main", "branch that will be used (default is main)") + + fs.Parse(args) + + configDir := path.Join(dir.XdgConfigHome(), "toolbx") + dir.Ensure(configDir) + + configFile := path.Join(configDir, "toolbx.yaml") + err := saveConfig(config, configFile) + if err != nil { + fmt.Printf("Error: %w", err) + } +} + +func saveConfig(cfg *config.Configuration, file string) error { + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + + err = ioutil.WriteFile(file, data, 0664) + return err +} diff --git a/api/command.go b/pkg/command/command.go similarity index 51% rename from api/command.go rename to pkg/command/command.go index d34cc2e..e0a1989 100644 --- a/api/command.go +++ b/pkg/command/command.go @@ -1,26 +1,33 @@ -package api +package command import ( "runtime" "strings" ) -type Command struct { - Parent *Command +type CommandInstance struct { + Parent *CommandInstance Name string Dir string Args []string Metadata *Metadata } -// GetInstallationID returns you identifier in form of a flat -// name with all parents e.g. for command 'hello world' it +type CommandMetadata struct { + Version string `yaml:"version"` + Description string `yaml:"description",omitempty` + Usage string `yaml:"usage",omitempty` + Packages []Package `yaml:"packages"` +} + +// GetToolID returns you tool's ID in form of a flat-name +// with all parents e.g. for command 'hello world' it // returns 'hello-world' -func (cmd *Command) GetInstallationID() string { +func (cmd *CommandInstance) GetToolID() string { var prefix = "" if cmd.Parent != nil { if cmd.Parent.Name != "" { - prefix = cmd.Parent.GetInstallationID() + "-" + prefix = cmd.Parent.GetToolID() + "-" } } return prefix + cmd.Name @@ -28,7 +35,7 @@ func (cmd *Command) GetInstallationID() string { // GetPackage returns you package for your OS and ARCH. If there // is no package for your platform available, then it returns nil -func (cmd *Command) GetPackage() *Package { +func (cmd *CommandInstance) GetPackage() *Package { platform := strings.ToLower(runtime.GOOS + "-" + runtime.GOARCH) for _, pkg := range cmd.Metadata.Packages { if strings.ToLower(pkg.Platform) == platform { diff --git a/pkg/command/errors.go b/pkg/command/errors.go new file mode 100644 index 0000000..f16ac31 --- /dev/null +++ b/pkg/command/errors.go @@ -0,0 +1,7 @@ +package command + +import "errors" + +// NoChildError signalising there is no child command of parent +// command +var NoChildError = errors.New("no child subcommands") diff --git a/api/metadata.go b/pkg/command/metadata.go similarity index 92% rename from api/metadata.go rename to pkg/command/metadata.go index ef7d23b..33dcf2e 100644 --- a/api/metadata.go +++ b/pkg/command/metadata.go @@ -1,4 +1,4 @@ -package api +package command type Metadata struct { Version string `yaml:"version"` diff --git a/api/package.go b/pkg/command/package.go similarity index 69% rename from api/package.go rename to pkg/command/package.go index b4eaf01..98fa3eb 100644 --- a/api/package.go +++ b/pkg/command/package.go @@ -1,5 +1,6 @@ -package api +package command +// Tool's package that can be installed type Package struct { Platform string `yaml:"platform"` Uri string `yaml:"uri"` diff --git a/repo_commands.go b/pkg/command/repository.go similarity index 80% rename from repo_commands.go rename to pkg/command/repository.go index 89051a9..e880f1b 100644 --- a/repo_commands.go +++ b/pkg/command/repository.go @@ -1,4 +1,4 @@ -package toolbx +package command import ( "errors" @@ -7,7 +7,6 @@ import ( "io/ioutil" "os" "path/filepath" - "toolbx/api" ) type CommandsRepository struct { @@ -20,8 +19,8 @@ func CreateCommandsRepository(commandsDir string) *CommandsRepository { } } -func (repo *CommandsRepository) GetCommand(args []string) (*api.Command, error) { - rootCmd := &api.Command{ +func (repo *CommandsRepository) GetCommand(args []string) (*CommandInstance, error) { + rootCmd := &CommandInstance{ Name: "", Dir: repo.commandsDir, Args: args, @@ -40,12 +39,12 @@ func (repo *CommandsRepository) GetCommand(args []string) (*api.Command, error) return subCmd, nil } -func (repo *CommandsRepository) GetSubcommands(cmd *api.Command) ([]*api.Command, error) { - subcommands := make([]*api.Command, 0) +func (repo *CommandsRepository) GetSubcommands(cmd *CommandInstance) ([]*CommandInstance, error) { + subcommands := make([]*CommandInstance, 0) items, _ := ioutil.ReadDir(cmd.Dir) for _, item := range items { if item.IsDir() && item.Name()[0] != '.' { - cmd := &api.Command{ + cmd := &CommandInstance{ Parent: cmd, Name: item.Name(), Dir: filepath.Join(cmd.Dir, item.Name()), @@ -63,12 +62,12 @@ func (repo *CommandsRepository) GetSubcommands(cmd *api.Command) ([]*api.Command return subcommands, nil } -func getSubCommand(cmd *api.Command, args []string) (*api.Command, error) { +func getSubCommand(cmd *CommandInstance, args []string) (*CommandInstance, error) { if len(args) == 0 { return nil, NoChildError } - subCmd := &api.Command{ + subCmd := &CommandInstance{ Parent: cmd, Name: args[0], Dir: filepath.Join(cmd.Dir, args[0]), @@ -94,10 +93,10 @@ func getSubCommand(cmd *api.Command, args []string) (*api.Command, error) { // // If file is not present, then is returned empty Metadata with no // error. -func loadMetadata(cmd *api.Command) error { +func loadMetadata(cmd *CommandInstance) error { path := filepath.Join(cmd.Dir, "command.yaml") - meta := &api.Metadata{} + meta := &Metadata{} // if 'command.yaml' is missing, we will return empty // metadata. diff --git a/repo_commands_test.go b/pkg/command/repository_test.go similarity index 90% rename from repo_commands_test.go rename to pkg/command/repository_test.go index a1ca3f6..9412bb0 100644 --- a/repo_commands_test.go +++ b/pkg/command/repository_test.go @@ -1,10 +1,9 @@ -package toolbx +package command import ( + "github.com/sn3d/toolbx/pkg/tempfs" "strings" "testing" - v1 "toolbx/api" - "toolbx/testutil" ) func Test_GetCommand(t *testing.T) { @@ -29,12 +28,12 @@ func Test_GetCommand(t *testing.T) { }, } { t.Run(table.description, func(t *testing.T) { - dir, err := testutil.CreateTestData("./testdata/commands") + dir, err := tempfs.New("./testdata/commands") if err != nil { t.FailNow() } - cmdRepo := CreateCommandsRepository(dir) + cmdRepo := CreateCommandsRepository(dir.GetRoot()) cmd, err := cmdRepo.GetCommand(table.args) if err != nil { t.FailNow() @@ -78,7 +77,7 @@ func Test_GetSubcommands(t *testing.T) { // and: description isn't empty func Test_loadMetadata(t *testing.T) { - cmd := v1.Command{ + cmd := CommandInstance{ Dir: "./testdata/commands/k8s/create", } diff --git a/testdata/commands/k8s/command.yaml b/pkg/command/testdata/commands/k8s/command.yaml similarity index 100% rename from testdata/commands/k8s/command.yaml rename to pkg/command/testdata/commands/k8s/command.yaml diff --git a/testdata/commands/k8s/create/command.yaml b/pkg/command/testdata/commands/k8s/create/command.yaml similarity index 100% rename from testdata/commands/k8s/create/command.yaml rename to pkg/command/testdata/commands/k8s/create/command.yaml diff --git a/testdata/commands/k8s/list/command.yaml b/pkg/command/testdata/commands/k8s/list/command.yaml similarity index 100% rename from testdata/commands/k8s/list/command.yaml rename to pkg/command/testdata/commands/k8s/list/command.yaml diff --git a/testdata/commands/storage/command.yaml b/pkg/command/testdata/commands/storage/command.yaml similarity index 100% rename from testdata/commands/storage/command.yaml rename to pkg/command/testdata/commands/storage/command.yaml diff --git a/testdata/commands/storage/kafka/command.yaml b/pkg/command/testdata/commands/storage/kafka/command.yaml similarity index 100% rename from testdata/commands/storage/kafka/command.yaml rename to pkg/command/testdata/commands/storage/kafka/command.yaml diff --git a/testdata/commands/storage/postgres/command.yaml b/pkg/command/testdata/commands/storage/postgres/command.yaml similarity index 100% rename from testdata/commands/storage/postgres/command.yaml rename to pkg/command/testdata/commands/storage/postgres/command.yaml diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..0de94a6 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,20 @@ +package config + +type Configuration struct { + // URL to Git repository with commands you want to sync with (e.g. https://gitlab.myorg.com/toolbx-commands.git + CommandsRepository string `yaml:"repository",omitempty` + + // Git's branch that will be used for syncing command to local machine from remote Git + CommandsBranch string `yaml:"branch",omitempty` + + // GitLab token used for cloning command repository from GitLab or getting tools hosted + // in GitLab as artifacts + GitlabToken string + + // Absolute path to data directory where are stored installed tools, + // synced repo etc. + DataDir string `yaml:"data",omitempty` + + // Here you can change 'toolbx' with your brand label. It's for whitelabeling purpose + BrandLabel string +} diff --git a/pkg/dir/dir.go b/pkg/dir/dir.go new file mode 100644 index 0000000..3a2cb04 --- /dev/null +++ b/pkg/dir/dir.go @@ -0,0 +1,36 @@ +package dir + +import ( + "os" + "path" +) + +func UserHome() string { + homeDir, _ := os.UserHomeDir() + return homeDir +} + +func XdgDataHome() string { + xdgConfigHome := os.Getenv("XDG_DATA_HOME") + if xdgConfigHome == "" { + xdgConfigHome = path.Join(UserHome(), ".local", "share") + } + return xdgConfigHome + +} + +func XdgConfigHome() string { + xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") + if xdgConfigHome == "" { + xdgConfigHome = path.Join(UserHome(), ".config") + } + return xdgConfigHome +} + +func Ensure(dirElms ...string) string { + dir := path.Join(dirElms...) + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.MkdirAll(dir, os.ModePerm) + } + return dir +} diff --git a/pkg/executor/errors.go b/pkg/executor/errors.go new file mode 100644 index 0000000..7c5be53 --- /dev/null +++ b/pkg/executor/errors.go @@ -0,0 +1,11 @@ +package executor + +import "errors" + +// MissingRepoError indicates you have probably not configured toolbx. You should +// create configuration file and define Git repository where are commands +// defined: +// +// echo "repository: https://github.com/sn3d/toolbx-demo.git" > ~/.toolbx.yaml +// +var MissingRepoError = errors.New("no repository with commands defined") diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go new file mode 100644 index 0000000..4ac434b --- /dev/null +++ b/pkg/executor/executor.go @@ -0,0 +1,201 @@ +package executor + +import ( + "fmt" + "github.com/fatih/color" + "github.com/sn3d/toolbx/pkg/cli" + "github.com/sn3d/toolbx/pkg/command" + "github.com/sn3d/toolbx/pkg/config" + "github.com/sn3d/toolbx/pkg/dir" + "github.com/sn3d/toolbx/pkg/installer" + "github.com/sn3d/toolbx/pkg/tool" + "log" + "net/url" + "os" + "os/exec" + "path" + "strings" +) + +type ToolbxExecutor struct { + config config.Configuration + installedToolsRepo *tool.InstalledToolsRepository +} + +// Create and initialize new instance with +// given configuration options +func Initialize(cfg config.Configuration) (*ToolbxExecutor, error) { + + // set default values + executor := &ToolbxExecutor{ + config: cfg, + } + + // validation & post-initialization + if executor.config.CommandsRepository == "" { + return nil, MissingRepoError + } + + dir.Ensure(executor.getCommandsDir()) + dir.Ensure(executor.getToolsDir()) + + err := sync(cfg.CommandsRepository, cfg.CommandsBranch, cfg.GitlabToken, executor.getSyncFile(), executor.getCommandsDir()) + if err != nil { + return nil, err + } + + executor.installedToolsRepo = tool.CreateInstallationsRepository(executor.getToolsDir()) + + return executor, nil +} + +// Execute the given command +func (e *ToolbxExecutor) Execute(args []string) error { + if len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], ".") { + return e.runDotCommand(args) + } else { + return e.runCommand(args) + } +} + +func (e *ToolbxExecutor) runCommand(args []string) error { + if len(args) < 1 { + return nil + } + + commands := command.CreateCommandsRepository(e.getCommandsDir()) + cmd, err := commands.GetCommand(args[1:]) + if err != nil { + return err + } + + subCmds, err := commands.GetSubcommands(cmd) + if err != nil { + return err + } + + // is it a group or final executable command? + if len(subCmds) > 0 { + // it's group because having sub commands, + // let's print list of sub-commands + name := cmd.Name + if name == "" { + name = e.config.BrandLabel + } + + if len(cmd.Args) > 0 { + fmt.Printf("Unknown sub-command '%s'\n", cmd.Args[0]) + } + + fmt.Printf("\n%s\n", cmd.Metadata.Description) + + d := color.New(color.FgHiWhite, color.Bold) + d.Printf("\nAvailable sub-commands for %s\n\n", name) + + for _, subcommand := range subCmds { + fmt.Printf(" %s - %s\n", subcommand.Name, subcommand.Metadata.Description) + } + + fmt.Printf("\n") + } else { + // it's command because there is no sub commands + // let's execute it + var t *tool.ToolInstance + t, isInstalled := e.isInstalledAndUpdated(cmd) + if !isInstalled { + t, err = e.install(cmd) + if err != nil { + return err + } + } + + binaryPath := t.Binary(e.getToolsDir()) + cmd := exec.Command(binaryPath, cmd.Args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + + if err != nil { + return err + } + } + + return nil +} + +func (e *ToolbxExecutor) runDotCommand(args []string) error { + dotCommand := args[1][1:] + switch dotCommand { + case "configure": + cli.ConfigureCmd(os.Args[2:]) + default: + log.Fatalf("Unsupported dot command '%s' \n", dotCommand) + } + + return nil +} + +func (e *ToolbxExecutor) install(cmd *command.CommandInstance) (*tool.ToolInstance, error) { + pkg := cmd.GetPackage() + d := color.New(color.FgHiBlack) + d.Printf("Installing %s (version:%s platform:%s)...\n", cmd.GetToolID(), cmd.Metadata.Version, pkg.Platform) + + tool := &tool.ToolInstance{ + ID: cmd.GetToolID(), + InstalledVersion: cmd.Metadata.Version, + InstalledCmd: pkg.Cmd, + } + + toolDir := dir.Ensure(tool.Dir(e.getToolsDir())) + + uri, err := url.Parse(pkg.Uri) + if err != nil { + return nil, err + } + + opts := installer.InstallationOptions{BearerToken: e.config.GitlabToken} + + err = installer.Install(*uri, toolDir, opts) + if err != nil { + return nil, err + } + + err = e.installedToolsRepo.SaveTool(tool) + if err != nil { + return nil, err + } + + return tool, nil +} + +func (e *ToolbxExecutor) isInstalledAndUpdated(cmd *command.CommandInstance) (*tool.ToolInstance, bool) { + installation := e.installedToolsRepo.GetToolForCommand(cmd) + if installation == nil { + return nil, false + } + + if installation.InstalledVersion != cmd.Metadata.Version { + return nil, false + } + + return installation, true +} + +// function returns directory where are commands defined. Usually it's +// $TOOLBX_DATA/commands +func (e *ToolbxExecutor) getCommandsDir() string { + return path.Join(e.config.DataDir, "commands") +} + +// function returns directory where are all tools installed. Usually it's +// $TOOLBX_DATA/installed_tools +func (e *ToolbxExecutor) getToolsDir() string { + return path.Join(e.config.DataDir, "installed_tools") +} + +// function returns path to sync fileUsually it's +// $TOOLBX_DATA/sync +func (e *ToolbxExecutor) getSyncFile() string { + return path.Join(e.config.DataDir, "sync") +} diff --git a/sync.go b/pkg/executor/sync.go similarity index 88% rename from sync.go rename to pkg/executor/sync.go index ab6db02..6974061 100644 --- a/sync.go +++ b/pkg/executor/sync.go @@ -1,4 +1,4 @@ -package toolbx +package executor import ( "fmt" @@ -10,6 +10,9 @@ import ( "time" ) +// sync clone the toolbx definition from repository to commandsDir folder. The +// sync process is executed everytime when toolbx is executed. To keep it fast, +// the function do real cloning only once per day func sync(repo, branch, token, syncFile string, commandsDir string) error { var err error diff --git a/sync_test.go b/pkg/executor/sync_test.go similarity index 96% rename from sync_test.go rename to pkg/executor/sync_test.go index 7f957ac..c373b14 100644 --- a/sync_test.go +++ b/pkg/executor/sync_test.go @@ -1,4 +1,4 @@ -package toolbx +package executor import ( "os" diff --git a/install/archive/installer.go b/pkg/installer/archive/installer.go similarity index 75% rename from install/archive/installer.go rename to pkg/installer/archive/installer.go index 793f494..97214d8 100644 --- a/install/archive/installer.go +++ b/pkg/installer/archive/installer.go @@ -10,19 +10,11 @@ import ( "path/filepath" ) -type ArchiveInstaller struct { - BearerToken string -} - -func Installer(bearerToken string) *ArchiveInstaller { - return &ArchiveInstaller{ - BearerToken: bearerToken, - } -} - // Install is downloading artifact archive and unzip it into destination -// folder -func (i *ArchiveInstaller) Install(uri url.URL, installationDir string) error { +// folder. Usually it's somewhere in data directory. You can also provide +// bearer token. This is needed if you're downloading archive e.g. from +// corporate GitLab +func Install(uri url.URL, destDir string, bearerToken string) error { uri.Scheme = "https" @@ -32,9 +24,9 @@ func (i *ArchiveInstaller) Install(uri url.URL, installationDir string) error { return err } - if i.BearerToken != "" { - req.Header.Add("PRIVATE-TOKEN", i.BearerToken) - req.Header.Add("Bearer", i.BearerToken) + if bearerToken != "" { + req.Header.Add("PRIVATE-TOKEN", bearerToken) + req.Header.Add("Bearer", bearerToken) } // Get the data @@ -45,7 +37,7 @@ func (i *ArchiveInstaller) Install(uri url.URL, installationDir string) error { defer resp.Body.Close() // write body to 'archive.zip' - archivePath := filepath.Join(installationDir, "archive.zip") + archivePath := filepath.Join(destDir, "archive.zip") out, err := os.OpenFile(archivePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err @@ -57,7 +49,7 @@ func (i *ArchiveInstaller) Install(uri url.URL, installationDir string) error { } // unzip the archive - err = extractZip(archivePath, installationDir) + err = extractZip(archivePath, destDir) if err != nil { return err } diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go new file mode 100644 index 0000000..d90a714 --- /dev/null +++ b/pkg/installer/installer.go @@ -0,0 +1,23 @@ +package installer + +import ( + "fmt" + "github.com/sn3d/toolbx/pkg/installer/archive" + "net/url" +) + +type Installer interface { + Install(uri url.URL, destDir string) error +} + +type InstallationOptions struct { + BearerToken string +} + +func Install(uri url.URL, destDir string, opts InstallationOptions) error { + if uri.Scheme == "https+zip" { + return archive.Install(uri, destDir, opts.BearerToken) + } else { + return fmt.Errorf("unsupported package scheme %s", uri.Scheme) + } +} diff --git a/testutil/testing.go b/pkg/tempfs/tempfs.go similarity index 64% rename from testutil/testing.go rename to pkg/tempfs/tempfs.go index d6a0d3d..d2542b2 100644 --- a/testutil/testing.go +++ b/pkg/tempfs/tempfs.go @@ -1,22 +1,39 @@ -package testutil +package tempfs import ( "fmt" "io/ioutil" "os" + "path/filepath" ) -func CreateTestData(src string) (string, error) { +type TempFs struct { + rootDir string +} - // create temporary testutil dir - dest, err := os.MkdirTemp("", "toolbx-") +func New(src string) (*TempFs, error) { + // create temporary root dir + rootDir, err := os.MkdirTemp("", "tempfs-") if err != nil { - return "", err + return nil, err } // copy content - err = copyDir(src, dest) - return dest, err + err = copyDir(src, rootDir) + if err != nil { + return nil, err + } + + return &TempFs{rootDir}, nil +} + +func (tfs *TempFs) GetRoot() string { + return tfs.rootDir +} + +// Get returns you absolute path for given path +func (tfs *TempFs) Get(path string) string { + return filepath.Join(tfs.rootDir, path) } func copyDir(src string, dest string) error { diff --git a/pkg/tool/repository.go b/pkg/tool/repository.go new file mode 100644 index 0000000..22e182e --- /dev/null +++ b/pkg/tool/repository.go @@ -0,0 +1,53 @@ +package tool + +import ( + "github.com/sn3d/toolbx/pkg/command" + "gopkg.in/yaml.v3" + "io/ioutil" + "path/filepath" +) + +type InstalledToolsRepository struct { + // directory where are tools installed on your system + InstallationsDir string +} + +func CreateInstallationsRepository(InstallationsDir string) *InstalledToolsRepository { + return &InstalledToolsRepository{ + InstallationsDir: InstallationsDir, + } +} + +func (fsi *InstalledToolsRepository) GetToolForCommand(cmd *command.CommandInstance) *ToolInstance { + id := cmd.GetToolID() + path := filepath.Join(fsi.InstallationsDir, id+".yaml") + + installedTool := &ToolInstance{} + yamlFile, err := ioutil.ReadFile(path) + if err != nil { + return nil + + } + + err = yaml.Unmarshal(yamlFile, installedTool) + if err != nil { + return nil + } + + installedTool.ID = id + return installedTool +} + +func (fsi *InstalledToolsRepository) SaveTool(t *ToolInstance) error { + path := filepath.Join(fsi.InstallationsDir, t.ID+".yaml") + data, err := yaml.Marshal(t) + if err != nil { + return err + } + + err = ioutil.WriteFile(path, data, 0640) + if err != nil { + return err + } + return nil +} diff --git a/repo_installations_test.go b/pkg/tool/repository_test.go similarity index 54% rename from repo_installations_test.go rename to pkg/tool/repository_test.go index 057fd4b..ce23e83 100644 --- a/repo_installations_test.go +++ b/pkg/tool/repository_test.go @@ -1,9 +1,9 @@ -package toolbx +package tool import ( + "github.com/sn3d/toolbx/pkg/command" "os" "testing" - "toolbx/api" ) func Test_SaveAndLoadInstallation(t *testing.T) { @@ -11,33 +11,33 @@ func Test_SaveAndLoadInstallation(t *testing.T) { installationsPath, _ := os.MkdirTemp("", "toolbx-installations-*") repo := CreateInstallationsRepository(installationsPath) - // save installation - inst := api.Installation{ + // save installed tool + toolInstance := ToolInstance{ ID: "hello", InstalledVersion: "1.0.0", } - err := repo.SaveInstallation(&inst) + err := repo.SaveTool(&toolInstance) if err != nil { t.FailNow() } // get the installation - cmd := api.Command{ + cmd := command.CommandInstance{ Name: "hello", - Metadata: &api.Metadata{}, + Metadata: &command.Metadata{}, } - savedInst := repo.GetInstallationForCommand(&cmd) - if savedInst == nil { + savedTool := repo.GetToolForCommand(&cmd) + if savedTool == nil { t.FailNow() } - if savedInst.InstalledVersion != "1.0.0" { + if savedTool.InstalledVersion != "1.0.0" { t.FailNow() } - if savedInst.ID != "hello" { + if savedTool.ID != "hello" { t.FailNow() } } diff --git a/api/installation.go b/pkg/tool/tool.go similarity index 55% rename from api/installation.go rename to pkg/tool/tool.go index 05581f7..2a5215f 100644 --- a/api/installation.go +++ b/pkg/tool/tool.go @@ -1,20 +1,22 @@ -package api +package tool import ( "path/filepath" ) -type Installation struct { +// Tool represent installed tool in your system. Tool is installed +// from Package and executed from Command +type ToolInstance struct { ID string `yaml:"id"` InstalledVersion string `yaml:"installedVersion"` InstalledCmd string `yaml:"installedCmd"` } -func (i *Installation) Dir(rootDir string) string { +func (i *ToolInstance) Dir(rootDir string) string { installationDir := filepath.Join(rootDir, i.ID, i.InstalledVersion) return installationDir } -func (i *Installation) Binary(rootDir string) string { +func (i *ToolInstance) Binary(rootDir string) string { return filepath.Join(i.Dir(rootDir), i.InstalledCmd) } diff --git a/repo_installations.go b/repo_installations.go deleted file mode 100644 index f6d28c0..0000000 --- a/repo_installations.go +++ /dev/null @@ -1,52 +0,0 @@ -package toolbx - -import ( - "gopkg.in/yaml.v3" - "io/ioutil" - "path/filepath" - "toolbx/api" -) - -type InstallationsRepository struct { - InstallationsDir string -} - -func CreateInstallationsRepository(installationDir string) *InstallationsRepository { - return &InstallationsRepository{ - InstallationsDir: installationDir, - } -} - -func (fsi *InstallationsRepository) GetInstallationForCommand(cmd *api.Command) *api.Installation { - id := cmd.GetInstallationID() - path := filepath.Join(fsi.InstallationsDir, id+".yaml") - - installation := &api.Installation{} - yamlFile, err := ioutil.ReadFile(path) - if err != nil { - return nil - - } - - err = yaml.Unmarshal(yamlFile, installation) - if err != nil { - return nil - } - - installation.ID = id - return installation -} - -func (fsi *InstallationsRepository) SaveInstallation(i *api.Installation) error { - path := filepath.Join(fsi.InstallationsDir, i.ID+".yaml") - data, err := yaml.Marshal(i) - if err != nil { - return err - } - - err = ioutil.WriteFile(path, data, 0640) - if err != nil { - return err - } - return nil -} diff --git a/sdk/options.go b/sdk/options.go new file mode 100644 index 0000000..eb81537 --- /dev/null +++ b/sdk/options.go @@ -0,0 +1,93 @@ +package sdk + +import ( + "github.com/sn3d/toolbx/pkg/config" + "github.com/sn3d/toolbx/pkg/dir" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" + "path" +) + +type ToolbxOption func(cfg *config.Configuration) + +// load configuration file from $HOME/.config/{brand label}/{brand label}.yaml +func WithXdgConfig() ToolbxOption { + return func(cfg *config.Configuration) { + configHome := dir.XdgConfigHome() + toolbxConfigFile := path.Join(configHome, cfg.BrandLabel, cfg.BrandLabel+".yaml") + WithConfigFile(toolbxConfigFile)(cfg) + } +} + +// data like installations will be stored in $HOME/.local/share/{brand label} +func WithXdgData() ToolbxOption { + return func(cfg *config.Configuration) { + dataHome := dir.XdgDataHome() + toolbxDataHome := path.Join(dataHome, cfg.BrandLabel) + WithDataDir(toolbxDataHome)(cfg) + } +} + +// WithConfigFile ensure the configuration will be loaded +// from path. Path need to contain also YAML file. +func WithConfigFile(path string) ToolbxOption { + return func(cfg *config.Configuration) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return + } + + yamlFile, err := ioutil.ReadFile(path) + if err != nil { + return + } + + var loadedCfg config.Configuration + err = yaml.Unmarshal(yamlFile, &loadedCfg) + if err != nil { + return + } + + if loadedCfg.CommandsRepository != "" { + cfg.CommandsRepository = loadedCfg.CommandsRepository + } + + if loadedCfg.CommandsBranch != "" { + cfg.CommandsBranch = loadedCfg.CommandsBranch + } + } +} + +// If you want to use GitLab for distribution, you need to +// provide GitLab token +func WithGitlab(personalAccessToken string) ToolbxOption { + return func(cfg *config.Configuration) { + cfg.GitlabToken = personalAccessToken + } +} + +func WithCommandsRepository(repo string, branch string) ToolbxOption { + return func(cfg *config.Configuration) { + cfg.CommandsRepository = repo + if branch != "" { + cfg.CommandsBranch = branch + } + } +} + +// WithDataDir set data directory, where are placed +// installations etc... +func WithDataDir(dataDir string) ToolbxOption { + return func(cfg *config.Configuration) { + if dataDir != "" { + dir.Ensure(dataDir) + cfg.DataDir = dataDir + } + } +} + +func WithBrandLabel(brand string) ToolbxOption { + return func(cfg *config.Configuration) { + cfg.BrandLabel = brand + } +} diff --git a/sdk/toolbx.go b/sdk/toolbx.go new file mode 100644 index 0000000..4678a06 --- /dev/null +++ b/sdk/toolbx.go @@ -0,0 +1,30 @@ +package sdk + +import ( + "fmt" + "github.com/sn3d/toolbx/pkg/config" + "github.com/sn3d/toolbx/pkg/executor" + "os" +) + +func RunToolbx(options ...ToolbxOption) error { + cfg := config.Configuration{} + + for _, option := range options { + option(&cfg) + } + + exec, err := executor.Initialize(cfg) + if err != nil { + fmt.Errorf("error %v", err) + os.Exit(1) + } + + err = exec.Execute(os.Args) + if err != nil { + fmt.Errorf("error %v", err) + os.Exit(1) + } + + return nil +} diff --git a/toolbx.go b/toolbx.go deleted file mode 100644 index 022c66e..0000000 --- a/toolbx.go +++ /dev/null @@ -1,201 +0,0 @@ -package toolbx - -import ( - "fmt" - "github.com/fatih/color" - "log" - "net/url" - "os" - "os/exec" - "toolbx/api" - "toolbx/install" - "toolbx/install/archive" -) - -type Toolbx struct { - name string - commandsDir string - gitlabToken string - syncRepo string - syncRepoBranch string - syncFile string - installationsDir string - installers map[string]install.Installer -} - -func Main(options ...ToolbxOption) error { - tlbx, err := Create(options...) - if err == MissingRepoError { - fmt.Println("Hello, and welcome!") - fmt.Println("") - fmt.Println("Do one simple configuration step before you start using Toolbx:") - fmt.Println("") - fmt.Println(" echo \"repository: https://github.com/sn3d/toolbx-demo.git\" > ~/.toolbx.yaml") - fmt.Println("") - return nil - } else if err != nil { - log.Fatalln("error starting toolbx:", err) - } - - err = tlbx.Execute(os.Args) - if err != nil { - log.Fatalln("error executing command:", err) - } - - return nil -} - -// Create and initialize new instance with -// given configuration options -func Create(options ...ToolbxOption) (*Toolbx, error) { - - // set default values - toolbx := &Toolbx{} - defaultValues(toolbx) - - // apply options - for _, o := range options { - o(toolbx) - } - - // validation & post-initialization - if toolbx.syncRepo == "" { - return nil, MissingRepoError - } - - toolbx.installers = map[string]install.Installer{ - "https+zip": archive.Installer(toolbx.gitlabToken), - } - ensureDir(toolbx.commandsDir) - ensureDir(toolbx.installationsDir) - - err := sync(toolbx.syncRepo, toolbx.syncRepoBranch, toolbx.gitlabToken, toolbx.syncFile, toolbx.commandsDir) - if err != nil { - return nil, err - } - - return toolbx, nil -} - -// Execute the given command -func (t *Toolbx) Execute(args []string) error { - if len(args) < 1 { - return nil - } - - commands := CreateCommandsRepository(t.commandsDir) - cmd, err := commands.GetCommand(args[1:]) - if err != nil { - return err - } - - subCmds, err := commands.GetSubcommands(cmd) - if err != nil { - return err - } - - // is it a group or final executable command? - if len(subCmds) > 0 { - // it's group because having sub commands, - // let's print list of sub-commands - name := cmd.Name - if name == "" { - name = t.name - } - - if len(cmd.Args) > 0 { - fmt.Printf("Unknown sub-command '%s'\n", cmd.Args[0]) - } - - fmt.Printf("\n%s\n", cmd.Metadata.Description) - - d := color.New(color.FgHiWhite, color.Bold) - d.Printf("\nAvailable sub-commands for %s\n\n", name) - - for _, subcommand := range subCmds { - fmt.Printf(" %s - %s\n", subcommand.Name, subcommand.Metadata.Description) - } - - fmt.Printf("\n") - } else { - // it's command because there is no sub commands - // let's execute it - var installation *api.Installation - installation, installed := t.isInstalledAndUpdated(cmd) - if !installed { - installation, err = t.install(cmd) - if err != nil { - return err - } - } - - binaryPath := installation.Binary(t.installationsDir) - cmd := exec.Command(binaryPath, cmd.Args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Run() - - if err != nil { - return err - } - } - - return nil -} - -func (t *Toolbx) install(cmd *api.Command) (*api.Installation, error) { - pkg := cmd.GetPackage() - d := color.New(color.FgHiBlack) - d.Printf("Installing %s (version:%s platform:%s)...\n", cmd.GetInstallationID(), cmd.Metadata.Version, pkg.Platform) - - installation := &api.Installation{ - ID: cmd.GetInstallationID(), - InstalledVersion: cmd.Metadata.Version, - InstalledCmd: pkg.Cmd, - } - - dir := ensureDir(installation.Dir(t.installationsDir)) - - uri, err := url.Parse(pkg.Uri) - if err != nil { - return nil, err - } - - installer := t.installers[uri.Scheme] - if installer == nil { - return nil, fmt.Errorf("unsupported package scheme %s", uri.Scheme) - } - - err = installer.Install(*uri, dir) - if err != nil { - return nil, err - } - - err = CreateInstallationsRepository(t.installationsDir).SaveInstallation(installation) - if err != nil { - return nil, err - } - - return installation, nil -} - -func (t *Toolbx) isInstalledAndUpdated(cmd *api.Command) (*api.Installation, bool) { - installation := CreateInstallationsRepository(t.installationsDir).GetInstallationForCommand(cmd) - if installation == nil { - return nil, false - } - - if installation.InstalledVersion != cmd.Metadata.Version { - return nil, false - } - - return installation, true -} - -func ensureDir(dir string) string { - if _, err := os.Stat(dir); os.IsNotExist(err) { - os.MkdirAll(dir, os.ModePerm) - } - return dir -} diff --git a/toolbx_test.go b/toolbx_test.go deleted file mode 100644 index 20cd963..0000000 --- a/toolbx_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package toolbx - -import ( - "os" - "testing" -) - -func Test_Execute(t *testing.T) { - repo := os.Getenv("TOOLBXREPO") - if repo == "" { - t.Skip("set TOOLBXREPO to demo repository if you want to run this testutil") - } - - toolbxpath, err := os.MkdirTemp("", "toolbx-exec") - if err != nil { - t.FailNow() - } - - toolbx, err := Create( - WithToolbxPath(toolbxpath), - WithSyncRepo(os.Getenv("TOOLBXREPO"), "main"), - ) - - if err != nil { - t.FailNow() - } - - t.Run("simple-test", func(t *testing.T) { - args := []string{"toolbx", "k8s", "create", "arg1", "arg2"} - err = toolbx.Execute(args) - if err != nil { - t.FailNow() - } - }) - - t.Run("empty-subcommands", func(t *testing.T) { - args := []string{"toolbx"} - err = toolbx.Execute(args) - if err != nil { - t.FailNow() - } - }) - - t.Run("group", func(t *testing.T) { - args := []string{"toolbx", "k8s"} - err = toolbx.Execute(args) - if err != nil { - t.FailNow() - } - }) - -}