feature(kubeconfig): access to all kube environment contexts from within the Portainer UI [EE-1727] (#5966)
parent
c0a4727114
commit
6be1ff4d9c
|
@ -7,6 +7,7 @@ storybook-static
|
|||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
__debug_bin
|
||||
|
|
|
@ -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
|
||||
|
|
29
api/go.sum
29
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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<span
|
||||
class="interactive"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
uib-tooltip="Kubeconfig file will {{ $ctrl.state.expiryDays }}"
|
||||
>
|
||||
<button
|
||||
ng-if="$ctrl.state.isHTTPS"
|
||||
type="button"
|
||||
class="btn btn-xs btn-primary"
|
||||
ng-click="$ctrl.downloadKubeconfig()"
|
||||
analytics-on
|
||||
analytics-category="kubernetes"
|
||||
analytics-event="kubernetes-kubectl-kubeconfig"
|
||||
>
|
||||
Kubeconfig <i class="fas fa-download space-right"></i>
|
||||
</button>
|
||||
</span>
|
|
@ -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,
|
||||
});
|
|
@ -11,8 +11,6 @@
|
|||
<i class="fa fa-terminal space-right"></i> kubectl shell
|
||||
</button>
|
||||
|
||||
<kube-config-download-button></kube-config-download-button>
|
||||
|
||||
<div ng-if="$ctrl.state.shell.connected" class="{{ $ctrl.state.css }}-kubectl-shell">
|
||||
<div class="shell-container">
|
||||
<div class="shell-item"><i class="fas fa-terminal" style="margin-right: 5px;"></i>kubectl shell</div>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -12,6 +12,17 @@
|
|||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.snapshotAction()" data-cy="home-refreshEndpointsButton">
|
||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
<button
|
||||
ng-if="$ctrl.showKubeconfigButton()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.showKubeconfigModal()"
|
||||
analytics-on
|
||||
analytics-category="kubernetes"
|
||||
analytics-event="kubernetes-kubectl-kubeconfig-multi"
|
||||
>
|
||||
<i class="fas fa-download space-right"></i> kubeconfig
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="searchBar">
|
||||
|
|
|
@ -82,17 +82,20 @@ angular.module('portainer.app').factory('ModalService', [
|
|||
applyBoxCSS(box);
|
||||
}
|
||||
|
||||
function customPrompt(options, optionToggled) {
|
||||
function customCheckboxPrompt(options) {
|
||||
var box = bootbox.prompt({
|
||||
title: options.title,
|
||||
inputType: options.inputType,
|
||||
inputType: 'checkbox',
|
||||
inputOptions: options.inputOptions,
|
||||
buttons: confirmButtons(options),
|
||||
callback: options.callback,
|
||||
});
|
||||
applyBoxCSS(box);
|
||||
box.find('.bootbox-body').prepend('<p>' + options.message + '</p>');
|
||||
box.find('.bootbox-input-checkbox').prop('checked', optionToggled);
|
||||
box.find('.bootbox-input-checkbox').prop('checked', options.optionToggled);
|
||||
if (options.showCheck) {
|
||||
box.find('.bootbox-input-checkbox').addClass('visible');
|
||||
}
|
||||
}
|
||||
|
||||
service.confirmAccessControlUpdate = function (callback) {
|
||||
|
@ -231,28 +234,25 @@ angular.module('portainer.app').factory('ModalService', [
|
|||
};
|
||||
|
||||
service.confirmContainerRecreation = function (callback) {
|
||||
customPrompt(
|
||||
{
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
"You're about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.",
|
||||
inputType: 'checkbox',
|
||||
inputOptions: [
|
||||
{
|
||||
text: 'Pull latest image<i></i>',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Recreate',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
customCheckboxPrompt({
|
||||
title: 'Are you sure?',
|
||||
message:
|
||||
"You're about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.",
|
||||
inputOptions: [
|
||||
{
|
||||
text: 'Pull latest image<i></i>',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Recreate',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
callback: callback,
|
||||
},
|
||||
false
|
||||
);
|
||||
callback: callback,
|
||||
optionToggled: false,
|
||||
});
|
||||
};
|
||||
|
||||
service.confirmEndpointSnapshot = function (callback) {
|
||||
|
@ -285,27 +285,24 @@ angular.module('portainer.app').factory('ModalService', [
|
|||
|
||||
service.confirmServiceForceUpdate = function (message, callback) {
|
||||
message = $sanitize(message);
|
||||
customPrompt(
|
||||
{
|
||||
title: 'Are you sure ?',
|
||||
message: message,
|
||||
inputType: 'checkbox',
|
||||
inputOptions: [
|
||||
{
|
||||
text: 'Pull latest image version<i></i>',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Update',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
customCheckboxPrompt({
|
||||
title: 'Are you sure ?',
|
||||
message: message,
|
||||
inputOptions: [
|
||||
{
|
||||
text: 'Pull latest image version<i></i>',
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Update',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
callback: callback,
|
||||
},
|
||||
false
|
||||
);
|
||||
callback: callback,
|
||||
optionToggled: false,
|
||||
});
|
||||
};
|
||||
|
||||
service.selectRegistry = function (options) {
|
||||
|
@ -319,6 +316,24 @@ angular.module('portainer.app').factory('ModalService', [
|
|||
applyBoxCSS(box);
|
||||
};
|
||||
|
||||
service.confirmKubeconfigSelection = function (options, expiryMessage, callback) {
|
||||
const message = 'Select the kubernetes environment(s) to add to the kubeconfig file.</br>' + expiryMessage;
|
||||
customCheckboxPrompt({
|
||||
title: 'Download kubeconfig file',
|
||||
message: $sanitize(message),
|
||||
inputOptions: options,
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Download file',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
callback: callback,
|
||||
optionToggled: true,
|
||||
showCheck: true,
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
|
Loading…
Reference in New Issue