Compare commits

..

65 Commits

Author SHA1 Message Date
Dmitry Salakhov
c0cb06f1d5 refactor: serve timeout static content using Go 2022-03-31 13:42:41 +13:00
Oscar Zhou
c3b2635aa4 refactor(adminmonitor): adjust the unit test 2022-03-30 17:10:32 +13:00
Oscar Zhou
03ac0c8aed refactor(admintimeout): merge the admin monitor middleware into the admin monitor service 2022-03-30 15:08:06 +13:00
Oscar Zhou
4d698c532a refactor(admintimeout): apply few changes after code review and remove the redundant code 2022-03-29 23:11:57 +13:00
Chaim Lev-Ari
4c57d40a24 feat(security): add a different html for timeout 2022-03-28 12:27:55 +03:00
Oscar Zhou
71300d8811 refactor: correct the default init time 2022-03-26 12:17:55 +13:00
Oscar Zhou
c4bbecb3ae refactor(admintimeout): apply few changes after code review 2022-03-25 16:23:39 +13:00
Oscar Zhou
f031fc8965 refactor(middleware): apply few changes after code review 2022-03-24 20:05:41 +13:00
Oscar Zhou
c656573d83 refactor(timeout): rename the file name 2022-03-23 12:58:51 +13:00
Oscar Zhou
752f92888b refactor(offlinegate): add extra api filter exclusion in offlinegate wrapper 2022-03-23 12:42:03 +13:00
Oscar Zhou
6cf0608254 refactor(offlinegate): remove the specific api endpoint restriction 2022-03-22 14:52:24 +13:00
Oscar Zhou
410c4048bb test: add unit tests for offlinegate wrapper 2022-03-22 14:15:31 +13:00
Oscar Zhou
16a03dad84 refactor(offlinegate): move the timeout channel receiver to wrapper initialization 2022-03-22 14:14:02 +13:00
Oscar Zhou
383d41b3ef feat(timeout): add a new interceptor and init timeout view 2022-03-22 12:02:42 +13:00
Oscar Zhou
adeda52b5f refactor(adminmonitor): adjust the unit test 2022-03-22 11:52:11 +13:00
Oscar Zhou
185e4cdfbc feat(adminmonitor): redirect api request if the admin is not initialized in 5 mins 2022-03-22 11:47:09 +13:00
Marcelo Rydel
c486130a9f fix(kube): Use KubeClusterAccessService for Helm operations [EE-2500] (#6559) 2022-03-21 09:51:29 -03:00
Chaim Lev-Ari
cf7746082b fix(stacks): show force pull image for git stacks [EE-2579] (#6607) 2022-03-21 14:35:31 +02:00
andres-portainer
1ab65a4b4f fix(offlinegate): fix data race in offlinegate EE-2713 (#6626) 2022-03-18 13:20:10 -03:00
andres-portainer
a66e863646 fix(boltdb): upgrade to the latest version to avoid problems with the race detector EE-2729 (#6638) 2022-03-18 13:16:31 -03:00
Marcelo Rydel
d962c300f9 fix(containers/datatable): disable autoreset expanded and selected rows [EE-2347] (#6563) 2022-03-17 14:55:11 -03:00
Richard Wei
9aeedf1bfa fix(ingress): fix-multiple-route-on-same-ingress EE-2597 (#6609)
* fix multiple route for same ingress & improvement for multiple ingress controller
2022-03-17 10:25:36 +13:00
andres-portainer
98d8cd99fb fix(chisel): upgrade chisel to v1.7.7 to fix a data race EE-2718 (#6650) 2022-03-16 12:17:56 -03:00
andres-portainer
226ffdcd20 fix(snapshots): fix a data race in the snapshot code EE-2717 (#6654) 2022-03-16 11:27:28 -03:00
andres-portainer
78150a738f fix(scheduler): fix a data race in the scheduler EE-2716 (#6629) 2022-03-16 10:33:15 -03:00
andres-portainer
ecf5e90783 fix(admin-monitor): fix a data race in the admin monitor EE-2761 (#6658) 2022-03-16 09:13:45 -03:00
Chaim Lev-Ari
f63b07bbb9 refactor(access-control): create access-control-panel component [EE-2345] (#6486) 2022-03-16 08:35:32 +02:00
Chao Geng
07294c19bb fix(k8s/application): check name unique in k8s cluster (#6610)
* EE-2353 Check unique name when creating new deployment in kubernetes

* EE-2353 fix warning from gofmt

* EE-2353 add miss methon in kubernetes_mock.go

* EE-2353 add missing space

* EE-2353 Use kubernetes cli to instead exec.command

* EE-2353 remove useless parameter

* EE-2353 remove unnecessary log in handle

* EE-2353 fix gofmt warning

* EE-2353 use ListOptions to filter the list

* EE-2353 add function description

* EE-2353 fix error

* Update api/kubernetes/cli/deploment.go

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>

* EE-2353 change function name to HasStackName

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2022-03-16 08:32:12 +08:00
andres-portainer
f8cbb54ba5 fix(tunnels): fix a deadlock with the tunnels EE-2751 (#6649) 2022-03-15 12:37:09 -03:00
andres-portainer
f8fd28bb61 fix(scheduler): fix a data race in a scheduler unit test EE-2715 (#6628) 2022-03-15 09:52:58 -03:00
andres-portainer
78f7cd0d6c fix(adminmonitor): fix a data race in a unit test EE-2714 (#6627) 2022-03-15 09:52:41 -03:00
Chaim Lev-Ari
9a42d4c506 fix(auth/ldap): show server url [EE-2069] (#6651) 2022-03-15 07:13:39 +02:00
itsconquest
f2c48409e0 refactor(azure/aci): migrate sidebar to react [EE-2569] (#6593)
* refactor(azure/aci): migrate sidebar to react [EE-2569]

* add test files

* add story

* fix(sidebar): get styles from sidebar

* make suggested changes + update icon story

* use template in second story + change some english

* use camel case in test

* use icon instead of span

* refactor(types): use existing environmentid type

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-03-14 19:26:30 +13:00
Oscar Zhou
5188ead870 fix(home): fix homepage edge heartbeat judgement [EE-2041] (#6624)
* fix(home): judge LastCheckInDate with QueryDate for heartbeat

* refactor(environments): remove deprecated variable homepageLoadTime

* style(environments): run yarn format

Co-authored-by: sam@gemibook <huapox@126.com>
2022-03-14 14:53:23 +13:00
Chaim Lev-Ari
f1ea2b5c02 chore(account): write tests for CreateAccessToken [EE-2561] (#6578) 2022-03-13 09:14:41 +02:00
KyKlen
b7d18ef50f fix(volumes): add addr field in options when creating a CIFS volume [EE-2349] (#6359) 2022-03-10 13:06:52 -03:00
sunportainer
20405e9803 fix(docker/service): send registry id on update EE-2061 (#6606) 2022-03-10 07:35:11 +02:00
Chaim Lev-Ari
0f3c7b1424 refactor(home): migrate view to react [EE-1810] (#6314)
* refactor(http): parse axios errors (#6325)

* refactor(home): use endpoint-list as react component [EE-1814] (#6060)

* refactor(home): use endpoint-list as react component

fix(home): add missing features and refactors

- kubebutton
- group name
- poll when endpoint is off
- state management

refactor(endpoints): use stat component

fix(endpoints): add space between items

refactor(endpoints): move stats to components

refactor(endpoints): fetch time

refactor(home): move logic

refactor(home): move fe render logic

refactor(settings): use vanilla js for publicSettings

refactor(kube): remove angular from kube config service

feat(home): add kubeconfig button

feat(home): send analytics when opening kubeconfig modal

fix(home): memoize footer

refactor(home): use react-query for loading

fix(home): show correct control for kubeconfig modal

refactor(home): use debounce

refactor(home): use new components

refactor(home): replace endpoints with environments

refactor(home): move endpoint-list component to home

fix(home): show group name

refactor(home): use switch for environment icon

fix(kubeconfig): fix default case

refactor(axios): use parse axios error

refactor(home): use link components for navigate

fix(home): align azure icon

refactor(home): refactor stats

refactor(home): export envstatusbadge

refactor(home): remove unused bindings

* chore(home): write tests for edge indicator

* chore(home): basic stories for environment item

* style(settings): reformat

* fix(environments): add publicurl

* refactor(home): use table components

* refactor(datatables): merge useSearchBarState

* refactor(home): fetch group in env item

* chore(tests): basic tests

* chore(home): test when no envs

* refactor(tags): use axios for tagService

* refactor(env-groups): use axios for getGroups

* feat(app): ui-state context provider

* refactor(home): create MotdPanel

* refactor(app): create InformationPanel

* feat(endpoints): fetch number of total endpoints

* refactor(app): merge hooks

* refactor(home): migrate view to react [EE-1810]

fixes [EE-1810]

refactor(home): wip use react view

feat(home): show message if no endpoints

refactor(home): show endpoint list

refactor(home): don't use home to manage link

refactor(home): move state

refactor(home): check if edge using util

refactor(home): move inf panels

chore(home): tests

refactor(home): load groups and tags in env-item

refactor(settings): revert publicSettings change

refactor(home): move confirm snapshot method

* fix(home): show tags

* fix(environments): handle missing snapshots

* fix(kube/volumes): fetch pesistent volume claims

* refactor(kube): remove use of endpointProvider

* refactor(endpoints): set current endpoint

* chore(home): add data-cy for tests

* chore(tests): mock axios-progress-bar

* refactor(home): move use env list to env module

* feat(app): sync home view changes with ee

* fix(home): sort page header

* fix(app): fix tests

* chore(github): use yarn cache

* refactor(environments): load list of groups

* chore(babel): remove auto 18n keys extraction

* chore(environments): fix tests

* refactor(k8s/application): use current endpoint

* fix(app/header): add margin to header

* refactor(app): remove unused types

* refactor(app): use rq onError handler

* refactor(home): wrap element with button
2022-03-08 14:14:23 +02:00
sunportainer
c442d936d3 fix(compose):filter out symlink in custom template EE-1928 (#6579)
* fix prevent symlink in customtemplate
2022-03-04 12:05:34 +08:00
testA113
0cd164bada add data-cy attributes (#6623) 2022-03-04 14:56:04 +13:00
itsconquest
ee42e44246 refactor(edge-compute): remove toggle from settings (release) [EE-2686] (#6619) 2022-03-03 13:31:01 +13:00
itsconquest
6695d75468 fix(endpoints): fix broken style (release) [EE-2659] (#6613)
* fix(endpoints): fix broken style (release) [EE-2659]

* fix(endpoints): show margin under env var field [EE-2659]

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-03-03 11:48:30 +13:00
Prabhat Khera
eb6cdf1229 created bucket if not exists during restore sequence (#6614) 2022-03-03 09:10:26 +13:00
andres-portainer
a3b1466b96 fix(tunnel): fix data race on tunnels EE-2577 (#6601) 2022-03-02 13:51:22 -03:00
Marcelo Rydel
8b7dcf20bf feat(db): add CreateObjectWithStringId function [EE-2612] (#6611) 2022-03-02 09:22:03 -03:00
Prabhat Khera
14ed6ed2a3 DB upgrade failes if bucket does not exists (#6608) 2022-03-01 10:31:33 +13:00
Chaim Lev-Ari
9f4549212d fix(auth): remove caching of user (#6591) 2022-03-01 09:38:16 +13:00
Chao Geng
37209918ad fix(docker/stacks): upgrade docker-compose-wrapper [EE-1975] (#6598)
* updated docker-compose-wrapper

* keep the same
2022-02-28 17:24:15 +08:00
Chaim Lev-Ari
aefa34d6d2 fix(k8s/application): allow app name to start with alphabetic character [EE-2596] (#6603)
fixes [EE-2596]
2022-02-28 07:15:49 +02:00
Hao Zhang
eaffde39f6 fix(stack): incorrect stack name (#6587) 2022-02-27 16:04:48 +08:00
Hao Zhang
d71d291895 fix(stack): git repo auto update not working (#6573) 2022-02-27 16:03:05 +08:00
itsconquest
a894e3182a refactor(azure/aci): migrate dashboard view to react [EE-2189] (#6518)
* refactor(azure/aci): migrate dashboard view to react [EE-2189]

* move aggregate function to azure utils file

* fix type

* introduce dashboard item component

* add error notificatons

* hide resource groups widget if failed to load

* make dashboard a default export

* revert mistake

* refactor based on suggestions

* use object for error data instead of array

* return unused utils file

* move length calculations out of return statement

* only return first error of resource groups queries

* refactor imports/exports, fix bug with errors & add test

* WIP dashboard tests

* allow mocking multiple resource groups

* test for total number of resource groups

* update lock file to fix lint action issue

* finish dashboard tests

* dashboarditem story

* fix(auth): remove caching of user

* add option for link to dashboard item

* rename dashboard test case to match file

* remove optional link and update storybook

* create aria label based on already provided text

* change param name to be clearer
2022-02-25 12:22:56 +13:00
Chaim Lev-Ari
ff7847aaa5 chore(git): ignore prettier commits on git blame (#6584)
* chore(git): ignore prettier commits on git blame

* chore(vscode): fix launch command
2022-02-22 16:27:35 +02:00
Matt Hook
a89c3773dd fix(datastore): export/import the bolt sequence number EE-2451 (#6571)
* Implement setter/getter for the sequence

* import/export counts

* fix go tests.  rename vars

* Improved and simplified the logic. Made it more generic

* Remove unused methods

* remove unused methods

* not part of branch fix
2022-02-22 09:53:17 +13:00
Hao Zhang
5d75ca34ea fix(stack): git force pull image toggle only for non-kubernetes git based stacks (#6574) 2022-02-21 08:43:22 +08:00
Marcelo Rydel
d47a9d590e fix(kube): namespace parameter is not used in kube redeploy (#6569) 2022-02-18 16:36:20 +13:00
Anthony Lapenna
bd679ae806 feat(endpoint): add an input to source env vars [EE-2436] (#6517)
* feat(endpoint): add an input to source env vars

* fix(endpoint): fix invalid version in deployment instructions

* fix(endpoint): fix copy Edge command

* fix(endpoint): fix invalid Edge deployment instruction

* feat(endpoint): add missing parameter to edge deploy script

* feat(edge): use temporary manifest url

* refactor(endpoint): update method and placeholder

* fix(endpoint): fix missing agent name in Edge deployment instructions on Swarm

* fix(endpoint): fix invalid Edge deployment instructions for Kubernetes

* fix(build): commit yarn.lock

* chore(deps): run yarn

* feat(endpoint): do not support kubernetes with Edge env vars

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-02-17 10:25:59 +13:00
LP B
5de7ecb5f0 chore(deps): freeze lockfile on Github Actions (#6570) 2022-02-16 18:20:15 +01:00
Marcelo Rydel
b3cd9c69df fix(edge/settings): render view after loading settings [EE-2532] (#6560) 2022-02-15 18:26:42 -03:00
Chaim Lev-Ari
73311b6f32 fix(edge/devices): make actions button larger [EE-2471] (#6542)
* fix(edge/devices): make actions button larger [EE-2471]

fixes [EE-2471]

* fix(edge/devices): fix table-actions-title padding

Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2022-02-16 08:38:24 +13:00
Sven Dowideit
93ddcfecd9 fix(templates): show docker-compose app templates when in swarm mode [EE-2117] (#6177)
* fix(templates): EE-2117: show docker-compose app templates when in swarm mode and the user selects 'showContainers

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix(templates): keep original behavior for standalone

* fix(templates): display all templates on Swarm

* refactor(templates): update method name

Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2022-02-15 07:30:02 +13:00
Marcelo Rydel
2bffba7371 fix(edge): only show expand row for Edge Devices with AMT activated [EE-2489] (#6519) 2022-02-14 11:44:55 -03:00
Hao Zhang
37ca62eb06 feat(webhook): teasers of pull images and webhook for EE EE-1332 (#6278)
* feat(webhook): teasers of pull images and webhook for EE
2022-02-14 21:51:43 +08:00
Chaim Lev-Ari
fa208c7f2a docs(github): fix slack link [EE-2438] (#6541)
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2022-02-11 10:07:14 -03:00
testA113
6fac3fa127 add data-cy attributes for backup/restore (#6546)
Co-authored-by: testA113 <42307911+aliharriss@users.noreply.github.com>
2022-02-11 15:24:44 +13:00
365 changed files with 10889 additions and 9939 deletions

View File

@@ -1,4 +1,5 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2 (put here after fix/EE-2344/fix-eslint-issues is merged)
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169

View File

@@ -2,7 +2,7 @@
Thanks for opening an issue on Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this

View File

@@ -12,7 +12,7 @@ Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
@@ -47,7 +47,7 @@ You can see how [here](https://documentation.portainer.io/r/portainer-logs)
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Browser:
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
- Have you reviewed our technical documentation and knowledge base? Yes/No
**Additional context**

View File

@@ -4,11 +4,11 @@ about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
Before you start, we need a little bit more information from you:
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
Have you reviewed our technical documentation and knowledge base? Yes/No
@@ -16,7 +16,7 @@ Have you reviewed our technical documentation and knowledge base? Yes/No
You can find more information about Portainer support framework policy here: https://old.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->

View File

@@ -4,14 +4,13 @@ about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this

View File

@@ -18,17 +18,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
# ESLint and Prettier must be in `package.json`
- name: Install Node.js dependencies
run: yarn
node-version: '14'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run linters
uses: wearerequired/lint-action@v1

View File

@@ -1,11 +1,15 @@
name: Test Frontend
on: push
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: yarn
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- name: Run tests
run: yarn test:client

View File

@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
"program": "${workspaceRoot}/api/cmd/portainer",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,

View File

@@ -4,5 +4,5 @@
"gopls": {
"build.expandWorkspaceToModule": false
},
"gitlens.advanced.blame.customArguments": ["ignore-revs-file", ".git-blame-ignore-revs"]
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
}

View File

@@ -42,10 +42,10 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
Learn more about Portainer's community support channels [here.](https://www.portainer.io/community_help)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.

View File

@@ -2,7 +2,12 @@ package adminmonitor
import (
"context"
"embed"
"io/fs"
"log"
"net/http"
"strings"
"sync"
"time"
portainer "github.com/portainer/portainer/api"
@@ -11,24 +16,37 @@ import (
var logFatalf = log.Fatalf
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.Mutex
adminInitDisabled bool
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
// New creates a monitor that when started will wait for an admin account being created for timeout duration
// if and admin account would still be missing, it'll disable the http traffic handling
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc != nil {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
@@ -41,7 +59,11 @@ func (m *Monitor) Start() {
logFatalf("%s", err)
}
if !initialized {
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
log.Println("[INFO] [internal,init] The Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer")
m.mu.Lock()
defer m.mu.Unlock()
m.adminInitDisabled = true
return
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
@@ -53,6 +75,9 @@ func (m *Monitor) Start() {
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc == nil {
return
}
@@ -68,3 +93,35 @@ func (m *Monitor) WasInitialized() (bool, error) {
}
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.adminInitDisabled
}
//go:embed timeout
var timeoutFiles embed.FS
// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason.
// Otherwise, it will pass through the request to next
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.WasInstanceDisabled() {
if r.RequestURI == `/` || strings.HasPrefix(r.RequestURI, "/api") {
w.Header().Set("redirect-reason", `Administrator initialization timeout`)
http.Redirect(w, r, `/timeout.html`, http.StatusSeeOther)
return
}
files, err := fs.Sub(timeoutFiles, "timeout")
if err != nil {
log.Printf("Error %s\n", err)
}
http.FileServer(http.FS(files)).ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -21,6 +21,18 @@ func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor.Stop()
}
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
go monitor.Start()
monitor.Start()
go monitor.Stop()
monitor.Stop()
time.Sleep(2 * time.Second)
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
@@ -30,21 +42,13 @@ func Test_canStopStartedMonitor(t *testing.T) {
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
assert.True(t, fataled, "monitor should been timeout and fatal")
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="420" height="420" viewBox="0 0 315.000000 315.000000"><path d="M163 13.3v6.4l-38.3 22.1-38.2 22.1h-34c-2.8.1-3 .3-3.2 4.5-.2 3.4.1 4.5 1.5 4.8.9.2 16 .3 33.5.2 17.4 0 31.7.2 31.8.5.1.3.2 10.3.4 22.1.1 11.8.3 21.8.4 22.2 0 .5 5.5.8 12 .8h11.9l-.1-22.8-.1-22.7 11.2-.1H163V204l3.5.1c1.9.1 4.1.2 4.7.3 1 .1 1.3-13.7 1.3-65.3V73.7l3.3-.3 3.2-.2.1 66.6v66.7l4.8 3.1 4.7 3.2v-69c-.1-38 .1-69.3.4-69.7.3-.4 17.2-.7 37.5-.7h37l.3-4.6.3-4.6-8.3-.3-8.3-.4-37-21.4c-20.3-11.8-37.2-21.6-37.5-21.8-.3-.3-.5-2.8-.6-5.6-.1-2.9-.2-5.8-.3-6.5-.1-.7-1.7-1.2-4.6-1.2H163v6.3zm0 34.1v16.5h-28.2c-28 0-28.3 0-25.8-1.9 1.4-1 4.3-2.8 6.5-4 2.2-1.1 13.5-7.6 25-14.4 11.6-6.9 21.3-12.5 21.8-12.5.4-.1.7 7.3.7 16.3zm28.6-5.4c7.5 4.4 13.7 8 13.9 8 .1 0 4.5 2.5 9.6 5.7 5.2 3.1 10.5 6.2 11.9 6.9 2.1 1.1-2 1.3-26 1.1l-28.5-.2V47.3l-.1-16.1 2.8 1.4c1.5.8 8.9 5 16.4 9.4zm-55.4 40.5c0 4.9-.1 9.7-.1 10.5-.1 1.3-1.5 1.6-7.3 1.5l-7.3-.1-.5-10.5-.5-10.6 7.8.1 7.9.1v9zm-12.3 23.9c.1 6.6-.2 8.5-1.4 9-.8.3-1.5.2-1.6-.2-.4-3.4-.5-15-.1-16 .2-.6 1-1.2 1.7-1.2.9 0 1.3 2.3 1.4 8.4zm6.7.4c0 4.5-.3 8.2-.8 8.2-.4 0-1.1.1-1.5.2-.5.2-.8-3.7-.9-8.5 0-7.5.2-8.7 1.5-8.5 1.3.3 1.6 1.9 1.7 8.6zm6.9 0c.1 7.2-.1 8.2-1.6 8.2-1.6 0-1.8-1-1.7-8.5.1-7.3.3-8.5 1.7-8.3 1.3.3 1.6 1.8 1.6 8.6z"/><path d="M61.9 108c0 .3-.1 5.7-.2 12l-.1 11.5 12.2.3 12.2.3v-24.6H74c-6.6 0-12 .2-12.1.5zm7.1 11.4c0 8-1 11.1-2.6 8.5-.5-.9-.8-8.6-.5-15.2.1-.9.8-1.7 1.6-1.7 1.2 0 1.5 1.6 1.5 8.4zm7-.1c0 8.6-.3 9.9-2.3 9.1-.8-.3-1.1-3-1-7.7.2-10 .1-9.7 1.8-9.7 1.2 0 1.5 1.6 1.5 8.3zm6.8-.3c.3 8.4-.2 10.7-2.1 9.2-.9-.7-1.2-3.6-1.1-8.9.2-9.2.1-8.6 1.7-8.1.8.3 1.3 3 1.5 7.8zM89.4 107.9c-.2.2-.4 5.8-.4 12.3V132h24.5l-.1-11.3c-.1-6.1-.2-11.7-.3-12.2-.1-1-22.7-1.5-23.7-.6zm7.2 11.2c.1 8.6-.3 10-2.2 9.2-1-.4-1.4-2.5-1.4-8.2 0-4.3.3-8.1.7-8.5 1.8-1.8 2.8.7 2.9 7.5zm6.5-7.4c.4 3.9.3 15.7-.2 16.5-1.7 2.7-2.9-.9-2.9-8.8 0-6.8.3-8.4 1.5-8.4.8 0 1.5.3 1.6.7zm7 .5c.5 5.5.3 13.6-.4 15.1-1.6 3.6-2.7.4-2.7-7.9 0-6.8.3-8.4 1.5-8.4.8 0 1.5.6 1.6 1.2zM94.3 134.8l-5.3.3v12c0 8.8.3 11.9 1.3 12 1.7.1 15.5.1 19.7 0l3.5-.1-.1-10.8c-.1-5.9-.2-11.4-.3-12.2-.1-1.5-6.2-1.9-18.8-1.2zm2.2 12.3c0 6.8-.3 8.4-1.5 8.4s-1.6-1.6-1.8-7.3c-.4-8.1.1-10.6 2.1-9.9.8.2 1.2 2.9 1.2 8.8zm6.6-7.4c.5 5.9.3 14.6-.2 15.5-.4.6-1.2.7-1.8.4-1.2-.8-1.6-15.8-.4-16.9 1.2-1.2 2.3-.7 2.4 1zm7-1c0 .5.1 4.4.2 8.8 0 6.3-.2 8-1.4 8-1.1 0-1.5-1.9-1.7-8.8-.2-7.3 0-8.7 1.3-8.7.8 0 1.5.3 1.6.7zM120 134.8l-3.5.3-.1 11.9c0 7.6.4 12 1.1 12.1 4.6.3 21.5 0 22.3-.5.5-.3 1-5.7 1-12.1l.1-11.5-5.7-.1c-3.1 0-7-.1-8.7-.2-1.6-.1-4.6-.1-6.5.1zm3.4 4.8c1 2.7.7 15.2-.5 15.9-.6.4-1.3.4-1.6.1-.7-.7-1.2-15.7-.6-16.9.7-1.2 2.1-.8 2.7.9zm7.3 6c.2 7.9-.4 10.7-2 10.1-.8-.2-1.2-3.2-1.3-8.3-.1-8.5.1-9.7 1.9-9.1.7.2 1.3 3.1 1.4 7.3zm6.9 0c.2 8.5-.3 10.5-2.2 9.7-1.1-.4-1.4-2.2-1.3-8.2.2-8.3.5-9.4 2.2-8.8.8.2 1.2 3 1.3 7.3zM61.9 136.7c-.4 9-.3 21.6.3 21.9.7.4 13.9.7 21.1.5l2.7-.1v-24H74c-10.7 0-12 .2-12.1 1.7zM69 147c0 5-.4 9-.9 9-1.6 0-2.2-1.7-2.2-7.2-.2-9.6 0-10.8 1.6-10.8 1.2 0 1.5 1.7 1.5 9zm6.8-.5c.3 7.2-.7 10.9-2.4 9.2-.4-.4-.7-3.5-.7-6.9.1-10.2.2-11 1.6-10.6.8.3 1.3 3.1 1.5 8.3zm7 0c.2 4.7-.1 8.2-.8 8.9-1.8 1.8-2.6-.8-2.4-8.6.2-9.3.2-9 1.7-8.6.8.3 1.3 3.1 1.5 8.3zM59.4 166.7c-3.1 7.2-4.3 13.6-4 21.2.5 12.5 4.9 22.4 13.7 31.1 4.1 4.1 5.4 4.8 7.2 4.1 1.2-.4 3.9-.7 5.9-.6 3.6.1 3.8-.1 6.8-6.2 8.2-16.7 29.1-23.3 47.6-15.2 2.4 1.1 4.6 2.4 4.9 2.9 2.5 4.1 5.4-3.3 5.9-15.3.4-8-.6-14-3.5-20.7l-2.1-5H61l-1.6 3.7z"/><path d="M118.5 202.8c-6 .9-11 2.8-15.1 5.7-5.2 3.7-11 11.3-11.9 15.6-.6 2.4-1.2 2.9-3.8 3-9.6.2-17.3 3.5-23.7 10-6 6.2-8.4 12.1-8.6 20.9-.3 10.2 2.1 16.5 9.1 23.5 6.6 6.6 12.5 9 22 9 6.7 0 7.1.1 8.9 3 2.6 4.2 7.3 8.3 13.1 11.3 4.2 2.2 6.3 2.6 13 2.6 9.5 0 16.9-2.9 22.8-8.9l3.7-3.8 6.1 3.3c13 6.9 29.4 3.7 38.8-7.6 5.3-6.5 7.2-11.7 7.3-20.4.2-7-.2-8.6-2.8-13.7-2.6-5-2.9-6.4-2.1-9.3 2.3-8.9-1.7-22.2-8.9-29.4-9.8-9.8-22.8-11.7-38.1-5.5-3 1.2-3.4 1.1-5.8-1.4-5.1-5-17.1-9-24-7.9z"/></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 940 300" style="enable-background:new 0 0 940 300;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#13BEF9;}
.st1{fill:#13BEF9;}
</style>
<g>
<polygon class="st0" points="84.3,76.6 80.3,76.6 80.3,97.3 84.3,97.3 84.3,76.6 "/>
<polygon class="st0" points="101.5,76.6 97.5,76.6 97.5,97.3 101.5,97.3 101.5,76.6 "/>
<polygon class="st0" points="125,37.1 120.9,30 52.5,69.5 56.6,76.6 125,37.1 "/>
<polygon class="st0" points="124.6,37.1 128.7,30 197.1,69.5 193,76.6 124.6,37.1 "/>
<polygon class="st0" points="209.2,76.7 209.2,68.5 21.8,68.5 21.8,76.7 209.2,76.7 "/>
<path class="st0" d="M135,192.5V71h8.2v127.4C141,195.9,138.2,194.1,135,192.5L135,192.5z"/>
<path class="st0" d="M121,190.4V19h8.2v172.4C126.9,190.3,121.3,190.4,121,190.4L121,190.4z"/>
<path class="st0" d="M43.3,207.5c-10-7.4-16.6-19.2-16.6-32.6c0-7.1,1.9-14.1,5.4-20.2h70c3.6,6.1,5.4,13.1,5.4,20.2
c0,6.2-0.8,12-3.3,17.2c-5.3-5.1-13.1-7.3-21-7.3c-14,0-26,8.7-29.1,21.7c-1.1-0.1-1.8-0.2-2.9-0.2
C48.5,206.4,45.9,206.8,43.3,207.5L43.3,207.5z"/>
<path class="st1" d="M219.8,115.5c-10.6,0-19.9,4.9-26.3,12.5v-11.4h-10.6v101.3h10.6v-42.7c6.3,7.8,15.7,12.8,26.3,12.8
c19.8,0,36.1-16.9,36.1-36.4C255.9,131.8,239.6,115.5,219.8,115.5L219.8,115.5z M220.1,177.5c-13.8,0-26-12.2-26-26
c0-14.1,12.2-25.6,26-25.6c14.1,0,24.7,11.5,24.7,25.6C244.8,165.3,234.2,177.5,220.1,177.5L220.1,177.5z"/>
<path class="st1" d="M302.3,187.9c19.8,0,36.1-16.9,36.1-36.4c0-19.8-16.3-36.1-36.1-36.1c-19.8,0-36.1,16.3-36.1,36.1
C266.2,171,282.5,187.9,302.3,187.9L302.3,187.9z M302.3,125.9c14.1,0,25,11.5,25,25.6c0,13.8-10.9,26-25,26c-14.1,0-25-12.2-25-26
C277.3,137.5,288.2,125.9,302.3,125.9L302.3,125.9z"/>
<path class="st1" d="M365.6,116.6H355v69.6h10.6v-38.5c0-14.2,11.2-21.8,23.6-21.8v-10.4c-9.6,0-17.9,4.1-23.6,10.6V116.6
L365.6,116.6z"/>
<polygon class="st1" points="433.8,126.2 433.8,116.6 418.1,116.6 418.1,89.2 407.5,89.2 407.5,116.6 397.1,116.6 397.1,126.2
407.5,126.2 407.5,186.2 418.1,186.2 418.1,126.2 433.8,126.2 "/>
<path class="st1" d="M478.6,187.9c10.6,0,19.9-5.1,26.3-12.8v11.4h10.6v-69.9h-10.6V128c-6.3-7.6-15.7-12.5-26.3-12.5
c-19.8,0-36.1,16.3-36.1,36.1C442.5,171,458.8,187.9,478.6,187.9L478.6,187.9z M478.2,177.5c-14.1,0-24.7-12.2-24.7-26
c0-14.1,10.6-25.6,24.7-25.6c13.8,0,26,11.5,26,25.6C504.2,165.3,492,177.5,478.2,177.5L478.2,177.5z"/>
<path class="st1" d="M543.6,102.5c4,0,7.4-3.3,7.4-7.6c0-3.8-3.5-7.3-7.4-7.3c-4.3,0-7.6,3.5-7.6,7.3
C536,99.2,539.3,102.5,543.6,102.5L543.6,102.5z M538.2,186.2h11.1v-69.6h-11.1V186.2L538.2,186.2z"/>
<path class="st1" d="M571.6,186.2h10.6v-37c0-15.7,8.7-23.6,22.8-23.3c11.6,0,17.9,6.8,17.9,20.6v39.7h10.6v-39.7
c0-22.2-8.5-31-28.5-31c-9.5,0-17.2,3.5-22.8,9.5v-8.4h-10.6V186.2L571.6,186.2z"/>
<path class="st1" d="M720.7,151.5c0-19.8-16.3-36.1-36.1-36.1c-19.8,0-36.1,16.3-36.1,36.1c0,19.5,16.3,36.4,36.1,36.4
c14.1,0,26.6-8.1,32.4-20.1h-13.1c-4.4,5.7-11.2,9.7-19.3,9.7c-12.3,0-22.3-9.5-24.5-21h60.6L720.7,151.5L720.7,151.5z
M684.6,125.9c12.2,0,22.2,8.9,24.5,20.4h-49.1C662.4,134.8,672.3,125.9,684.6,125.9L684.6,125.9z"/>
<path class="st1" d="M747.9,116.6h-10.6v69.6h10.6v-38.5c0-14.2,11.2-21.8,23.6-21.8v-10.4c-9.7,0-17.9,4.1-23.6,10.6V116.6
L747.9,116.6z"/>
<path class="st1" d="M787.5,187c4.7,0,8.7-4,8.7-8.9c0-4.7-4-8.7-8.7-8.7c-4.9,0-8.9,4-8.9,8.7C778.6,183,782.6,187,787.5,187
L787.5,187z"/>
<path class="st1" d="M823.5,102.5c4,0,7.4-3.3,7.4-7.6c0-3.8-3.5-7.3-7.4-7.3c-4.3,0-7.6,3.5-7.6,7.3
C816,99.2,819.3,102.5,823.5,102.5L823.5,102.5z M818.2,186.2h11.1v-69.6h-11.1V186.2L818.2,186.2z"/>
<path class="st1" d="M882.1,187.9c19.8,0,36.1-16.9,36.1-36.4c0-19.8-16.3-36.1-36.1-36.1c-19.8,0-36.1,16.3-36.1,36.1
C846,171,862.3,187.9,882.1,187.9L882.1,187.9z M882.1,125.9c14.1,0,25,11.5,25,25.6c0,13.8-10.9,26-25,26c-14.1,0-25-12.2-25-26
C857.1,137.5,868,125.9,882.1,125.9L882.1,125.9z"/>
<polygon class="st0" points="77.7,106.5 56.5,106.5 56.5,127.8 77.7,127.8 77.7,106.5 "/>
<polygon class="st0" points="53.8,106.5 32.6,106.5 32.6,127.8 53.8,127.8 53.8,106.5 "/>
<polygon class="st0" points="53.8,130.2 32.6,130.2 32.6,151.5 53.8,151.5 53.8,130.2 "/>
<polygon class="st0" points="77.7,130.2 56.5,130.2 56.5,151.5 77.7,151.5 77.7,130.2 "/>
<polygon class="st0" points="101.5,130.2 80.3,130.2 80.3,151.5 101.5,151.5 101.5,130.2 "/>
<polygon class="st0" points="101.5,95.1 80.3,95.1 80.3,116.4 101.5,116.4 101.5,95.1 "/>
<path class="st0" d="M57.6,210.7c2.9-12.3,14-21.5,27.2-21.5c8.5,0,16.1,3.8,21.3,9.8c4.5-3.1,9.9-4.9,15.8-4.9
c15.4,0,27.9,12.5,27.9,27.9c0,3.2-0.5,6.2-1.5,9.1c3.4,4.6,5.5,10.4,5.5,16.6c0,15.4-12.5,27.9-27.9,27.9c-6.8,0-13-2.4-17.8-6.4
c-5.1,7.1-13.4,11.8-22.8,11.8c-10.8,0-20.2-6.2-24.9-15.2c-1.9,0.4-3.8,0.6-5.8,0.6c-15.4,0-28-12.5-28-27.9s12.5-27.9,28-27.9
C55.6,210.5,56.6,210.5,57.6,210.7L57.6,210.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Portainer</title>
<meta name="description" content="" />
<base id="base" />
<!-- Fav and touch icons -->
<link rel="apple-touch-icon" sizes="180x180" href="./assets/ico/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="./assets/ico/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./assets/ico/favicon-16x16.png" />
<link rel="mask-icon" href="./assets/ico/safari-pinned-tab.svg" color="#5b" />
<link rel="shortcut icon" href="./assets/ico/favicon.ico" />
<meta name="msapplication-config" content="./assets/ico/browserconfig.xml" />
<meta name="theme-color" content="#ffffff" />
</head>
<body>
<div class="page-wrapper">
<!-- timeout info box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- simple box logo -->
<div class="row">
<img src="./assets/images/logo_alt.svg" class="simple-box-logo" alt="Portainer" />
</div>
<div class="panel panel-default">
<div class="panel-body">
<!-- toggle -->
<div style="padding-bottom: 24px">
<a>
<span style="padding-left: 10px">New Portainer installation</span>
</a>
</div>
<!-- init admin init timeout notification -->
<div class="simple-box" style="padding-left: 30px">
<div class="col-sm-12">
<span class="small text-muted" style="margin-left: 2px">
Your Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer.
</span>
<br /><br />
<span class="text-muted small" style="margin-left: 2px">
For further information, view our <a href="https://docs.portainer.io/v/ce-2.11/start/install" target="_blank">documentation</a>.
</span>
</div>
</div>
<!-- !init admin init timeout notification -->
</div>
</div>
</div>
</div>
<!-- !timeout info box -->
</div>
</body>
</html>

View File

@@ -1,14 +1,13 @@
package chisel
import (
"strconv"
portainer "github.com/portainer/portainer/api"
)
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
tunnel := service.GetTunnelDetails(endpointID)
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
@@ -24,24 +23,25 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
tunnel.Jobs[existingJobIndex] = *edgeJob
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.mu.Unlock()
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnelDetails := item.Val.(*portainer.TunnelDetails)
service.mu.Lock()
updatedJobs := make([]portainer.EdgeJob, 0)
for _, edgeJob := range tunnelDetails.Jobs {
if edgeJob.ID == edgeJobID {
continue
for _, tunnel := range service.tunnelDetailsMap {
// Filter in-place
n := 0
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
tunnel.Jobs[n] = edgeJob
n++
}
updatedJobs = append(updatedJobs, edgeJob)
}
tunnelDetails.Jobs = updatedJobs
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
tunnel.Jobs = tunnel.Jobs[:n]
}
service.mu.Unlock()
}

View File

@@ -3,17 +3,16 @@ package chisel
import (
"context"
"fmt"
"github.com/portainer/portainer/api/http/proxy"
"log"
"net/http"
"strconv"
"sync"
"time"
chserver "github.com/andres-portainer/chisel/server"
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
)
const (
@@ -28,18 +27,19 @@ const (
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap cmap.ConcurrentMap
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
mu sync.Mutex
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context) *Service {
return &Service{
tunnelDetailsMap: cmap.New(),
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
@@ -58,11 +58,7 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
Timeout: 3 * time.Second,
}
_, err = httpClient.Do(req)
if err != nil {
return err
}
return nil
return err
}
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
@@ -185,42 +181,37 @@ func (service *Service) startTunnelVerificationLoop() {
}
func (service *Service) checkTunnels() {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
service.mu.Lock()
for key, tunnel := range service.tunnelDetailsMap {
tunnels[key] = *tunnel
}
service.mu.Unlock()
for endpointID, tunnel := range tunnels {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
elapsed := time.Since(tunnel.LastActivity)
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", endpointID, tunnel.Status, elapsed.Seconds())
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
endpointID, err := strconv.Atoi(item.Key)
err := service.snapshotEnvironment(endpointID, tunnel.Port)
if err != nil {
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %d): %s", endpointID, err)
}
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err)
}
}
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
continue
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))

View File

@@ -4,13 +4,11 @@ import (
"encoding/base64"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/portainer/libcrypto"
"github.com/dchest/uniuri"
"github.com/portainer/libcrypto"
portainer "github.com/portainer/portainer/api"
)
@@ -19,13 +17,13 @@ const (
maxAvailablePort = 65535
)
// NOTE: it needs to be called with the lock acquired
// getUnusedPort is used to generate an unused random port in the dynamic port range.
// Dynamic ports (also called private ports) are 49152 to 65535.
func (service *Service) getUnusedPort() int {
port := randomInt(minAvailablePort, maxAvailablePort)
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
for _, tunnel := range service.tunnelDetailsMap {
if tunnel.Port == port {
return service.getUnusedPort()
}
@@ -38,26 +36,32 @@ func randomInt(min, max int) int {
return min + rand.Intn(max-min)
}
// NOTE: it needs to be called with the lock acquired
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
return tunnel
}
tunnel := &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
}
service.tunnelDetailsMap[endpointID] = tunnel
return tunnel
}
// GetTunnelDetails returns information about the tunnel associated to an environment(endpoint).
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
key := strconv.Itoa(int(endpointID))
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portainer.TunnelDetails {
service.mu.Lock()
defer service.mu.Unlock()
if item, ok := service.tunnelDetailsMap.Get(key); ok {
tunnelDetails := item.(*portainer.TunnelDetails)
return tunnelDetails
}
jobs := make([]portainer.EdgeJob, 0)
return &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
Port: 0,
Jobs: jobs,
Credentials: "",
}
return *service.getTunnelDetails(endpointID)
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
@@ -68,13 +72,13 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
return portainer.TunnelDetails{}, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
@@ -83,29 +87,27 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
return service.GetTunnelDetails(endpoint.ID), nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
tunnel := service.GetTunnelDetails(endpointID)
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentActive
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.mu.Unlock()
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
tunnel := service.GetTunnelDetails(endpointID)
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
tunnel.LastActivity = time.Now()
@@ -116,10 +118,9 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.ProxyManager.DeleteEndpointProxy(endpointID)
service.mu.Unlock()
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
@@ -128,7 +129,10 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
tunnel := service.GetTunnelDetails(endpointID)
tunnel := service.getTunnelDetails(endpointID)
service.mu.Lock()
defer service.mu.Unlock()
if tunnel.Port == 0 {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
@@ -152,9 +156,6 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
return err
}
tunnel.Credentials = credentials
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
return nil

View File

@@ -47,7 +47,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
HTTPEnabled: kingpin.Flag("http-enabled", "Serve portainer on http").Default(defaultHTTPEnabled).Bool(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
SSLCacert: kingpin.Flag("sslcacert", "Path to the SSL CA certificate used to validate the edge agent cert").String(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),

View File

@@ -18,7 +18,6 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCacertPath = "/certs/portainer-ca.crt"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultBaseURL = "/"

View File

@@ -15,7 +15,6 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCacertPath = "C:\\certs\\portainer-ca.crt"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"

View File

@@ -1,7 +1,9 @@
package main
import (
"fmt"
"log"
"strings"
"github.com/sirupsen/logrus"
)
@@ -10,14 +12,30 @@ type portainerFormatter struct {
logrus.TextFormatter
}
func (f *portainerFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var levelColor int
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
levelColor = 31 // gray
case logrus.WarnLevel:
levelColor = 33 // yellow
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
levelColor = 31 // red
default:
levelColor = 36 // blue
}
return []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m %s %s\n", levelColor, strings.ToUpper(entry.Level.String()), entry.Time.Format(f.TimestampFormat), entry.Message)), nil
}
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
formatterLogrus := &portainerFormatter{logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true, TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true}}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)
logrus.SetFormatter(formatterLogrus)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)

View File

@@ -208,7 +208,7 @@ func initGitService() portainer.GitService {
return git.NewService()
}
func initSSLService(addr, dataPath, certPath, keyPath, cacertPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
@@ -217,7 +217,7 @@ func initSSLService(addr, dataPath, certPath, keyPath, cacertPath string, fileSe
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
err := sslService.Init(host, certPath, keyPath, cacertPath)
err := sslService.Init(host, certPath, keyPath)
if err != nil {
return nil, err
}
@@ -568,7 +568,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, *flags.SSLCacert, fileService, dataStore, shutdownTrigger)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
logrus.Fatal(err)
}
@@ -599,7 +599,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
@@ -706,7 +706,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeConfigService: kubeConfigService,
KubeClusterAccessService: kubeClusterAccessService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
@@ -716,7 +716,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
BaseURL: *flags.BaseURL,
}
}

View File

@@ -29,8 +29,12 @@ type Connection interface {
GetNextIdentifier(bucketName string) int
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
CreateObjectWithId(bucketName string, id int, obj interface{}) error
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
ConvertToKey(v int) []byte
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
}

View File

@@ -10,9 +10,9 @@ import (
"path"
"time"
"github.com/boltdb/bolt"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
const (
@@ -181,10 +181,7 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.Batch(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return nil
return err
})
}
@@ -314,6 +311,19 @@ func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, ob
})
}
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(id, data)
})
}
// CreateObjectWithSetSequence creates a new object in the bucket, using the specified id, and sets the bucket sequence
// avoid this :)
func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error {
@@ -376,3 +386,43 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
})
return err
}
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
bucket = tx.Bucket([]byte(bucketName))
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
var err error
for bucketName, v := range s {
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
if !ok {
logrus.Errorf("Failed to restore metadata to bucket %s, skipped", bucketName)
continue
}
err = connection.Batch(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return bucket.SetSequence(uint64(id))
})
}
return err
}

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"time"
"github.com/boltdb/bolt"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
// inspired by github.com/konoui/boltdb-exporter (which has no license)

View File

@@ -2,6 +2,7 @@ package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)

View File

@@ -22,23 +22,18 @@ func (store *Store) Init() error {
return err
}
err = store.checkOrCreateDefaultData()
if err != nil {
return err
}
return nil
return store.checkOrCreateDefaultData()
}
func (store *Store) checkOrCreateInstanceID() error {
instanceID, err := store.VersionService.InstanceID()
_, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
instanceID := uid.String()
return store.VersionService.StoreInstanceID(instanceID)
}
return err

View File

@@ -369,6 +369,7 @@ type storeExport struct {
User []portainer.User `json:"users,omitempty"`
Version map[string]string `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (store *Store) Export(filename string) (err error) {
@@ -561,6 +562,11 @@ func (store *Store) Export(filename string) (err error) {
"INSTANCE_ID": instance,
}
backup.Metadata, err = store.connection.BackupMetadata()
if err != nil {
logrus.WithError(err).Errorf("Exporting Metadata")
}
b, err := json.MarshalIndent(backup, "", " ")
if err != nil {
return err
@@ -569,6 +575,7 @@ func (store *Store) Export(filename string) (err error) {
}
func (store *Store) Import(filename string) (err error) {
backup := storeExport{}
s, err := ioutil.ReadFile(filename)
@@ -669,5 +676,5 @@ func (store *Store) Import(filename string) (err error) {
store.Webhook().UpdateWebhook(v.ID, &v)
}
return nil
return store.connection.RestoreMetadata(backup.Metadata)
}

View File

@@ -60,8 +60,6 @@ const (
DefaultSSLCertFilename = "cert.pem"
// DefaultSSLKeyFilename represents the default ssl key file name
DefaultSSLKeyFilename = "key.pem"
// DefaultSSLCacertFilename represents the default CA ssl certificate file name for mTLS
DefaultSSLCacertFilename = "ca-cert.pem"
)
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
@@ -163,7 +161,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
}
if !exists {
return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath))
return errors.New("File doesn't exist")
}
finput, err := os.Open(fromFilePath)
@@ -354,9 +352,6 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
// GetFileContent returns the content of a file as bytes.
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
if trustedRoot == "" {
trustedRoot = "/"
}
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
if err != nil {
if filePath == "" {
@@ -632,25 +627,6 @@ func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, strin
return defCertPath, defKeyPath, nil
}
// GetDefaultSSLCacertsPath returns the ssl cacert path
func (service *Service) GetDefaultSSLCacertsPath() string {
cacertPath := JoinPaths(SSLCertPath, DefaultSSLCacertFilename)
return service.wrapFileStore(cacertPath)
}
// CopySSLCacert copies the specified cacert pem file
func (service *Service) CopySSLCacert(cacertPath string) (string, error) {
defCacertPath := service.GetDefaultSSLCacertsPath()
err := service.Copy(cacertPath, defCacertPath, true)
if err != nil {
return "", err
}
return defCacertPath, nil
}
// FileExists checks for the existence of the specified file.
func FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {

View File

@@ -4,11 +4,11 @@ go 1.17
require (
github.com/Microsoft/go-winio v0.4.17
github.com/andres-portainer/chisel v1.7.8-0.20220314202502-97e2b32f6bd8
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/aws/aws-sdk-go-v2 v1.11.1
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
github.com/boltdb/bolt v1.3.1
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
@@ -19,30 +19,27 @@ require (
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v4.0.0+incompatible
github.com/google/gops v0.3.22
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-version v1.4.0
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.11
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840
github.com/portainer/docker-compose-wrapper v0.0.0-20220225003350-cec58db3549e
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-20211208103139-07a5f798eb3f
github.com/prometheus/client_golang v1.7.1
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
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.8
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
@@ -54,9 +51,6 @@ require (
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
@@ -64,8 +58,6 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
@@ -74,15 +66,11 @@ require (
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.1.0 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
@@ -92,13 +80,10 @@ require (
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 // indirect
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 // indirect
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 // indirect
github.com/jpillora/ansi v1.0.2 // indirect
github.com/jpillora/requestlog v1.0.0 // indirect
github.com/jpillora/sizestr v1.0.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
@@ -109,9 +94,6 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.10.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
@@ -120,12 +102,11 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.7 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect

View File

@@ -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,14 +62,9 @@ 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/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -82,6 +75,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/andres-portainer/chisel v1.7.8-0.20220314202502-97e2b32f6bd8 h1:jyKZnBKYNRl6TmNokn7Rp5YGr5f/NnabahCcAZ5NoSY=
github.com/andres-portainer/chisel v1.7.8-0.20220314202502-97e2b32f6bd8/go.mod h1:KmC2waRLjHvJCPI2QPlzWcuretdka631DNOFLNx7PR4=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
@@ -112,7 +107,6 @@ github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
@@ -120,17 +114,13 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
@@ -307,8 +297,9 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
@@ -344,23 +335,13 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
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-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
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.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
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.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/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=
@@ -424,8 +405,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gops v0.3.22 h1:lyvhDxfPLHAOR2xIYwjPhN387qHxyU21Sk9sz/GhmhQ=
github.com/google/gops v0.3.22/go.mod h1:7diIdLsqpCihPSX3fQagksT/Ku/y4RL9LHTlKyEUDl8=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -456,8 +435,9 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -468,8 +448,6 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@@ -497,17 +475,13 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
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=
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389 h1:K3JsoRqX6C4gmTvY4jqtFGCfK8uToj9DMahciJaoWwE=
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389/go.mod h1:wHQUFFnFySoqdAOzjHkTvb4DsVM1h/73PS9l2vnioRM=
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 h1:7ufdyC3aMxFcCv+ABZy/dmIVGKFoGNBCqOgLYPIckD8=
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM=
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s=
github.com/jpillora/ansi v1.0.2/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA=
github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -519,7 +493,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -545,15 +518,12 @@ 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=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
@@ -633,12 +603,6 @@ github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -649,8 +613,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840 h1:Nciddt8Y8G8nTMmyDfWxeN23PZUcsqbZE2zOFB/F1xg=
github.com/portainer/docker-compose-wrapper v0.0.0-20220113045708-6569596db840/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/docker-compose-wrapper v0.0.0-20220225003350-cec58db3549e h1:wLnlzAXeVkjkZxuc81nEVUukl52fSEPUlOsUItrTK10=
github.com/portainer/docker-compose-wrapper v0.0.0-20220225003350-cec58db3549e/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
@@ -663,20 +627,17 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -688,7 +649,6 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777 h1:rDj3WeO+TiWyxfcydUnKegWAZoR5kQsnW0wzhggdOrw=
@@ -703,7 +663,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/shirou/gopsutil/v3 v3.21.9/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
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=
@@ -716,7 +675,6 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -747,14 +705,10 @@ 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.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
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=
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
@@ -764,7 +718,8 @@ 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/viney-shih/go-lock v1.1.1 h1:SwzDPPAiHpcwGCr5k8xD15d2gQSo8d4roRYd7TDV2eI=
github.com/viney-shih/go-lock v1.1.1/go.mod h1:Yijm78Ljteb3kRiJrbLAxVntkUukGu5uzSxq/xV7OO8=
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=
@@ -785,17 +740,17 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -809,7 +764,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -821,8 +775,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -853,13 +809,10 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/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=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -892,10 +845,11 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -908,6 +862,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -916,7 +871,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -966,6 +920,7 @@ golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -975,18 +930,20 @@ golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7w
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=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1040,8 +997,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.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
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=
@@ -1145,7 +1100,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=
@@ -1205,7 +1159,6 @@ k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g=
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=

View File

@@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"os"
"regexp"
"strconv"
@@ -271,14 +272,19 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
if err != nil {
return nil, err
}
isValidProject := true
defer func() {
if !isValidProject {
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}
}()
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
exists, err := handler.FileService.FileExists(entryPath)
if err != nil || !exists {
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
}
isValidProject = false
}
if err != nil {
@@ -289,6 +295,16 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
}
info, err := os.Lstat(entryPath)
if err != nil {
isValidProject = false
return nil, err
}
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
isValidProject = false
return nil, errors.New("Invalid Compose file, ensure that the Compose file is not a symbolic link")
}
return customTemplate, nil
}

View File

@@ -1,393 +0,0 @@
package endpointedge
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"sync"
"time"
version "github.com/hashicorp/go-version"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/sirupsen/logrus"
)
// https://datatracker.ietf.org/doc/html/rfc6902
// Doing this manually, because at this point, i don't want to marshal to json, make a diff - for now, just using 'add' (as its really an upsert)
type JSONPatch struct {
Operation string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}
// TODO: copied from edgestack_status_update
type updateStatusPayload struct {
Error string
Status *portainer.EdgeStackStatusType
EndpointID *portainer.EndpointID
}
type edgeJobResponse struct {
// EdgeJob Identifier
ID portainer.EdgeJobID `json:"Id" example:"2"`
// Whether to collect logs
CollectLogs bool `json:"CollectLogs" example:"true"`
// A cron expression to schedule this job
CronExpression string `json:"CronExpression" example:"* * * * *"`
// Script to run
Script string `json:"Script" example:"echo hello"`
// Version of this EdgeJob
Version int `json:"Version" example:"2"`
}
// An empty request ~~ just a ping.
type Snapshot struct {
Docker *portainer.DockerSnapshot
Kubernetes *portainer.KubernetesSnapshot
}
type AsyncRequest struct {
CommandId string `json: optional`
Snapshot *Snapshot `json: optional` // todo
StackStatus map[portainer.EdgeStackID]updateStatusPayload
}
func (payload *AsyncRequest) Validate(r *http.Request) error {
// TODO:
return nil
}
type AsyncResponse struct {
CommandInterval time.Duration `json: optional`
PingInterval time.Duration `json: optional`
SnapshotInterval time.Duration `json: optional`
ServerCommandId string // should be easy to detect if its larger / smaller: this is the response that tells the agent there are new commands waiting for it
SendDiffSnapshotTime time.Time `json: optional` // might be optional
Commands []JSONPatch `json: optional` // todo
Status string // give the agent some idea if the server thinks its OK, or if it should STOP
}
// Yup, this should be environment specific, and not global
var lastcheckinStatusMutex sync.Mutex
// for testing with mTLS..:
//sven@p1:~/src/portainer/portainer$ curl -k --cacert ~/.config/portainer/certs/ca.pem --cert ~/.config/portainer/certs/agent-cert.pem --key ~/.config/portainer/certs/agent-key.pem -X POST --header "X-PortainerAgent-EdgeID: 7e2b0143-c511-43c3-844c-a7a91ab0bedc" --data '{"CommandId": "okok", "Snapshot": {}}' https://p1:9443/api/endpoints/edge/async/
//{"CommandInterval":0,"PingInterval":0,"SnapshotInterval":0,"ServerCommandId":"8888","SendDiffSnapshotTime":"0001-01-01T00:00:00Z","Commands":{}}
// @id endpointAsync
// @summary Get environment(endpoint) status
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
// @description **Access policy**: restricted only to Edge environments(endpoints) TODO: with mTLS cert....
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {object} AsyncResponse "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access environment(endpoint)"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /endpoints/edge/async/ [post]
func (handler *Handler) endpointAsync(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
// TODO: get endpointID from the mTLS cert info
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
logrus.WithField("portainer.PortainerAgentEdgeIDHeader", edgeIdentifier).Debug("missing agent edge id")
return &httperror.HandlerError{http.StatusInternalServerError, "missing Edge identifier", errors.New("missing Edge identifier")}
}
// TODO: if the mTLS certs are valid, and we don't have a matching environment registered, CREATE IT (and maybe untrusted...)
endpoint, err := handler.getEdgeEndpoint(edgeIdentifier)
if err != nil {
// TODO: if its a valid cert, or the user hasn't limited to mTLS / portainer set id, the
// create new untrusted environment
// portainer.HTTPResponseAgentPlatform tells us what platform it is too...
logrus.WithField("portainer.PortainerAgentEdgeIDHeader", edgeIdentifier).WithField("Agent Addr", r.RemoteAddr).Debug("edge id not found in existing endpoints!")
}
// if agent mTLS is on, drop the connection if the client cert isn't CA'd (or if its revoked)
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
// TODO: an assumption that needs testing, is that using the right EdgeId means we're also talking to the same DOckerd / Kube cluster.
// I suspect that without getting the Dockerd UUID, or the "by convention" kube uuid, we can't know if someone's re-used info which then would cause some kind of state flapping.
// Don't ask me what happens in non-async mode if you have more than one agent running - surely that would make the tunnel go boom?
requestLogger := logrus.
WithField("Agent Addr", r.RemoteAddr).
WithField("Agent Version", r.Header.Get(portainer.HTTPAgentVersionHeaderName)).
WithField("Agent PID", r.Header.Get(portainer.HTTPAgentPIDName)).
WithField("Agent EdgeID", r.Header.Get(portainer.PortainerAgentEdgeIDHeader)) //.
//WithField("Agent UniqueID", r.Header.Get(portainer.HTTPAgentUUIDHeaderName))
// Any request we can identify as coming from a valid agent is treated as a Ping
endpoint.LastCheckInDate = time.Now().Unix()
endpoint.Status = portainer.EndpointStatusUp
// TODO: update endpoint contact time
lastcheckinStatusMutex.Lock()
if endpoint.AgentHistory == nil {
endpoint.AgentHistory = make(map[string]portainer.AgentInfo)
}
info, ok := endpoint.AgentHistory[r.RemoteAddr]
if !ok {
info = portainer.AgentInfo{
LastCheckInDate: endpoint.LastCheckInDate,
Version: r.Header.Get(portainer.HTTPAgentVersionHeaderName),
RemoteAddr: r.RemoteAddr,
Status: "OK",
}
}
info.CheckInCount = info.CheckInCount + 1
info.Status = "OK"
info.LastCheckInDate = endpoint.LastCheckInDate
requestLogger.Debugf("Checkin count = %d", info.CheckInCount)
endpoint.AgentHistory[r.RemoteAddr] = info
// Determine if there's more than one active agent, and if so, tell them to STOP
okCount := 0
infoVersion, _ := version.NewVersion(info.Version)
for key, val := range endpoint.AgentHistory {
timeToLastCheckIn := time.Second * time.Duration(endpoint.LastCheckInDate-val.LastCheckInDate)
// Timeout for last best agent (currently based on version number)
if timeToLastCheckIn > time.Second*time.Duration(endpoint.EdgeCheckinInterval*100) {
delete(endpoint.AgentHistory, key)
continue
}
if timeToLastCheckIn > time.Second*time.Duration(endpoint.EdgeCheckinInterval*10) {
val.Status = "GONE"
endpoint.AgentHistory[key] = val
continue
}
if timeToLastCheckIn > time.Second*time.Duration(endpoint.EdgeCheckinInterval*2) {
val.Status = "TROUBLED"
endpoint.AgentHistory[key] = val
continue
}
if val.Status == "OK" {
// if there's a info.Version difference, choose the more up to date agent...
valVersion, _ := version.NewVersion(val.Version)
if infoVersion.GreaterThan(valVersion) {
val.Status = "STOP VERSION - " + info.Version
endpoint.AgentHistory[val.RemoteAddr] = val
continue
}
if valVersion.GreaterThan(infoVersion) {
info.Status = "STOP VERSION - " + val.Version
endpoint.AgentHistory[info.RemoteAddr] = info
info = val
continue
}
// TODO: UX question - trade off between user expectation on upgrade, vs stability at staeday state.
// if we have two or more otherwise just as good agents, pick the newer one, on the presumption that it was started on purpose
// the risk with this is the flapping you get if you gave two identical agents with --restart always - so maybe it should get tuned
// for eg, only use the younger one if the user has initiated an upgrade? otherwise prefer the old?
if val.CheckInCount < info.CheckInCount {
info.Status = "STOP AGE"
endpoint.AgentHistory[info.RemoteAddr] = info
info = val
continue
}
okCount++
}
}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
}
lastcheckinStatusMutex.Unlock()
var payload AsyncRequest
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
// an "" request ~~ same as {}
requestLogger.WithError(err).WithField("payload", r).Debug("decode payload")
}
//if endpoint.AgentHistory[r.RemoteAddr].Status != "OK" {
// okCount of zero can happen - basically, we havn't timed out a newer agent's last contact, and so we're hoping it will come back, so we refuse the old version
// we could instead allow this, cos in some circumstances, it may be better to have more than one agent giving the user control
requestLogger.Debugf("Checkin STATUS = %s (okCount = %d)", endpoint.AgentHistory[r.RemoteAddr].Status, okCount)
//}
asyncResponse := AsyncResponse{
ServerCommandId: "8888", // the most current id of a new command on the server
Status: endpoint.AgentHistory[r.RemoteAddr].Status,
}
// TODO: need a way to detect that these are changed, and send them to the agent...
// CommandInterval time.Duration `json: optional`
// PingInterval time.Duration `json: optional`
// SnapshotInterval time.Duration `json: optional`
if payload.CommandId == "" && payload.Snapshot == nil {
// just a ping.
return response.JSON(w, asyncResponse)
}
if payload.Snapshot != nil {
asyncResponse.SendDiffSnapshotTime = handler.saveSnapshot(requestLogger, endpoint, payload)
}
if payload.CommandId != "" {
asyncResponse.Commands = handler.sendCommandsSince(requestLogger, endpoint, payload.CommandId)
}
return response.JSON(w, asyncResponse)
}
// TODO: yup, next step is for these to be JSONDiff's and to be rehydrated
func (handler *Handler) saveSnapshot(requestLogger *logrus.Entry, endpoint *portainer.Endpoint, payload AsyncRequest) time.Time {
for stackID, status := range payload.StackStatus {
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
// TODO: work out what we can do with the errors
if err == nil {
stack.Status[*status.EndpointID] = portainer.EdgeStackStatus{
Type: *status.Status,
Error: status.Error,
EndpointID: *status.EndpointID,
}
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
}
}
switch endpoint.Type {
// case portainer.AzureEnvironment:
// return time.Now()
case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment:
requestLogger.Debug("Got a Kubernetes Snapshot")
endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*payload.Snapshot.Kubernetes}
return time.Unix(payload.Snapshot.Kubernetes.Time, 0)
case portainer.DockerEnvironment, portainer.AgentOnDockerEnvironment, portainer.EdgeAgentOnDockerEnvironment:
requestLogger.Debug("Got a Docker Snapshot")
endpoint.Snapshots = []portainer.DockerSnapshot{*payload.Snapshot.Docker}
return time.Unix(payload.Snapshot.Docker.Time, 0)
default:
return time.Time{}
}
}
func (handler *Handler) sendCommandsSince(requestLogger *logrus.Entry, endpoint *portainer.Endpoint, commandId string) []JSONPatch {
var commandList []JSONPatch
// TODO: later, figure out if it is scalable to do diff's, as it means the server needs to store what it sent to all million agents (if the database had time based versioning, this would be trivial...)
// I suspect the easiest thing will be to add a "modified timestamp" to edge stacks and edge jobs, and to send them only when the modified time > requested time
requestLogger.WithField("endpoint", endpoint.Name).WithField("from command", commandId).Debug("Sending commands")
// schedules := []edgeJobResponse{}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
for _, job := range tunnel.Jobs {
schedule := edgeJobResponse{
ID: job.ID,
CronExpression: job.CronExpression,
CollectLogs: job.Endpoints[endpoint.ID].CollectLogs,
Version: job.Version,
}
file, err := handler.FileService.GetFileContent("/", job.ScriptPath)
if err != nil {
// TODO: this should maybe just skip thi job?
requestLogger.WithError(err).Error("Unable to retrieve Edge job script file")
continue
}
schedule.Script = base64.RawStdEncoding.EncodeToString(file)
cmd := JSONPatch{
Operation: "add",
Path: fmt.Sprintf("/edgejob/%d", schedule.ID),
Value: schedule,
}
commandList = append(commandList, cmd)
}
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
requestLogger.WithError(err).Error("Unable to retrieve relation object from the database")
return commandList
}
// TODO: this is the datatype the agent uses in the end
type edgeStackData struct {
ID portainer.EdgeStackID
Version int
StackFileContent string
Name string
}
for stackID := range relation.EdgeStacks {
stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
if err != nil {
requestLogger.WithError(err).Error("Unable to retrieve edge stack from the database")
continue
}
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if handler.DataStore.IsErrObjectNotFound(err) {
requestLogger.WithError(err).Error("Unable to find an edge stack with the specified identifier inside the database")
continue
} else if err != nil {
requestLogger.WithError(err).Error("Unable to find an edge stack with the specified identifier inside the database")
continue
}
fileName := edgeStack.EntryPoint
if endpointutils.IsDockerEndpoint(endpoint) {
if fileName == "" {
requestLogger.Error("Docker is not supported by this stack")
continue
}
}
if endpointutils.IsKubernetesEndpoint(endpoint) {
fileName = edgeStack.ManifestPath
if fileName == "" {
requestLogger.Error("Kubernetes is not supported by this stack")
continue
}
}
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
if err != nil {
requestLogger.WithError(err).Error("Unable to retrieve Compose file from disk")
continue
}
stackStatus := edgeStackData{
StackFileContent: string(stackFileContent),
Name: edgeStack.Name,
ID: stack.ID,
Version: stack.Version,
}
cmd := JSONPatch{
Operation: "add",
Path: fmt.Sprintf("/edgestack/%d", stack.ID),
Value: stackStatus,
}
commandList = append(commandList, cmd)
}
return commandList
}
// TODO: this probably should be in the data layer.. (like, somewhere that depends dataservices/errors)
func (handler *Handler) getEdgeEndpoint(edgeIdentifier string) (*portainer.Endpoint, error) {
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return nil, err
}
for _, endpoint := range endpoints {
if endpoint.EdgeID == edgeIdentifier {
return &endpoint, nil
}
}
return nil, err
}

View File

@@ -31,7 +31,5 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
h.Handle("/{id}/edge/jobs/{jobID}/logs",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
h.Handle("/edge/async/",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointAsync))).Methods(http.MethodPost)
return h
}

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/portainer/libhttp/request"
@@ -80,6 +81,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
totalAvailableEndpoints := len(filteredEndpoints)
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
@@ -124,9 +126,11 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
if paginatedEndpoints[idx].EdgeCheckinInterval == 0 {
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
paginatedEndpoints[idx].QueryDate = time.Now().Unix()
}
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
w.Header().Set("X-Total-Available", strconv.Itoa(totalAvailableEndpoints))
return response.JSON(w, paginatedEndpoints)
}

View File

@@ -43,7 +43,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint)
// TODO: so huh? why are we getting the endpoint a second time? if there's a reason to do this - please add that as a comment!
latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
if latestEndpointReference == nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}

View File

@@ -11,7 +11,6 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
type stackStatusResponse struct {
@@ -49,7 +48,6 @@ type endpointStatusInspectResponse struct {
Stacks []stackStatusResponse `json:"stacks"`
}
// TODO: first up, why is this not in ../endpointedge/???
// @id EndpointStatusInspect
// @summary Get environment(endpoint) status
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
@@ -72,7 +70,6 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
logrus.WithError(err).WithField("env", endpointID).WithField("remote", r.RemoteAddr).Error("Unable to find an environment with the specified identifier inside the database")
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}

View File

@@ -21,7 +21,6 @@ import (
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/metrics"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@@ -78,7 +77,6 @@ type Handler struct {
UserHandler *users.Handler
WebSocketHandler *websocket.Handler
WebhookHandler *webhooks.Handler
MetricsHandler *metrics.Handler
}
// @title PortainerCE API
@@ -243,10 +241,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/storybook"):
http.StripPrefix("/storybook", h.StorybookHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/metrics"):
if h.MetricsHandler != nil {
h.MetricsHandler.ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/"):
h.FileHandler.ServeHTTP(w, r)
}

View File

@@ -2,6 +2,7 @@ package helm
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
@@ -14,10 +15,6 @@ import (
"github.com/portainer/portainer/api/kubernetes"
)
const (
handlerActivityContext = "Kubernetes"
)
type requestBouncer interface {
AuthenticatedAccess(h http.Handler) http.Handler
}
@@ -25,24 +22,24 @@ type requestBouncer interface {
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
type Handler struct {
*mux.Router
requestBouncer requestBouncer
dataStore dataservices.DataStore
jwtService dataservices.JWTService
kubeConfigService kubernetes.KubeConfigService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
requestBouncer requestBouncer
dataStore dataservices.DataStore
jwtService dataservices.JWTService
kubeClusterAccessService kubernetes.KubeClusterAccessService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer requestBouncer, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
func NewHandler(bouncer requestBouncer, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeClusterAccessService kubernetes.KubeClusterAccessService) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
dataStore: dataStore,
jwtService: jwtService,
kubernetesDeployer: kubernetesDeployer,
helmPackageManager: helmPackageManager,
kubeConfigService: kubeConfigService,
Router: mux.NewRouter(),
requestBouncer: bouncer,
dataStore: dataStore,
jwtService: jwtService,
kubernetesDeployer: kubernetesDeployer,
helmPackageManager: helmPackageManager,
kubeClusterAccessService: kubeClusterAccessService,
}
h.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
@@ -104,10 +101,20 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
return nil, &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
}
kubeConfigInternal := handler.kubeConfigService.GetKubeConfigInternal(endpoint.ID, bearerToken)
sslSettings, err := handler.dataStore.SSLSettings().Settings()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = strings.Split(r.Host, ":")[0]
}
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
return &options.KubernetesClusterAccess{
ClusterServerURL: kubeConfigInternal.ClusterServerURL,
CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile,
AuthToken: kubeConfigInternal.AuthToken,
AuthToken: bearerToken,
}, nil
}

View File

@@ -36,8 +36,8 @@ func Test_helmDelete(t *testing.T) {
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")

View File

@@ -39,8 +39,8 @@ func Test_helmInstall(t *testing.T) {
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")

View File

@@ -38,8 +38,8 @@ func Test_helmList(t *testing.T) {
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeConfigService)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}

View File

@@ -2,6 +2,7 @@ package kubernetes
import (
"errors"
"github.com/portainer/portainer/api/kubernetes"
"net/http"
"github.com/gorilla/mux"
@@ -17,21 +18,22 @@ import (
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
type Handler struct {
*mux.Router
dataStore dataservices.DataStore
kubernetesClientFactory *cli.ClientFactory
authorizationService *authorization.Service
JwtService dataservices.JWTService
BaseURL string
authorizationService *authorization.Service
dataStore dataservices.DataStore
jwtService dataservices.JWTService
kubernetesClientFactory *cli.ClientFactory
kubeClusterAccessService kubernetes.KubeClusterAccessService
}
// NewHandler creates a handler to process pre-proxied requests to external APIs.
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, kubernetesClientFactory *cli.ClientFactory, baseURL string) *Handler {
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubeClusterAccessService kubernetes.KubeClusterAccessService, kubernetesClientFactory *cli.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dataStore: dataStore,
kubernetesClientFactory: kubernetesClientFactory,
authorizationService: authorizationService,
BaseURL: baseURL,
Router: mux.NewRouter(),
authorizationService: authorizationService,
dataStore: dataStore,
jwtService: jwtService,
kubeClusterAccessService: kubeClusterAccessService,
kubernetesClientFactory: kubernetesClientFactory,
}
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()

View File

@@ -39,7 +39,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
bearerToken, err := handler.jwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
@@ -126,7 +126,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
instanceID := handler.kubernetesClientFactory.GetInstanceID()
serviceAccountName := kcli.UserServiceAccountName(int(tokenData.ID), instanceID)
configClusters[idx] = buildCluster(r, handler.BaseURL, endpoint)
configClusters[idx] = handler.buildCluster(r, endpoint)
configContexts[idx] = buildContext(serviceAccountName, endpoint)
if !authInfosSet[serviceAccountName] {
configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccountName, bearerToken))
@@ -144,15 +144,13 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
}, nil
}
func buildCluster(r *http.Request, baseURL string, endpoint portainer.Endpoint) clientV1.NamedCluster {
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
proxyURL := fmt.Sprintf("https://%s%sapi/endpoints/%d/kubernetes", r.Host, baseURL, endpoint.ID)
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
hostURL := strings.Split(r.Host, ":")[0]
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{
Server: proxyURL,
Server: kubeConfigInternal.ClusterServerURL,
InsecureSkipTLSVerify: true,
},
}

View File

@@ -1,33 +0,0 @@
package metrics
import (
"github.com/google/gops/agent"
"github.com/gorilla/mux"
"github.com/portainer/portainer/api/http/security"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)
// Handler is the HTTP handler used to handle Prometheus metrics operations.
type Handler struct {
*mux.Router
}
// NewHandler creates a handler to manage settings operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/metrics", promhttp.Handler())
// h.Handle("/metrics", bouncer.PublicAccess(promhttp.Handler()))
logrus.Debugf("metricsHandler creation")
// also add gops agent support
if err := agent.Listen(agent.Options{}); err != nil {
logrus.WithError(err).Debugf("failed to start gops agent")
} else {
logrus.Debug("started gops agent")
}
return h
}

View File

@@ -14,9 +14,9 @@ import (
type resourceControlCreatePayload struct {
//
ResourceID string `example:"617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" validate:"required"`
// Type of Docker resource. Valid values are: container, volume\
// service, secret, config or stack
Type string `example:"container" validate:"required"`
// Type of Resource. Valid values are: 1 - container, 2 - service
// 3 - volume, 4 - network, 5 - secret, 6 - stack, 7 - config, 8 - custom template, 9 - azure-container-group
Type portainer.ResourceControlType `example:"1" validate:"required" enums:"1,2,3,4,5,6,7,8,9"`
// Permit access to the associated resource to any user
Public bool `example:"true"`
// Permit access to resource only to admins
@@ -39,8 +39,8 @@ func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
return errors.New("invalid payload: invalid resource identifier")
}
if govalidator.IsNull(payload.Type) {
return errors.New("invalid payload: invalid type")
if payload.Type <= 0 || payload.Type >= 10 {
return errors.New("invalid payload: Invalid type value. Value must be one of: 1 - container, 2 - service, 3 - volume, 4 - network, 5 - secret, 6 - stack, 7 - config, 8 - custom template, 9 - azure-container-group")
}
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
@@ -75,29 +75,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
var resourceControlType portainer.ResourceControlType
switch payload.Type {
case "container":
resourceControlType = portainer.ContainerResourceControl
case "container-group":
resourceControlType = portainer.ContainerGroupResourceControl
case "service":
resourceControlType = portainer.ServiceResourceControl
case "volume":
resourceControlType = portainer.VolumeResourceControl
case "network":
resourceControlType = portainer.NetworkResourceControl
case "secret":
resourceControlType = portainer.SecretResourceControl
case "stack":
resourceControlType = portainer.StackResourceControl
case "config":
resourceControlType = portainer.ConfigResourceControl
default:
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", errInvalidResourceControlType}
}
rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType)
rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(payload.ResourceID, payload.Type)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
}
@@ -126,7 +104,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
resourceControl := portainer.ResourceControl{
ResourceID: payload.ResourceID,
SubResourceIDs: payload.SubResourceIDs,
Type: resourceControlType,
Type: payload.Type,
Public: payload.Public,
AdministratorsOnly: payload.AdministratorsOnly,
UserAccesses: userAccesses,

View File

@@ -106,7 +106,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
@@ -177,7 +177,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
@@ -291,7 +291,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks"
)
@@ -34,15 +35,16 @@ type Handler struct {
stackDeletionMutex *sync.Mutex
requestBouncer *security.RequestBouncer
*mux.Router
DataStore dataservices.DataStore
DockerClientFactory *docker.ClientFactory
FileService portainer.FileService
GitService portainer.GitService
SwarmStackManager portainer.SwarmStackManager
ComposeStackManager portainer.ComposeStackManager
KubernetesDeployer portainer.KubernetesDeployer
Scheduler *scheduler.Scheduler
StackDeployer stacks.StackDeployer
DataStore dataservices.DataStore
DockerClientFactory *docker.ClientFactory
FileService portainer.FileService
GitService portainer.GitService
SwarmStackManager portainer.SwarmStackManager
ComposeStackManager portainer.ComposeStackManager
KubernetesDeployer portainer.KubernetesDeployer
KubernetesClientFactory *cli.ClientFactory
Scheduler *scheduler.Scheduler
StackDeployer stacks.StackDeployer
}
func stackExistsError(name string) *httperror.HandlerError {
@@ -148,6 +150,31 @@ func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name
return true, nil
}
func (handler *Handler) checkUniqueStackNameInKubernetes(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, namespace string) (bool, error) {
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
if err != nil {
return false, err
}
if !isUniqueStackName {
// Check if this stack name is really used in the kubernetes.
// Because the stack with this name could be removed via kubectl cli outside and the datastore does not be informed of this action.
if namespace == "" {
namespace = "default"
}
kubeCli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return false, err
}
isUniqueStackName, err = kubeCli.HasStackName(namespace, name)
if err != nil {
return false, err
}
}
return isUniqueStackName, nil
}
func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
if err != nil {

View File

@@ -123,15 +123,6 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
}
}
// Must not be git based stack. stop the auto update job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
stack.AutoUpdate = nil
}
if stack.GitConfig != nil {
stack.FromAppTemplate = true
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
if updateError != nil {
return updateError
@@ -171,6 +162,15 @@ func (handler *Handler) updateAndDeployStack(r *http.Request, stack *portainer.S
}
func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
// Must not be git based stack. stop the auto update job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
stack.AutoUpdate = nil
}
if stack.GitConfig != nil {
stack.FromAppTemplate = true
}
var payload updateComposeStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -199,6 +199,15 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
}
func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
// Must not be git based stack. stop the auto update job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
stack.AutoUpdate = nil
}
if stack.GitConfig != nil {
stack.FromAppTemplate = true
}
var payload updateSwarmStackPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {

View File

@@ -211,9 +211,6 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, end
}
case portainer.KubernetesStack:
if stack.Namespace == "" {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}

View File

@@ -3,69 +3,48 @@ package offlinegate
import (
"log"
"net/http"
"sync"
"strings"
"time"
httperror "github.com/portainer/libhttp/error"
lock "github.com/viney-shih/go-lock"
)
// OfflineGate is a entity that works similar to a mutex with a signaling
// Only the caller that have Locked an gate can unlock it, otherw will be blocked with a call to Lock.
// OfflineGate is an entity that works similar to a mutex with signaling
// Only the caller that has Locked a gate can unlock it, otherwise it will be blocked with a call to Lock.
// Gate provides a passthrough http middleware that will wait for a locked gate to be unlocked.
// For a safety reasons, middleware will timeout
// For safety reasons, the middleware will timeout
type OfflineGate struct {
lock *sync.Mutex
signalingCh chan interface{}
lock *lock.CASMutex
}
// NewOfflineGate creates a new gate
func NewOfflineGate() *OfflineGate {
return &OfflineGate{
lock: &sync.Mutex{},
lock: lock.NewCASMutex(),
}
}
// Lock locks readonly gate and returns a function to unlock
func (o *OfflineGate) Lock() func() {
o.lock.Lock()
o.signalingCh = make(chan interface{})
return o.unlock
}
func (o *OfflineGate) unlock() {
if o.signalingCh == nil {
return
}
close(o.signalingCh)
o.signalingCh = nil
o.lock.Unlock()
}
// Watch returns a signaling channel.
// Unless channel is nil, client needs to watch for a signal on a channel to know when gate is unlocked.
// Signal channel is disposable: onced signaled, has to be disposed and acquired again.
func (o *OfflineGate) Watch() chan interface{} {
return o.signalingCh
return o.lock.Unlock
}
// WaitingMiddleware returns an http handler that waits for the gate to be unlocked before continuing
func (o *OfflineGate) WaitingMiddleware(timeout time.Duration, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
signalingCh := o.Watch()
if signalingCh != nil {
if r.Method != "GET" && r.Method != "HEAD" && r.Method != "OPTIONS" {
select {
case <-signalingCh:
case <-time.After(timeout):
log.Println("error: Timeout waiting for the offline gate to signal")
httperror.WriteError(w, http.StatusRequestTimeout, "Timeout waiting for the offline gate to signal", http.ErrHandlerTimeout)
}
}
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" || strings.HasPrefix(r.URL.Path, "/api/backup") || strings.HasPrefix(r.URL.Path, "/api/restore") {
next.ServeHTTP(w, r)
return
}
if !o.lock.RTryLockWithTimeout(timeout) {
log.Println("error: Timeout waiting for the offline gate to signal")
httperror.WriteError(w, http.StatusRequestTimeout, "Timeout waiting for the offline gate to signal", http.ErrHandlerTimeout)
return
}
next.ServeHTTP(w, r)
o.lock.RUnlock()
})
}

View File

@@ -58,77 +58,6 @@ func Test_hasToBeUnlockedToLockAgain(t *testing.T) {
}
func Test_waitChannelWillBeEmpty_ifGateIsUnlocked(t *testing.T) {
o := NewOfflineGate()
signalingCh := o.Watch()
if signalingCh != nil {
t.Error("Signaling channel should be empty")
}
}
func Test_startWaitingForSignal_beforeGateGetsUnlocked(t *testing.T) {
// scenario:
// 1. main routing locks the gate and waits for a consumer to start up
// 2. consumer starts up, notifies main and begins waiting for the gate to be unlocked
// 3. main unlocks the gate
// 4. consumer be able to continue
o := NewOfflineGate()
unlock := o.Lock()
signalingCh := o.Watch()
wg := sync.WaitGroup{}
wg.Add(1)
readerIsReady := sync.WaitGroup{}
readerIsReady.Add(1)
go func(t *testing.T) {
readerIsReady.Done()
// either wait for a signal or timeout
select {
case <-signalingCh:
case <-time.After(10 * time.Second):
t.Error("Failed to wait for a signal, exit by timeout")
}
wg.Done()
}(t)
readerIsReady.Wait()
unlock()
wg.Wait()
}
func Test_startWaitingForSignal_afterGateGetsUnlocked(t *testing.T) {
// scenario:
// 1. main routing locks, gets waiting channel and unlocks
// 2. consumer starts up and begins waiting for the gate to be unlocked
// 3. consumer gets signal immediately and continues
o := NewOfflineGate()
unlock := o.Lock()
signalingCh := o.Watch()
unlock()
wg := sync.WaitGroup{}
wg.Add(1)
go func(t *testing.T) {
// either wait for a signal or timeout
select {
case <-signalingCh:
case <-time.After(10 * time.Second):
t.Error("Failed to wait for a signal, exit by timeout")
}
wg.Done()
}(t)
wg.Wait()
}
func Test_waitingMiddleware_executesImmediately_whenNotLocked(t *testing.T) {
// scenario:
// 1. create an gate

View File

@@ -272,6 +272,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
if match, _ := path.Match("/services/*/*", requestPath); match {
// Handle /services/{id}/{action} requests
serviceID := path.Base(path.Dir(requestPath))
transport.decorateRegistryAuthenticationHeader(request)
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
} else if match, _ := path.Match("/services/*", requestPath); match {
// Handle /services/{id} requests
@@ -396,9 +397,14 @@ func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Resp
}
func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) {
transport.decorateRegistryAuthenticationHeader(request)
return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl)
}
func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.Request) error {
accessContext, err := transport.createRegistryAccessContext(request)
if err != nil {
return nil, err
return err
}
originalHeader := request.Header.Get("X-Registry-Auth")
@@ -407,23 +413,23 @@ func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Re
decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
if err != nil {
return nil, err
return err
}
var originalHeaderData portainerRegistryAuthenticationHeader
err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
if err != nil {
return nil, err
return err
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, originalHeaderData.RegistryId, accessContext)
if err != nil {
return nil, err
return err
}
headerData, err := json.Marshal(authenticationHeader)
if err != nil {
return nil, err
return err
}
header := base64.StdEncoding.EncodeToString(headerData)
@@ -431,7 +437,7 @@ func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Re
request.Header.Set("X-Registry-Auth", header)
}
return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl)
return nil
}
func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, dockerResourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) {
@@ -492,7 +498,6 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
return utils.WriteAccessDeniedResponse()
}
}
return transport.executeDockerRequest(request)
}

View File

@@ -1,10 +1,8 @@
package security
import (
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
@@ -14,9 +12,6 @@ import (
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/sirupsen/logrus"
)
type (
@@ -42,21 +37,6 @@ type (
const apiKeyHeader = "X-API-KEY"
var (
apiProcessed = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "portainer_api_bouncer_results",
Help: "The total number of request access success/failures",
},
[]string{"permission", "path"},
)
agentApiProcessed = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "portainer_agent_api_bouncer_results",
Help: "The total number of agent request access success/failures",
},
[]string{"permission", "path"},
)
)
// NewRequestBouncer initializes a new RequestBouncer
func NewRequestBouncer(dataStore dataservices.DataStore, jwtService dataservices.JWTService, apiKeyService apikey.APIKeyService) *RequestBouncer {
return &RequestBouncer{
@@ -116,111 +96,58 @@ func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
tokenData, err := RetrieveTokenData(r)
if err != nil {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "error"}).Inc()
return err
}
if tokenData.Role == portainer.AdministratorRole {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "admin"}).Inc()
return nil
}
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
if err != nil {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "error"}).Inc()
return err
}
group, err := bouncer.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "error"}).Inc()
return err
}
if !authorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) {
apiProcessed.With(prometheus.Labels{"permission": "denied"}).Inc()
return httperrors.ErrEndpointAccessDenied
}
apiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "ok"}).Inc()
return nil
}
// AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge environment(endpoint)
func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
// tls.RequireAndVerifyClientCert would be nice, but that would require the same certs for browser and api use
sslsettings, _ := bouncer.dataStore.SSLSettings().Settings()
if sslsettings.CacertPath != "" {
// if a caCert is set, then reject any requests that don't have a client Auth cert signed with it
if len(r.TLS.PeerCertificates) == 0 {
logrus.Error("No clientAuth Agent certificate offered")
return errors.New("No clientAuth Agent certificate offered")
}
caChainIdx := len(r.TLS.VerifiedChains)
chainCaCert := r.TLS.VerifiedChains[0][caChainIdx]
caCert, _ := ioutil.ReadFile(sslsettings.CacertPath)
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
logrus.
WithField("chain Subject", chainCaCert.Subject.String()).
WithField("tls DNSNames", chainCaCert.DNSNames).
WithField("Agent Addr", r.RemoteAddr).
WithField("Agent Version", r.Header.Get(portainer.HTTPAgentVersionHeaderName)).
WithField("Agent PID", r.Header.Get(portainer.HTTPAgentPIDName)).
WithField("Agent EdgeID", r.Header.Get(portainer.PortainerAgentEdgeIDHeader)).
Debugf("TLS client chain")
opts := x509.VerifyOptions{
//DNSName: name, // Not normally used on server side - important on the client side
Roots: certPool, // as used in ListenAndServe
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
remoteCert := r.TLS.PeerCertificates[0]
if _, err := remoteCert.Verify(opts); err != nil {
logrus.WithError(err).Error("Agent certificate not signed by the CACert")
return errors.New("Agent certificate wasn't signed by required CA Cert")
}
// TODO: test revoke cert list.
}
if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_error"}).Inc()
return errors.New("Invalid environment type")
}
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_noiderror"}).Inc()
return errors.New("missing Edge identifier")
}
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_iderror"}).Inc()
return errors.New("invalid Edge identifier")
}
if endpoint.LastCheckInDate > 0 || endpoint.UserTrusted {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_ok"}).Inc()
return nil
}
settings, err := bouncer.dataStore.Settings().Settings()
if err != nil {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_error"}).Inc()
return fmt.Errorf("could not retrieve the settings: %w", err)
}
if settings.DisableTrustOnFirstConnect {
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_untrusted"}).Inc()
return errors.New("the device has not been trusted yet")
}
agentApiProcessed.With(prometheus.Labels{"path": r.RequestURI, "permission": "edge_ok"}).Inc()
return nil
}

View File

@@ -3,7 +3,6 @@ package http
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
@@ -35,7 +34,6 @@ import (
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/metrics"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@@ -63,7 +61,6 @@ import (
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/scheduler"
stackdeployer "github.com/portainer/portainer/api/stacks"
"github.com/sirupsen/logrus"
)
// Server implements the portainer.Server interface
@@ -90,7 +87,7 @@ type Server struct {
SwarmStackManager portainer.SwarmStackManager
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
KubeConfigService k8s.KubeConfigService
KubeClusterAccessService k8s.KubeClusterAccessService
Handler *handler.Handler
SSLService *ssl.Service
DockerClientFactory *docker.ClientFactory
@@ -101,7 +98,6 @@ type Server struct {
ShutdownCtx context.Context
ShutdownTrigger context.CancelFunc
StackDeployer stackdeployer.StackDeployer
BaseURL string
}
// Start starts the HTTP server
@@ -178,12 +174,11 @@ func (server *Server) Start() error {
endpointProxyHandler.ProxyManager = server.ProxyManager
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory, server.BaseURL)
kubernetesHandler.JwtService = server.JWTService
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeConfigService)
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)
@@ -224,6 +219,7 @@ func (server *Server) Start() error {
stackHandler.DataStore = server.DataStore
stackHandler.DockerClientFactory = server.DockerClientFactory
stackHandler.FileService = server.FileService
stackHandler.KubernetesClientFactory = server.KubernetesClientFactory
stackHandler.KubernetesDeployer = server.KubernetesDeployer
stackHandler.GitService = server.GitService
stackHandler.Scheduler = server.Scheduler
@@ -266,11 +262,6 @@ func (server *Server) Start() error {
webhookHandler.DataStore = server.DataStore
webhookHandler.DockerClientFactory = server.DockerClientFactory
var metricsHandler *metrics.Handler
if server.DataStore.Settings().IsFeatureFlagEnabled("dev-metrics") {
metricsHandler = metrics.NewHandler(requestBouncer)
}
server.Handler = &handler.Handler{
RoleHandler: roleHandler,
AuthHandler: authHandler,
@@ -307,11 +298,9 @@ func (server *Server) Start() error {
UserHandler: userHandler,
WebSocketHandler: websocketHandler,
WebhookHandler: webhookHandler,
MetricsHandler: metricsHandler,
}
handler := offlineGate.WaitingMiddleware(time.Minute, server.Handler)
handler := adminMonitor.WithRedirect(offlineGate.WaitingMiddleware(time.Minute, server.Handler))
if server.HTTPEnabled {
go func() {
log.Printf("[INFO] [http,server] [message: starting HTTP server on port %s]", server.BindAddress)
@@ -339,17 +328,6 @@ func (server *Server) Start() error {
return server.SSLService.GetRawCertificate(), nil
}
if caCert := server.SSLService.GetCacertificatePem(); len(caCert) > 0 {
logrus.Debugf("using CA certificate for %s", server.BindAddressHTTPS)
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
httpsServer.TLSConfig.ClientCAs = certPool
// can't use tls.RequireAndVerifyClientCert, and this port is also used for the browser (though it would be a strong feature to allow the user to enable)
httpsServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
httpsServer.TLSConfig.BuildNameToCertificate()
}
go shutdown(server.ShutdownCtx, httpsServer)
return httpsServer.ListenAndServeTLS("", "")
}

View File

@@ -15,7 +15,7 @@ import (
// specific Docker/Kubernetes environment(endpoint) snapshot methods.
type Service struct {
dataStore dataservices.DataStore
refreshSignal chan struct{}
snapshotIntervalCh chan time.Duration
snapshotIntervalInSeconds float64
dockerSnapshotter portainer.DockerSnapshotter
kubernetesSnapshotter portainer.KubernetesSnapshotter
@@ -24,14 +24,15 @@ type Service struct {
// NewService creates a new instance of a service
func NewService(snapshotIntervalFromFlag string, dataStore dataservices.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter, shutdownCtx context.Context) (*Service, error) {
snapshotFrequency, err := parseSnapshotFrequency(snapshotIntervalFromFlag, dataStore)
interval, err := parseSnapshotFrequency(snapshotIntervalFromFlag, dataStore)
if err != nil {
return nil, err
}
return &Service{
dataStore: dataStore,
snapshotIntervalInSeconds: snapshotFrequency,
snapshotIntervalCh: make(chan time.Duration),
snapshotIntervalInSeconds: interval,
dockerSnapshotter: dockerSnapshotter,
kubernetesSnapshotter: kubernetesSnapshotter,
shutdownCtx: shutdownCtx,
@@ -58,35 +59,17 @@ func parseSnapshotFrequency(snapshotInterval string, dataStore dataservices.Data
// Start will start a background routine to execute periodic snapshots of environments(endpoints)
func (service *Service) Start() {
if service.refreshSignal != nil {
return
}
service.refreshSignal = make(chan struct{})
service.startSnapshotLoop()
}
func (service *Service) Stop() {
if service.refreshSignal == nil {
return
}
// clear refreshSignal to mark the service as disabled
close(service.refreshSignal)
service.refreshSignal = nil
go service.startSnapshotLoop()
}
// SetSnapshotInterval sets the snapshot interval and resets the service
func (service *Service) SetSnapshotInterval(snapshotInterval string) error {
service.Stop()
snapshotFrequency, err := time.ParseDuration(snapshotInterval)
interval, err := time.ParseDuration(snapshotInterval)
if err != nil {
return err
}
service.snapshotIntervalInSeconds = snapshotFrequency.Seconds()
service.Start()
service.snapshotIntervalCh <- interval
return nil
}
@@ -140,34 +123,29 @@ func (service *Service) snapshotDockerEndpoint(endpoint *portainer.Endpoint) err
return nil
}
func (service *Service) startSnapshotLoop() error {
func (service *Service) startSnapshotLoop() {
ticker := time.NewTicker(time.Duration(service.snapshotIntervalInSeconds) * time.Second)
go func() {
err := service.snapshotEndpoints()
if err != nil {
log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (environment snapshot).] [error: %s]", err)
}
for {
select {
case <-ticker.C:
err := service.snapshotEndpoints()
if err != nil {
log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (environment snapshot).] [error: %s]", err)
}
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]")
ticker.Stop()
return
case <-service.refreshSignal:
log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]")
ticker.Stop()
return
err := service.snapshotEndpoints()
if err != nil {
log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (environment snapshot).] [error: %s]", err)
}
for {
select {
case <-ticker.C:
err := service.snapshotEndpoints()
if err != nil {
log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (environment snapshot).] [error: %s]", err)
}
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]")
ticker.Stop()
return
case interval := <-service.snapshotIntervalCh:
ticker.Reset(interval)
}
}()
return nil
}
}
func (service *Service) snapshotEndpoints() error {

View File

@@ -3,7 +3,6 @@ package ssl
import (
"context"
"crypto/tls"
"io/ioutil"
"log"
"os"
"time"
@@ -32,7 +31,7 @@ func NewService(fileService portainer.FileService, dataStore dataservices.DataSt
}
// Init initializes the service
func (service *Service) Init(host, certPath, keyPath, cacertPath string) error {
func (service *Service) Init(host, certPath, keyPath string) error {
pathSupplied := certPath != "" && keyPath != ""
if pathSupplied {
newCertPath, newKeyPath, err := service.fileService.CopySSLCertPair(certPath, keyPath)
@@ -40,19 +39,7 @@ func (service *Service) Init(host, certPath, keyPath, cacertPath string) error {
return errors.Wrap(err, "failed copying supplied certs")
}
newCacertPath := ""
if cacertPath != "" {
newCacertPath, err = service.fileService.CopySSLCacert(cacertPath)
if err != nil {
return errors.Wrap(err, "failed copying supplied cacert")
}
}
return service.cacheInfo(newCertPath, newKeyPath, newCacertPath, false)
}
if cacertPath != "" {
return errors.Errorf("supplying a CA cert path (%s) requires an SSL cert and key file", cacertPath)
return service.cacheInfo(newCertPath, newKeyPath, false)
}
settings, err := service.GetSSLSettings()
@@ -81,24 +68,10 @@ func (service *Service) Init(host, certPath, keyPath, cacertPath string) error {
return errors.Wrap(err, "failed generating self signed certs")
}
return service.cacheInfo(certPath, keyPath, "", true)
return service.cacheInfo(certPath, keyPath, true)
}
// GetRawCertificate gets the raw certificate
func (service *Service) GetCacertificatePem() (pemData []byte) {
settings, _ := service.GetSSLSettings()
if settings.CacertPath == "" {
return pemData
}
caCert, err := ioutil.ReadFile(settings.CacertPath)
if err != nil {
log.Printf("reading ca cert: %s", err)
return pemData
}
return caCert
}
// GetRawCertificate gets the raw certificate
func (service *Service) GetRawCertificate() *tls.Certificate {
return service.rawCert
@@ -125,13 +98,7 @@ func (service *Service) SetCertificates(certData, keyData []byte) error {
return err
}
settings, err := service.dataStore.SSLSettings().Settings()
if err != nil {
return err
}
// Don't unset the settings.CacertPath when uploading a new cert from the UI
// TODO: should also add UI to update thecacert, or to disable it..
service.cacheInfo(certPath, keyPath, settings.CacertPath, false)
service.cacheInfo(certPath, keyPath, false)
service.shutdownTrigger()
@@ -160,7 +127,6 @@ func (service *Service) SetHTTPEnabled(httpEnabled bool) error {
return nil
}
//TODO: why is this being cached in memory? is it actually loaded more than once?
func (service *Service) cacheCertificate(certPath, keyPath string) error {
rawCert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
@@ -172,7 +138,7 @@ func (service *Service) cacheCertificate(certPath, keyPath string) error {
return nil
}
func (service *Service) cacheInfo(certPath, keyPath, cacertPath string, selfSigned bool) error {
func (service *Service) cacheInfo(certPath, keyPath string, selfSigned bool) error {
err := service.cacheCertificate(certPath, keyPath)
if err != nil {
return err
@@ -185,7 +151,6 @@ func (service *Service) cacheInfo(certPath, keyPath, cacertPath string, selfSign
settings.CertPath = certPath
settings.KeyPath = keyPath
settings.CacertPath = cacertPath
settings.SelfSigned = selfSigned
err = service.dataStore.SSLSettings().UpdateSettings(settings)

View File

@@ -1,23 +0,0 @@
package testhelpers
import portainer "github.com/portainer/portainer/api"
type ReverseTunnelService struct{}
func (r ReverseTunnelService) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
return nil
}
func (r ReverseTunnelService) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
return "nil"
}
func (r ReverseTunnelService) SetTunnelStatusToActive(endpointID portainer.EndpointID) {}
func (r ReverseTunnelService) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
return nil
}
func (r ReverseTunnelService) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {}
func (r ReverseTunnelService) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
return nil
}
func (r ReverseTunnelService) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
}
func (r ReverseTunnelService) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {}

View File

@@ -0,0 +1,22 @@
package cli
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
)
// HasStackName checks whether the given name is used in the given namespace.
func (kcl *KubeClient) HasStackName(namespace string, stackName string) (bool, error) {
querySet := labels.Set{"io.portainer.kubernetes.application.stack": stackName}
listOpts := metav1.ListOptions{LabelSelector: labels.SelectorFromSet(querySet).String()}
list, err := kcl.cli.AppsV1().Deployments(namespace).List(context.TODO(), listOpts)
if err != nil {
return false, err
}
if len(list.Items) > 0 {
return false, nil
}
return true, nil
}

View File

@@ -7,26 +7,27 @@ import (
"fmt"
"io/ioutil"
"log"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
// KubeConfigService represents a service that is responsible for handling kubeconfig operations
type KubeConfigService interface {
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
type KubeClusterAccessService interface {
IsSecure() bool
GetKubeConfigInternal(endpointId portainer.EndpointID, authToken string) kubernetesClusterAccess
GetData(hostURL string, endpointId portainer.EndpointID) kubernetesClusterAccessData
}
// KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data
type kubernetesClusterAccess struct {
type kubernetesClusterAccessData struct {
ClusterServerURL string `example:"https://mycompany.k8s.com"`
CertificateAuthorityFile string `example:"/data/tls/localhost.crt"`
CertificateAuthorityData string `example:"MIIC5TCCAc2gAwIBAgIJAJ+...+xuhOaFXwQ=="`
AuthToken string `example:"ey..."`
}
type kubeConfigCAService struct {
type kubeClusterAccessService struct {
baseURL string
httpsBindAddr string
certificateAuthorityFile string
certificateAuthorityData string
@@ -39,14 +40,15 @@ var (
errTLSCertValidation = errors.New("failed to parse tls certificate")
)
// NewKubeConfigCAService encapsulates generation of core KubeConfig data
func NewKubeConfigCAService(httpsBindAddr string, tlsCertPath string) KubeConfigService {
// NewKubeClusterAccessService creates a new instance of a KubeClusterAccessService
func NewKubeClusterAccessService(baseURL, httpsBindAddr, tlsCertPath string) KubeClusterAccessService {
certificateAuthorityData, err := getCertificateAuthorityData(tlsCertPath)
if err != nil {
log.Printf("[DEBUG] [internal,kubeconfig] [message: %s, generated KubeConfig will be insecure]", err.Error())
}
return &kubeConfigCAService{
return &kubeClusterAccessService{
baseURL: baseURL,
httpsBindAddr: httpsBindAddr,
certificateAuthorityFile: tlsCertPath,
certificateAuthorityData: certificateAuthorityData,
@@ -82,23 +84,27 @@ func getCertificateAuthorityData(tlsCertPath string) (string, error) {
// this is based on the fact that we can successfully extract `certificateAuthorityData` from
// certificate file at `tlsCertPath`. If we can successfully extract `certificateAuthorityData`,
// then this will be used as `certificate-authority-data` attribute in a generated KubeConfig.
func (kccas *kubeConfigCAService) IsSecure() bool {
return kccas.certificateAuthorityData != ""
func (service *kubeClusterAccessService) IsSecure() bool {
return service.certificateAuthorityData != ""
}
// GetKubeConfigInternal returns K8s cluster access details for the specified environment(endpoint).
// On startup, portainer generates a certificate against localhost at specified `httpsBindAddr` port, hence
// the kubeconfig generated should only be utilised by internal portainer binaries as the `ClusterServerURL`
// points to the internally accessible `https` based `localhost` address.
// GetData returns K8s cluster access details for the specified environment(endpoint).
// The struct can be used to:
// - generate a kubeconfig file
// - pass down params to binaries
func (kccas *kubeConfigCAService) GetKubeConfigInternal(endpointId portainer.EndpointID, authToken string) kubernetesClusterAccess {
clusterServerUrl := fmt.Sprintf("https://localhost%s/api/endpoints/%s/kubernetes", kccas.httpsBindAddr, fmt.Sprint(endpointId))
return kubernetesClusterAccess{
ClusterServerURL: clusterServerUrl,
CertificateAuthorityFile: kccas.certificateAuthorityFile,
CertificateAuthorityData: kccas.certificateAuthorityData,
AuthToken: authToken,
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
baseURL := service.baseURL
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
clusterURL := hostURL + service.httpsBindAddr + baseURL
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
return kubernetesClusterAccessData{
ClusterServerURL: clusterServerURL,
CertificateAuthorityFile: service.certificateAuthorityFile,
CertificateAuthorityData: service.certificateAuthorityData,
}
}

View File

@@ -78,11 +78,11 @@ func Test_getCertificateAuthorityData(t *testing.T) {
})
}
func TestKubeConfigService_IsSecure(t *testing.T) {
func TestKubeClusterAccessService_IsSecure(t *testing.T) {
is := assert.New(t)
t.Run("IsSecure should be false", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
kcs := NewKubeClusterAccessService("", "", "")
is.False(kcs.IsSecure(), "should be false if TLS cert not provided")
})
@@ -90,39 +90,32 @@ func TestKubeConfigService_IsSecure(t *testing.T) {
filePath, teardown := createTempFile("valid-cert.crt", certData)
defer teardown()
kcs := NewKubeConfigCAService("", filePath)
kcs := NewKubeClusterAccessService("", "", filePath)
is.True(kcs.IsSecure(), "should be true if valid TLS cert (path and content) provided")
})
}
func TestKubeConfigService_GetKubeConfigInternal(t *testing.T) {
func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
is := assert.New(t)
t.Run("GetKubeConfigInternal returns localhost address", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "https://localhost"), "should contain localhost address")
t.Run("GetData contains host address", func(t *testing.T) {
kcs := NewKubeClusterAccessService("/", "", "")
clusterAccessDetails := kcs.GetData("mysite.com", 1)
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "https://mysite.com"), "should contain host address")
})
t.Run("GetKubeConfigInternal contains https bind address port", func(t *testing.T) {
kcs := NewKubeConfigCAService(":1010", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, ":1010"), "should contain bind address port")
})
t.Run("GetKubeConfigInternal contains environment proxy url", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(100, "some-token")
t.Run("GetData contains environment proxy url", func(t *testing.T) {
kcs := NewKubeClusterAccessService("/", "", "")
clusterAccessDetails := kcs.GetData("mysite.com", 100)
is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "api/endpoints/100/kubernetes"), "should contain environment proxy url")
})
t.Run("GetKubeConfigInternal returns insecure cluster access config", func(t *testing.T) {
kcs := NewKubeConfigCAService("", "")
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
t.Run("GetData returns insecure cluster access config", func(t *testing.T) {
kcs := NewKubeClusterAccessService("/", ":9443", "")
clusterAccessDetails := kcs.GetData("mysite.com", 1)
wantClusterAccessDetails := kubernetesClusterAccess{
ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes",
AuthToken: "some-token",
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
CertificateAuthorityFile: "",
CertificateAuthorityData: "",
}
@@ -130,16 +123,15 @@ func TestKubeConfigService_GetKubeConfigInternal(t *testing.T) {
is.Equal(clusterAccessDetails, wantClusterAccessDetails)
})
t.Run("GetKubeConfigInternal returns secure cluster access config", func(t *testing.T) {
t.Run("GetData returns secure cluster access config", func(t *testing.T) {
filePath, teardown := createTempFile("valid-cert.crt", certData)
defer teardown()
kcs := NewKubeConfigCAService("", filePath)
clusterAccessDetails := kcs.GetKubeConfigInternal(1, "some-token")
kcs := NewKubeClusterAccessService("/", "", filePath)
clusterAccessDetails := kcs.GetData("localhost", 1)
wantClusterAccessDetails := kubernetesClusterAccess{
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes",
AuthToken: "some-token",
CertificateAuthorityFile: filePath,
CertificateAuthorityData: certDataString,
}

View File

@@ -116,7 +116,6 @@ type (
HTTPDisabled *bool
HTTPEnabled *bool
SSL *bool
SSLCacert *string
SSLCert *string
SSLKey *string
Rollback *bool
@@ -326,6 +325,8 @@ type (
AMTDeviceGUID string `json:"AMTDeviceGUID,omitempty" example:"4c4c4544-004b-3910-8037-b6c04f504633"`
// LastCheckInDate mark last check-in date on checkin
LastCheckInDate int64
// QueryDate of each query with the endpoints list
QueryDate int64
// IsEdgeDevice marks if the environment was created as an EdgeDevice
IsEdgeDevice bool
// Whether the device has been trusted or not by the user
@@ -344,17 +345,6 @@ type (
// Deprecated in DBVersion == 22
Tags []string `json:"Tags"`
AgentHistory map[string]AgentInfo
}
AgentInfo struct {
LastCheckInDate int64
CheckInCount int64
Version string
RemoteAddr string
Status string
//UniqueId string
}
// EndpointAuthorizations represents the authorizations associated to a set of environments(endpoints)
@@ -849,7 +839,6 @@ type (
SSLSettings struct {
CertPath string `json:"certPath"`
KeyPath string `json:"keyPath"`
CacertPath string `json:"cacertPath"`
SelfSigned bool `json:"selfSigned"`
HTTPEnabled bool `json:"httpEnabled"`
}
@@ -1249,7 +1238,6 @@ type (
GetDefaultSSLCertsPath() (string, string)
StoreSSLCertPair(cert, key []byte) (string, string, error)
CopySSLCertPair(certPath, keyPath string) (string, string, error)
CopySSLCacert(cacertPath string) (string, error)
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
}
@@ -1274,6 +1262,7 @@ type (
GetServiceAccountBearerToken(userID int) (string, error)
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
HasStackName(namespace string, stackName string) (bool, error)
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
GetNodesLimits() (K8sNodesLimits, error)
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
@@ -1319,8 +1308,8 @@ type (
SetTunnelStatusToRequired(endpointID EndpointID) error
SetTunnelStatusToIdle(endpointID EndpointID)
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
GetActiveTunnel(endpoint *Endpoint) (*TunnelDetails, error)
GetTunnelDetails(endpointID EndpointID) TunnelDetails
GetActiveTunnel(endpoint *Endpoint) (TunnelDetails, error)
AddEdgeJob(endpointID EndpointID, edgeJob *EdgeJob)
RemoveEdgeJob(edgeJobID EdgeJobID)
}
@@ -1333,7 +1322,6 @@ type (
// SnapshotService represents a service for managing environment(endpoint) snapshots
SnapshotService interface {
Start()
Stop()
SetSnapshotInterval(snapshotInterval string) error
SnapshotEndpoint(endpoint *Endpoint) error
}
@@ -1377,10 +1365,6 @@ const (
PortainerAgentKubernetesSATokenHeader = "X-PortainerAgent-SA-Token"
// PortainerAgentSignatureMessage represents the message used to create a digital signature
// to be used when communicating with an agent
HTTPAgentVersionHeaderName = "X-Portainer-Agent-Version"
HTTPAgentPIDName = "X-Portainer-Process-Id"
HTTPAgentUUIDHeaderName = "X-Portainer-Agent-UUID"
PortainerAgentSignatureMessage = "Portainer-App"
// DefaultSnapshotInterval represents the default interval between each environment snapshot job
DefaultSnapshotInterval = "5m"
@@ -1401,7 +1385,7 @@ const (
)
// List of supported features
var SupportedFeatureFlags = []Feature{"dev-metrics"}
var SupportedFeatureFlags = []Feature{}
const (
_ AuthenticationMethod = iota

View File

@@ -4,6 +4,7 @@ import (
"context"
"log"
"strconv"
"sync"
"time"
"github.com/pkg/errors"
@@ -14,6 +15,7 @@ import (
type Scheduler struct {
crontab *cron.Cron
activeJobs map[cron.EntryID]context.CancelFunc
mu sync.Mutex
}
func NewScheduler(ctx context.Context) *Scheduler {
@@ -45,11 +47,13 @@ func (s *Scheduler) Shutdown() error {
ctx := s.crontab.Stop()
<-ctx.Done()
s.mu.Lock()
for _, job := range s.crontab.Entries() {
if cancel, ok := s.activeJobs[job.ID]; ok {
cancel()
}
}
s.mu.Unlock()
err := ctx.Err()
if err == context.Canceled {
@@ -65,9 +69,12 @@ func (s *Scheduler) StopJob(jobID string) error {
return errors.Wrapf(err, "failed convert jobID %q to int", jobID)
}
entryID := cron.EntryID(id)
s.mu.Lock()
if cancel, ok := s.activeJobs[entryID]; ok {
cancel()
}
s.mu.Unlock()
return nil
}
@@ -87,7 +94,9 @@ func (s *Scheduler) StartJobEvery(duration time.Duration, job func() error) stri
entryID := s.crontab.Schedule(cron.Every(duration), j)
s.mu.Lock()
s.activeJobs[entryID] = cancel
s.mu.Unlock()
go func(entryID cron.EntryID) {
<-ctx.Done()

View File

@@ -2,6 +2,7 @@ package scheduler
import (
"context"
"errors"
"fmt"
"testing"
"time"
@@ -53,12 +54,15 @@ func Test_JobShouldStop_UponError(t *testing.T) {
defer s.Shutdown()
var acc int
ch := make(chan struct{})
s.StartJobEvery(jobInterval, func() error {
acc++
close(ch)
return fmt.Errorf("failed")
})
<-time.After(3 * jobInterval)
<-ch
assert.Equal(t, 1, acc, "job stop after the first run because it returns an error")
}
@@ -98,3 +102,19 @@ func Test_CanTerminateAllJobs_ByCancellingParentContext(t *testing.T) {
<-ctx.Done()
assert.False(t, workDone, "job shouldn't had a chance to run")
}
func Test_StartJobEvery_Concurrently(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
s := NewScheduler(ctx)
f := func() error {
return errors.New("error")
}
go s.StartJobEvery(jobInterval, f)
s.StartJobEvery(jobInterval, f)
cancel()
<-ctx.Done()
}

View File

@@ -0,0 +1 @@
export function loadProgressBar() {}

View File

@@ -0,0 +1,39 @@
import * as i18nextMocks from './i18next';
describe('mockT', () => {
it('should return correctly with no arguments', async () => {
const testText = `The company's new IT initiative, code named Phoenix Project, is critical to the
future of Parts Unlimited, but the project is massively over budget and very late. The CEO wants
Bill to report directly to him and fix the mess in ninety days or else Bill's entire department
will be outsourced.`;
const translatedText = i18nextMocks.mockT(testText);
expect(translatedText).toBe(testText);
});
test.each`
testText | args | expectedText
${'{{fileName}} is invalid.'} | ${{ fileName: 'example_5.csv' }} | ${'example_5.csv is invalid.'}
${'{{fileName}} {is}.'} | ${{ fileName: ' ' }} | ${' {is}.'}
${'{{number}} of {{total}}'} | ${{ number: 0, total: 999 }} | ${'0 of 999'}
${'There was an error:\n{{error}}'} | ${{ error: 'Failed' }} | ${'There was an error:\nFailed'}
${'Click:{{li}}{{li2}}{{li_3}}'} | ${{ li: '', li2: 'https://', li_3: '!@#$%' }} | ${'Click:https://!@#$%'}
${'{{happy}}😏y✔{{sad}}{{laugh}}'} | ${{ happy: '😃', sad: '😢', laugh: '🤣' }} | ${'😃😏y✔😢🤣'}
`(
'should return correctly while handling arguments in different scenarios',
({ testText, args, expectedText }) => {
const translatedText = i18nextMocks.mockT(testText, args);
expect(translatedText).toBe(expectedText);
}
);
});
describe('language', () => {
it('should return language', async () => {
const { language } = i18nextMocks.default;
expect(language).toBe('en');
});
});

36
app/__mocks__/i18next.ts Normal file
View File

@@ -0,0 +1,36 @@
function replaceBetween(
startIndex: number,
endIndex: number,
original: string,
insertion: string
) {
const result =
original.substring(0, startIndex) +
insertion +
original.substring(endIndex);
return result;
}
export function mockT(i18nKey: string, args?: Record<string, string>) {
let key = i18nKey;
while (key.includes('{{') && args) {
const startIndex = key.indexOf('{{');
const endIndex = key.indexOf('}}');
const currentArg = key.substring(startIndex + 2, endIndex);
const value = args[currentArg];
key = replaceBetween(startIndex, endIndex + 2, key, value);
}
return key;
}
const i18next: Record<string, unknown> = jest.createMockFromModule('i18next');
i18next.t = mockT;
i18next.language = 'en';
i18next.changeLanguage = () => new Promise(() => {});
i18next.use = () => i18next;
export default i18next;

View File

@@ -0,0 +1,16 @@
import { PropsWithChildren } from 'react';
import { mockT } from './i18next';
export function useTranslation() {
return {
t: mockT,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
}
export function Trans({ children }: PropsWithChildren<unknown>) {
return <>{children}</>;
}

View File

@@ -38,7 +38,7 @@ export function onStartupAngular($rootScope, $state, $interval, LocalStorage, En
function ping(EndpointProvider, SystemService) {
const endpoint = EndpointProvider.currentEndpoint();
if (endpoint !== undefined && endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
if (endpoint && endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
SystemService.ping(endpoint.Id);
}
}

View File

@@ -375,10 +375,6 @@ a[ng-click] {
background-color: var(--white-color) fff;
}
.pagination-controls {
margin-left: 10px;
}
.user-box {
margin-right: 25px;
}
@@ -832,6 +828,18 @@ json-tree .branch-preview {
align-items: center;
}
.space-x-2 > * + * {
margin-left: 0.5rem;
}
.space-x-3 > * + * {
margin-left: 0.75rem;
}
.space-x-4 > * + * {
margin-left: 1rem;
}
.space-y-8 > * + * {
margin-top: 2rem;
}

View File

@@ -222,33 +222,6 @@ json-tree .branch-preview {
background-color: var(--bg-progress-color);
}
.pagination > .disabled > span,
.pagination > .disabled > span:hover,
.pagination > .disabled > span:focus,
.pagination > .disabled > a,
.pagination > .disabled > a:hover,
.pagination > .disabled > a:focus {
color: var(--text-pagination-color);
background-color: var(--bg-pagination-color);
border-color: var(--border-pagination-color);
}
.pagination > li > a,
.pagination > li > span {
background-color: var(--bg-pagination-span-color);
border-color: var(--border-pagination-span-color);
color: var(--text-pagination-span-color);
}
.pagination > li > a:hover,
.pagination > li > span:hover,
.pagination > li > a:focus,
.pagination > li > span:focus {
background-color: var(--bg-pagination-hover-color);
border-color: var(--border-pagination-hover-color);
color: var(--text-pagination-span-hover-color);
}
.ui-select-bootstrap .ui-select-choices-row > span {
color: var(--text-ui-select-color);
}

View File

@@ -0,0 +1,34 @@
import { render, within } from '@/react-tools/test-utils';
import { AzureSidebar } from './AzureSidebar';
test('dashboard items should render correctly', () => {
const { getByLabelText } = renderComponent();
const dashboardItem = getByLabelText('Dashboard');
expect(dashboardItem).toBeVisible();
expect(dashboardItem).toHaveTextContent('Dashboard');
const dashboardItemElements = within(dashboardItem);
expect(dashboardItemElements.getByLabelText('itemIcon')).toBeVisible();
expect(dashboardItemElements.getByLabelText('itemIcon')).toHaveClass(
'fa-tachometer-alt',
'fa-fw'
);
const containerInstancesItem = getByLabelText('ContainerInstances');
expect(containerInstancesItem).toBeVisible();
expect(containerInstancesItem).toHaveTextContent('Container instances');
const containerInstancesItemElements = within(containerInstancesItem);
expect(
containerInstancesItemElements.getByLabelText('itemIcon')
).toBeVisible();
expect(containerInstancesItemElements.getByLabelText('itemIcon')).toHaveClass(
'fa-cubes',
'fa-fw'
);
});
function renderComponent() {
return render(<AzureSidebar environmentId={1} />);
}

View File

@@ -0,0 +1,36 @@
import { r2a } from '@/react-tools/react2angular';
import { SidebarMenuItem } from '@/portainer/components/sidebar/SidebarMenuItem';
import type { EnvironmentId } from '@/portainer/environments/types';
interface Props {
environmentId: EnvironmentId;
}
export function AzureSidebar({ environmentId }: Props) {
return (
<>
<SidebarMenuItem
path="azure.dashboard"
pathParams={{ endpointId: environmentId }}
iconClass="fa-tachometer-alt fa-fw"
className="sidebar-list"
itemName="Dashboard"
data-cy="azureSidebar-dashboard"
>
Dashboard
</SidebarMenuItem>
<SidebarMenuItem
path="azure.containerinstances"
pathParams={{ endpointId: environmentId }}
iconClass="fa-cubes fa-fw"
className="sidebar-list"
itemName="ContainerInstances"
data-cy="azureSidebar-containerInstances"
>
Container instances
</SidebarMenuItem>
</>
);
}
export const AzureSidebarAngular = r2a(AzureSidebar, ['environmentId']);

View File

@@ -0,0 +1 @@
export { AzureSidebar, AzureSidebarAngular } from './AzureSidebar';

View File

@@ -6,10 +6,10 @@ import { Input, Select } from '@/portainer/components/form-components/Input';
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
import { InputListError } from '@/portainer/components/form-components/InputList/InputList';
import { AccessControlForm } from '@/portainer/components/accessControlForm';
import { ContainerInstanceFormValues } from '@/azure/types';
import * as notifications from '@/portainer/services/notifications';
import { isAdmin, useUser } from '@/portainer/hooks/useUser';
import { useUser } from '@/portainer/hooks/useUser';
import { AccessControlForm } from '@/portainer/access-control/AccessControlForm';
import { validationSchema } from './CreateContainerInstanceForm.validation';
import { PortMapping, PortsMappingField } from './PortsMappingField';
@@ -29,19 +29,14 @@ export function CreateContainerInstanceForm() {
throw new Error('endpointId url param is required');
}
const { user } = useUser();
const isUserAdmin = isAdmin(user);
const { isAdmin } = useUser();
const { initialValues, isLoading, providers, subscriptions, resourceGroups } =
useLoadFormState(environmentId, isUserAdmin);
useLoadFormState(environmentId, isAdmin);
const router = useRouter();
const { mutateAsync } = useCreateInstance(
resourceGroups,
environmentId,
user?.Id
);
const { mutateAsync } = useCreateInstance(resourceGroups, environmentId);
if (isLoading) {
return null;
@@ -50,7 +45,7 @@ export function CreateContainerInstanceForm() {
return (
<Formik<ContainerInstanceFormValues>
initialValues={initialValues}
validationSchema={() => validationSchema(isUserAdmin)}
validationSchema={() => validationSchema(isAdmin)}
onSubmit={onSubmit}
validateOnMount
validateOnChange

View File

@@ -1,6 +1,6 @@
import { object, string, number, boolean } from 'yup';
import { validationSchema as accessControlSchema } from '@/portainer/components/accessControlForm/AccessControlForm.validation';
import { validationSchema as accessControlSchema } from '@/portainer/access-control/AccessControlForm/AccessControlForm.validation';
import { validationSchema as portsSchema } from './PortsMappingField.validation';

View File

@@ -8,8 +8,7 @@ import {
ContainerInstanceFormValues,
ResourceGroup,
} from '@/azure/types';
import { UserId } from '@/portainer/users/types';
import { applyResourceControl } from '@/portainer/resource-control/resource-control.service';
import { applyResourceControl } from '@/portainer/access-control/access-control.service';
import { getSubscriptionResourceGroups } from './utils';
@@ -17,8 +16,7 @@ export function useCreateInstance(
resourceGroups: {
[k: string]: ResourceGroup[];
},
environmentId: EnvironmentId,
userId?: UserId
environmentId: EnvironmentId
) {
const queryClient = useQueryClient();
return useMutation<ContainerGroup, unknown, ContainerInstanceFormValues>(
@@ -47,13 +45,13 @@ export function useCreateInstance(
},
{
async onSuccess(containerGroup, values) {
if (!userId) {
throw new Error('missing user id');
const resourceControl = containerGroup.Portainer?.ResourceControl;
if (!resourceControl) {
throw new PortainerError('resource control expected after creation');
}
const resourceControl = containerGroup.Portainer.ResourceControl;
const accessControlData = values.accessControl;
await applyResourceControl(userId, accessControlData, resourceControl);
await applyResourceControl(accessControlData, resourceControl);
queryClient.invalidateQueries(['azure', 'container-instances']);
},
}

View File

@@ -9,7 +9,7 @@ import { getResourceGroups } from '@/azure/services/resource-groups.service';
import { getSubscriptions } from '@/azure/services/subscription.service';
import { getContainerInstanceProvider } from '@/azure/services/provider.service';
import { ContainerInstanceFormValues, Subscription } from '@/azure/types';
import { parseFromResourceControl } from '@/portainer/components/accessControlForm/model';
import { parseAccessControlFormData } from '@/portainer/access-control/utils';
import {
getSubscriptionLocations,
@@ -58,7 +58,7 @@ export function useLoadFormState(
cpu: 1,
ports: [{ container: '80', host: '80', protocol: 'TCP' }],
allocatePublicIP: true,
accessControl: parseFromResourceControl(isUserAdmin),
accessControl: parseAccessControlFormData(isUserAdmin),
};
return {

View File

@@ -0,0 +1,144 @@
import { renderWithQueryClient, within } from '@/react-tools/test-utils';
import { UserContext } from '@/portainer/hooks/useUser';
import { UserViewModel } from '@/portainer/models/user';
import { server, rest } from '@/setup-tests/server';
import {
createMockResourceGroups,
createMockSubscriptions,
} from '@/react-tools/test-mocks';
import { DashboardView } from './DashboardView';
jest.mock('@uirouter/react', () => ({
...jest.requireActual('@uirouter/react'),
useCurrentStateAndParams: jest.fn(() => ({
params: { endpointId: 1 },
})),
}));
test('dashboard items should render correctly', async () => {
const { getByLabelText } = await renderComponent();
const subscriptionsItem = getByLabelText('Subscriptions');
expect(subscriptionsItem).toBeVisible();
const subscriptionElements = within(subscriptionsItem);
expect(subscriptionElements.getByLabelText('value')).toBeVisible();
expect(subscriptionElements.getByLabelText('icon')).toHaveClass('fa-th-list');
expect(subscriptionElements.getByLabelText('resourceType')).toHaveTextContent(
'Subscriptions'
);
const resourceGroupsItem = getByLabelText('Resource groups');
expect(resourceGroupsItem).toBeVisible();
const resourceGroupElements = within(resourceGroupsItem);
expect(resourceGroupElements.getByLabelText('value')).toBeVisible();
expect(resourceGroupElements.getByLabelText('icon')).toHaveClass(
'fa-th-list'
);
expect(
resourceGroupElements.getByLabelText('resourceType')
).toHaveTextContent('Resource groups');
});
test('when there are no subscriptions, should show 0 subscriptions and 0 resource groups', async () => {
const { getByLabelText } = await renderComponent();
const subscriptionElements = within(getByLabelText('Subscriptions'));
expect(subscriptionElements.getByLabelText('value')).toHaveTextContent('0');
const resourceGroupElements = within(getByLabelText('Resource groups'));
expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('0');
});
test('when there is subscription & resource group data, should display these', async () => {
const { getByLabelText } = await renderComponent(1, { 'subscription-1': 2 });
const subscriptionElements = within(getByLabelText('Subscriptions'));
expect(subscriptionElements.getByLabelText('value')).toHaveTextContent('1');
const resourceGroupElements = within(getByLabelText('Resource groups'));
expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('2');
});
test('should correctly show total number of resource groups across multiple subscriptions', async () => {
const { getByLabelText } = await renderComponent(2, {
'subscription-1': 2,
'subscription-2': 3,
});
const resourceGroupElements = within(getByLabelText('Resource groups'));
expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('5');
});
test('when only subscriptions fail to load, dont show the dashboard', async () => {
const { queryByLabelText } = await renderComponent(
1,
{ 'subscription-1': 1 },
500,
200
);
expect(queryByLabelText('Subscriptions')).not.toBeInTheDocument();
expect(queryByLabelText('Resource groups')).not.toBeInTheDocument();
});
test('when only resource groups fail to load, still show the subscriptions', async () => {
const { queryByLabelText } = await renderComponent(
1,
{ 'subscription-1': 1 },
200,
500
);
expect(queryByLabelText('Subscriptions')).toBeInTheDocument();
expect(queryByLabelText('Resource groups')).not.toBeInTheDocument();
});
async function renderComponent(
subscriptionsCount = 0,
resourceGroups: Record<string, number> = {},
subscriptionsStatus = 200,
resourceGroupsStatus = 200
) {
const user = new UserViewModel({ Username: 'user' });
const state = { user };
server.use(
rest.get(
'/api/endpoints/:endpointId/azure/subscriptions',
(req, res, ctx) =>
res(
ctx.json(createMockSubscriptions(subscriptionsCount)),
ctx.status(subscriptionsStatus)
)
),
rest.get(
'/api/endpoints/:endpointId/azure/subscriptions/:subscriptionId/resourcegroups',
(req, res, ctx) => {
if (typeof req.params.subscriptionId !== 'string') {
throw new Error("Provided subscriptionId must be of type: 'string'");
}
const { subscriptionId } = req.params;
return res(
ctx.json(
createMockResourceGroups(
req.params.subscriptionId,
resourceGroups[subscriptionId] || 0
)
),
ctx.status(resourceGroupsStatus)
);
}
)
);
const renderResult = renderWithQueryClient(
<UserContext.Provider value={state}>
<DashboardView />
</UserContext.Provider>
);
await expect(renderResult.findByText(/Home/)).resolves.toBeVisible();
return renderResult;
}

View File

@@ -0,0 +1,75 @@
import { useEffect } from 'react';
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
import { PageHeader } from '@/portainer/components/PageHeader';
import { DashboardItem } from '@/portainer/components/Dashboard/DashboardItem';
import { error as notifyError } from '@/portainer/services/notifications';
import PortainerError from '@/portainer/error';
import { r2a } from '@/react-tools/react2angular';
import { useResourceGroups, useSubscriptions } from '../queries';
export function DashboardView() {
const environmentId = useEnvironmentId();
const subscriptionsQuery = useSubscriptions(environmentId);
useEffect(() => {
if (subscriptionsQuery.isError) {
notifyError(
'Failure',
subscriptionsQuery.error as PortainerError,
'Unable to retrieve subscriptions'
);
}
}, [subscriptionsQuery.error, subscriptionsQuery.isError]);
const resourceGroupsQuery = useResourceGroups(
environmentId,
subscriptionsQuery.data
);
useEffect(() => {
if (resourceGroupsQuery.isError && resourceGroupsQuery.error) {
notifyError(
'Failure',
resourceGroupsQuery.error as PortainerError,
`Unable to retrieve resource groups`
);
}
}, [resourceGroupsQuery.error, resourceGroupsQuery.isError]);
const isLoading =
subscriptionsQuery.isLoading || resourceGroupsQuery.isLoading;
if (isLoading) {
return null;
}
const subscriptionsCount = subscriptionsQuery?.data?.length;
const resourceGroupsCount = Object.values(
resourceGroupsQuery?.resourceGroups
).flatMap((x) => Object.values(x)).length;
return (
<>
<PageHeader title="Home" breadcrumbs={[{ label: 'Dashboard' }]} />
{!subscriptionsQuery.isError && (
<div className="row">
<DashboardItem
value={subscriptionsCount as number}
icon="fa fa-th-list"
type="Subscriptions"
/>
{!resourceGroupsQuery.isError && (
<DashboardItem
value={resourceGroupsCount as number}
icon="fa fa-th-list"
type="Resource groups"
/>
)}
</div>
)}
</>
);
}
export const DashboardViewAngular = r2a(DashboardView, []);

View File

@@ -0,0 +1 @@
export { DashboardViewAngular, DashboardView } from './DashboardView';

View File

@@ -1,82 +1,87 @@
import angular from 'angular';
import { AzureSidebarAngular } from './AzureSidebar/AzureSidebar';
import { DashboardViewAngular } from './Dashboard/DashboardView';
import { containerInstancesModule } from './ContainerInstances';
angular.module('portainer.azure', ['portainer.app', containerInstancesModule]).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
angular
.module('portainer.azure', ['portainer.app', containerInstancesModule])
.config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
var azure = {
name: 'azure',
url: '/azure',
parent: 'endpoint',
abstract: true,
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, Notifications, StateManager) {
return $async(async () => {
if (endpoint.Type !== 3) {
$state.go('portainer.home');
return;
}
try {
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
await StateManager.updateEndpointState(endpoint);
} catch (e) {
Notifications.error('Failed loading environment', e);
$state.go('portainer.home', {}, { reload: true });
}
});
},
};
var containerInstances = {
name: 'azure.containerinstances',
url: '/containerinstances',
views: {
'content@': {
templateUrl: './views/containerinstances/containerinstances.html',
controller: 'AzureContainerInstancesController',
var azure = {
name: 'azure',
url: '/azure',
parent: 'endpoint',
abstract: true,
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, Notifications, StateManager) {
return $async(async () => {
if (endpoint.Type !== 3) {
$state.go('portainer.home');
return;
}
try {
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
await StateManager.updateEndpointState(endpoint, []);
} catch (e) {
Notifications.error('Failed loading environment', e);
$state.go('portainer.home', {}, { reload: true });
}
});
},
},
};
};
var containerInstance = {
name: 'azure.containerinstances.container',
url: '/:id',
views: {
'content@': {
component: 'containerInstanceDetails',
var containerInstances = {
name: 'azure.containerinstances',
url: '/containerinstances',
views: {
'content@': {
templateUrl: './views/containerinstances/containerinstances.html',
controller: 'AzureContainerInstancesController',
},
},
},
};
};
var containerInstanceCreation = {
name: 'azure.containerinstances.new',
url: '/new/',
views: {
'content@': {
component: 'createContainerInstanceView',
var containerInstance = {
name: 'azure.containerinstances.container',
url: '/:id',
views: {
'content@': {
component: 'containerInstanceDetails',
},
},
},
};
};
var dashboard = {
name: 'azure.dashboard',
url: '/dashboard',
views: {
'content@': {
templateUrl: './views/dashboard/dashboard.html',
controller: 'AzureDashboardController',
var containerInstanceCreation = {
name: 'azure.containerinstances.new',
url: '/new/',
views: {
'content@': {
component: 'createContainerInstanceView',
},
},
},
};
};
$stateRegistryProvider.register(azure);
$stateRegistryProvider.register(containerInstances);
$stateRegistryProvider.register(containerInstance);
$stateRegistryProvider.register(containerInstanceCreation);
$stateRegistryProvider.register(dashboard);
},
]);
var dashboard = {
name: 'azure.dashboard',
url: '/dashboard',
views: {
'content@': {
component: 'dashboardView',
},
},
};
$stateRegistryProvider.register(azure);
$stateRegistryProvider.register(containerInstances);
$stateRegistryProvider.register(containerInstance);
$stateRegistryProvider.register(containerInstanceCreation);
$stateRegistryProvider.register(dashboard);
},
])
.component('azureSidebar', AzureSidebarAngular)
.component('dashboardView', DashboardViewAngular);

View File

@@ -1,19 +0,0 @@
<sidebar-menu-item
path="azure.dashboard"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-tachometer-alt fa-fw"
class-name="sidebar-list"
data-cy="azureSidebar-dashboard"
>
Dashboard
</sidebar-menu-item>
<sidebar-menu-item
path="azure.containerinstances"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-cubes fa-fw"
class-name="sidebar-list"
data-cy="azureSidebar-containerInstances"
>
Container instances
</sidebar-menu-item>

View File

@@ -1,8 +0,0 @@
import angular from 'angular';
angular.module('portainer.azure').component('azureSidebar', {
templateUrl: './azure-sidebar.html',
bindings: {
endpointId: '<',
},
});

View File

@@ -1,5 +1,5 @@
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
export function ContainerGroupDefaultModel() {
this.Location = '';

70
app/azure/queries.ts Normal file
View File

@@ -0,0 +1,70 @@
import _ from 'lodash';
import { useQueries, useQuery } from 'react-query';
import { EnvironmentId } from '@/portainer/environments/types';
import { getResourceGroups } from './services/resource-groups.service';
import { getSubscriptions } from './services/subscription.service';
import { Subscription } from './types';
export function useSubscriptions(environmentId: EnvironmentId) {
return useQuery(
'azure.subscriptions',
() => getSubscriptions(environmentId),
{
meta: {
error: {
title: 'Failure',
message: 'Unable to retrieve Azure subscriptions',
},
},
}
);
}
export function useResourceGroups(
environmentId: EnvironmentId,
subscriptions: Subscription[] = []
) {
const queries = useQueries(
subscriptions.map((subscription) => ({
queryKey: [
'azure',
environmentId,
'subscriptions',
subscription.subscriptionId,
'resourceGroups',
],
queryFn: async () => {
const groups = await getResourceGroups(
environmentId,
subscription.subscriptionId
);
return [subscription.subscriptionId, groups] as const;
},
meta: {
error: {
title: 'Failure',
message: 'Unable to retrieve Azure resource groups',
},
},
}))
);
return {
resourceGroups: Object.fromEntries(
_.compact(
queries.map((q) => {
if (q.data) {
return q.data;
}
return null;
})
)
),
isLoading: queries.some((q) => q.isLoading),
isError: queries.some((q) => q.isError),
error: queries.find((q) => q.error)?.error || null,
};
}

View File

@@ -1,5 +1,7 @@
import { AccessControlFormData } from '@/portainer/components/accessControlForm/model';
import { ResourceControlResponse } from '@/portainer/models/resourceControl/resourceControl';
import {
AccessControlFormData,
ResourceControlResponse,
} from '@/portainer/access-control/types';
import { PortMapping } from './ContainerInstances/CreateContainerInstanceForm/PortsMappingField';

View File

@@ -123,8 +123,12 @@
</rd-widget-body>
</rd-widget>
</div>
<!-- access-control-panel -->
<por-access-control-panel ng-if="$ctrl.container" resource-id="$ctrl.container.Id" resource-control="$ctrl.container.ResourceControl" resource-type="'container-group'">
</por-access-control-panel>
<!-- !access-control-panel -->
<access-control-panel
ng-if="$ctrl.container"
resource-id="$ctrl.container.Id"
resource-control="$ctrl.container.ResourceControl"
resource-type="$ctrl.resourceType"
on-update-success="($ctrl.onUpdateSuccess)"
></access-control-panel>
</div>

View File

@@ -1,3 +1,5 @@
import { ResourceControlType } from '@/portainer/access-control/types';
class ContainerInstanceDetailsController {
/* @ngInject */
constructor($state, AzureService, ContainerGroupService, Notifications, ResourceGroupService, SubscriptionService) {
@@ -7,9 +9,16 @@ class ContainerInstanceDetailsController {
loading: false,
};
this.resourceType = ResourceControlType.ContainerGroup;
this.container = null;
this.subscription = null;
this.resourceGroup = null;
this.onUpdateSuccess = this.onUpdateSuccess.bind(this);
}
onUpdateSuccess() {
this.$state.reload();
}
async $onInit() {

View File

@@ -1,33 +0,0 @@
<rd-header>
<rd-header-title title-text="Home"></rd-header-title>
<rd-header-content>Dashboard</rd-header-content>
</rd-header>
<div class="row" ng-if="subscriptions">
<div class="col-sm-12 col-md-6">
<a ui-sref="azure.subscriptions">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-th-list"></i>
</div>
<div class="title">{{ subscriptions.length }}</div>
<div class="comment">Subscriptions</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
<div class="col-sm-12 col-md-6" ng-if="resourceGroups">
<a ui-sref="azure.resourceGroups">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-th-list"></i>
</div>
<div class="title">{{ resourceGroups.length }}</div>
<div class="comment">Resource groups</div>
</rd-widget-body>
</rd-widget>
</a>
</div>
</div>

View File

@@ -1,23 +0,0 @@
angular.module('portainer.azure').controller('AzureDashboardController', [
'$scope',
'AzureService',
'Notifications',
function ($scope, AzureService, Notifications) {
function initView() {
AzureService.subscriptions()
.then(function success(data) {
var subscriptions = data;
$scope.subscriptions = subscriptions;
return AzureService.resourceGroups(subscriptions);
})
.then(function success(data) {
$scope.resourceGroups = AzureService.aggregate(data);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load dashboard data');
});
}
initView();
},
]);

View File

@@ -15,6 +15,7 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
return LocalStorage.getJWT();
},
});
$httpProvider.interceptors.push('jwtInterceptor');
$httpProvider.interceptors.push('EndpointStatusInterceptor');
$httpProvider.defaults.headers.post['Content-Type'] = 'application/json';

View File

@@ -33,6 +33,7 @@ export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
export const BROWSER_OS_PLATFORM = navigator.userAgent.indexOf('Windows NT') > -1 ? 'win' : 'lin';
export const NEW_LINE_BREAKER = BROWSER_OS_PLATFORM === 'win' ? '\r\n' : '\n';
export const REDIRECT_REASON_TIMEOUT = 'AdminInitTimeout';
// don't declare new constants, either:
// - if only used in one file or module, declare in that file or module (as a regular js constant)
@@ -66,4 +67,5 @@ angular
.constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS)
.constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY)
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX)
.constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS);
.constant('PREDEFINED_NETWORKS', PREDEFINED_NETWORKS)
.constant('REDIRECT_REASON_TIMEOUT', REDIRECT_REASON_TIMEOUT);

View File

@@ -1,4 +1,4 @@
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
import { ResourceControlOwnership as RCO } from '@/portainer/access-control/types';
angular.module('portainer.docker').directive('networkRowContent', [
function networkRowContent() {

View File

@@ -30,8 +30,8 @@ import { ColumnVisibilityMenu } from '@/portainer/components/datatables/componen
import { useRepeater } from '@/portainer/components/datatables/components/useRepeater';
import { useDebounce } from '@/portainer/hooks/useDebounce';
import {
useSearchBarContext,
SearchBar,
useSearchBarState,
} from '@/portainer/components/datatables/components/SearchBar';
import type {
ContainersTableSettings,
@@ -63,7 +63,7 @@ export function ContainersDatatable({
}: ContainerTableProps) {
const { settings, setTableSettings } =
useTableSettings<ContainersTableSettings>();
const [searchBarValue, setSearchBarValue] = useSearchBarContext();
const [searchBarValue, setSearchBarValue] = useSearchBarState('containers');
const columns = useColumns();
@@ -100,6 +100,10 @@ export function ContainersDatatable({
isRowSelectable(row: Row<DockerContainer>) {
return !row.original.IsPortainer;
},
autoResetSelectedRows: false,
getRowId(originalRow: DockerContainer) {
return originalRow.Id;
},
selectCheckboxComponent: Checkbox,
},
useFilters,

Some files were not shown because too many files have changed in this diff Show More