diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ed466..e6e3012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # RELEASE NOTES +## 1.6.0 (September 5, 2024) + +### Enhancements + +* Added support to show the `Installed Version` of commands during `search` +* Updated the list of packages in `packages-list.json` ([GH#192](https://github.com/akamai/cli/issues/192)) +* Removed versions of the packages from `package-list.json` +* Changed package installation order + * Cli will first check if new binaries are available. If the package has no binaries or no valid + binaries can be found, it will build the package locally + * --force flag has been deprecated for both install and update +* Migrated to go 1.21 +* Updated various dependencies + +### Fixes + +* Fixed uninstalling of a command when binaries are not found + ## 1.5.6 (January 22, 2024) ### Enhancements diff --git a/README.md b/README.md index 8b9edf2..609b6f4 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ This command installs the CLI and persists the configuration and packages in `$H ### Compile from Source -**Prerequisite:** Make sure you install Go 1.17 or later. +**Prerequisite:** Make sure you install Go 1.21 or later. To compile Akamai CLI from source: diff --git a/go.mod b/go.mod index 401fc14..68c09e4 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/akamai/cli -go 1.20 +go 1.21 require ( - github.com/AlecAivazis/survey/v2 v2.3.5 + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Masterminds/semver v1.5.0 github.com/apex/log v1.9.0 github.com/briandowns/spinner v1.23.0 diff --git a/go.sum b/go.sum index b687161..717645b 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= -github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -13,12 +13,14 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 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/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= @@ -37,6 +39,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -44,11 +47,13 @@ github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU= @@ -59,6 +64,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -83,9 +89,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/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= 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/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= @@ -101,6 +109,7 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -110,6 +119,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -180,6 +190,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -191,7 +202,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -202,7 +212,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -230,6 +239,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/pkg/commands/command.go b/pkg/commands/command.go index 7d61575..782d258 100644 --- a/pkg/commands/command.go +++ b/pkg/commands/command.go @@ -209,7 +209,7 @@ func createBuiltinCommands() []*cli.Command { Flags: []cli.Flag{ &cli.BoolFlag{ Name: "force", - Usage: "Force binary installation if available when source installation fails", + Usage: "[deprecated] Force binary installation if available when source installation fails", }, }, HideHelp: true, @@ -254,7 +254,7 @@ func createBuiltinCommands() []*cli.Command { Flags: []cli.Flag{ &cli.BoolFlag{ Name: "force", - Usage: "Force binary installation if available when source installation fails", + Usage: "[deprecated] Force binary installation if available when source installation fails", }, }, HideHelp: true, diff --git a/pkg/commands/command_install.go b/pkg/commands/command_install.go index 3eec5de..83fba5e 100644 --- a/pkg/commands/command_install.go +++ b/pkg/commands/command_install.go @@ -34,6 +34,7 @@ import ( var ( thirdPartyDisclaimer = color.CyanString("Disclaimer: You are installing a third-party package, subject to its own terms and conditions. Akamai makes no warranty or representation with respect to the third-party package.") + githubRawURLTemplate = "https://raw.githubusercontent.com/akamai/%s/master/cli.json" ) func cmdInstall(git git.Repository, langManager packages.LangManager) cli.ActionFunc { @@ -62,7 +63,7 @@ func cmdInstall(git git.Repository, langManager packages.LangManager) cli.Action for _, repo := range c.Args().Slice() { repo = tools.Githubize(repo) - subCmd, err := installPackage(c.Context, git, langManager, repo, c.Bool("force")) + subCmd, err := installPackage(c.Context, git, langManager, repo) if err != nil { return err @@ -124,7 +125,7 @@ func packageListDiff(c *cli.Context, oldcmds []subcommands) { listInstalledCommands(c, added, removed) } -func installPackage(ctx context.Context, gitRepo git.Repository, langManager packages.LangManager, repo string, forceBinary bool) (*subcommands, error) { +func installPackage(ctx context.Context, gitRepo git.Repository, langManager packages.LangManager, repo string) (*subcommands, error) { logger := log.FromContext(ctx) srcPath, err := tools.GetAkamaiCliSrcPath() if err != nil { @@ -132,19 +133,51 @@ func installPackage(ctx context.Context, gitRepo git.Repository, langManager pac } term := terminal.Get(ctx) - spin := term.Spinner() - spin.Start("Attempting to fetch command from %s...", repo) - dirName := strings.TrimSuffix(filepath.Base(repo), ".git") packageDir := filepath.Join(srcPath, dirName) + if _, err = os.Stat(packageDir); err == nil { - spin.Stop(terminal.SpinnerStatusWarn) warningMsg := fmt.Sprintf("Package directory already exists (%s). To reinstall this package, first run 'akamai uninstall' command.", packageDir) return nil, cli.Exit(color.YellowString(warningMsg), 0) } + spin.Start("Attempting to fetch package configuration from %s...", repo) + + base := filepath.Base(dirName) + url := fmt.Sprintf(githubRawURLTemplate, base) + cmdPackage, err := readPackageFromGithub(url, dirName) + if err != nil { + spin.Stop(terminal.SpinnerStatusFail) + logger.Error(err.Error()) + if _, err := term.Writeln(err.Error()); err != nil { + term.WriteError(err.Error()) + } + if strings.Contains(err.Error(), "404") { + return nil, cli.Exit(color.RedString(tools.CapitalizeFirstWord(git.ErrPackageNotAvailable.Error())), 1) + } + return nil, cli.Exit(color.RedString("Unable to install selected package"), 1) + } + spin.OK() + + if isBinary(cmdPackage) { + + ok, subCmd := installPackageBinaries(ctx, packageDir, cmdPackage, logger) + if ok { + return subCmd, nil + } + // delete package directory + if err := os.RemoveAll(packageDir); err != nil { + return nil, err + } + + } + spin.Start("Attempting to fetch command from %s...", repo) + + if !strings.HasPrefix(repo, "https://github.com/akamai/cli-") && !strings.HasPrefix(repo, "git@github.com:akamai/cli-") { + term.Printf(color.CyanString(thirdPartyDisclaimer)) + } err = gitRepo.Clone(ctx, packageDir, repo, false, spin) if err != nil { if err := os.RemoveAll(packageDir); err != nil { @@ -157,11 +190,7 @@ func installPackage(ctx context.Context, gitRepo git.Repository, langManager pac } spin.OK() - if !strings.HasPrefix(repo, "https://github.com/akamai/cli-") && !strings.HasPrefix(repo, "git@github.com:akamai/cli-") { - term.Printf(color.CyanString(thirdPartyDisclaimer)) - } - - ok, subCmd := installPackageDependencies(ctx, langManager, packageDir, forceBinary, logger) + ok, subCmd := installPackageDependencies(ctx, langManager, packageDir, logger) if !ok { if err := os.RemoveAll(packageDir); err != nil { return nil, err @@ -172,7 +201,7 @@ func installPackage(ctx context.Context, gitRepo git.Repository, langManager pac return subCmd, nil } -func installPackageDependencies(ctx context.Context, langManager packages.LangManager, dir string, forceBinary bool, logger log.Logger) (bool, *subcommands) { +func installPackageDependencies(ctx context.Context, langManager packages.LangManager, dir string, logger log.Logger) (bool, *subcommands) { cmdPackage, err := readPackage(dir) term := terminal.Get(ctx) @@ -209,70 +238,57 @@ func installPackageDependencies(ctx context.Context, langManager packages.LangMa return true, &cmdPackage } - if err == nil { - term.Spinner().OK() - return true, &cmdPackage + if err != nil { + term.Spinner().Stop(terminal.SpinnerStatusFail) + term.WriteError(err.Error()) + return false, nil + } - first := true - for _, cmd := range cmdPackage.Commands { - if cmd.Bin != "" { - if first { - first = false - term.Spinner().Stop(terminal.SpinnerStatusWarn) - if _, err := term.Writeln(color.CyanString(err.Error())); err != nil { - term.WriteError(err.Error()) - return false, nil - } - logger.Warn(err.Error()) - if !forceBinary { - if !term.IsTTY() { - return false, nil - } - - answer, err := term.Confirm("Binary command(s) found, would you like to download and install it?", true) - if err != nil { - term.WriteError(err.Error()) - logger.Error(err.Error()) - return false, nil - } - - if !answer { - return false, nil - } - } + term.Spinner().OK() + return true, &cmdPackage - if err := os.MkdirAll(filepath.Join(dir, "bin"), 0700); err != nil { - return false, nil - } +} - term.Spinner().Start("Downloading binary...") - } +func installPackageBinaries(ctx context.Context, dir string, cmdPackage subcommands, logger log.Logger) (bool, *subcommands) { - if err = downloadBin(ctx, filepath.Join(dir, "bin"), cmd); err != nil { - term.Spinner().Stop(terminal.SpinnerStatusFail) - errorMsg := "Unable to download binary: " + err.Error() - if _, err := term.Writeln(color.RedString(errorMsg)); err != nil { - term.WriteError(err.Error()) - return false, nil - } - logger.Error(errorMsg) - return false, nil - } - } + term := terminal.Get(ctx) + spin := term.Spinner() + spin.Start("Installing Binaries...") - if first { - term.Spinner().Stop(terminal.SpinnerStatusFail) - if _, err := term.Writeln(color.RedString(err.Error())); err != nil { + if err := os.MkdirAll(filepath.Join(dir, "bin"), 0700); err != nil { + return false, nil + } + + for _, cmd := range cmdPackage.Commands { + err := downloadBin(ctx, filepath.Join(dir, "bin"), cmd) + if err != nil { + warnMsg := fmt.Sprintf("Unable to download binary: %v", err.Error()) + spin.Stop(terminal.SpinnerStatusWarn) + if _, err := term.Writeln(color.YellowString(warnMsg)); err != nil { term.WriteError(err.Error()) return false, nil } - logger.Error(err.Error()) + logger.Warn(warnMsg) + return false, nil + } } - term.Spinner().Stop(terminal.SpinnerStatusOK) + err := os.WriteFile(filepath.Join(dir, "cli.json"), cmdPackage.raw, 0644) + if err != nil { + spin.Stop(terminal.SpinnerStatusWarn) + warnMsg := "Unable to save configuration file " + err.Error() + if _, err := term.Writeln(color.YellowString(warnMsg)); err != nil { + term.WriteError(err.Error()) + return false, nil + } + logger.Warn(warnMsg) + return false, nil + } + spin.OK() return true, &cmdPackage + } diff --git a/pkg/commands/command_install_test.go b/pkg/commands/command_install_test.go index 7fecf40..b8ecc57 100644 --- a/pkg/commands/command_install_test.go +++ b/pkg/commands/command_install_test.go @@ -25,25 +25,40 @@ import ( func TestCmdInstall(t *testing.T) { cliTestCmdRepo := filepath.Join(".", "testdata", ".akamai-cli", "src", "cli-test-cmd") cliJSON := filepath.Join(".", "testdata", "repo", "cli.json") + cliInvalidJSON := filepath.Join(".", "testdata", "repo_invalid_json", "cli.json") + cliNoBinaryJSON := filepath.Join(".", "testdata", "repo_no_binary", "cli.json") + cliTestCmdJSON := filepath.Join(".", "testdata", ".akamai-cli", "src", "cli-test-cmd", "cli.json") cliTestInvalidJSONRepo := filepath.Join(".", "testdata", ".akamai-cli", "src", "cli-test-invalid-json") tests := map[string]struct { args []string - init func(*testing.T, *mocked) + init func(*testing.T, *mocked, *httptest.Server) teardown func(*testing.T) binaryResponseStatus int withError string }{ "install from official akamai repository, build from source": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliNoBinaryJSON) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { - mustCopyFile(t, cliJSON, cliTestCmdRepo) + mustCopyFile(t, cliNoBinaryJSON, cliTestCmdRepo) }) m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() @@ -69,10 +84,21 @@ func TestCmdInstall(t *testing.T) { }, "install from official akamai repository, build from source + ldflags": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliNoBinaryJSON) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { @@ -102,36 +128,26 @@ func TestCmdInstall(t *testing.T) { }, "install from official akamai repository, download binary": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliJSON) + output := strings.ReplaceAll(string(configJSON), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL")) + require.NoError(t, err) + _, err = w.Write([]byte(output)) + require.NoError(t, err) + + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() - m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). - Run(func(args mock.Arguments) { - mustCopyFile(t, cliJSON, cliTestCmdRepo) - input, err := ioutil.ReadFile(cliTestCmdJSON) - require.NoError(t, err) - output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL")) - err = ioutil.WriteFile(cliTestCmdJSON, []byte(output), 0755) - require.NoError(t, err) - }) - m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() - - m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(fmt.Errorf("oops")).Once() - m.term.On("Spinner").Return(m.term).Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() - m.term.On("Writeln", []interface{}{color.CyanString("oops")}).Return(0, nil).Once() - m.term.On("IsTTY").Return(true).Once() - m.term.On("Confirm", "Binary command(s) found, would you like to download and install it?", true).Return(true, nil).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Downloading binary...", []interface{}(nil)).Return().Once() + m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusOK).Return().Once() + m.term.On("Start", "Installing Binaries...", []interface{}(nil)).Return().Once() + m.term.On("OK").Return().Once() // list all packages m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() @@ -145,7 +161,7 @@ func TestCmdInstall(t *testing.T) { }, "package directory already exists": { args: []string{"installed"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-installed.git"}).Return().Once() m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() @@ -154,14 +170,25 @@ func TestCmdInstall(t *testing.T) { }, "no args passed": { args: []string{}, - init: func(t *testing.T, m *mocked) {}, + init: func(t *testing.T, m *mocked, h *httptest.Server) {}, withError: "You must specify a repository URL", }, "git clone error": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliNoBinaryJSON) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(git.ErrPackageNotAvailable).Once(). @@ -173,11 +200,23 @@ func TestCmdInstall(t *testing.T) { }, withError: "Package is not available. Supported packages can be found here: https://techdocs.akamai.com/home/page/products-tools-a-z", }, - "error reading downloaded package, invalid cli.json": { + "error reading downloaded package, invalid cli.json in repository": { args: []string{"test-invalid-json"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-invalid-json.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliNoBinaryJSON) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-invalid-json.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-invalid-json"), "https://github.com/akamai/cli-test-invalid-json.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { @@ -198,63 +237,62 @@ func TestCmdInstall(t *testing.T) { }, withError: "Unable to install selected package", }, - "install from official akamai repository, unknown lang": { - args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() - m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). - Run(func(args mock.Arguments) { - mustCopyFile(t, cliJSON, cliTestCmdRepo) - }) - m.term.On("Spinner").Return(m.term).Once() - m.term.On("OK").Return().Once() + "error reading downloaded package, invalid cli.json": { + args: []string{"test-invalid-json"}, + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-invalid-json.git"}).Return().Once() + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliInvalidJSON) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-invalid-json.git"}).Return().Once() + m.term.On("OK").Return().Once() m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(packages.ErrUnknownLang).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("WarnOK").Return().Once() - // list all packages - m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() m.term.On("Writeln", mock.Anything).Return(0, nil) }, teardown: func(t *testing.T) { - require.NoError(t, os.RemoveAll(cliTestCmdRepo)) + require.NoError(t, os.RemoveAll(cliTestInvalidJSONRepo)) }, + withError: "Unable to install selected package", }, - "install from official akamai repository, user does not install binary": { + "install from official akamai repository, unknown lang": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliNoBinaryJSON) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { - mustCopyFile(t, cliJSON, cliTestCmdRepo) - input, err := ioutil.ReadFile(cliTestCmdJSON) - require.NoError(t, err) - output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL")) - err = ioutil.WriteFile(cliTestCmdJSON, []byte(output), 0755) - require.NoError(t, err) + mustCopyFile(t, cliNoBinaryJSON, cliTestCmdRepo) }) m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(fmt.Errorf("oops")).Once() + packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(packages.ErrUnknownLang).Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() - m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() - m.term.On("Writeln", []interface{}{color.CyanString("oops")}).Return(0, nil).Once() - m.term.On("IsTTY").Return(true).Once() - m.term.On("Confirm", "Binary command(s) found, would you like to download and install it?", true).Return(false, nil).Once() + m.term.On("WarnOK").Return().Once() // list all packages m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() @@ -263,92 +301,123 @@ func TestCmdInstall(t *testing.T) { teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) }, - withError: "Unable to install selected package", }, - "install from official akamai repository, error downloading binary, invalid URL": { + "install from official akamai repository, error downloading binary, invalid URL, build from source": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliJSON) + output := strings.ReplaceAll(string(configJSON), "${REPOSITORY_URL}", "invalid url") + require.NoError(t, err) + _, err = w.Write([]byte(output)) + require.NoError(t, err) + + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Installing Binaries...", []interface{}(nil)).Return().Once() + m.term.On("Stop", terminal.SpinnerStatusWarn) + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { mustCopyFile(t, cliJSON, cliTestCmdRepo) }) - m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(fmt.Errorf("oops")).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() - m.term.On("Writeln", []interface{}{color.CyanString("oops")}).Return(0, nil).Once() - m.term.On("IsTTY").Return(true).Once() - m.term.On("Confirm", "Binary command(s) found, would you like to download and install it?", true).Return(true, nil).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Downloading binary...", []interface{}(nil)).Return().Once() + packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(nil).Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.term.On("OK").Return().Once() // list all packages m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() m.term.On("Writeln", mock.Anything).Return(0, nil) }, binaryResponseStatus: http.StatusOK, - withError: "Unable to install selected package", teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) }, }, - "install from official akamai repository, error downloading binary, invalid response status": { + "install from official akamai repository, error downloading binary, invalid response status, build from source": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliJSON) + output := strings.ReplaceAll(string(configJSON), "${REPOSITORY_URL}", "invalid url") + require.NoError(t, err) + _, err = w.Write([]byte(output)) + require.NoError(t, err) + + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Installing Binaries...", []interface{}(nil)).Return().Once() + m.term.On("Stop", terminal.SpinnerStatusWarn) + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { mustCopyFile(t, cliJSON, cliTestCmdRepo) - input, err := ioutil.ReadFile(cliTestCmdJSON) - require.NoError(t, err) - output := strings.ReplaceAll(string(input), "${REPOSITORY_URL}", os.Getenv("REPOSITORY_URL")) - err = ioutil.WriteFile(cliTestCmdJSON, []byte(output), 0755) - require.NoError(t, err) }) - m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), - packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(fmt.Errorf("oops")).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() - m.term.On("Writeln", []interface{}{color.CyanString("oops")}).Return(0, nil).Once() - m.term.On("IsTTY").Return(true).Once() - m.term.On("Confirm", "Binary command(s) found, would you like to download and install it?", true).Return(true, nil).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Downloading binary...", []interface{}(nil)).Return().Once() + packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(nil).Once() m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.term.On("OK").Return().Once() // list all packages m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() m.term.On("Writeln", mock.Anything).Return(0, nil) }, binaryResponseStatus: http.StatusNotFound, - withError: "Unable to install selected package", teardown: func(t *testing.T) { require.NoError(t, os.RemoveAll(cliTestCmdRepo)) }, }, "error on install from source, binary does not exist": { args: []string{"test-cmd"}, - init: func(t *testing.T, m *mocked) { + init: func(t *testing.T, m *mocked, h *httptest.Server) { m.term.On("Spinner").Return(m.term).Once() - m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("Start", "Attempting to fetch package configuration from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + + h = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(cliNoBinaryJSON) + output := strings.ReplaceAll(string(configJSON), "${REPOSITORY_URL}", "invalid url") + require.NoError(t, err) + _, err = w.Write([]byte(output)) + require.NoError(t, err) + + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Installing Binaries...", []interface{}(nil)).Return().Once() + m.term.On("Stop", terminal.SpinnerStatusWarn) + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-test-cmd"), "https://github.com/akamai/cli-test-cmd.git", false, m.term).Return(nil).Once(). Run(func(args mock.Arguments) { @@ -359,6 +428,10 @@ func TestCmdInstall(t *testing.T) { err = ioutil.WriteFile(cliTestCmdJSON, []byte(output), 0755) require.NoError(t, err) }) + m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-test-cmd.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.term.On("Spinner").Return(m.term).Once() m.term.On("OK").Return().Once() m.term.On("Spinner").Return(m.term).Once() @@ -368,9 +441,7 @@ func TestCmdInstall(t *testing.T) { packages.LanguageRequirements{Go: "1.14.0"}, []string{"app-1-cmd-1"}, []string{""}).Return(fmt.Errorf("oops")).Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Stop", terminal.SpinnerStatusWarn).Return().Once() - m.term.On("Writeln", []interface{}{color.CyanString("oops")}).Return(0, nil).Once() - m.term.On("Spinner").Return(m.term).Once() - m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + m.term.On("WriteError", "oops").Return(0, nil).Once() // list all packages m.term.On("Printf", mock.AnythingOfType("string"), mock.Anything).Return() @@ -393,9 +464,12 @@ func TestCmdInstall(t *testing.T) { assert.NoError(t, err) })) defer srv.Close() + require.NoError(t, os.Setenv("REPOSITORY_URL", srv.URL)) require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", filepath.Join(".", "testdata"))) m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.MockRepo{}, &packages.Mock{}, nil} + h := &httptest.Server{} + command := &cli.Command{ Name: "install", Action: cmdInstall(m.gitRepo, m.langManager), @@ -405,7 +479,7 @@ func TestCmdInstall(t *testing.T) { args = append(args, "install") args = append(args, test.args...) - test.init(t, m) + test.init(t, m, h) if test.teardown != nil { defer test.teardown(t) } diff --git a/pkg/commands/command_list_test.go b/pkg/commands/command_list_test.go index dea6c38..ca3b411 100644 --- a/pkg/commands/command_list_test.go +++ b/pkg/commands/command_list_test.go @@ -75,6 +75,10 @@ func TestCmdListWithRemote(t *testing.T) { m.term.On("Writeln", []interface{}{fmt.Sprintf(" [package: %s]", color.BlueString("SAMPLE"))}).Return(0, nil).Once() m.term.On("Printf", " test for single match\n", []interface{}(nil)).Return().Once() + m.term.On("Printf", bold.Sprint(" echo-uninstall"), []interface{}(nil)).Return().Once() + m.term.On("Writeln", []interface{}{fmt.Sprintf(" [package: %s]", color.BlueString("echo"))}).Return(0, nil).Once() + m.term.On("Printf", " test for single match\n", []interface{}(nil)).Return().Once() + m.term.On("Printf", "\nInstall using \"%s\".\n", []interface{}{color.BlueString("%s install [package]", tools.Self())}).Return().Once() }, packages: packagesForTest, diff --git a/pkg/commands/command_search.go b/pkg/commands/command_search.go index dbdd56a..9fec11b 100644 --- a/pkg/commands/command_search.go +++ b/pkg/commands/command_search.go @@ -16,7 +16,14 @@ package commands import ( "context" + "encoding/json" "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" "sort" "strings" "time" @@ -28,6 +35,10 @@ import ( "github.com/urfave/cli/v2" ) +var ( + githubURLTemplate = "https://raw.githubusercontent.com/akamai/%s/master/cli.json" +) + func cmdSearch(c *cli.Context) (e error) { pr := newPackageReader(embeddedPackages) return cmdSearchWithPackageReader(c, pr) @@ -134,21 +145,39 @@ func searchPackages(ctx context.Context, keywords []string, packageList *package term.Printf(color.YellowString("Results Found:")+" %d\n\n", len(resultPkgs)) + return printResult(resultHits, resultPkgs, results, term, bold) +} + +func printResult(resultHits []int, resultPkgs []string, results map[int]map[string]packageListItem, term terminal.Terminal, bold *color.Color) error { + var installedVersion, availableVersion string for _, hits := range resultHits { for _, pkgName := range resultPkgs { if _, ok := results[hits][pkgName]; ok { pkg := results[hits][pkgName] term.Printf(color.GreenString("Package: ")+"%s [%s]\n", pkg.Title, color.BlueString(pkg.Name)) - for _, cmd := range results[hits][pkgName].Commands { + for _, cmd := range pkg.Commands { var aliases string if len(cmd.Aliases) == 1 { aliases = fmt.Sprintf("(alias: %s)", cmd.Aliases[0]) } else if len(cmd.Aliases) > 1 { aliases = fmt.Sprintf("(aliases: %s)", strings.Join(cmd.Aliases, ", ")) } - term.Printf(bold.Sprintf(" Command:")+" %s %s\n", cmd.Name, aliases) - term.Printf(bold.Sprintf(" Version:")+" %s\n", cmd.Version) + + url := pkg.URL + var err error + availableVersion, err = getLatestVersion(url) + if err != nil { + return cli.Exit(color.RedString(err.Error()), 1) + } + term.Printf(bold.Sprintf(" Available Version:")+" %s\n", availableVersion) + installedVersion, err = getVersionFromSystem(pkg.Name) + if err != nil { + return cli.Exit(color.RedString(err.Error()), 1) + } + if installedVersion != "" { + term.Printf(bold.Sprintf(" Installed Version:")+" %s\n", installedVersion) + } term.Printf(bold.Sprintf(" Description:")+" %s\n\n", cmd.Description) } } @@ -156,8 +185,94 @@ func searchPackages(ctx context.Context, keywords []string, packageList *package } if len(resultHits) > 0 { - term.Printf("\nInstall using \"%s\".\n", color.BlueString("%s install [package]", tools.Self())) + if installedVersion == "" { + term.Printf("\nInstall using \"%s\".\n", color.BlueString("%s install [package]", tools.Self())) + } else if installedVersion != availableVersion { + term.Printf("\nUpdate using \"%s\".\n", color.BlueString("%s update [package]", tools.Self())) + } else { + term.Printf(color.BlueString("Package is already up-to-date on your system")) + } } - return nil } + +func getLatestVersion(s string) (string, error) { + + u, err := url.Parse(s) + if err != nil { + return "", fmt.Errorf("error parsing URL: %s", err.Error()) + } + + // extract the last string of the package URL + lastSegment := path.Base(u.Path) + + repoURL := fmt.Sprintf(githubURLTemplate, lastSegment) + resp, err := http.Get(repoURL) + if err != nil { + return "", fmt.Errorf("error fetching the URL: %s", err.Error()) + } + defer func() { + if err := resp.Body.Close(); err != nil { + fmt.Println("error closing the response body:", err) + } + }() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("error: status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading the response body: %w", err) + } + + var cli CLI + if err := json.Unmarshal(body, &cli); err != nil { + return "", fmt.Errorf("error parsing the JSON: %w", err) + } + + if len(cli.CommandList) > 0 { + return cli.CommandList[0].Version, nil + } + return "", fmt.Errorf("no latest version found") +} + +// CLI struct represents an individual command object in package-list.json +type CLI struct { + CommandList []CommandObject `json:"commands"` +} + +// CommandObject contains details for particular command +type CommandObject struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` +} + +func getVersionFromSystem(command string) (string, error) { + paths := filepath.SplitList(getPackageBinPaths()) + suffix := "cli-" + command + finalPath := "" + for _, path := range paths { + if strings.HasSuffix(path, suffix) { + finalPath = path + break + } + } + + if finalPath == "" { + return "", nil + } + body, err := os.ReadFile(filepath.Join(finalPath, "cli.json")) + if err != nil { + return "", fmt.Errorf("Error reading the file: %s", err.Error()) + + } + + var cli CLI + if err := json.Unmarshal(body, &cli); err != nil { + return "", fmt.Errorf("Error parsing the JSON: %s", err.Error()) + } + + return cli.CommandList[0].Version, nil +} diff --git a/pkg/commands/command_search_test.go b/pkg/commands/command_search_test.go index c1a6220..86845ef 100644 --- a/pkg/commands/command_search_test.go +++ b/pkg/commands/command_search_test.go @@ -2,13 +2,19 @@ package commands import ( "encoding/json" + "net/http" + "net/http/httptest" "os" + "path/filepath" "testing" - "github.com/akamai/cli/pkg/config" - "github.com/akamai/cli/pkg/terminal" + "github.com/akamai/cli/pkg/packages" "github.com/akamai/cli/pkg/tools" "github.com/fatih/color" + + "github.com/akamai/cli/pkg/config" + "github.com/akamai/cli/pkg/git" + "github.com/akamai/cli/pkg/terminal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" @@ -17,97 +23,210 @@ import ( func TestCmdSearch(t *testing.T) { tests := map[string]struct { args []string - init func(*terminal.Mock) + init func(*mocked) packages *packageList withError string }{ - "search and find single package - sample": { + "search and find single package - sample when package is not installed": { args: []string{"sample"}, - init: func(m *terminal.Mock) { + init: func(m *mocked) { bold := color.New(color.FgWhite, color.Bold) - m.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{1}) + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{1}) + + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"sample", color.BlueString("SAMPLE")}). + Return().Once() + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"sample", ""}). + Return().Once() + + h := mockedServer("sample", "2.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"2.0.0"}).Return().Once() + + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for single match"}). + Return().Once() + m.term.On("Printf", "\nInstall using \"%s\".\n", []interface{}{color.BlueString("%s install [package]", tools.Self())}). + Return().Once() + }, + packages: packagesForTest, + }, + "search and find single package - echo when installed version is less than available version": { + args: []string{"echo-uninstall"}, + init: func(m *mocked) { + bold := color.New(color.FgWhite, color.Bold) + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{1}) + + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"echo", color.BlueString("echo")}). + Return().Once() + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"echo-uninstall", ""}). + Return().Once() + + h := mockedServer("echo-uninstall", "2.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"2.0.0"}).Return().Once() + + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, "echo-uninstall").Return([]string{}, packages.ErrNoExeFound).Once() + m.langManager.On("GetPackageBinPaths").Return("/path/to/echo-uninstall").Once() + m.term.On("Printf", bold.Sprintf(" Installed Version:")+" %s\n", []interface{}{"1.0.0"}).Return().Once() - m.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"sample", color.BlueString("SAMPLE")}). + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for single match"}). Return().Once() - m.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"sample", ""}). + m.term.On("Printf", "\nUpdate using \"%s\".\n", []interface{}{color.BlueString("%s update [package]", tools.Self())}). Return().Once() - m.On("Printf", bold.Sprintf(" Version:")+" %s\n", []interface{}{"2.0.0"}). + }, + packages: packagesForTest, + }, + "search and find single package - echo when installed version is equal to available version": { + args: []string{"echo-uninstall"}, + init: func(m *mocked) { + bold := color.New(color.FgWhite, color.Bold) + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{1}) + + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"echo", color.BlueString("echo")}). Return().Once() - m.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for single match"}). + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"echo-uninstall", ""}). Return().Once() - m.On("Printf", "\nInstall using \"%s\".\n", []interface{}{color.BlueString("%s install [package]", tools.Self())}). + h := mockedServer("echo-uninstall", "1.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"1.0.0"}).Return().Once() + + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, "echo-uninstall").Return([]string{}, packages.ErrNoExeFound).Once() + m.langManager.On("GetPackageBinPaths").Return("/path/to/echo-uninstall").Once() + m.term.On("Printf", bold.Sprintf(" Installed Version:")+" %s\n", []interface{}{"1.0.0"}).Return().Once() + + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for single match"}). + Return().Once() + m.term.On("Printf", color.BlueString("Package is already up-to-date on your system"), []interface{}(nil)). Return().Once() }, packages: packagesForTest, }, "search and find multiple packages - cli": { args: []string{"cli"}, - init: func(m *terminal.Mock) { + init: func(m *mocked) { bold := color.New(color.FgWhite, color.Bold) - m.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{5}) + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{5}) - m.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"CLI no cmd match", color.BlueString("cli-no-cmd-match")}). + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"CLI no cmd match", color.BlueString("cli-no-cmd-match")}). Return().Once() - m.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"abc-2", color.BlueString("cli-2")}). + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"abc-2", color.BlueString("cli-2")}). Return().Once() - m.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"cli-2", "(aliases: abc, abc2)"}). + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"cli-2", "(aliases: abc, abc2)"}). Return().Once() - m.On("Printf", bold.Sprintf(" Version:")+" %s\n", []interface{}{"1.0.0"}). + + h := mockedServer("cli-2", "1.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"1.0.0"}). Return().Once() - m.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for match on name"}). + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for match on name"}). Return().Once() - m.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"cli-1", color.BlueString("abc-1")}). + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"cli-1", color.BlueString("abc-1")}). Return().Once() - m.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"ClI-1", ""}). + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"ClI-1", ""}). Return().Once() - m.On("Printf", bold.Sprintf(" Version:")+" %s\n", []interface{}{"1.0.0"}). + h = mockedServer("CLI-1", "1.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"1.0.0"}). Return().Once() - m.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for match on title"}). + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for match on title"}). Return().Once() - m.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"abc-5", color.BlueString("abc-5")}). + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"abc-5", color.BlueString("abc-5")}). Return().Once() - m.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"cli", ""}). + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"cli", ""}). Return().Once() - m.On("Printf", bold.Sprintf(" Version:")+" %s\n", []interface{}{"1.0.0"}). + h = mockedServer("cli", "1.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"1.0.0"}). Return().Once() - m.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for match on command name"}). + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"test for match on command name"}). Return().Once() - m.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"abc-3", color.BlueString("abc-3")}). + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"abc-3", color.BlueString("abc-3")}). Return().Once() - m.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"abc-3", ""}). + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"abc-3", ""}). Return().Once() - m.On("Printf", bold.Sprintf(" Version:")+" %s\n", []interface{}{"1.0.0"}). + + h = mockedServer("abc-3", "1.0.0", t) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Printf", bold.Sprintf(" Available Version:")+" %s\n", []interface{}{"1.0.0"}). Return().Once() - m.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"CLI - test for match on description"}). + m.term.On("Printf", bold.Sprintf(" Description:")+" %s\n\n", []interface{}{"CLI - test for match on description"}). Return().Once() - m.On("Printf", "\nInstall using \"%s\".\n", []interface{}{color.BlueString("%s install [package]", tools.Self())}). + m.term.On("Printf", "\nInstall using \"%s\".\n", []interface{}{color.BlueString("%s install [package]", tools.Self())}). Return().Once() }, packages: packagesForTest, }, "search with no results - terraform": { args: []string{"terraform"}, - init: func(m *terminal.Mock) { - m.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{0}) + init: func(m *mocked) { + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{0}) }, packages: packagesForTest, }, "no args passed": { args: []string{}, - init: func(m *terminal.Mock) {}, + init: func(m *mocked) {}, withError: "You must specify one or more keywords", }, + "search and find single package - 404": { + args: []string{"sample"}, + init: func(m *mocked) { + bold := color.New(color.FgWhite, color.Bold) + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{1}) + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"sample", color.BlueString("SAMPLE")}). + Return().Once() + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"sample", ""}). + Return().Once() + + h := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Not Found", http.StatusNotFound) + + })) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + }, + packages: packagesForTest, + withError: "error: status code 404", + }, + "search and find single package - when no latest version found": { + args: []string{"sample"}, + init: func(m *mocked) { + bold := color.New(color.FgWhite, color.Bold) + m.term.On("Printf", color.YellowString("Results Found:")+" %d\n\n", []interface{}{1}) + + m.term.On("Printf", color.GreenString("Package: ")+"%s [%s]\n", []interface{}{"sample", color.BlueString("SAMPLE")}). + Return().Once() + m.term.On("Printf", bold.Sprintf(" Command:")+" %s %s\n", []interface{}{"sample", ""}). + Return().Once() + + h := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mockResponse := CLI{ + CommandList: []CommandObject{}, + } + respBody, _ := json.Marshal(mockResponse) + w.WriteHeader(http.StatusOK) + var _, _ = w.Write(respBody) + })) + githubURLTemplate = h.URL + "/akamai/%s/master/cli.json" + }, + withError: "no latest version found", + packages: packagesForTest, + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { - m := &mocked{&terminal.Mock{}, &config.Mock{}, nil, nil, nil} + require.NoError(t, os.Setenv("AKAMAI_CLI_HOME", filepath.Join(".", "testdata"))) + m := &mocked{&terminal.Mock{}, &config.Mock{}, &git.MockRepo{}, &packages.Mock{}, nil} pr := &mockPackageReader{} pr.On("readPackage").Return(test.packages.copy(t), nil).Once() @@ -123,7 +242,7 @@ func TestCmdSearch(t *testing.T) { args = append(args, "search") args = append(args, test.args...) - test.init(m.term) + test.init(m) err := app.RunContext(ctx, args) m.cfg.AssertExpectations(t) @@ -167,6 +286,20 @@ var packagesForTest = &packageList{ Node: "7.0.0", }, }, + { + Title: "echo", + Name: "echo", + Commands: []command{ + { + Name: "echo-uninstall", + Version: "1.0.0", + Description: "test for single match", + }, + }, + Requirements: requirements{ + Go: "1.14.0", + }, + }, { Title: "abc-2", Name: "cli-2", @@ -255,3 +388,30 @@ var packagesForTest = &packageList{ }, }, } + +func mockedServer(name, version string, t *testing.T) *httptest.Server { + h := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mockResponse := CLI{ + CommandList: []CommandObject{ + { + Name: name, + Version: version, + }, + }, + } + respBody, err := json.Marshal(mockResponse) + if err != nil { + t.Errorf("Error marshalling the response: %v", err) + t.Fail() + return + } + w.WriteHeader(http.StatusOK) + _, err = w.Write(respBody) + if err != nil { + t.Errorf("Error writing the response: %v", err) + t.Fail() + } + + })) + return h +} diff --git a/pkg/commands/command_subcommand.go b/pkg/commands/command_subcommand.go index cadca1b..f278902 100644 --- a/pkg/commands/command_subcommand.go +++ b/pkg/commands/command_subcommand.go @@ -91,7 +91,7 @@ func cmdSubcommand(git git.Repository, langManager packages.LangManager) cli.Act return err } - if _, err = installPackage(c.Context, git, langManager, commandName, false); err != nil { + if _, err = installPackage(c.Context, git, langManager, commandName); err != nil { return err } } diff --git a/pkg/commands/command_uninstall.go b/pkg/commands/command_uninstall.go index abf4dd9..f56e26f 100644 --- a/pkg/commands/command_uninstall.go +++ b/pkg/commands/command_uninstall.go @@ -16,9 +16,11 @@ package commands import ( "context" + "errors" "fmt" "os" "path/filepath" + "strings" "time" "github.com/akamai/cli/pkg/log" @@ -26,6 +28,7 @@ import ( "github.com/akamai/cli/pkg/terminal" "github.com/akamai/cli/pkg/tools" "github.com/fatih/color" + "github.com/mitchellh/go-homedir" "github.com/urfave/cli/v2" ) @@ -56,8 +59,30 @@ func cmdUninstall(langManager packages.LangManager) cli.ActionFunc { func uninstallPackage(ctx context.Context, langManager packages.LangManager, cmd string, logger log.Logger) error { term := terminal.Get(ctx) + home, err := homedir.Dir() + if err != nil { + return fmt.Errorf("no home directory detected: %s", err) + } + home += string(filepath.Separator) exec, _, err := findExec(ctx, langManager, cmd) if err != nil { + if !errors.Is(err, packages.ErrNoExeFound) { + return fmt.Errorf("command \"%s\" not found. Try \"%s help\" : %s", cmd, tools.Self(), err) + } + // err = ErrNoExeFound - there is a directory but without any executables + paths := filepath.SplitList(getPackageBinPaths()) + for i, path := range paths { + + // trim home directory part of a path to exclude cases where command name could be a part of it + path = strings.TrimPrefix(path, home) + // if trimmed path (akamai-cli defined) contains name of command to uninstall, delete directory + if strings.Contains(path, cmd) { + if err = os.RemoveAll(paths[i]); err != nil { + return fmt.Errorf("could not remove directory %s: %s", paths[i], err) + } + return nil + } + } return fmt.Errorf("command \"%s\" not found. Try \"%s help\"", cmd, tools.Self()) } diff --git a/pkg/commands/command_uninstall_test.go b/pkg/commands/command_uninstall_test.go index 155f87d..f70800d 100644 --- a/pkg/commands/command_uninstall_test.go +++ b/pkg/commands/command_uninstall_test.go @@ -81,11 +81,22 @@ func TestCmdUninstall(t *testing.T) { }, withError: "unable to uninstall, was it installed using " + color.CyanString("\"akamai install\"") + "?", }, - "executable not found": { - args: []string{"invalid"}, + "uninstall command when executable not found": { + args: []string{"echo-uninstall"}, init: func(t *testing.T, m *mocked) { + mustCopyFile(t, cliEchoJSON, cliEchoUninstallRepo) + mustCopyFile(t, cliEchoBin, cliEchoUninstallBinDir) + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, "echo-uninstall").Return([]string{}, packages.ErrNoExeFound).Once() + m.langManager.On("GetPackageBinPaths").Return("/path/to/echo-uninstall").Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", `Attempting to uninstall "echo-uninstall" command...`, []interface{}(nil)).Return().Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() + + m.term.On("RemoveAll", "/path/to/echo-uninstall").Return(nil).Once() + }, - withError: fmt.Sprintf(`command "invalid" not found. Try "%s help"`, tools.Self()), }, } diff --git a/pkg/commands/command_update.go b/pkg/commands/command_update.go index 5c6c23c..b0e270e 100644 --- a/pkg/commands/command_update.go +++ b/pkg/commands/command_update.go @@ -18,7 +18,9 @@ import ( "context" "errors" "fmt" + "os" "path/filepath" + "reflect" "strings" "time" @@ -54,7 +56,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act for _, cmd := range getCommands(c) { for _, command := range cmd.Commands { if _, ok := builtinCmds[command.Name]; !ok { - if err := updatePackage(c.Context, gitRepo, langManager, logger, command.Name, c.Bool("force")); err != nil { + if err := updatePackage(c.Context, gitRepo, langManager, logger, command.Name); err != nil { return err } } @@ -65,7 +67,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act } for _, cmd := range c.Args().Slice() { - if err := updatePackage(c.Context, gitRepo, langManager, logger, cmd, c.Bool("force")); err != nil { + if err := updatePackage(c.Context, gitRepo, langManager, logger, cmd); err != nil { return err } } @@ -74,7 +76,7 @@ func cmdUpdate(gitRepo git.Repository, langManager packages.LangManager) cli.Act } } -func updatePackage(ctx context.Context, gitRepo git.Repository, langManager packages.LangManager, logger log.Logger, cmd string, forceBinary bool) error { +func updatePackage(ctx context.Context, gitRepo git.Repository, langManager packages.LangManager, logger log.Logger, cmd string) error { term := terminal.Get(ctx) exec, _, err := findExec(ctx, langManager, cmd) if err != nil { @@ -102,16 +104,88 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack err = gitRepo.Open(repoDir) if err != nil { + logger.Debug("Unable to open repo") - term.Spinner().Fail() - return cli.Exit(color.RedString("unable to update, there an issue with the package repo: %s", err.Error()), 1) + + cmdPackage, err := readPackage(repoDir) + if err != nil { + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + } + + packageVersions := map[string]string{} + for _, command := range cmdPackage.Commands { + packageVersions[command.Name] = command.Version + } + + repo := filepath.Base(repoDir) + url := fmt.Sprintf(githubRawURLTemplate, repo) + + remotePackage, err := readPackageFromGithub(url, repoDir) + if err != nil { + return cli.Exit(color.RedString("unable to update, there was an issue with fetching latest configuration file: %s", err.Error()), 1) + } + + remoteVersions := map[string]string{} + for _, command := range remotePackage.Commands { + remoteVersions[command.Name] = command.Version + } + + if reflect.DeepEqual(packageVersions, remoteVersions) { + term.Spinner().WarnOK() + debugMessage := fmt.Sprintf("command \"%s\" already up-to-date", cmd) + logger.Warn(debugMessage) + if _, err := term.Writeln(color.CyanString(debugMessage)); err != nil { + return err + } + return nil + } + + tempDir := filepath.Dir(repoDir) + "/.tmp_" + filepath.Base(repoDir) + logger.Debugf("Moving package to temporary dir: %s", tempDir) + if err = os.Rename(repoDir, tempDir); err != nil { + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + } + + _, err = installPackage(ctx, gitRepo, langManager, tools.Githubize(cmd)) + if err != nil { + term.Spinner().Fail() + if err := os.Rename(tempDir, repoDir); err != nil { + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + } + return cli.Exit(color.RedString("unable to update: %s", err.Error()), 1) + } + + if err := os.RemoveAll(tempDir); err != nil { + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) + } + + logger.Debug("Repo updated successfully") + term.Spinner().OK() + return nil + + } + + err = updateRepo(ctx, gitRepo, logger, term, cmd) + if err != nil { + return err } + if ok, _ := installPackageDependencies(ctx, langManager, repoDir, logger); !ok { + logger.Trace("Error updating dependencies") + return cli.Exit("Unable to update command", 1) + } + logger.Debug("Repo updated successfully") + term.Spinner().OK() + + return nil +} + +func updateRepo(ctx context.Context, gitRepo git.Repository, logger log.Logger, term terminal.Terminal, cmd string) error { w, err := gitRepo.Worktree() if err != nil { logger.Debug("Unable to open repo") term.Spinner().Fail() - return cli.Exit(color.RedString("unable to update, there an issue with the package repo: %s", err.Error()), 1) + return cli.Exit(color.RedString("unable to update, there was an issue with the package repo: %s", err.Error()), 1) } if err := gitRepo.Reset(&gogit.ResetOptions{Mode: gogit.HardReset}); err != nil { @@ -167,14 +241,5 @@ func updatePackage(ctx context.Context, gitRepo git.Repository, langManager pack return err } } - - logger.Debug("Repo updated successfully") - term.Spinner().OK() - - if ok, _ := installPackageDependencies(ctx, langManager, repoDir, forceBinary, logger); !ok { - logger.Trace("Error updating dependencies") - return cli.Exit("Unable to update command", 1) - } - return nil } diff --git a/pkg/commands/command_update_test.go b/pkg/commands/command_update_test.go index cf38e55..d3d7c84 100644 --- a/pkg/commands/command_update_test.go +++ b/pkg/commands/command_update_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "github.com/akamai/cli/pkg/config" @@ -26,7 +27,7 @@ import ( func TestCmdUpdate(t *testing.T) { cliEchoRepo := filepath.Join("testdata", ".akamai-cli", "src", "cli-echo") cliEchoBin := filepath.Join("testdata", ".akamai-cli", "src", "cli-echo", "bin", "akamai-echo") - + tempTestDir := filepath.Join(".", "testdata", "temp") tests := map[string]struct { args []string init func(*testing.T, *mocked) @@ -177,6 +178,7 @@ func TestCmdUpdate(t *testing.T) { m.langManager.On("Install", cliEchoRepo, packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(fmt.Errorf("oops")).Once() + m.term.On("WriteError", "oops") m.term.On("OK").Return().Once() }, @@ -279,22 +281,133 @@ func TestCmdUpdate(t *testing.T) { m.term.On("Spinner").Return(m.term).Once() m.term.On("Fail").Return().Once() }, - withError: "unable to update, there an issue with the package repo: oops", + withError: "unable to update, there was an issue with the package repo: oops", + }, + "error opening repository, up to date with remote": { + args: []string{"echo"}, + init: func(t *testing.T, m *mocked) { + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() + + h := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configJSON, err := os.ReadFile(filepath.Join(cliEchoRepo, "cli.json")) + require.NoError(t, err) + _, err = w.Write(configJSON) + require.NoError(t, err) + })) + + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", `Attempting to update "%s" command...`, []interface{}{"echo"}).Return().Once() + + m.gitRepo.On("Open", cliEchoRepo).Return(fmt.Errorf("oops")).Once() + + m.term.On("Writeln", []interface{}{color.CyanString("command \"echo\" already up-to-date")}).Return(0, nil).Once() + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("WarnOK").Return().Once() + }, + }, + "error opening repository, update from remote, success": { + args: []string{"echo"}, + init: func(t *testing.T, m *mocked) { + + mustCopyDirectory(t, cliEchoRepo, tempTestDir) + + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() + configJSON, err := os.ReadFile(filepath.Join(cliEchoRepo, "cli.json")) + require.NoError(t, err) + h := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + output := strings.ReplaceAll(string(configJSON), "1.0.0", "9.9.9") + _, err = w.Write([]byte(output)) + require.NoError(t, err) + })) + + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", `Attempting to update "%s" command...`, []interface{}{"echo"}).Return().Once() + + m.gitRepo.On("Open", cliEchoRepo).Return(fmt.Errorf("oops")).Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", `Attempting to fetch package configuration from %s...`, []interface{}{"https://github.com/akamai/cli-echo.git"}).Return().Once() + m.term.On("OK").Return().Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-echo.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-echo"), + "https://github.com/akamai/cli-echo.git", false, m.term).Return(nil).Once(). + Run(func(args mock.Arguments) { + mustCopyFile(t, filepath.Join(tempTestDir, "cli.json"), cliEchoRepo) + }) + m.term.On("OK").Return().Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() + m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-echo"), + packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(nil).Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("OK").Return().Once() + + }, + teardown: func(t *testing.T) { + require.NoError(t, os.RemoveAll(cliEchoRepo)) + require.NoError(t, os.Rename(tempTestDir, cliEchoRepo)) + + }, }, - "error opening repository": { + "error opening repository, update from remote, fail": { args: []string{"echo"}, init: func(t *testing.T, m *mocked) { + + mustCopyDirectory(t, cliEchoRepo, tempTestDir) + m.langManager.On("FindExec", packages.LanguageRequirements{Go: "1.14.0"}, cliEchoBin).Return([]string{cliEchoBin}, nil).Once() + configJSON, err := os.ReadFile(filepath.Join(cliEchoRepo, "cli.json")) + require.NoError(t, err) + h := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + output := strings.ReplaceAll(string(configJSON), "1.0.0", "9.9.9") + _, err = w.Write([]byte(output)) + require.NoError(t, err) + })) + githubRawURLTemplate = h.URL + "/akamai/%s/master/cli.json" m.term.On("Spinner").Return(m.term).Once() m.term.On("Start", `Attempting to update "%s" command...`, []interface{}{"echo"}).Return().Once() m.gitRepo.On("Open", cliEchoRepo).Return(fmt.Errorf("oops")).Once() + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", `Attempting to fetch package configuration from %s...`, []interface{}{"https://github.com/akamai/cli-echo.git"}).Return().Once() + m.term.On("OK").Return().Once() + + m.term.On("Spinner").Return(m.term).Once() + m.term.On("Start", "Attempting to fetch command from %s...", []interface{}{"https://github.com/akamai/cli-echo.git"}).Return().Once() + m.term.On("OK").Return().Once() + m.term.On("Stop", terminal.SpinnerStatusFail).Return().Once() + + m.gitRepo.On("Clone", filepath.Join("testdata", ".akamai-cli", "src", "cli-echo"), + "https://github.com/akamai/cli-echo.git", false, m.term).Return(nil).Once(). + Run(func(args mock.Arguments) { + mustCopyFile(t, filepath.Join(tempTestDir, "cli.json"), cliEchoRepo) + }) + m.term.On("OK").Return().Once() + m.term.On("Spinner").Return(m.term).Once() + + m.langManager.On("Install", filepath.Join("testdata", ".akamai-cli", "src", "cli-echo"), + packages.LanguageRequirements{Go: "1.14.0"}, []string{"echo"}, []string{""}).Return(fmt.Errorf("oops")).Once() + m.term.On("Start", "Installing...", []interface{}(nil)).Return().Once() m.term.On("Spinner").Return(m.term).Once() m.term.On("Fail").Return().Once() + m.term.On("WriteError", "oops") + + }, + teardown: func(t *testing.T) { + require.NoError(t, os.RemoveAll(cliEchoRepo)) + require.NoError(t, os.Rename(tempTestDir, cliEchoRepo)) }, - withError: "unable to update, there an issue with the package repo: oops", + withError: "unable to update: Unable to install selected package", }, "error finding executable": { args: []string{"not-found"}, @@ -339,7 +452,9 @@ func TestCmdUpdate(t *testing.T) { assert.Contains(t, err.Error(), test.withError) return } + require.NoError(t, err) }) } + } diff --git a/pkg/commands/helpers_test.go b/pkg/commands/helpers_test.go index 683647d..ab124ce 100644 --- a/pkg/commands/helpers_test.go +++ b/pkg/commands/helpers_test.go @@ -136,3 +136,55 @@ func copyFile(src, dst string) error { func mustCopyFile(t *testing.T, src, dst string) { require.NoError(t, copyFile(src, dst)) } + +func mustCopyDirectory(t *testing.T, src, dst string) { + require.NoError(t, copyDirectory(src, dst)) +} + +func copyDirectory(src, dst string) error { + entries, err := os.ReadDir(src) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(src, entry.Name()) + destPath := filepath.Join(dst, entry.Name()) + + fileInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + + if fileInfo.Mode().IsDir() { + perm := fileInfo.Mode().Perm() + if err := createIfNotExists(destPath, perm); err != nil { + return err + } + if err := copyDirectory(sourcePath, destPath); err != nil { + return err + } + } else { + if err := copyFile(sourcePath, dst); err != nil { + return err + } + } + + } + return nil +} + +func exists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} + +func createIfNotExists(dir string, perm os.FileMode) error { + if exists(dir) { + return nil + } + + err := os.MkdirAll(dir, perm) + return err +} diff --git a/pkg/commands/package_list/package-list.json b/pkg/commands/package_list/package-list.json index 200553c..74f0305 100644 --- a/pkg/commands/package_list/package-list.json +++ b/pkg/commands/package_list/package-list.json @@ -4,36 +4,31 @@ { "title": "Adaptive Acceleration", "name": "adaptive-acceleration", - "version": "0.1", "url": "https://github.com/akamai/cli-adaptive-acceleration", "issues": "https://github.com/akamai/cli-adaptive-acceleration/issues", - "commands": [{"name":"adaptive-acceleration","aliases":["a2"],"version":"0.1","description":"Reset A2 Push and Preconnect policy"}], + "commands": [{"name":"adaptive-acceleration","aliases":["a2"],"description":"Reset A2 Push and Preconnect policy"}], "requirements": {"python":"3.0.0"} }, { "title": "API Gateway", - "name": "akamai/api-gateway", - "version": "0.1.0", + "name": "api-gateway", "url": "https://github.com/akamai/cli-api-gateway", "issues": "https://github.com/akamai/cli-api-gateway/issues", "commands": [ { "name": "api-gateway", - "version": "0.1.0", "description": "Manage API definitions and endpoints", "auto-complete": true, "bin": "https://github.com/akamai/cli-api-gateway/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}" }, { "name": "api-keys", - "version": "0.1.0", "description": "Manage API keys", "auto-complete": true, "bin": "https://github.com/akamai/cli-api-gateway/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}" }, { "name": "api-security", - "version": "0.1.0", "description": "Manage API protections", "auto-complete": true, "bin": "https://github.com/akamai/cli-api-gateway/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}" @@ -46,34 +41,22 @@ { "title": "Application Security", "name": "appsec", - "version": "2.7.0", "url": "https://github.com/akamai/cli-appsec", "issues": "https://github.com/akamai/cli-appsec/issues", - "commands": [{"name":"appsec","version":"2.7.0","description":"Akamai Security tools for protecting websites."}], - "requirements": {"node":"7.0.0"} - }, - { - "title": "Authentication", - "name": "auth", - "version": "0.0.3", - "url": "https://github.com/akamai/cli-auth", - "issues": "https://github.com/akamai/cli-auth/issues", - "commands": [{"name":"auth","version":"0.0.3","description":"Interface for Akamai Edgegrid Authentication","bin":"https://github.com/akamai/cli-auth/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}"}], - "requirements": {"node":"7.0.0"} + "commands": [{"name":"appsec","description":"Akamai Security tools for protecting websites."}], + "requirements": {"node":"10.0.0"} }, { "title": "Client Access Control (CAC)", "name": "cac", - "version": "v1.0.8", "url": "https://github.com/akamai/cli-cac", "issues": "https://github.com/akamai/cli-cac/issues", - "commands": [{"name":"cac","aliases":["cac"],"version":"v1.0.8","description":"An Akamai CLI package for Client Access Control"}], + "commands": [{"name":"cac","aliases":["cac"],"description":"An Akamai CLI package for Client Access Control"}], "requirements": {"python":"3.0.0"} }, { "title": "Certificate Provisioning Service (CPS)", "name": "cps", - "version": "v2.0.0", "url": "https://github.com/akamai/cli-cps", "issues": "https://github.com/akamai/cli-cps/issues", "commands": [ @@ -82,7 +65,6 @@ "aliases": [ "certs" ], - "version": "2.0.0", "description": "Access Certificate Provisioning System (CPS) Information" } ], @@ -93,61 +75,62 @@ { "title": "Cloudlets", "name": "cloudlets", - "version": "v1.0.1", "url": "https://github.com/akamai/cli-cloudlets", "issues": "https://github.com/akamai/cli-cloudlets/issues", - "commands": [{"name":"cloudlets","aliases":["cloudlets"],"version":"1.0.1","description":"Manage Akamai Cloudlets"}], - "requirements": {"python":"3.0.0"} + "commands": [{"name":"cloudlets","aliases":["cloudlets"],"description":"Manage Akamai Cloudlets"}], + "requirements": {"python":"3.6.0"} }, { "title": "Diagnostics", "name": "diagnostics", - "version": "v1.1.0", "url": "https://github.com/akamai/cli-diagnostics", "issues": "https://github.com/akamai/cli-diagnostics/issues", - "commands": [{"name":"diagnostics","aliases":["diag", "edge-diagnostics"],"version":"v1.1.0","description":"Edge Diagnostics enables you to identify, analyze, and troubleshoot common content delivery network issues that your users may encounter.","bin": "https://github.com/akamai/cli-diagnostics/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}"}], + "commands": [ + { + "name":"diagnostics", + "aliases":["diag", "edge-diagnostics"], + "description":"Edge Diagnostics enables you to identify, analyze, and troubleshoot common content delivery network issues that your users may encounter.", + "bin": "https://github.com/akamai/cli-diagnostics/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}", + "auto-complete": true + } + ], "requirements": {"go":"1.17.1"} }, { "title": "EdgeWorkers", "name": "edgeworkers", - "version": "1.7.3", "url": "https://github.com/akamai/cli-edgeworkers", "issues": "https://github.com/akamai/cli-edgeworkers/issues", "commands": [ - {"name":"edgeworkers","aliases":["ew", "edgeworkers"],"version":"1.7.3","description":"Manage Akamai EdgeWorkers code bundles."}, - {"name":"edgekv","aliases":["ekv", "edgekv"],"version":"1.7.3","description":"Manage Akamai EdgeKV database."} + {"name":"edgeworkers","aliases":["ew", "edgeworkers"],"description":"Manage Akamai EdgeWorkers code bundles."}, + {"name":"edgekv","aliases":["ekv", "edgekv"],"description":"Manage Akamai EdgeKV database."} ], - "requirements": {"node":"7.0.0"} + "requirements": {"node":"14.0.0"} }, { "title": "Akamai Sandbox", "name": "sandbox", - "version": "v1.7.1", "url": "https://github.com/akamai/cli-sandbox", "issues": "https://github.com/akamai/cli-sandbox/issues", - "commands": [{"name":"sandbox","version":"1.7.1","description":"Manage Akamai Sandbox environments.","bin": "https://github.com/akamai/cli-sandbox/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}"}], + "commands": [{"name":"sandbox","description":"Manage Akamai Sandbox environments.","bin": "https://github.com/akamai/cli-sandbox/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}"}], "requirements": {"node":"14.21.3"} }, { "title": "Edge DNS", "name": "dns", - "version": "0.5.0", "url": "https://github.com/akamai/cli-dns", "issues": "https://github.com/akamai/cli-dns/issues", - "commands": [{"name":"dns","version":"0.5.0","description":"Manage DNS zones with Edge DNS","bin":"https://github.com/akamai/cli-dns/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true}], + "commands": [{"name":"dns","description":"Manage DNS zones with Edge DNS","bin":"https://github.com/akamai/cli-dns/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true}], "requirements": {"go":"1.18.0"} }, { "title": "Enterprise Application Access", "name": "eaa", - "version": "0.5.7", "url": "https://github.com/akamai/cli-eaa", "issues": "https://github.com/akamai/cli-eaa/issues", "commands": [ { "name": "eaa", - "version": "0.5.7", "description": "Akamai CLI for Enterprise Application Access (EAA)" } ], @@ -158,40 +141,36 @@ { "title": "Firewall and Site Shield", "name": "firewall", - "version": "v0.2.1", "url": "https://github.com/akamai/cli-firewall", "issues": "https://github.com/akamai/cli-firewall/issues", "commands": [ - {"name":"firewall","aliases":["fw"],"version":"0.2.3","description":"Access Akamai Firewall Rules Services, Subscriptions, and CIDRs"}, - {"name":"site-shield","aliases":["ss"],"version":"0.2.3","description":"Access details of Site-Shield Maps, CIDRs and acknowledgement"} + {"name":"firewall","aliases":["fw"],"description":"Access Akamai Firewall Rules Services, Subscriptions, and CIDRs"}, + {"name":"site-shield","aliases":["ss"],"description":"Access details of Site-Shield Maps, CIDRs and acknowledgement"} ], "requirements": {"python":"3.0.0"} }, { "title": "Global Traffic Management", "name": "gtm", - "version": "0.5.0", "url": "https://github.com/akamai/cli-gtm", "issues": "https://github.com/akamai/cli-gtm/issues", - "commands": [{"name":"gtm","version":"0.5.0","description":"Manage GTM Domains","bin":"https://github.com/akamai/cli-gtm/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true}], + "commands": [{"name":"gtm","description":"Manage GTM Domains","bin":"https://github.com/akamai/cli-gtm/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true}], "requirements": {"go":"1.18.0"} }, { "title": "Image Manager", "name": "image-manager", - "version": "0.1.9", "url": "https://github.com/akamai/cli-image-manager", "issues": "https://github.com/akamai/cli-image-manager/issues", "commands": [ - {"name":"image-manager","aliases":["im"],"version":"0.1.9","description":"An Akamai CLI package for Image Manager"}, - {"name":"video-manager","aliases":["im"],"version":"0.1.9","description":"An Akamai CLI package for Video Manager"} + {"name":"image-manager","aliases":["im"],"description":"An Akamai CLI package for Image Manager"}, + {"name":"video-manager","aliases":["vm"],"description":"An Akamai CLI package for Video Manager"} ], "requirements": {"python":"3.0.0"} }, { "title": "Jsonnet", "name": "jsonnet", - "version": "0.8.0", "url": "https://github.com/akamai/cli-jsonnet", "issues": "https://github.com/akamai/cli-jsonnet/issues", "commands": [ @@ -200,7 +179,6 @@ "aliases": [ "jsonnet" ], - "version": "0.8.0", "description": "Utilities for managing Akamai as jsonnet" } ], @@ -211,70 +189,54 @@ { "title": "NetStorage", "name": "netstorage", - "version": "1.0.1", "url": "https://github.com/akamai/cli-netstorage", "issues": "https://github.com/akamai/cli-netstorage/issues", - "commands": [{"name":"netstorage","version":"1.0.1","description":"Interface for Akamai NetStorage","bin":"https://github.com/akamai/cli-netstorage/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}"}], + "commands": [{"name":"netstorage","description":"Interface for Akamai NetStorage","bin":"https://github.com/akamai/cli-netstorage/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}"}], "requirements": {"node":"7.0.0"} }, { "title": "Onboard", "name": "onboard", - "version": "2.2.0", "url": "https://github.com/akamai/cli-onboard", "issues": "https://github.com/akamai/cli-onboard/issues", - "commands": [{"name":"onboard","aliases":["onboard"],"version":"2.2.0","description":"Onboard Akamai delivery and WAF configuration"}], + "commands": [{"name":"onboard","aliases":["onboard"],"description":"Onboard Akamai delivery and WAF configuration"}], "requirements": {"python":"3.6.0"} }, - { - "title": "Property Manager 1.0", - "name": "property", - "version": "1.1.6", - "url": "https://github.com/akamai/cli-property", - "issues": "https://github.com/akamai/cli-property/issues", - "commands": [{"name":"property","version":"1.1.6","description":"Manage configurations for Akamai properties","bin":"https://github.com/akamai/cli-property/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}"}], - "requirements": {"node":"7.0.0"} - }, { "title": "Property Manager", "name": "property-manager", - "version": "0.7.8-RELEASE", "url": "https://github.com/akamai/cli-property-manager", "issues": "https://github.com/akamai/cli-property-manager/issues", "commands": [ - {"name":"snippets","aliases":["pm","property-manager"],"version":"0.7.8-RELEASE","description":"Property Manager CLI for DevOps"}, - {"name":"pipeline","aliases":["pl","pipeline","pd","proddeploy"],"version":"0.7.8-RELEASE","description":"Akamai Pipeline for DevOps"} + {"name":"property-manager","aliases":["pm","snippets"],"description":"Property Manager CLI for DevOps"}, + {"name":"pipeline","aliases":["pl","pd","proddeploy"],"description":"Akamai Pipeline for DevOps"} ], "requirements": {"node":"8.9.1"} }, { "title": "Purge", "name": "purge", - "version": "1.1.0", "url": "https://github.com/akamai/cli-purge", "issues": "https://github.com/akamai/cli-purge/issues", - "commands": [{"name":"purge","version":"1.1.0","description":"Purge content from the Edge","bin":"https://github.com/akamai/cli-purge/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true}], + "commands": [{"name":"purge","description":"Purge content from the Edge","bin":"https://github.com/akamai/cli-purge/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true}], "requirements": {"go":"1.18.0"} }, { "title": "Terraform Client Configuration", "name": "cli terraform", - "version": "1.5.0", "url": "https://github.com/akamai/cli-terraform", "issues": "https://github.com/akamai/cli-terraform/issues", - "commands": [{"name":"terraform","version":"1.5.0","description":"Administer and Manage Akamai Terraform configurations","bin":"https://github.com/akamai/cli-terraform/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true,"ldflags": "-X 'github.com/akamai/cli-terraform/cli.Version=%s'"}], - "requirements": {"go":"1.18.0"} + "commands": [{"name":"terraform","description":"Administer and Manage Akamai Terraform configurations","bin":"https://github.com/akamai/cli-terraform/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}","auto-complete":true,"ldflags": "-X 'github.com/akamai/cli-terraform/cli.Version=%s'"}], + "requirements": {"go":"1.21.0"} }, { "title": "Test Center", "name": "test-center", - "version": "0.2.0", "url": "https://github.com/akamai/cli-test-center", "issues": "https://github.com/akamai/cli-test-center/issues", "commands": [ { "name": "test-center", - "version": "0.2.0", "description": "Test Center is a testing tool that checks the effect of configuration changes on your web property. Use this tool as part of your testing protocol to increase your confidence in the safety and accuracy of your configuration changes.", "bin": "https://github.com/akamai/cli-test-center/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}{{.Arch}}{{.BinSuffix}}" } @@ -284,10 +246,33 @@ { "title": "Visitor Prioritization", "name": "visitor-prioritization", - "version": "v0.3.0", "url": "https://github.com/akamai/cli-visitor-prioritization", "issues": "https://github.com/akamai/cli-visitor-prioritization/issues", - "commands": [{"name":"visitor-prioritization","aliases":["vp"],"version":"0.3.0","description":"Access and control Visitor Prioritization cloudlet"}], + "commands": [{"name":"visitor-prioritization","aliases":["vp"],"description":"Access and control Visitor Prioritization cloudlet"}], + "requirements": {"python":"3.0.0"} + }, + { + "title": "Enterprise Threat Protector", + "name": "etp", + "url": "https://github.com/akamai/cli-etp", + "issues": "https://github.com/akamai/cli-etp/issues", + "commands": [{"name":"etp","description":"Akamai CLI for Secure Internet Access Enterprise (f.k.a. Enterprise Threat Protector)"}], + "requirements": {"python":"3.6.0"} + }, + { + "title": "Akamai MFA", + "name": "mfa", + "url": "https://github.com/akamai/cli-mfa", + "issues": "https://github.com/akamai/cli-mfa/issues", + "commands": [{"name":"mfa","description":"Akamai CLI for Akamai MFA"}], + "requirements": {"python":"3.7.0"} + }, + { + "title": "mPulse", + "name": "mpulse", + "url": "https://github.com/akamai/cli-mpulse", + "issues": "https://github.com/akamai/cli-mpulse/issues", + "commands": [{"name":"mpulse", "aliases": ["mp"],"description":"Get mPulse reports for your applications"}], "requirements": {"python":"3.0.0"} } ] diff --git a/pkg/commands/subcommands.go b/pkg/commands/subcommands.go index 5c269fb..08bdac8 100644 --- a/pkg/commands/subcommands.go +++ b/pkg/commands/subcommands.go @@ -39,6 +39,7 @@ type subcommands struct { Requirements packages.LanguageRequirements `json:"requirements"` Action cli.ActionFunc `json:"-"` Pkg string `json:"pkg"` + raw []byte } func readPackage(dir string) (subcommands, error) { @@ -69,6 +70,39 @@ func readPackage(dir string) (subcommands, error) { return packageData, nil } +func readPackageFromGithub(url, dir string) (subcommands, error) { + response, err := http.Get(url) + if err != nil { + return subcommands{}, err + } + if response.StatusCode == http.StatusOK { + cliJSON, err := io.ReadAll(response.Body) + if err != nil { + return subcommands{}, err + } + + var packageData subcommands + + err = json.Unmarshal(cliJSON, &packageData) + if err != nil { + return subcommands{}, err + } + + packageData.raw = cliJSON + + for key := range packageData.Commands { + packageData.Commands[key].Name = strings.ToLower(packageData.Commands[key].Name) + } + + packageData.Pkg = filepath.Base(strings.Replace(dir, "cli-", "", 1)) + + return packageData, nil + + } + + return subcommands{}, fmt.Errorf("Invalid response status while fetching cli.json: %d", response.StatusCode) +} + func getPackagePaths() []string { akamaiCliPath, err := tools.GetAkamaiCliSrcPath() if err == nil && akamaiCliPath != "" { @@ -81,6 +115,16 @@ func getPackagePaths() []string { return []string{} } +func isBinary(cmdPackage subcommands) bool { + for _, cmd := range cmdPackage.Commands { + if len(cmd.Bin) == 0 { + return false + } + } + return true + +} + func findPackageDir(dir string) string { if stat, err := os.Stat(dir); err == nil && stat != nil && !stat.IsDir() { dir = filepath.Dir(dir) @@ -88,7 +132,7 @@ func findPackageDir(dir string) string { if _, err := os.Stat(filepath.Join(dir, "cli.json")); err != nil { if os.IsNotExist(err) { - if filepath.Dir(dir) == "" || filepath.Dir(dir) == "." { + if filepath.Dir(dir) == "" || filepath.Dir(dir) == "." || filepath.Dir(dir) == "/" { return "" } diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/cli.json b/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/cli.json index c1e4d05..825d15b 100644 --- a/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/cli.json +++ b/pkg/commands/testdata/.akamai-cli/src/cli-echo-python/cli.json @@ -6,7 +6,8 @@ { "name": "echo-python", "aliases": ["e"], - "description": "echo command" + "description": "echo command", + "version": "1.0.0" } ] } diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-echo/cli.json b/pkg/commands/testdata/.akamai-cli/src/cli-echo/cli.json index 87687de..4ab094d 100644 --- a/pkg/commands/testdata/.akamai-cli/src/cli-echo/cli.json +++ b/pkg/commands/testdata/.akamai-cli/src/cli-echo/cli.json @@ -6,7 +6,8 @@ { "name": "echo", "aliases": ["e"], - "description": "echo command" + "description": "echo command", + "version": "1.0.0" } ] } diff --git a/pkg/commands/testdata/.akamai-cli/src/cli-installed/cli.json b/pkg/commands/testdata/.akamai-cli/src/cli-installed/cli.json index 284a70a..121fe78 100644 --- a/pkg/commands/testdata/.akamai-cli/src/cli-installed/cli.json +++ b/pkg/commands/testdata/.akamai-cli/src/cli-installed/cli.json @@ -6,7 +6,8 @@ { "name": "installed", "aliases": ["ac2"], - "description": "Test command" + "description": "Test command", + "version": "1.0.0" } ] } diff --git a/pkg/version/version.go b/pkg/version/version.go index 4d47580..b95e952 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -4,7 +4,7 @@ import "github.com/Masterminds/semver" const ( // Version Application Version - Version = "1.5.6" + Version = "1.6.0" // Equals p1==p2 in version.Compare(p1, p2) Equals = 0 // Error failure parsing one of the parameters in version.Compare(p1, p2)