Compare commits
65 Commits
feat/suppo
...
EE-2691-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0cb06f1d5 | ||
|
|
c3b2635aa4 | ||
|
|
03ac0c8aed | ||
|
|
4d698c532a | ||
|
|
4c57d40a24 | ||
|
|
71300d8811 | ||
|
|
c4bbecb3ae | ||
|
|
f031fc8965 | ||
|
|
c656573d83 | ||
|
|
752f92888b | ||
|
|
6cf0608254 | ||
|
|
410c4048bb | ||
|
|
16a03dad84 | ||
|
|
383d41b3ef | ||
|
|
adeda52b5f | ||
|
|
185e4cdfbc | ||
|
|
c486130a9f | ||
|
|
cf7746082b | ||
|
|
1ab65a4b4f | ||
|
|
a66e863646 | ||
|
|
d962c300f9 | ||
|
|
9aeedf1bfa | ||
|
|
98d8cd99fb | ||
|
|
226ffdcd20 | ||
|
|
78150a738f | ||
|
|
ecf5e90783 | ||
|
|
f63b07bbb9 | ||
|
|
07294c19bb | ||
|
|
f8cbb54ba5 | ||
|
|
f8fd28bb61 | ||
|
|
78f7cd0d6c | ||
|
|
9a42d4c506 | ||
|
|
f2c48409e0 | ||
|
|
5188ead870 | ||
|
|
f1ea2b5c02 | ||
|
|
b7d18ef50f | ||
|
|
20405e9803 | ||
|
|
0f3c7b1424 | ||
|
|
c442d936d3 | ||
|
|
0cd164bada | ||
|
|
ee42e44246 | ||
|
|
6695d75468 | ||
|
|
eb6cdf1229 | ||
|
|
a3b1466b96 | ||
|
|
8b7dcf20bf | ||
|
|
14ed6ed2a3 | ||
|
|
9f4549212d | ||
|
|
37209918ad | ||
|
|
aefa34d6d2 | ||
|
|
eaffde39f6 | ||
|
|
d71d291895 | ||
|
|
a894e3182a | ||
|
|
ff7847aaa5 | ||
|
|
a89c3773dd | ||
|
|
5d75ca34ea | ||
|
|
d47a9d590e | ||
|
|
bd679ae806 | ||
|
|
5de7ecb5f0 | ||
|
|
b3cd9c69df | ||
|
|
73311b6f32 | ||
|
|
93ddcfecd9 | ||
|
|
2bffba7371 | ||
|
|
37ca62eb06 | ||
|
|
fa208c7f2a | ||
|
|
6fac3fa127 |
@@ -1,4 +1,5 @@
|
||||
# prettier
|
||||
cf5056d9c03b62d91a25c3b9127caac838695f98
|
||||
|
||||
# prettier v2 (put here after fix/EE-2344/fix-eslint-issues is merged)
|
||||
# prettier v2
|
||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -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**
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/Custom.md
vendored
6
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -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/
|
||||
-->
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -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
|
||||
|
||||
15
.github/workflows/lint.yml
vendored
15
.github/workflows/lint.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/test-client.yaml
vendored
10
.github/workflows/test-client.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
BIN
api/adminmonitor/timeout/assets/ico/apple-touch-icon.png
Normal file
BIN
api/adminmonitor/timeout/assets/ico/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
api/adminmonitor/timeout/assets/ico/favicon-16x16.png
Normal file
BIN
api/adminmonitor/timeout/assets/ico/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 772 B |
BIN
api/adminmonitor/timeout/assets/ico/favicon-32x32.png
Normal file
BIN
api/adminmonitor/timeout/assets/ico/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
api/adminmonitor/timeout/assets/ico/favicon.ico
Normal file
BIN
api/adminmonitor/timeout/assets/ico/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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 |
60
api/adminmonitor/timeout/assets/images/logo_alt.svg
Normal file
60
api/adminmonitor/timeout/assets/images/logo_alt.svg
Normal 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 |
59
api/adminmonitor/timeout/timeout.html
Normal file
59
api/adminmonitor/timeout/timeout.html
Normal 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>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = "/"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ package fdoprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
45
api/go.mod
45
api/go.mod
@@ -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
|
||||
|
||||
119
api/go.sum
119
api/go.sum
@@ -41,8 +41,6 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@@ -64,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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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("", "")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {}
|
||||
22
api/kubernetes/cli/deploment.go
Normal file
22
api/kubernetes/cli/deploment.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
1
app/__mocks__/axios-progress-bar.ts
Normal file
1
app/__mocks__/axios-progress-bar.ts
Normal file
@@ -0,0 +1 @@
|
||||
export function loadProgressBar() {}
|
||||
39
app/__mocks__/i18next.test.ts
Normal file
39
app/__mocks__/i18next.test.ts
Normal 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
36
app/__mocks__/i18next.ts
Normal 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;
|
||||
16
app/__mocks__/react-i18next.tsx
Normal file
16
app/__mocks__/react-i18next.tsx
Normal 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}</>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
34
app/azure/AzureSidebar/AzureSidebar.test.tsx
Normal file
34
app/azure/AzureSidebar/AzureSidebar.test.tsx
Normal 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} />);
|
||||
}
|
||||
36
app/azure/AzureSidebar/AzureSidebar.tsx
Normal file
36
app/azure/AzureSidebar/AzureSidebar.tsx
Normal 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']);
|
||||
1
app/azure/AzureSidebar/index.ts
Normal file
1
app/azure/AzureSidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AzureSidebar, AzureSidebarAngular } from './AzureSidebar';
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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']);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
144
app/azure/Dashboard/DashboardView.test.tsx
Normal file
144
app/azure/Dashboard/DashboardView.test.tsx
Normal 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;
|
||||
}
|
||||
75
app/azure/Dashboard/DashboardView.tsx
Normal file
75
app/azure/Dashboard/DashboardView.tsx
Normal 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, []);
|
||||
1
app/azure/Dashboard/index.ts
Normal file
1
app/azure/Dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DashboardViewAngular, DashboardView } from './DashboardView';
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.azure').component('azureSidebar', {
|
||||
templateUrl: './azure-sidebar.html',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
},
|
||||
});
|
||||
@@ -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
70
app/azure/queries.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
},
|
||||
]);
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user