diff --git a/.gitignore b/.gitignore index 60bed44fc..4dbf5787f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ storybook-static .tmp **/.vscode/settings.json **/.vscode/tasks.json +*.DS_Store .eslintcache __debug_bin diff --git a/api/go.mod b/api/go.mod index 108a5207d..bfca6833c 100644 --- a/api/go.mod +++ b/api/go.mod @@ -33,11 +33,10 @@ require ( github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19 github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 - github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 + github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 - github.com/swaggo/swag v1.7.3 github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d diff --git a/api/go.sum b/api/go.sum index a700e5a31..541f18754 100644 --- a/api/go.sum +++ b/api/go.sum @@ -41,8 +41,6 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -64,9 +62,7 @@ github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5 github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= @@ -320,19 +316,11 @@ github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= -github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -456,8 +444,6 @@ github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs= github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= @@ -503,8 +489,6 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -608,6 +592,8 @@ github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cE github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= +github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc h1:vxVN9srGND+iA9oBmyFgtbtOvnmOCLmxw20ncYCJ5HA= +github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc/go.mod h1:nyQA6IahOruIvENCcBk54aaUvV2WHFdXkvBjIutg+SY= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -648,7 +634,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -691,8 +676,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/swaggo/swag v1.7.3 h1:ucB7irEdRrhjmW+Z1Ss4GjO68oPKQFjSgOR8BCAvcbU= -github.com/swaggo/swag v1.7.3/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -706,7 +689,6 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= @@ -790,7 +772,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -827,7 +808,6 @@ golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= @@ -908,7 +888,6 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -926,7 +905,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -973,8 +951,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1078,7 +1054,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 31cd0a5e2..44687147c 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -32,21 +32,23 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz authorizationService: authorizationService, } - kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter() - + kubeRouter := h.PathPrefix("/kubernetes").Subrouter() kubeRouter.Use(bouncer.AuthenticatedAccess) - kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id")) - kubeRouter.Use(kubeOnlyMiddleware) - kubeRouter.PathPrefix("/config").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet) - kubeRouter.PathPrefix("/nodes_limits").Handler( + + // endpoints + endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter() + endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id")) + endpointRouter.Use(kubeOnlyMiddleware) + + endpointRouter.PathPrefix("/nodes_limits").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).Methods(http.MethodGet) // namespaces // in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?) // to keep it simple, we've decided to leave it like this. - namespaceRouter := kubeRouter.PathPrefix("/namespaces/{namespace}").Subrouter() + namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter() namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut) return h diff --git a/api/http/handler/kubernetes/kubernetes_config.go b/api/http/handler/kubernetes/kubernetes_config.go index a77e8afd5..0c9f86b52 100644 --- a/api/http/handler/kubernetes/kubernetes_config.go +++ b/api/http/handler/kubernetes/kubernetes_config.go @@ -5,12 +5,14 @@ import ( "fmt" "net/http" + clientV1 "k8s.io/client-go/tools/clientcmd/api/v1" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" - bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/endpointutils" kcli "github.com/portainer/portainer/api/kubernetes/cli" ) @@ -22,88 +24,184 @@ import ( // @security jwt // @accept json // @produce json -// @param id path int true "Environment(Endpoint) identifier" +// @param ids query []int false "will include only these environments(endpoints)" +// @param excludeIds query []int false "will exclude these environments(endpoints)" // @success 200 "Success" // @failure 400 "Invalid request" // @failure 401 "Unauthorized" // @failure 403 "Permission denied" // @failure 404 "Environment(Endpoint) or ServiceAccount not found" // @failure 500 "Server error" -// @router /kubernetes/{id}/config [get] +// @router /kubernetes/config [get] func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err} - } - - endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err} - } - tokenData, err := security.RetrieveTokenData(r) if err != nil { return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} } - bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err} } - cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} + endpoints, handlerErr := handler.filterUserKubeEndpoints(r) + if handlerErr != nil { + return handlerErr + } + if len(endpoints) == 0 { + return &httperror.HandlerError{http.StatusBadRequest, "empty endpoints list", errors.New("empty endpoints list")} } - apiServerURL := getProxyUrl(r, endpointID) - - config, err := cli.GetKubeConfig(r.Context(), apiServerURL, bearerToken, tokenData) - if err != nil { - return &httperror.HandlerError{http.StatusNotFound, "Unable to generate Kubeconfig", err} + config, handlerErr := handler.buildConfig(r, tokenData, bearerToken, endpoints) + if handlerErr != nil { + return handlerErr } - filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, endpoint.Name) - contentAcceptHeader := r.Header.Get("Accept") - if contentAcceptHeader == "text/yaml" { + return writeFileContent(w, r, endpoints, tokenData, config) +} + +func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.Endpoint, *httperror.HandlerError) { + var endpointIDs []portainer.EndpointID + _ = request.RetrieveJSONQueryParameter(r, "ids", &endpointIDs, true) + + var excludeEndpointIDs []portainer.EndpointID + _ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true) + + if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 { + return nil, &httperror.HandlerError{http.StatusBadRequest, "Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters")} + } + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + + endpointGroups, err := handler.dataStore.EndpointGroup().EndpointGroups() + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err} + } + + if len(endpointIDs) > 0 { + var endpoints []portainer.Endpoint + for _, endpointID := range endpointIDs { + endpoint, err := handler.dataStore.Endpoint().Endpoint(endpointID) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err} + } + if !endpointutils.IsKubernetesEndpoint(endpoint) { + continue + } + endpoints = append(endpoints, *endpoint) + } + filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext) + return filteredEndpoints, nil + } + + var kubeEndpoints []portainer.Endpoint + endpoints, err := handler.dataStore.Endpoint().Endpoints() + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err} + } + for _, endpoint := range endpoints { + if !endpointutils.IsKubernetesEndpoint(&endpoint) { + continue + } + kubeEndpoints = append(kubeEndpoints, endpoint) + } + + filteredEndpoints := security.FilterEndpoints(kubeEndpoints, endpointGroups, securityContext) + if len(excludeEndpointIDs) > 0 { + filteredEndpoints = endpointutils.FilterByExcludeIDs(filteredEndpoints, excludeEndpointIDs) + } + return filteredEndpoints, nil +} + +func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenData, bearerToken string, endpoints []portainer.Endpoint) (*clientV1.Config, *httperror.HandlerError) { + configClusters := make([]clientV1.NamedCluster, len(endpoints)) + configContexts := make([]clientV1.NamedContext, len(endpoints)) + var configAuthInfos []clientV1.NamedAuthInfo + authInfosSet := make(map[string]bool) + + for idx, endpoint := range endpoints { + cli, err := handler.kubernetesClientFactory.GetKubeClient(&endpoint) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} + } + + serviceAccount, err := cli.GetServiceAccount(tokenData) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username), err} + } + + configClusters[idx] = buildCluster(r, endpoint) + configContexts[idx] = buildContext(serviceAccount.Name, endpoint) + if !authInfosSet[serviceAccount.Name] { + configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccount.Name, bearerToken)) + authInfosSet[serviceAccount.Name] = true + } + } + + return &clientV1.Config{ + APIVersion: "v1", + Kind: "Config", + Clusters: configClusters, + Contexts: configContexts, + CurrentContext: configContexts[0].Name, + AuthInfos: configAuthInfos, + }, nil +} + +func buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster { + proxyURL := fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpoint.ID) + return clientV1.NamedCluster{ + Name: buildClusterName(endpoint.Name), + Cluster: clientV1.Cluster{ + Server: proxyURL, + InsecureSkipTLSVerify: true, + }, + } +} + +func buildClusterName(endpointName string) string { + return fmt.Sprintf("portainer-cluster-%s", endpointName) +} + +func buildContext(serviceAccountName string, endpoint portainer.Endpoint) clientV1.NamedContext { + contextName := fmt.Sprintf("portainer-ctx-%s", endpoint.Name) + return clientV1.NamedContext{ + Name: contextName, + Context: clientV1.Context{ + AuthInfo: serviceAccountName, + Cluster: buildClusterName(endpoint.Name), + }, + } +} + +func buildAuthInfo(serviceAccountName string, bearerToken string) clientV1.NamedAuthInfo { + return clientV1.NamedAuthInfo{ + Name: serviceAccountName, + AuthInfo: clientV1.AuthInfo{ + Token: bearerToken, + }, + } +} + +func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portainer.Endpoint, tokenData *portainer.TokenData, config *clientV1.Config) *httperror.HandlerError { + filenameSuffix := "kubeconfig" + if len(endpoints) == 1 { + filenameSuffix = endpoints[0].Name + } + filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, filenameSuffix) + + if r.Header.Get("Accept") == "text/yaml" { yaml, err := kcli.GenerateYAML(config) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Failed to generate Kubeconfig", err} } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase)) - return YAML(w, yaml) + return response.YAML(w, yaml) } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase)) return response.JSON(w, config) } - -// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server -func getProxyUrl(r *http.Request, endpointID int) string { - return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID) -} - -// YAML writes yaml response as string to writer. Returns a pointer to a HandlerError if encoding fails. -// This could be moved to a more useful place; but that place is most likely not in this project. -// It should actually go in https://github.com/portainer/libhttp - since that is from where we use response.JSON. -// We use `data interface{}` as parameter - since im trying to keep it as close to (or the same as) response.JSON method signature: -// https://github.com/portainer/libhttp/blob/d20481a3da823c619887c440a22fdf4fa8f318f2/response/response.go#L13 -func YAML(rw http.ResponseWriter, data interface{}) *httperror.HandlerError { - rw.Header().Set("Content-Type", "text/yaml") - - strData, ok := data.(string) - if !ok { - return &httperror.HandlerError{ - StatusCode: http.StatusInternalServerError, - Message: "Unable to write YAML response", - Err: errors.New("failed to convert input to string"), - } - } - - fmt.Fprint(rw, strData) - - return nil -} diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go index aa5046cfe..e928647d4 100644 --- a/api/http/handler/webhooks/handler.go +++ b/api/http/handler/webhooks/handler.go @@ -17,7 +17,7 @@ type Handler struct { DockerClientFactory *docker.ClientFactory } -// NewHandler creates a handler to manage settings operations. +// NewHandler creates a handler to manage webhooks operations. func NewHandler(bouncer *security.RequestBouncer) *Handler { h := &Handler{ Router: mux.NewRouter(), diff --git a/api/http/security/filter.go b/api/http/security/filter.go index f784827dd..a2593de80 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -81,7 +81,7 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea } // FilterEndpoints filters environments(endpoints) based on user role and team memberships. -// Non administrator users only have access to authorized environments(endpoints) (can be inherited via endoint groups). +// Non administrator users only have access to authorized environments(endpoints) (can be inherited via endpoint groups). func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint { filteredEndpoints := endpoints diff --git a/api/internal/endpointutils/endpoint_test.go b/api/internal/endpointutils/endpoint_test.go index 35793b6e5..8d65552a2 100644 --- a/api/internal/endpointutils/endpoint_test.go +++ b/api/internal/endpointutils/endpoint_test.go @@ -45,3 +45,60 @@ func Test_IsKubernetesEndpoint(t *testing.T) { assert.Equal(t, test.expected, ans) } } + +func Test_FilterByExcludeIDs(t *testing.T) { + tests := []struct { + name string + inputArray []portainer.Endpoint + inputExcludeIDs []portainer.EndpointID + asserts func(*testing.T, []portainer.Endpoint) + }{ + { + name: "filter endpoints", + inputArray: []portainer.Endpoint{ + {ID: portainer.EndpointID(1)}, + {ID: portainer.EndpointID(2)}, + {ID: portainer.EndpointID(3)}, + {ID: portainer.EndpointID(4)}, + }, + inputExcludeIDs: []portainer.EndpointID{ + portainer.EndpointID(2), + portainer.EndpointID(3), + }, + asserts: func(t *testing.T, output []portainer.Endpoint) { + assert.Contains(t, output, portainer.Endpoint{ID: portainer.EndpointID(1)}) + assert.NotContains(t, output, portainer.Endpoint{ID: portainer.EndpointID(2)}) + assert.NotContains(t, output, portainer.Endpoint{ID: portainer.EndpointID(3)}) + assert.Contains(t, output, portainer.Endpoint{ID: portainer.EndpointID(4)}) + }, + }, + { + name: "empty input", + inputArray: []portainer.Endpoint{}, + inputExcludeIDs: []portainer.EndpointID{ + portainer.EndpointID(2), + }, + asserts: func(t *testing.T, output []portainer.Endpoint) { + assert.Equal(t, 0, len(output)) + }, + }, + { + name: "no filter", + inputArray: []portainer.Endpoint{ + {ID: portainer.EndpointID(1)}, + {ID: portainer.EndpointID(2)}, + }, + inputExcludeIDs: []portainer.EndpointID{}, + asserts: func(t *testing.T, output []portainer.Endpoint) { + assert.Equal(t, 2, len(output)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := FilterByExcludeIDs(tt.inputArray, tt.inputExcludeIDs) + tt.asserts(t, output) + }) + } +} \ No newline at end of file diff --git a/api/internal/endpointutils/endpointutils.go b/api/internal/endpointutils/endpointutils.go index 4c54c4196..537f52315 100644 --- a/api/internal/endpointutils/endpointutils.go +++ b/api/internal/endpointutils/endpointutils.go @@ -11,7 +11,7 @@ func IsLocalEndpoint(endpoint *portainer.Endpoint) bool { return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5 } -// IsKubernetesEndpoint returns true if this is a kubernetes endpoint +// IsKubernetesEndpoint returns true if this is a kubernetes environment(endpoint) func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool { return endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || @@ -29,3 +29,24 @@ func IsDockerEndpoint(endpoint *portainer.Endpoint) bool { func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool { return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment } + +// FilterByExcludeIDs receives an environment(endpoint) array and returns a filtered array using an excludeIds param +func FilterByExcludeIDs(endpoints []portainer.Endpoint, excludeIds []portainer.EndpointID) []portainer.Endpoint { + if len(excludeIds) == 0 { + return endpoints + } + + filteredEndpoints := make([]portainer.Endpoint, 0) + + idsSet := make(map[portainer.EndpointID]bool) + for _, id := range excludeIds { + idsSet[id] = true + } + + for _, endpoint := range endpoints { + if !idsSet[endpoint.ID] { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + return filteredEndpoints +} diff --git a/api/kubernetes/cli/kubeconfig.go b/api/kubernetes/cli/kubeconfig.go deleted file mode 100644 index c4ca71e28..000000000 --- a/api/kubernetes/cli/kubeconfig.go +++ /dev/null @@ -1,66 +0,0 @@ -package cli - -import ( - "context" - "fmt" - - portainer "github.com/portainer/portainer/api" - clientV1 "k8s.io/client-go/tools/clientcmd/api/v1" -) - -// GetKubeConfig returns kubeconfig for the current user based on: -// - portainer server url -// - portainer user bearer token -// - portainer token data - which maps to k8s service account -func (kcl *KubeClient) GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *portainer.TokenData) (*clientV1.Config, error) { - serviceAccount, err := kcl.GetServiceAccount(tokenData) - if err != nil { - errText := fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username) - return nil, fmt.Errorf("%s; err=%w", errText, err) - } - - kubeconfig := generateKubeconfig(apiServerURL, bearerToken, serviceAccount.Name) - - return kubeconfig, nil -} - -// generateKubeconfig will generate and return kubeconfig resource - usable by `kubectl` cli -// which will allow the client to connect directly to k8s server environment(endpoint) via portainer (proxy) -func generateKubeconfig(apiServerURL, bearerToken, serviceAccountName string) *clientV1.Config { - const ( - KubeConfigPortainerContext = "portainer-ctx" - KubeConfigPortainerCluster = "portainer-cluster" - ) - - return &clientV1.Config{ - APIVersion: "v1", - Kind: "Config", - CurrentContext: KubeConfigPortainerContext, - Contexts: []clientV1.NamedContext{ - { - Name: KubeConfigPortainerContext, - Context: clientV1.Context{ - AuthInfo: serviceAccountName, - Cluster: KubeConfigPortainerCluster, - }, - }, - }, - Clusters: []clientV1.NamedCluster{ - { - Name: KubeConfigPortainerCluster, - Cluster: clientV1.Cluster{ - Server: apiServerURL, - InsecureSkipTLSVerify: true, - }, - }, - }, - AuthInfos: []clientV1.NamedAuthInfo{ - { - Name: serviceAccountName, - AuthInfo: clientV1.AuthInfo{ - Token: bearerToken, - }, - }, - }, - } -} diff --git a/api/kubernetes/cli/kubeconfig_test.go b/api/kubernetes/cli/kubeconfig_test.go deleted file mode 100644 index 2ea64f075..000000000 --- a/api/kubernetes/cli/kubeconfig_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package cli - -import ( - "context" - "errors" - "testing" - - portainer "github.com/portainer/portainer/api" - v1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kfake "k8s.io/client-go/kubernetes/fake" -) - -func Test_GetKubeConfig(t *testing.T) { - - t.Run("returns error if SA non-existent", func(t *testing.T) { - k := &KubeClient{ - cli: kfake.NewSimpleClientset(), - instanceID: "test", - } - - tokenData := &portainer.TokenData{ - ID: 1, - Role: portainer.AdministratorRole, - Username: portainerClusterAdminServiceAccountName, - } - - _, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData) - - if err == nil { - t.Error("GetKubeConfig should fail as service account does not exist") - } - if k8sErr := errors.Unwrap(err); !k8serrors.IsNotFound(k8sErr) { - t.Error("GetKubeConfig should fail with service account not found k8s error") - } - }) - - t.Run("successfully obtains kubeconfig for cluster admin", func(t *testing.T) { - k := &KubeClient{ - cli: kfake.NewSimpleClientset(), - instanceID: "test", - } - - tokenData := &portainer.TokenData{ - Role: portainer.AdministratorRole, - Username: portainerClusterAdminServiceAccountName, - } - serviceAccount := &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{Name: tokenData.Username}, - } - - k.cli.CoreV1().ServiceAccounts(portainerNamespace).Create(context.Background(), serviceAccount, metav1.CreateOptions{}) - defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{}) - - _, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData) - - if err != nil { - t.Errorf("GetKubeConfig should succeed; err=%s", err) - } - }) - - t.Run("successfully obtains kubeconfig for standard user", func(t *testing.T) { - k := &KubeClient{ - cli: kfake.NewSimpleClientset(), - instanceID: "test", - } - - tokenData := &portainer.TokenData{ - ID: 1, - Role: portainer.StandardUserRole, - } - nonAdminUserName := userServiceAccountName(int(tokenData.ID), k.instanceID) - serviceAccount := &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{Name: nonAdminUserName}, - } - - k.cli.CoreV1().ServiceAccounts(portainerNamespace).Create(context.Background(), serviceAccount, metav1.CreateOptions{}) - defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{}) - - _, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData) - - if err != nil { - t.Errorf("GetKubeConfig should succeed; err=%s", err) - } - }) -} - -func Test_generateKubeconfig(t *testing.T) { - apiServerURL, bearerToken, serviceAccountName := "localhost", "test-token", "test-user" - - t.Run("generates Config resource kind", func(t *testing.T) { - config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName) - want := "Config" - if config.Kind != want { - t.Errorf("generateKubeconfig resource kind should be %s", want) - } - }) - - t.Run("generates v1 version", func(t *testing.T) { - config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName) - want := "v1" - if config.APIVersion != want { - t.Errorf("generateKubeconfig api version should be %s", want) - } - }) - - t.Run("generates single entry context cluster and authinfo", func(t *testing.T) { - config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName) - if len(config.Contexts) != 1 { - t.Error("generateKubeconfig should generate single context configuration") - } - if len(config.Clusters) != 1 { - t.Error("generateKubeconfig should generate single cluster configuration") - } - if len(config.AuthInfos) != 1 { - t.Error("generateKubeconfig should generate single user configuration") - } - }) - - t.Run("sets default context appropriately", func(t *testing.T) { - config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName) - want := "portainer-ctx" - if config.CurrentContext != want { - t.Errorf("generateKubeconfig set cluster to be %s", want) - } - }) - - t.Run("generates cluster with InsecureSkipTLSVerify to be set to true", func(t *testing.T) { - config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName) - if config.Clusters[0].Cluster.InsecureSkipTLSVerify != true { - t.Error("generateKubeconfig default cluster InsecureSkipTLSVerify should be true") - } - }) - - t.Run("should contain passed in value", func(t *testing.T) { - config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName) - if config.Clusters[0].Cluster.Server != apiServerURL { - t.Errorf("generateKubeconfig default cluster server url should be %s", apiServerURL) - } - - if config.AuthInfos[0].Name != serviceAccountName { - t.Errorf("generateKubeconfig default authinfo name should be %s", serviceAccountName) - } - - if config.AuthInfos[0].AuthInfo.Token != bearerToken { - t.Errorf("generateKubeconfig default authinfo user token should be %s", bearerToken) - } - }) -} diff --git a/api/portainer.go b/api/portainer.go index f951866f3..e189d7b79 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -7,7 +7,6 @@ import ( gittypes "github.com/portainer/portainer/api/git/types" v1 "k8s.io/api/core/v1" - clientV1 "k8s.io/client-go/tools/clientcmd/api/v1" ) type ( @@ -1286,7 +1285,6 @@ type ( DeleteRegistrySecret(registry *Registry, namespace string) error CreateRegistrySecret(registry *Registry, namespace string) error IsRegistrySecret(namespace, secretName string) (bool, error) - GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *TokenData) (*clientV1.Config, error) ToggleSystemState(namespace string, isSystem bool) error } diff --git a/app/assets/css/app.css b/app/assets/css/app.css index e84e4cd53..700e887df 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -440,10 +440,25 @@ a[ng-click] { display: none; } +.bootbox-form .visible { + position: initial !important; + display: initial !important; + margin-left: 10px !important; + margin-top: -2px !important; +} + .bootbox-form label { padding-left: 0; } +.bootbox-checkbox-list { + max-height: 200px; + overflow-y: scroll; + background-color: var(--white-color); + border: 1px solid var(--grey-48); + border-radius: 4px; +} + .small-select { display: inline-block; padding: 0px 6px; diff --git a/app/kubernetes/components/kube-config-download-button/kube-config-download-button.controller.js b/app/kubernetes/components/kube-config-download-button/kube-config-download-button.controller.js deleted file mode 100644 index 55a8ec548..000000000 --- a/app/kubernetes/components/kube-config-download-button/kube-config-download-button.controller.js +++ /dev/null @@ -1,45 +0,0 @@ -export default class KubeConfigController { - /* @ngInject */ - constructor($async, $window, KubernetesConfigService, SettingsService) { - this.$async = $async; - this.$window = $window; - this.KubernetesConfigService = KubernetesConfigService; - this.SettingsService = SettingsService; - } - - async downloadKubeconfig() { - await this.KubernetesConfigService.downloadConfig(); - } - - async expiryHoverMessage() { - const settings = await this.SettingsService.publicSettings(); - const expiryDays = settings.KubeconfigExpiry; - switch (expiryDays) { - case '0': - this.state.expiryDays = 'not expire'; - break; - case '24h': - this.state.expiryDays = 'expire in 1 day'; - break; - case '168h': - this.state.expiryDays = 'expire in 7 days'; - break; - case '720h': - this.state.expiryDays = 'expire in 30 days'; - break; - case '8640h': - this.state.expiryDays = 'expire in 1 year'; - break; - } - } - - $onInit() { - return this.$async(async () => { - this.state = { - isHTTPS: this.$window.location.protocol === 'https:', - expiryDays: '', - }; - await this.expiryHoverMessage(); - }); - } -} diff --git a/app/kubernetes/components/kube-config-download-button/kube-config-download-button.html b/app/kubernetes/components/kube-config-download-button/kube-config-download-button.html deleted file mode 100644 index d2c2c4cf4..000000000 --- a/app/kubernetes/components/kube-config-download-button/kube-config-download-button.html +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/app/kubernetes/components/kube-config-download-button/kube-config-download-button.js b/app/kubernetes/components/kube-config-download-button/kube-config-download-button.js deleted file mode 100644 index 1732e50f2..000000000 --- a/app/kubernetes/components/kube-config-download-button/kube-config-download-button.js +++ /dev/null @@ -1,7 +0,0 @@ -import angular from 'angular'; -import controller from './kube-config-download-button.controller'; - -angular.module('portainer.kubernetes').component('kubeConfigDownloadButton', { - templateUrl: './kube-config-download-button.html', - controller, -}); diff --git a/app/kubernetes/components/kubectl-shell/kubectl-shell.html b/app/kubernetes/components/kubectl-shell/kubectl-shell.html index 2bd45a7fc..cf7ef80fb 100644 --- a/app/kubernetes/components/kubectl-shell/kubectl-shell.html +++ b/app/kubernetes/components/kubectl-shell/kubectl-shell.html @@ -11,8 +11,6 @@ kubectl shell - -
kubectl shell
diff --git a/app/kubernetes/rest/kubeconfig.js b/app/kubernetes/rest/kubeconfig.js index 8d3a689c2..fb23029ee 100644 --- a/app/kubernetes/rest/kubeconfig.js +++ b/app/kubernetes/rest/kubeconfig.js @@ -6,11 +6,11 @@ angular.module('portainer.kubernetes').factory('KubernetesConfig', KubernetesCon function KubernetesConfigFactory($http, EndpointProvider, API_ENDPOINT_KUBERNETES) { return { get }; - async function get() { - const endpointID = EndpointProvider.endpointID(); + async function get(environmentIDs) { return $http({ method: 'GET', - url: `${API_ENDPOINT_KUBERNETES}/${endpointID}/config`, + url: `${API_ENDPOINT_KUBERNETES}/config`, + params: { ids: environmentIDs.map((x) => parseInt(x)) }, responseType: 'blob', headers: { Accept: 'text/yaml', diff --git a/app/kubernetes/services/kubeconfigService.js b/app/kubernetes/services/kubeconfigService.js index a3dd1d2fc..ed277bc6e 100644 --- a/app/kubernetes/services/kubeconfigService.js +++ b/app/kubernetes/services/kubeconfigService.js @@ -2,18 +2,38 @@ import angular from 'angular'; class KubernetesConfigService { /* @ngInject */ - constructor(KubernetesConfig, FileSaver) { + constructor(KubernetesConfig, FileSaver, SettingsService) { this.KubernetesConfig = KubernetesConfig; this.FileSaver = FileSaver; + this.SettingsService = SettingsService; } - async downloadConfig() { - const response = await this.KubernetesConfig.get(); + async downloadKubeconfigFile(environmentIDs) { + const response = await this.KubernetesConfig.get(environmentIDs); const headers = response.headers(); const contentDispositionHeader = headers['content-disposition']; const filename = contentDispositionHeader.replace('attachment;', '').trim(); return this.FileSaver.saveAs(response.data, filename); } + + async expiryMessage() { + const settings = await this.SettingsService.publicSettings(); + const expiryDays = settings.KubeconfigExpiry; + const prefix = 'Kubeconfig file will '; + switch (expiryDays) { + case '0': + return prefix + 'not expire.'; + case '24h': + return prefix + 'expire in 1 day.'; + case '168h': + return prefix + 'expire in 7 days.'; + case '720h': + return prefix + 'expire in 30 days.'; + case '8640h': + return prefix + 'expire in 1 year.'; + } + return ''; + } } export default KubernetesConfigService; diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js index fdafb9fdb..41ae6829d 100644 --- a/app/portainer/components/endpoint-list/endpoint-list-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js @@ -1,4 +1,5 @@ import _ from 'lodash-es'; +import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models'; const ENDPOINTS_POLLING_INTERVAL = 30000; // in ms const ENDPOINTS_CACHE_SIZE = 100; @@ -6,8 +7,10 @@ const ENDPOINTS_CACHE_SIZE = 100; angular.module('portainer.app').controller('EndpointListController', [ 'DatatableService', 'PaginationService', + 'ModalService', + 'KubernetesConfigService', 'Notifications', - function EndpointListController(DatatableService, PaginationService, Notifications) { + function EndpointListController(DatatableService, PaginationService, ModalService, KubernetesConfigService, Notifications) { this.state = { totalFilteredEndpoints: null, textFilter: '', @@ -126,6 +129,50 @@ angular.module('portainer.app').controller('EndpointListController', [ return status === 1 ? 'up' : 'down'; } + this.showKubeconfigButton = function () { + if (window.location.protocol !== 'https:') { + return false; + } + return _.some(this.endpoints, (endpoint) => isKubernetesMode(endpoint)); + }; + + function isKubernetesMode(endpoint) { + return [ + PortainerEndpointTypes.KubernetesLocalEnvironment, + PortainerEndpointTypes.AgentOnKubernetesEnvironment, + PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment, + ].includes(endpoint.Type); + } + + this.showKubeconfigModal = async function () { + const kubeEnvironments = _.filter(this.endpoints, (endpoint) => isKubernetesMode(endpoint)); + const options = kubeEnvironments.map(function (environment) { + return { + text: `${environment.Name} (${environment.URL})`, + value: environment.Id, + }; + }); + + let expiryMessage = ''; + try { + expiryMessage = await KubernetesConfigService.expiryMessage(); + } catch (e) { + Notifications.error('Failed fetching kubeconfig expiry time', e); + } + + ModalService.confirmKubeconfigSelection(options, expiryMessage, async function (selectedEnvironmentIDs) { + if (selectedEnvironmentIDs.length === 0) { + Notifications.warning('No environment was selected'); + return; + } + try { + await KubernetesConfigService.downloadKubeconfigFile(selectedEnvironmentIDs); + } catch (e) { + Notifications.error('Failed downloading kubeconfig file', e); + } + }); + }; + this.$onInit = function () { var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey); diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html index 98e12d2b6..92c9752ea 100644 --- a/app/portainer/components/endpoint-list/endpointList.html +++ b/app/portainer/components/endpoint-list/endpointList.html @@ -12,6 +12,17 @@ +