Compare commits
28 Commits
debug-api-
...
feat/ce-22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d3e4632ba | ||
|
|
7122ba3cdc | ||
|
|
67f30b3b3a | ||
|
|
83fd6aa225 | ||
|
|
bb7fbeb36c | ||
|
|
61850e1421 | ||
|
|
f8f95ba7f1 | ||
|
|
c085889468 | ||
|
|
bd0e285edd | ||
|
|
4df571aaa4 | ||
|
|
00ae1289f9 | ||
|
|
020687c443 | ||
|
|
a181db9882 | ||
|
|
74cc099260 | ||
|
|
0cccfb540c | ||
|
|
9c32ad2ff6 | ||
|
|
ec3a2e1cfa | ||
|
|
eab6282fae | ||
|
|
d72571b2f8 | ||
|
|
9a32718db6 | ||
|
|
50bcc589ca | ||
|
|
031a096f8b | ||
|
|
5b8dd37d58 | ||
|
|
313c69775a | ||
|
|
0290b837e6 | ||
|
|
d2974c91d0 | ||
|
|
0fd1d59bd8 | ||
|
|
779f880609 |
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -34,5 +34,3 @@ jobs:
|
||||
prettier_dir: app/
|
||||
gofmt: true
|
||||
gofmt_dir: api/
|
||||
- name: Typecheck
|
||||
uses: icrawl/action-tsc@v1
|
||||
|
||||
37
.github/workflows/nightly-security-scan.yml
vendored
37
.github/workflows/nightly-security-scan.yml
vendored
@@ -3,14 +3,15 @@ name: Nightly Code Security Scan
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
# if: >- # only run for develop branch
|
||||
# github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
@@ -49,8 +50,8 @@ jobs:
|
||||
server-dependencies:
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
# if: >- # only run for develop branch
|
||||
# github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
@@ -93,8 +94,8 @@ jobs:
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
# if: >-
|
||||
# github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||
steps:
|
||||
@@ -161,8 +162,8 @@ jobs:
|
||||
name: Analyse scan result
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
# if: >-
|
||||
# github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
|
||||
@@ -187,19 +188,20 @@ jobs:
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"attachments": [
|
||||
{
|
||||
"color": "#FF0000",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
@@ -227,4 +229,3 @@ jobs:
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
18
.github/workflows/pr-security.yml
vendored
18
.github/workflows/pr-security.yml
vendored
@@ -1,10 +1,7 @@
|
||||
name: PR Code Security Scan
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
- edited
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'api/go.mod'
|
||||
@@ -12,14 +9,14 @@ on:
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
github.ref != 'refs/heads/develop'
|
||||
outputs:
|
||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||
steps:
|
||||
@@ -71,8 +68,7 @@ jobs:
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
github.ref != 'refs/heads/develop'
|
||||
outputs:
|
||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||
steps:
|
||||
@@ -128,8 +124,7 @@ jobs:
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
github.ref != 'refs/heads/develop'
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
steps:
|
||||
@@ -209,8 +204,7 @@ jobs:
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
github.ref != 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
||||
|
||||
92
.github/workflows/quality-scan.yml
vendored
Normal file
92
.github/workflows/quality-scan.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
on:
|
||||
# runs on default branch
|
||||
schedule:
|
||||
- cron: "0 11 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["go", "javascript"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
client-dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
server-security:
|
||||
name: Scan server code
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Download dependencies
|
||||
run: cd api && go get -v -d
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: portainer/gosec@fix-sarif-format
|
||||
with:
|
||||
# we let the report trigger content trigger a failure using the GitHub Security features.
|
||||
args: "-no-fail -fmt sarif -out results.sarif ./..."
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
# Path to SARIF file relative to the root of the repository
|
||||
sarif_file: results.sarif
|
||||
server-dependencies:
|
||||
name: Scan server dependencies
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
NANCY_VERSION: v1.0.11
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Download Nancy binary
|
||||
run: curl -L "https://github.com/sonatype-nexus-community/nancy/releases/download/$NANCY_VERSION/nancy-$NANCY_VERSION-linux-amd64" -o nancy && chmod +x nancy
|
||||
- name: Scan modules
|
||||
run: cd api && go list -json -m all | ../nancy sleuth
|
||||
15
.github/workflows/test-client.yaml
vendored
Normal file
15
.github/workflows/test-client.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Test Frontend
|
||||
on: push
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
29
.github/workflows/test.yaml
vendored
29
.github/workflows/test.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Test
|
||||
on: push
|
||||
jobs:
|
||||
test-client:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
# test-server:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# GOPRIVATE: "github.com/portainer"
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - uses: actions/setup-go@v3
|
||||
# with:
|
||||
# go-version: '1.18'
|
||||
# - name: Run tests
|
||||
# run: |
|
||||
# cd api
|
||||
# go test ./...
|
||||
@@ -9,18 +9,7 @@ import (
|
||||
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
||||
func CreateServerTLSConfiguration() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -84,15 +83,6 @@ func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
||||
return service.connection.CreateObjectWithSetSequence(BucketName, int(endpoint.ID), endpoint)
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service *Service) CreateWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
|
||||
if endpoint.ID > 0 {
|
||||
return errors.New("the endpoint must not have an ID")
|
||||
}
|
||||
|
||||
return service.connection.CreateObject(BucketName, fn)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.connection.GetNextIdentifier(BucketName)
|
||||
|
||||
@@ -97,7 +97,6 @@ type (
|
||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||
Endpoints() ([]portainer.Endpoint, error)
|
||||
Create(endpoint *portainer.Endpoint) error
|
||||
CreateWithCallback(endpoint *portainer.Endpoint, fn func(uint64) (int, interface{})) error
|
||||
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
|
||||
DeleteEndpoint(ID portainer.EndpointID) error
|
||||
GetNextIdentifier() int
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
@@ -15,30 +13,6 @@ const (
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
cache *portainer.Settings
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func cloneSettings(src *portainer.Settings) *portainer.Settings {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := *src
|
||||
|
||||
if c.BlackListedLabels != nil {
|
||||
c.BlackListedLabels = make([]portainer.Pair, len(src.BlackListedLabels))
|
||||
copy(c.BlackListedLabels, src.BlackListedLabels)
|
||||
}
|
||||
|
||||
if src.FeatureFlagSettings != nil {
|
||||
c.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
||||
for k, v := range src.FeatureFlagSettings {
|
||||
c.FeatureFlagSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -59,18 +33,6 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
service.mu.RLock()
|
||||
if service.cache != nil {
|
||||
s := cloneSettings(service.cache)
|
||||
service.mu.RUnlock()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
service.mu.RUnlock()
|
||||
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
var settings portainer.Settings
|
||||
|
||||
err := service.connection.GetObject(BucketName, []byte(settingsKey), &settings)
|
||||
@@ -78,24 +40,12 @@ func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service.cache = cloneSettings(&settings)
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a Settings object.
|
||||
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
err := service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.cache = cloneSettings(settings)
|
||||
|
||||
return nil
|
||||
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
|
||||
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
@@ -111,9 +61,3 @@ func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *Service) InvalidateCache() {
|
||||
service.mu.Lock()
|
||||
service.cache = nil
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
|
||||
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: id,
|
||||
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
store.EndpointRelation().Create(relation)
|
||||
|
||||
@@ -31,8 +31,6 @@ func (store *Store) MigrateData() error {
|
||||
return werrors.Wrap(err, "while backing up db before migration")
|
||||
}
|
||||
|
||||
store.SettingsService.InvalidateCache()
|
||||
|
||||
migratorParams := &migrator.MigratorParameters{
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
|
||||
@@ -54,7 +54,7 @@ func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
|
||||
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
err = m.endpointRelationService.Create(relation)
|
||||
|
||||
@@ -12,7 +12,6 @@ require (
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fxamacker/cbor/v2 v2.3.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
@@ -33,7 +32,7 @@ require (
|
||||
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-20220407011010-3c7408969ad3
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
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/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
|
||||
@@ -376,8 +376,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
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/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
|
||||
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
|
||||
@@ -811,8 +809,6 @@ github.com/portainer/docker-compose-wrapper v0.0.0-20220407011010-3c7408969ad3 h
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220407011010-3c7408969ad3/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/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f h1:GMIjRVV2LADpJprPG2+8MdRH6XvrFgC7wHm7dFUdOpc=
|
||||
|
||||
@@ -164,9 +164,7 @@ func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
|
||||
edgeStackSet[edgeStackID] = true
|
||||
}
|
||||
|
||||
for edgeStackID := range edgeStackSet {
|
||||
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
|
||||
}
|
||||
relation.EdgeStacks = edgeStackSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta
|
||||
DeploymentType: payload.DeploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
@@ -227,6 +228,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
|
||||
Name: payload.Name,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
DeploymentType: payload.DeploymentType,
|
||||
Version: 1,
|
||||
}
|
||||
@@ -335,6 +337,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
|
||||
DeploymentType: payload.DeploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
@@ -408,7 +411,7 @@ func updateEndpointRelations(endpointRelationService dataservices.EndpointRelati
|
||||
return fmt.Errorf("unable to find endpoint relation in database: %w", err)
|
||||
}
|
||||
|
||||
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
|
||||
relation.EdgeStacks[edgeStackID] = true
|
||||
|
||||
err = endpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package edgestacks
|
||||
|
||||
/*
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
|
||||
edgeStackID := portainer.EdgeStackID(5)
|
||||
endpointRelations := []portainer.EndpointRelation{
|
||||
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
}
|
||||
|
||||
relatedIds := []portainer.EndpointID{2, 3}
|
||||
@@ -29,4 +36,3 @@ func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
|
||||
assert.Equal(t, shouldBeRelated, relation.EdgeStacks[edgeStackID])
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// @id EdgeStackList
|
||||
@@ -26,35 +25,5 @@ func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
|
||||
}
|
||||
|
||||
endpointRels, err := handler.DataStore.EndpointRelation().EndpointRelations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint relations from the database", err}
|
||||
}
|
||||
|
||||
m := make(map[portainer.EdgeStackID]map[portainer.EndpointID]portainer.EdgeStackStatus)
|
||||
|
||||
for _, r := range endpointRels {
|
||||
for edgeStackID, status := range r.EdgeStacks {
|
||||
if m[edgeStackID] == nil {
|
||||
m[edgeStackID] = make(map[portainer.EndpointID]portainer.EdgeStackStatus)
|
||||
}
|
||||
|
||||
m[edgeStackID][r.EndpointID] = status
|
||||
}
|
||||
}
|
||||
|
||||
type EdgeStackWithStatus struct {
|
||||
portainer.EdgeStack
|
||||
Status map[portainer.EndpointID]portainer.EdgeStackStatus
|
||||
}
|
||||
|
||||
var edgeStacksWS []EdgeStackWithStatus
|
||||
for _, s := range edgeStacks {
|
||||
edgeStacksWS = append(edgeStacksWS, EdgeStackWithStatus{
|
||||
EdgeStack: s,
|
||||
Status: m[s.ID],
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStacksWS)
|
||||
return response.JSON(w, edgeStacks)
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
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/http/middlewares"
|
||||
)
|
||||
|
||||
func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError {
|
||||
httpErr := &httperror.HandlerError{http.StatusInternalServerError, msg, err}
|
||||
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
httpErr.StatusCode = http.StatusNotFound
|
||||
}
|
||||
|
||||
return httpErr
|
||||
}
|
||||
|
||||
// @id EdgeStackStatusDelete
|
||||
// @summary Delete an EdgeStack status
|
||||
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
|
||||
// @tags edge_stacks
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 404
|
||||
// @failure 403
|
||||
// @router /edge_stacks/{id}/status/{endpoint_id} [delete]
|
||||
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a valid endpoint from the handler context", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err != nil {
|
||||
return handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
@@ -49,6 +49,13 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload updateStatusPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
@@ -67,28 +74,17 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment relations", err}
|
||||
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
|
||||
Type: *payload.Status,
|
||||
Error: payload.Error,
|
||||
EndpointID: *payload.EndpointID,
|
||||
}
|
||||
|
||||
endpointRelation.EdgeStacks[portainer.EdgeStackID(stackID)] = portainer.EdgeStackStatus{
|
||||
Type: *payload.Status,
|
||||
Error: payload.Error,
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package edgestacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -119,7 +118,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err}
|
||||
}
|
||||
|
||||
relation.EdgeStacks[stack.ID] = portainer.EdgeStackStatus{}
|
||||
relation.EdgeStacks[stack.ID] = true
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
@@ -181,6 +180,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if payload.Version != nil && *payload.Version != stack.Version {
|
||||
stack.Version = *payload.Version
|
||||
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -25,11 +24,10 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
DataStore: dataStore,
|
||||
}
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
|
||||
@@ -45,12 +43,6 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
|
||||
|
||||
edgeStackStatusRouter := h.NewRoute().Subrouter()
|
||||
edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id"))
|
||||
|
||||
edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointTestCase struct {
|
||||
endpoint portainer.Endpoint
|
||||
endpointRelation portainer.EndpointRelation
|
||||
expectedStatusCode int
|
||||
}
|
||||
|
||||
var endpointTestCases = []endpointTestCase{
|
||||
{
|
||||
portainer.Endpoint{},
|
||||
portainer.EndpointRelation{},
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
ID: -1,
|
||||
Name: "endpoint-id--1",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
},
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: -1,
|
||||
},
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
ID: 2,
|
||||
Name: "endpoint-id-2",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "",
|
||||
},
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: 2,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
ID: 4,
|
||||
Name: "endpoint-id-4",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
},
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: 4,
|
||||
},
|
||||
http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
func setupHandler() (*Handler, func(), error) {
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create a tmp dir: %w", err)
|
||||
}
|
||||
|
||||
fs, err := filesystem.NewService(tmpDir, "")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not start a new filesystem service: %w", err)
|
||||
}
|
||||
|
||||
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
ctx := context.Background()
|
||||
shutdownCtx, cancelFn := context.WithCancel(ctx)
|
||||
|
||||
teardown := func() {
|
||||
cancelFn()
|
||||
storeTeardown()
|
||||
}
|
||||
|
||||
jwtService, err := jwt.NewService("1h", store)
|
||||
if err != nil {
|
||||
teardown()
|
||||
return nil, nil, fmt.Errorf("could not start a new jwt service: %w", err)
|
||||
}
|
||||
|
||||
apiKeyService := apikey.NewAPIKeyService(nil, nil)
|
||||
|
||||
settings, err := store.Settings().Settings()
|
||||
if err != nil {
|
||||
teardown()
|
||||
return nil, nil, fmt.Errorf("could not create new settings: %w", err)
|
||||
}
|
||||
settings.TrustOnFirstConnect = true
|
||||
|
||||
err = store.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
teardown()
|
||||
return nil, nil, fmt.Errorf("could not update settings: %w", err)
|
||||
}
|
||||
|
||||
handler := NewHandler(
|
||||
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||||
store,
|
||||
fs,
|
||||
chisel.NewService(store, shutdownCtx),
|
||||
)
|
||||
|
||||
handler.ReverseTunnelService = chisel.NewService(store, shutdownCtx)
|
||||
|
||||
return handler, teardown, nil
|
||||
}
|
||||
|
||||
func createEndpoint(handler *Handler, endpoint portainer.Endpoint, endpointRelation portainer.EndpointRelation) (err error) {
|
||||
// Avoid setting ID below 0 to generate invalid test cases
|
||||
if endpoint.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().Create(&endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
}
|
||||
|
||||
func TestMissingEdgeIdentifier(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(45)
|
||||
err = createEndpoint(handler, portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "endpoint-id-45",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
}, portainer.EndpointRelation{EndpointID: endpointID})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithEndpoints(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range endpointTestCases {
|
||||
err = createEndpoint(handler, test.endpoint, test.endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != test.expectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastCheckInDateIncreases(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(56)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-56",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Greater(t, updatedEndpoint.LastCheckInDate, endpoint.LastCheckInDate)
|
||||
}
|
||||
|
||||
func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(44)
|
||||
edgeId := "edge-id"
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-44",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "",
|
||||
}
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, edgeId)
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, updatedEndpoint.EdgeID, edgeId)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestEdgeStackStatus(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(7)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-7",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
edgeStackID := portainer.EdgeStackID(17)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-17",
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpoint.ID},
|
||||
},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{1, 2},
|
||||
ProjectPath: "/project/path",
|
||||
EntryPoint: "entrypoint",
|
||||
Version: 237,
|
||||
ManifestPath: "/manifest/path",
|
||||
DeploymentType: 1,
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||||
edgeStack.ID: true,
|
||||
},
|
||||
}
|
||||
handler.DataStore.EdgeStack().Create(edgeStack.ID, &edgeStack)
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
var data endpointEdgeStatusInspectResponse
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
assert.Len(t, data.Stacks, 1)
|
||||
assert.Equal(t, edgeStack.ID, data.Stacks[0].ID)
|
||||
assert.Equal(t, edgeStack.Version, data.Stacks[0].Version)
|
||||
}
|
||||
*/
|
||||
|
||||
func TestEdgeJobsResponse(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(77)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-77",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
edgeJobID := portainer.EdgeJobID(35)
|
||||
edgeJob := portainer.EdgeJob{
|
||||
ID: edgeJobID,
|
||||
Created: time.Now().Unix(),
|
||||
CronExpression: "* * * * *",
|
||||
Name: "test-edge-job",
|
||||
ScriptPath: path,
|
||||
Recurring: true,
|
||||
Version: 57,
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
var data endpointEdgeStatusInspectResponse
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
assert.Len(t, data.Schedules, 1)
|
||||
assert.Equal(t, edgeJob.ID, data.Schedules[0].ID)
|
||||
assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
|
||||
assert.Equal(t, edgeJob.Version, data.Schedules[0].Version)
|
||||
}
|
||||
@@ -35,12 +35,11 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en
|
||||
}
|
||||
|
||||
endpointStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
updatedStacks := make(map[portainer.EdgeStackID]portainer.EdgeStackStatus)
|
||||
|
||||
stacksSet := map[portainer.EdgeStackID]bool{}
|
||||
for _, edgeStackID := range endpointStacks {
|
||||
updatedStacks[edgeStackID] = endpointRelation.EdgeStacks[edgeStackID]
|
||||
stacksSet[edgeStackID] = true
|
||||
}
|
||||
endpointRelation.EdgeStacks = updatedStacks
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
|
||||
@@ -209,15 +209,13 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
relationObject := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
relatedEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
for _, stackID := range relatedEdgeStacks {
|
||||
relationObject.EdgeStacks[stackID] = portainer.EdgeStackStatus{
|
||||
Type: portainer.StatusAcknowledged,
|
||||
}
|
||||
relationObject.EdgeStacks[stackID] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,17 +299,17 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
//endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
|
||||
portainerHost, err := edge.ParseHostForEdge(payload.URL)
|
||||
if err != nil {
|
||||
return nil, httperror.BadRequest("Unable to parse host", err)
|
||||
}
|
||||
|
||||
//edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
//ID: portainer.EndpointID(endpointID),
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: payload.Name,
|
||||
URL: portainerHost,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
@@ -319,12 +317,12 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
//EdgeKey: edgeKey,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
EdgeKey: edgeKey,
|
||||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
@@ -345,15 +343,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
endpoint.EdgeID = edgeID.String()
|
||||
}
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizationsWithCallback(endpoint, func(id uint64) (int, interface{}) {
|
||||
endpoint.ID = portainer.EndpointID(id)
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
endpoint.EdgeKey = handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, int(id))
|
||||
}
|
||||
|
||||
return int(id), endpoint
|
||||
})
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
|
||||
}
|
||||
@@ -521,42 +511,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) saveEndpointAndUpdateAuthorizationsWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
|
||||
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().CreateWithCallback(endpoint, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag.Endpoints[endpoint.ID] = true
|
||||
|
||||
err = handler.DataStore.Tag().UpdateTag(tagID, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *endpointCreatePayload) *httperror.HandlerError {
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
|
||||
|
||||
@@ -87,6 +87,22 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
|
||||
}
|
||||
|
||||
for idx := range edgeStacks {
|
||||
edgeStack := &edgeStacks[idx]
|
||||
if _, ok := edgeStack.Status[endpoint.ID]; ok {
|
||||
delete(edgeStack.Status, endpoint.ID)
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge stack", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
|
||||
@@ -24,11 +24,6 @@ const (
|
||||
EdgeDeviceFilterNone = "none"
|
||||
)
|
||||
|
||||
const (
|
||||
EdgeDeviceIntervalMultiplier = 2
|
||||
EdgeDeviceIntervalAdd = 20
|
||||
)
|
||||
|
||||
var endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
|
||||
// @id EndpointList
|
||||
@@ -133,7 +128,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
}
|
||||
|
||||
if len(statuses) > 0 {
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
@@ -226,19 +221,15 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
status := endpoint.Status
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
isCheckValid := false
|
||||
edgeCheckinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
|
||||
if endpoint.EdgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(endpoint.EdgeCheckinInterval*2+20)
|
||||
}
|
||||
status = portainer.EndpointStatusDown // Offline
|
||||
if isCheckValid {
|
||||
|
||||
@@ -304,9 +304,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
currentEdgeStackSet[edgeStackID] = true
|
||||
}
|
||||
|
||||
for edgeStackID := range currentEdgeStackSet {
|
||||
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
|
||||
}
|
||||
relation.EdgeStacks = currentEdgeStackSet
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,8 +3,8 @@ package endpoints
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fvbommel/sortorder"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/natsort"
|
||||
)
|
||||
|
||||
type EndpointsByName []portainer.Endpoint
|
||||
@@ -18,7 +18,7 @@ func (e EndpointsByName) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (e EndpointsByName) Less(i, j int) bool {
|
||||
return sortorder.NaturalLess(strings.ToLower(e[i].Name), strings.ToLower(e[j].Name))
|
||||
return natsort.Compare(strings.ToLower(e[i].Name), strings.ToLower(e[j].Name))
|
||||
}
|
||||
|
||||
type EndpointsByGroup []portainer.Endpoint
|
||||
@@ -39,5 +39,5 @@ func (e EndpointsByGroup) Less(i, j int) bool {
|
||||
groupA := endpointGroupNames[e[i].GroupID]
|
||||
groupB := endpointGroupNames[e[j].GroupID]
|
||||
|
||||
return sortorder.NaturalLess(strings.ToLower(groupA), strings.ToLower(groupB))
|
||||
return natsort.Compare(strings.ToLower(groupA), strings.ToLower(groupB))
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
@@ -156,20 +154,9 @@ type Handler struct {
|
||||
// @tag.name websocket
|
||||
// @tag.description Create exec sessions using websockets
|
||||
|
||||
func init() {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
}
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/debug/pprof/profile"):
|
||||
pprof.Profile(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/debug/pprof/trace"):
|
||||
pprof.Trace(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/debug/pprof"):
|
||||
pprof.Index(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/backup"):
|
||||
|
||||
@@ -77,7 +77,7 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
if payload.EdgePortainerURL != nil && *payload.EdgePortainerURL != "" {
|
||||
if payload.EdgePortainerURL != nil {
|
||||
_, err := edge.ParseHostForEdge(*payload.EdgePortainerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -126,12 +126,11 @@ func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edg
|
||||
}
|
||||
|
||||
endpointStacks := edge.EndpointRelatedEdgeStacks(&endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
updatedStacks := make(map[portainer.EdgeStackID]portainer.EdgeStackStatus)
|
||||
|
||||
stacksSet := map[portainer.EdgeStackID]bool{}
|
||||
for _, edgeStackID := range endpointStacks {
|
||||
updatedStacks[edgeStackID] = endpointRelation.EdgeStacks[edgeStackID]
|
||||
stacksSet[edgeStackID] = true
|
||||
}
|
||||
endpointRelation.EdgeStacks = updatedStacks
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
|
||||
}
|
||||
|
||||
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
|
||||
return errors.New(fmt.Sprintf("invalid Edge identifier for endpoint %d. Expecting: %s - Received: %s", endpoint.ID, endpoint.EdgeID, edgeIdentifier))
|
||||
return errors.New("invalid Edge identifier")
|
||||
}
|
||||
|
||||
if endpoint.LastCheckInDate > 0 || endpoint.UserTrusted {
|
||||
|
||||
@@ -139,7 +139,8 @@ func (server *Server) Start() error {
|
||||
edgeJobsHandler.FileService = server.FileService
|
||||
edgeJobsHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer, server.DataStore)
|
||||
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer)
|
||||
edgeStacksHandler.DataStore = server.DataStore
|
||||
edgeStacksHandler.FileService = server.FileService
|
||||
edgeStacksHandler.GitService = server.GitService
|
||||
edgeStacksHandler.KubernetesDeployer = server.KubernetesDeployer
|
||||
|
||||
@@ -19,10 +19,6 @@ func ParseHostForEdge(portainerURL string) (string, error) {
|
||||
portainerHost = parsedURL.Host
|
||||
}
|
||||
|
||||
if portainerHost == "" {
|
||||
return "", errors.New("hostname cannot be empty")
|
||||
}
|
||||
|
||||
if portainerHost == "localhost" {
|
||||
return "", errors.New("cannot use localhost as environment URL")
|
||||
}
|
||||
|
||||
126
api/internal/natsort/natsort.go
Normal file
126
api/internal/natsort/natsort.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Package natsort implements natural strings sorting
|
||||
|
||||
// An extension of the following package found here:
|
||||
// https://github.com/facette/natsort
|
||||
// Our extension adds ReverseSort
|
||||
//
|
||||
// Original 3-Clause BSD License below:
|
||||
// Copyright (c) 2015, Vincent Batoufflet and Marc Falzon
|
||||
// All rights reserved.
|
||||
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions
|
||||
// are met:
|
||||
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
|
||||
// * Neither the name of the authors nor the names of its contributors
|
||||
// may be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package natsort
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type natsort []string
|
||||
|
||||
func (s natsort) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s natsort) Less(a, b int) bool {
|
||||
return Compare(s[a], s[b])
|
||||
}
|
||||
|
||||
func (s natsort) Swap(a, b int) {
|
||||
s[a], s[b] = s[b], s[a]
|
||||
}
|
||||
|
||||
var chunkifyRegexp = regexp.MustCompile(`(\d+|\D+)`)
|
||||
|
||||
func chunkify(s string) []string {
|
||||
return chunkifyRegexp.FindAllString(s, -1)
|
||||
}
|
||||
|
||||
// Sort sorts a list of strings in a natural order
|
||||
func Sort(l []string) {
|
||||
sort.Sort(natsort(l))
|
||||
}
|
||||
|
||||
// ReverseSort sorts a list of strings in a natural decending order
|
||||
func ReverseSort(l []string) {
|
||||
sort.Sort(sort.Reverse(natsort(l)))
|
||||
}
|
||||
|
||||
// compare returns true if the first string < second (natsort order) e.g. 1.1.1 < 1.11
|
||||
func Compare(a, b string) bool {
|
||||
chunksA := chunkify(a)
|
||||
chunksB := chunkify(b)
|
||||
|
||||
nChunksA := len(chunksA)
|
||||
nChunksB := len(chunksB)
|
||||
|
||||
for i := range chunksA {
|
||||
if i >= nChunksB {
|
||||
return false
|
||||
}
|
||||
|
||||
aInt, aErr := strconv.Atoi(chunksA[i])
|
||||
bInt, bErr := strconv.Atoi(chunksB[i])
|
||||
|
||||
// If both chunks are numeric, compare them as integers
|
||||
if aErr == nil && bErr == nil {
|
||||
if aInt == bInt {
|
||||
if i == nChunksA-1 {
|
||||
// We reached the last chunk of A, thus B is greater than A
|
||||
return true
|
||||
} else if i == nChunksB-1 {
|
||||
// We reached the last chunk of B, thus A is greater than B
|
||||
return false
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return aInt < bInt
|
||||
}
|
||||
|
||||
// So far both strings are equal, continue to next chunk
|
||||
if chunksA[i] == chunksB[i] {
|
||||
if i == nChunksA-1 {
|
||||
// We reached the last chunk of A, thus B is greater than A
|
||||
return true
|
||||
} else if i == nChunksB-1 {
|
||||
// We reached the last chunk of B, thus A is greater than B
|
||||
return false
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return chunksA[i] < chunksB[i]
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -235,13 +235,6 @@ func (s *stubEndpointService) Create(endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubEndpointService) CreateWithCallback(endpoint *portainer.Endpoint, fn func(uint64) (int, interface{})) error {
|
||||
s.endpoints = append(s.endpoints, *endpoint)
|
||||
fn(uint64(len(s.endpoints)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubEndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
for i, e := range s.endpoints {
|
||||
if e.ID == ID {
|
||||
|
||||
@@ -3,14 +3,12 @@ package jwt
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/gorilla/securecookie"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// scope represents JWT scopes that are supported in JWT claims.
|
||||
@@ -166,12 +164,6 @@ func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt
|
||||
return "", fmt.Errorf("invalid scope: %v", scope)
|
||||
}
|
||||
|
||||
if _, ok := os.LookupEnv("DOCKER_EXTENSION"); ok {
|
||||
// Set expiration to 99 years for docker desktop extension.
|
||||
log.Infof("[message: detected docker desktop extension mode]")
|
||||
expiresAt = time.Now().Add(time.Hour * 8760 * 99).Unix()
|
||||
}
|
||||
|
||||
cl := claims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
|
||||
@@ -252,13 +252,14 @@ type (
|
||||
//EdgeStack represents an edge stack
|
||||
EdgeStack struct {
|
||||
// EdgeStack Identifier
|
||||
ID EdgeStackID `json:"Id" example:"1"`
|
||||
Name string `json:"Name"`
|
||||
CreationDate int64 `json:"CreationDate"`
|
||||
EdgeGroups []EdgeGroupID `json:"EdgeGroups"`
|
||||
ProjectPath string `json:"ProjectPath"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Version int `json:"Version"`
|
||||
ID EdgeStackID `json:"Id" example:"1"`
|
||||
Name string `json:"Name"`
|
||||
Status map[EndpointID]EdgeStackStatus `json:"Status"`
|
||||
CreationDate int64 `json:"CreationDate"`
|
||||
EdgeGroups []EdgeGroupID `json:"EdgeGroups"`
|
||||
ProjectPath string `json:"ProjectPath"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Version int `json:"Version"`
|
||||
ManifestPath string
|
||||
DeploymentType EdgeStackDeploymentType
|
||||
|
||||
@@ -273,8 +274,9 @@ type (
|
||||
|
||||
//EdgeStackStatus represents an edge stack status
|
||||
EdgeStackStatus struct {
|
||||
Type EdgeStackStatusType `json:"Type"`
|
||||
Error string `json:"Error"`
|
||||
Type EdgeStackStatusType `json:"Type"`
|
||||
Error string `json:"Error"`
|
||||
EndpointID EndpointID `json:"EndpointID"`
|
||||
}
|
||||
|
||||
//EdgeStackStatusType represents an edge stack status type
|
||||
@@ -413,7 +415,7 @@ type (
|
||||
// EndpointRelation represents a environment(endpoint) relation object
|
||||
EndpointRelation struct {
|
||||
EndpointID EndpointID
|
||||
EdgeStacks map[EdgeStackID]EdgeStackStatus
|
||||
EdgeStacks map[EdgeStackID]bool
|
||||
}
|
||||
|
||||
// Extension represents a deprecated Portainer extension
|
||||
|
||||
@@ -3,9 +3,8 @@ import angular from 'angular';
|
||||
import { EnvironmentStatus } from '@/portainer/environments/types';
|
||||
import containersModule from './containers';
|
||||
import { componentsModule } from './components';
|
||||
import { networksModule } from './networks';
|
||||
|
||||
angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule, networksModule]).config([
|
||||
angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule]).config([
|
||||
'$stateRegistryProvider',
|
||||
function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
@@ -324,7 +323,8 @@ angular.module('portainer.docker', ['portainer.app', containersModule, component
|
||||
url: '/:id?nodeName',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'networkDetailsView',
|
||||
templateUrl: './views/networks/edit/network.html',
|
||||
controller: 'NetworkController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,16 +2,10 @@ import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import axios from '@/portainer/services/axios';
|
||||
|
||||
import { NetworkId } from '../networks/types';
|
||||
import { genericHandler } from '../rest/response/handlers';
|
||||
|
||||
import { ContainerId, DockerContainer } from './types';
|
||||
|
||||
export interface Filters {
|
||||
label?: string[];
|
||||
network?: NetworkId[];
|
||||
}
|
||||
|
||||
export async function startContainer(
|
||||
endpointId: EnvironmentId,
|
||||
id: ContainerId
|
||||
@@ -92,37 +86,15 @@ export async function removeContainer(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContainers(
|
||||
environmentId: EnvironmentId,
|
||||
filters?: Filters
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<DockerContainer[]>(
|
||||
urlBuilder(environmentId, '', 'json'),
|
||||
{
|
||||
params: { all: 0, filters },
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new PortainerError('Unable to retrieve containers', e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
function urlBuilder(
|
||||
endpointId: EnvironmentId,
|
||||
id?: ContainerId,
|
||||
id: ContainerId,
|
||||
action?: string
|
||||
) {
|
||||
let url = `/endpoints/${endpointId}/docker/containers`;
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
const url = `/endpoints/${endpointId}/docker/containers/${id}`;
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
return `${url}/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
|
||||
import { getContainers, Filters } from './containers.service';
|
||||
|
||||
export function useContainers(environmentId: EnvironmentId, filters?: Filters) {
|
||||
return useQuery(
|
||||
['environments', environmentId, 'docker', 'containers', { filters }],
|
||||
() => getContainers(environmentId, filters),
|
||||
{
|
||||
meta: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to get containers in network',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { NetworkContainer } from '../types';
|
||||
|
||||
import { NetworkContainersTable } from './NetworkContainersTable';
|
||||
|
||||
const networkContainers: NetworkContainer[] = [
|
||||
{
|
||||
EndpointID:
|
||||
'069d703f3ff4939956233137c4c6270d7d46c04fb10c44d3ec31fde1b46d6610',
|
||||
IPv4Address: '10.0.1.3/24',
|
||||
IPv6Address: '',
|
||||
MacAddress: '02:42:0a:00:01:03',
|
||||
Name: 'portainer-agent_agent.8hjjodl4hoyhuq1kscmzccyqn.wnv2pp17f8ayeopke2z56yw5x',
|
||||
Id: 'd54c74b7e1c5649d2a880d3fc02c6201d1d2f85a4fee718f978ec8b147239295',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@uirouter/react', () => ({
|
||||
...jest.requireActual('@uirouter/react'),
|
||||
useCurrentStateAndParams: jest.fn(() => ({
|
||||
params: { endpointId: 1 },
|
||||
})),
|
||||
}));
|
||||
|
||||
test('Network container values should be visible and the link should be valid', async () => {
|
||||
const user = new UserViewModel({ Username: 'test', Role: 1 });
|
||||
const { findByText } = renderWithQueryClient(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<NetworkContainersTable
|
||||
networkContainers={networkContainers}
|
||||
nodeName=""
|
||||
environmentId={1}
|
||||
networkId="pc8xc9s6ot043vl1q5iz4zhfs"
|
||||
/>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await expect(findByText('Containers in network')).resolves.toBeVisible();
|
||||
await expect(findByText(networkContainers[0].Name)).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(networkContainers[0].IPv4Address)
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(networkContainers[0].MacAddress)
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText('Leave network', { exact: false })
|
||||
).resolves.toBeVisible();
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { DetailsTable } from '@/portainer/components/DetailsTable';
|
||||
import { Button } from '@/portainer/components/Button';
|
||||
import { Authorized } from '@/portainer/hooks/useUser';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
|
||||
import { NetworkContainer, NetworkId } from '../types';
|
||||
import { useDisconnectContainer } from '../queries';
|
||||
|
||||
type Props = {
|
||||
networkContainers: NetworkContainer[];
|
||||
nodeName: string;
|
||||
environmentId: EnvironmentId;
|
||||
networkId: NetworkId;
|
||||
};
|
||||
|
||||
const tableHeaders = [
|
||||
'Container Name',
|
||||
'IPv4 Address',
|
||||
'IPv6 Address',
|
||||
'MacAddress',
|
||||
'Actions',
|
||||
];
|
||||
|
||||
export function NetworkContainersTable({
|
||||
networkContainers,
|
||||
nodeName,
|
||||
environmentId,
|
||||
networkId,
|
||||
}: Props) {
|
||||
const disconnectContainer = useDisconnectContainer();
|
||||
|
||||
if (networkContainers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle title="Containers in network" icon="fa-server" />
|
||||
<WidgetBody className="nopadding">
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
dataCy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="fa fa-trash-alt space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { render } from '@/react-tools/test-utils';
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { ResourceControlOwnership } from '@/portainer/access-control/types';
|
||||
|
||||
import { DockerNetwork } from '../types';
|
||||
|
||||
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
||||
|
||||
jest.mock('@uirouter/react', () => ({
|
||||
...jest.requireActual('@uirouter/react'),
|
||||
useCurrentStateAndParams: jest.fn(() => ({
|
||||
params: { endpointId: 1 },
|
||||
})),
|
||||
}));
|
||||
|
||||
test('Network details values should be visible', async () => {
|
||||
const network = getNetwork('test');
|
||||
|
||||
const { findByText } = await renderComponent(true, network);
|
||||
|
||||
await expect(findByText(network.Name)).resolves.toBeVisible();
|
||||
await expect(findByText(network.Id)).resolves.toBeVisible();
|
||||
await expect(findByText(network.Driver)).resolves.toBeVisible();
|
||||
await expect(findByText(network.Scope)).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(network.IPAM?.Config[0].Gateway || 'not found', { exact: false })
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(network.IPAM?.Config[0].Subnet || 'not found', { exact: false })
|
||||
).resolves.toBeVisible();
|
||||
});
|
||||
|
||||
test(`System networks shouldn't show a delete button`, async () => {
|
||||
const systemNetwork = getNetwork('bridge');
|
||||
const { queryByText } = await renderComponent(true, systemNetwork);
|
||||
|
||||
const deleteButton = queryByText('Delete this network');
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
|
||||
test('Non system networks should have a delete button', async () => {
|
||||
const nonSystemNetwork = getNetwork('non system network');
|
||||
|
||||
const { queryByText } = await renderComponent(true, nonSystemNetwork);
|
||||
|
||||
const button = queryByText('Delete this network');
|
||||
expect(button).toBeVisible();
|
||||
});
|
||||
|
||||
async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
|
||||
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
|
||||
|
||||
const queries = render(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<NetworkDetailsTable
|
||||
network={network}
|
||||
onRemoveNetworkClicked={() => {}}
|
||||
/>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await expect(queries.findByText('Network details')).resolves.toBeVisible();
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
function getNetwork(networkName: string): DockerNetwork {
|
||||
return {
|
||||
Attachable: false,
|
||||
Containers: {
|
||||
a761fcafdae3bdae42cf3702c8554b3e1b0334f85dd6b65b3584aff7246279e4: {
|
||||
EndpointID:
|
||||
'404afa6e25cede7c0fd70180777b662249cd83e40fa9a41aa593d2bac0fc5e18',
|
||||
IPv4Address: '172.17.0.2/16',
|
||||
IPv6Address: '',
|
||||
MacAddress: '02:42:ac:11:00:02',
|
||||
Name: 'portainer',
|
||||
},
|
||||
},
|
||||
Driver: 'bridge',
|
||||
IPAM: {
|
||||
Config: [
|
||||
{
|
||||
Gateway: '172.17.0.1',
|
||||
Subnet: '172.17.0.0/16',
|
||||
},
|
||||
],
|
||||
Driver: 'default',
|
||||
Options: null,
|
||||
},
|
||||
Id: '4c52a72e3772fdfb5823cf519b759e3f716e6d98cfb3bfef056e32c9c878329f',
|
||||
Internal: false,
|
||||
Name: networkName,
|
||||
Options: {
|
||||
'com.docker.network.bridge.default_bridge': 'true',
|
||||
'com.docker.network.bridge.enable_icc': 'true',
|
||||
'com.docker.network.bridge.enable_ip_masquerade': 'true',
|
||||
'com.docker.network.bridge.host_binding_ipv4': '0.0.0.0',
|
||||
'com.docker.network.bridge.name': 'docker0',
|
||||
'com.docker.network.driver.mtu': '1500',
|
||||
},
|
||||
Portainer: {
|
||||
ResourceControl: {
|
||||
Id: 41,
|
||||
ResourceId:
|
||||
'85d807847e4a4adb374a2a105124eda607ef584bef2eb6acf8091f3afd8446db',
|
||||
Type: 4,
|
||||
UserAccesses: [
|
||||
{
|
||||
UserId: 2,
|
||||
AccessLevel: 1,
|
||||
},
|
||||
],
|
||||
TeamAccesses: [],
|
||||
Ownership: ResourceControlOwnership.PUBLIC,
|
||||
Public: true,
|
||||
System: false,
|
||||
},
|
||||
},
|
||||
Scope: 'local',
|
||||
};
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Fragment } from 'react';
|
||||
import DockerNetworkHelper from 'Docker/helpers/networkHelper';
|
||||
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { DetailsTable } from '@/portainer/components/DetailsTable';
|
||||
import { Button } from '@/portainer/components/Button';
|
||||
import { Authorized } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { isSystemNetwork } from '../network.helper';
|
||||
import { DockerNetwork, IPConfig } from '../types';
|
||||
|
||||
interface Props {
|
||||
network: DockerNetwork;
|
||||
onRemoveNetworkClicked: () => void;
|
||||
}
|
||||
|
||||
export function NetworkDetailsTable({
|
||||
network,
|
||||
onRemoveNetworkClicked,
|
||||
}: Props) {
|
||||
const allowRemoveNetwork = !isSystemNetwork(network.Name);
|
||||
const ipv4Configs: IPConfig[] = DockerNetworkHelper.getIPV4Configs(
|
||||
network.IPAM?.Config
|
||||
);
|
||||
const ipv6Configs: IPConfig[] = DockerNetworkHelper.getIPV6Configs(
|
||||
network.IPAM?.Config
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle title="Network details" icon="fa-sitemap" />
|
||||
<WidgetBody className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
dataCy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<i
|
||||
className="fa fa-trash-alt space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">
|
||||
{network.Driver}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function getConfigDetails(configValue?: string) {
|
||||
return configValue ? ` - ${configValue}` : '';
|
||||
}
|
||||
|
||||
function getAuxiliaryAddresses(auxiliaryAddresses?: object) {
|
||||
return auxiliaryAddresses
|
||||
? ` - ${Object.values(auxiliaryAddresses).join(' - ')}`
|
||||
: '';
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useCurrentStateAndParams } from '@uirouter/react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||
import { PageHeader } from '@/portainer/components/PageHeader';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { AccessControlPanel } from '@/portainer/access-control/AccessControlPanel/AccessControlPanel';
|
||||
import { ResourceControlType } from '@/portainer/access-control/types';
|
||||
import { DockerContainer } from '@/docker/containers/types';
|
||||
|
||||
import { useNetwork, useDeleteNetwork } from '../queries';
|
||||
import { isSystemNetwork } from '../network.helper';
|
||||
import { useContainers } from '../../containers/queries';
|
||||
import { DockerNetwork, NetworkContainer } from '../types';
|
||||
|
||||
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
||||
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
||||
import { NetworkContainersTable } from './NetworkContainersTable';
|
||||
|
||||
export function NetworkDetailsView() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [networkContainers, setNetworkContainers] = useState<
|
||||
NetworkContainer[]
|
||||
>([]);
|
||||
const {
|
||||
params: { id: networkId, nodeName },
|
||||
} = useCurrentStateAndParams();
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const networkQuery = useNetwork(environmentId, networkId);
|
||||
const deleteNetworkMutation = useDeleteNetwork();
|
||||
const filters = {
|
||||
network: [networkId],
|
||||
};
|
||||
const containersQuery = useContainers(environmentId, filters);
|
||||
|
||||
useEffect(() => {
|
||||
if (networkQuery.data && containersQuery.data) {
|
||||
setNetworkContainers(
|
||||
filterContainersInNetwork(networkQuery.data, containersQuery.data)
|
||||
);
|
||||
}
|
||||
}, [networkQuery.data, containersQuery.data]);
|
||||
|
||||
if (!networkQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Network details"
|
||||
breadcrumbs={[
|
||||
{ link: 'docker.networks', label: 'Networks' },
|
||||
{
|
||||
link: 'docker.networks.network',
|
||||
label: networkQuery.data.Name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<NetworkDetailsTable
|
||||
network={networkQuery.data}
|
||||
onRemoveNetworkClicked={onRemoveNetworkClicked}
|
||||
/>
|
||||
|
||||
<AccessControlPanel
|
||||
onUpdateSuccess={() =>
|
||||
queryClient.invalidateQueries([
|
||||
'environments',
|
||||
environmentId,
|
||||
'docker',
|
||||
'networks',
|
||||
networkId,
|
||||
])
|
||||
}
|
||||
resourceControl={networkQuery.data.Portainer?.ResourceControl}
|
||||
resourceType={ResourceControlType.Network}
|
||||
disableOwnershipChange={isSystemNetwork(networkQuery.data.Name)}
|
||||
resourceId={networkId}
|
||||
/>
|
||||
<NetworkOptionsTable options={networkQuery.data.Options} />
|
||||
<NetworkContainersTable
|
||||
networkContainers={networkContainers}
|
||||
nodeName={nodeName}
|
||||
environmentId={environmentId}
|
||||
networkId={networkId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
async function onRemoveNetworkClicked() {
|
||||
const message = 'Do you want to delete the network?';
|
||||
const confirmed = await confirmDeletionAsync(message);
|
||||
|
||||
if (confirmed) {
|
||||
deleteNetworkMutation.mutate(
|
||||
{ environmentId, networkId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.stateService.go('docker.networks');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterContainersInNetwork(
|
||||
network: DockerNetwork,
|
||||
containers: DockerContainer[]
|
||||
) {
|
||||
const containersInNetwork = _.compact(
|
||||
containers.map((container) => {
|
||||
const containerInNetworkResponse = network.Containers[container.Id];
|
||||
if (containerInNetworkResponse) {
|
||||
const containerInNetwork: NetworkContainer = {
|
||||
...containerInNetworkResponse,
|
||||
Id: container.Id,
|
||||
};
|
||||
return containerInNetwork;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
return containersInNetwork;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { render } from '@/react-tools/test-utils';
|
||||
|
||||
import { NetworkOptions } from '../types';
|
||||
|
||||
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
||||
|
||||
const options: NetworkOptions = {
|
||||
'com.docker.network.bridge.default_bridge': 'true',
|
||||
'com.docker.network.bridge.enable_icc': 'true',
|
||||
'com.docker.network.bridge.enable_ip_masquerade': 'true',
|
||||
'com.docker.network.bridge.host_binding_ipv4': '0.0.0.0',
|
||||
'com.docker.network.bridge.name': 'docker0',
|
||||
'com.docker.network.driver.mtu': '1500',
|
||||
};
|
||||
|
||||
test('Network options values should be visible', async () => {
|
||||
const { findByText, findAllByText } = render(
|
||||
<NetworkOptionsTable options={options} />
|
||||
);
|
||||
|
||||
await expect(findByText('Network options')).resolves.toBeVisible();
|
||||
// expect to find three 'true' values for the first 3 options
|
||||
const cells = await findAllByText('true');
|
||||
expect(cells).toHaveLength(3);
|
||||
await expect(
|
||||
findByText(options['com.docker.network.bridge.host_binding_ipv4'])
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(options['com.docker.network.bridge.name'])
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(options['com.docker.network.driver.mtu'])
|
||||
).resolves.toBeVisible();
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { DetailsTable } from '@/portainer/components/DetailsTable';
|
||||
|
||||
import { NetworkOptions } from '../types';
|
||||
|
||||
type Props = {
|
||||
options: NetworkOptions;
|
||||
};
|
||||
|
||||
export function NetworkOptionsTable({ options }: Props) {
|
||||
const networkEntries = Object.entries(options);
|
||||
|
||||
if (networkEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle title="Network options" icon="fa-cogs" />
|
||||
<WidgetBody className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import { NetworkDetailsView } from './NetworkDetailsView';
|
||||
|
||||
export const NetworkDetailsViewAngular = react2angular(NetworkDetailsView, []);
|
||||
@@ -1,7 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { NetworkDetailsViewAngular } from './edit';
|
||||
|
||||
export const networksModule = angular
|
||||
.module('portainer.docker.networks', [])
|
||||
.component('networkDetailsView', NetworkDetailsViewAngular).name;
|
||||
@@ -1,5 +0,0 @@
|
||||
const systemNetworks = ['host', 'bridge', 'none'];
|
||||
|
||||
export function isSystemNetwork(networkName: string) {
|
||||
return systemNetworks.includes(networkName);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
|
||||
import { ContainerId } from '../containers/types';
|
||||
|
||||
import { NetworkId, DockerNetwork } from './types';
|
||||
|
||||
type NetworkAction = 'connect' | 'disconnect' | 'create';
|
||||
|
||||
export async function getNetwork(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId
|
||||
) {
|
||||
try {
|
||||
const { data: network } = await axios.get<DockerNetwork>(
|
||||
buildUrl(environmentId, networkId)
|
||||
);
|
||||
return network;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve network details');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteNetwork(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId
|
||||
) {
|
||||
try {
|
||||
await axios.delete(buildUrl(environmentId, networkId));
|
||||
return networkId;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to remove network');
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectContainer(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId,
|
||||
containerId: ContainerId
|
||||
) {
|
||||
try {
|
||||
await axios.post(buildUrl(environmentId, networkId, 'disconnect'), {
|
||||
Container: containerId,
|
||||
Force: false,
|
||||
});
|
||||
return { networkId, environmentId };
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to disconnect container from network'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(
|
||||
environmentId: EnvironmentId,
|
||||
networkId?: NetworkId,
|
||||
action?: NetworkAction
|
||||
) {
|
||||
let url = `endpoints/${environmentId}/docker/networks`;
|
||||
|
||||
if (networkId) {
|
||||
url += `/${networkId}`;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import {
|
||||
error as notifyError,
|
||||
success as notifySuccess,
|
||||
} from '@/portainer/services/notifications';
|
||||
|
||||
import { ContainerId } from '../containers/types';
|
||||
|
||||
import {
|
||||
getNetwork,
|
||||
deleteNetwork,
|
||||
disconnectContainer,
|
||||
} from './network.service';
|
||||
import { NetworkId } from './types';
|
||||
|
||||
export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) {
|
||||
return useQuery(
|
||||
['environments', environmentId, 'docker', 'networks', networkId],
|
||||
() => getNetwork(environmentId, networkId),
|
||||
{
|
||||
onError: (err) => {
|
||||
notifyError('Failure', err as Error, 'Unable to get network');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDeleteNetwork() {
|
||||
return useMutation(
|
||||
({
|
||||
environmentId,
|
||||
networkId,
|
||||
}: {
|
||||
environmentId: EnvironmentId;
|
||||
networkId: NetworkId;
|
||||
}) => deleteNetwork(environmentId, networkId),
|
||||
{
|
||||
onSuccess: (networkId) => {
|
||||
notifySuccess('Network successfully removed', networkId);
|
||||
},
|
||||
onError: (err) => {
|
||||
notifyError('Failure', err as Error, 'Unable to remove network');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDisconnectContainer() {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
({
|
||||
containerId,
|
||||
environmentId,
|
||||
networkId,
|
||||
}: {
|
||||
containerId: ContainerId;
|
||||
environmentId: EnvironmentId;
|
||||
networkId: NetworkId;
|
||||
}) => disconnectContainer(environmentId, networkId, containerId),
|
||||
{
|
||||
onSuccess: ({ networkId, environmentId }) => {
|
||||
notifySuccess('Container successfully disconnected', networkId);
|
||||
return client.invalidateQueries([
|
||||
'environments',
|
||||
environmentId,
|
||||
'docker',
|
||||
'networks',
|
||||
networkId,
|
||||
]);
|
||||
},
|
||||
onError: (err) => {
|
||||
notifyError(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to disconnect container from network'
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||
|
||||
import { ContainerId } from '../containers/types';
|
||||
|
||||
export type IPConfig = {
|
||||
Subnet: string;
|
||||
Gateway: string;
|
||||
IPRange?: string;
|
||||
AuxiliaryAddresses?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type NetworkId = string;
|
||||
|
||||
export type NetworkOptions = Record<string, string>;
|
||||
|
||||
type IpamOptions = Record<string, string> | null;
|
||||
|
||||
export type NetworkResponseContainer = {
|
||||
EndpointID: string;
|
||||
IPv4Address: string;
|
||||
IPv6Address: string;
|
||||
MacAddress: string;
|
||||
Name: string;
|
||||
};
|
||||
|
||||
export interface NetworkContainer extends NetworkResponseContainer {
|
||||
Id: ContainerId;
|
||||
}
|
||||
|
||||
export type NetworkResponseContainers = Record<
|
||||
ContainerId,
|
||||
NetworkResponseContainer
|
||||
>;
|
||||
|
||||
export interface DockerNetwork {
|
||||
Name: string;
|
||||
Id: NetworkId;
|
||||
Driver: string;
|
||||
Scope: string;
|
||||
Attachable: boolean;
|
||||
Internal: boolean;
|
||||
IPAM: {
|
||||
Config: IPConfig[];
|
||||
Driver: string;
|
||||
Options: IpamOptions;
|
||||
};
|
||||
Portainer: { ResourceControl?: ResourceControlViewModel };
|
||||
Options: NetworkOptions;
|
||||
Containers: NetworkResponseContainers;
|
||||
}
|
||||
133
app/docker/views/networks/edit/network.html
Normal file
133
app/docker/views/networks/edit/network.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Network details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.networks">Networks</a> > <a ui-sref="docker.networks.network({id: network.Id})">{{ network.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title-text="Network details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ network.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ network.Id }}
|
||||
<button authorization="DockerNetworkDelete" ng-if="allowRemove()" class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"
|
||||
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scope</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attachable</td>
|
||||
<td>{{ network.Attachable }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Internal</td>
|
||||
<td>{{ network.Internal }}</td>
|
||||
</tr>
|
||||
<tr ng-if="network.IPAM.IPV4Configs.length > 0" ng-repeat-start="config in network.IPAM.IPV4Configs">
|
||||
<td>IPV4 Subnet - {{ config.Subnet }}</td>
|
||||
<td>IPV4 Gateway - {{ config.Gateway }}</td>
|
||||
</tr>
|
||||
<tr ng-if="network.IPAM.IPV4Configs.length > 0" ng-repeat-end>
|
||||
<td>IPV4 IP range - {{ config.IPRange }}</td>
|
||||
<td
|
||||
>IPV4 Excluded Ips<span ng-repeat="auxAddress in config.AuxiliaryAddresses"> - {{ auxAddress }}</span></td
|
||||
>
|
||||
</tr>
|
||||
<tr ng-if="network.IPAM.IPV6Configs.length > 0" ng-repeat-start="config in network.IPAM.IPV6Configs">
|
||||
<td>IPV6 Subnet - {{ config.Subnet }}</td>
|
||||
<td>IPV6 Gateway - {{ config.Gateway }}</td>
|
||||
</tr>
|
||||
<tr ng-if="network.IPAM.IPV6Configs.length > 0" ng-repeat-end>
|
||||
<td>IPV6 IP range - {{ config.IPRange }}</td>
|
||||
<td
|
||||
>IPV6 Excluded Ips<span ng-repeat="auxAddress in config.AuxiliaryAddresses"> - {{ auxAddress }}</span></td
|
||||
>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- access-control-panel -->
|
||||
<access-control-panel
|
||||
ng-if="network"
|
||||
resource-id="network.Id"
|
||||
resource-control="network.ResourceControl"
|
||||
resource-type="resourceType"
|
||||
disable-ownership-change="isSystemNetwork()"
|
||||
on-update-success="(onUpdateResourceControlSuccess)"
|
||||
>
|
||||
</access-control-panel>
|
||||
<!-- !access-control-panel -->
|
||||
|
||||
<div class="row" ng-if="!(network.Options | emptyobject)">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cogs" title-text="Network options"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, value) in network.Options">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="containersInNetwork.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-server" title-text="Containers in network"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Container Name</th>
|
||||
<th>IPv4 Address</th>
|
||||
<th>IPv6 Address</th>
|
||||
<th>MacAddress</th>
|
||||
<th authorization="DockerNetworkDisconnect">Actions</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in containersInNetwork">
|
||||
<td
|
||||
><a ui-sref="docker.containers.container({ id: container.Id, nodeName: nodeName })">{{ container.Name }}</a></td
|
||||
>
|
||||
<td>{{ container.IPv4Address || '-' }}</td>
|
||||
<td>{{ container.IPv6Address || '-' }}</td>
|
||||
<td>{{ container.MacAddress || '-' }}</td>
|
||||
<td authorization="DockerNetworkDisconnect">
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(network, container)"
|
||||
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Leave Network</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
120
app/docker/views/networks/edit/networkController.js
Normal file
120
app/docker/views/networks/edit/networkController.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { ResourceControlType } from '@/portainer/access-control/types';
|
||||
import DockerNetworkHelper from 'Docker/helpers/networkHelper';
|
||||
|
||||
angular.module('portainer.docker').controller('NetworkController', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'$transition$',
|
||||
'$filter',
|
||||
'NetworkService',
|
||||
'Container',
|
||||
'Notifications',
|
||||
'HttpRequestHelper',
|
||||
'NetworkHelper',
|
||||
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, NetworkHelper) {
|
||||
$scope.resourceType = ResourceControlType.Network;
|
||||
|
||||
$scope.onUpdateResourceControlSuccess = function () {
|
||||
$state.reload();
|
||||
};
|
||||
|
||||
$scope.removeNetwork = function removeNetwork() {
|
||||
NetworkService.remove($transition$.params().id, $transition$.params().id)
|
||||
.then(function success() {
|
||||
Notifications.success('Network removed', $transition$.params().id);
|
||||
$state.go('docker.networks', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove network');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(network, container) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName);
|
||||
NetworkService.disconnectContainer($transition$.params().id, container.Id, false)
|
||||
.then(function success() {
|
||||
Notifications.success('Container left network', $transition$.params().id);
|
||||
$state.go('docker.networks.network', { id: network.Id }, { reload: true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to disconnect container from network');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isSystemNetwork = function () {
|
||||
return $scope.network && NetworkHelper.isSystemNetwork($scope.network);
|
||||
};
|
||||
|
||||
$scope.allowRemove = function () {
|
||||
return !$scope.isSystemNetwork();
|
||||
};
|
||||
|
||||
function filterContainersInNetwork(network, containers) {
|
||||
var containersInNetwork = [];
|
||||
containers.forEach(function (container) {
|
||||
var containerInNetwork = network.Containers[container.Id];
|
||||
if (containerInNetwork) {
|
||||
containerInNetwork.Id = container.Id;
|
||||
// Name is not available in Docker 1.9
|
||||
if (!containerInNetwork.Name) {
|
||||
containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
|
||||
}
|
||||
containersInNetwork.push(containerInNetwork);
|
||||
}
|
||||
});
|
||||
$scope.containersInNetwork = containersInNetwork;
|
||||
}
|
||||
|
||||
function getContainersInNetwork(network) {
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
if (network.Containers) {
|
||||
if (apiVersion < 1.24) {
|
||||
Container.query(
|
||||
{},
|
||||
function success(data) {
|
||||
var containersInNetwork = data.filter(function filter(container) {
|
||||
if (container.HostConfig.NetworkMode === network.Name) {
|
||||
return container;
|
||||
}
|
||||
});
|
||||
filterContainersInNetwork(network, containersInNetwork);
|
||||
},
|
||||
function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve containers in network');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Container.query(
|
||||
{
|
||||
filters: { network: [$transition$.params().id] },
|
||||
},
|
||||
function success(data) {
|
||||
filterContainersInNetwork(network, data);
|
||||
},
|
||||
function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve containers in network');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
$scope.nodeName = nodeName;
|
||||
NetworkService.network($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
$scope.network = data;
|
||||
getContainersInNetwork(data);
|
||||
$scope.network.IPAM.IPV4Configs = DockerNetworkHelper.getIPV4Configs($scope.network.IPAM.Config);
|
||||
$scope.network.IPAM.IPV6Configs = DockerNetworkHelper.getIPV6Configs($scope.network.IPAM.Config);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve network info');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
||||
@@ -2,7 +2,6 @@ import { FormControl } from '@/portainer/components/form-components/FormControl'
|
||||
import { Input } from '@/portainer/components/form-components/Input';
|
||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
|
||||
import { OsSelector } from './OsSelector';
|
||||
import { EdgeProperties } from './types';
|
||||
@@ -28,26 +27,19 @@ export function EdgePropertiesForm({
|
||||
/>
|
||||
|
||||
{!hideIdGetter && (
|
||||
<>
|
||||
<FormControl
|
||||
label="Edge ID Generator"
|
||||
tooltip="A bash script one liner that will generate the edge id and will be assigned to the PORTAINER_EDGE_ID environment variable"
|
||||
inputId="edge-id-generator-input"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="edgeIdGenerator"
|
||||
value={values.edgeIdGenerator}
|
||||
id="edge-id-generator-input"
|
||||
onChange={(e) => setFieldValue(e.target.name, e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
<code>PORTAINER_EDGE_ID</code> environment variable is required to
|
||||
successfully connect the edge agent to Portainer
|
||||
</TextTip>
|
||||
</>
|
||||
<FormControl
|
||||
label="Edge ID Generator"
|
||||
tooltip="A bash script one liner that will generate the edge id"
|
||||
inputId="edge-id-generator-input"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="edgeIdGenerator"
|
||||
value={values.edgeIdGenerator}
|
||||
id="edge-id-generator-input"
|
||||
onChange={(e) => setFieldValue(e.target.name, e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { r2a } from '@/react-tools/react2angular';
|
||||
import { useSettings } from '@/portainer/settings/settings.service';
|
||||
|
||||
import { EdgePropertiesForm } from './EdgePropertiesForm';
|
||||
import { ScriptTabs } from './ScriptTabs';
|
||||
import { Scripts } from './Scripts';
|
||||
import { EdgeProperties } from './types';
|
||||
|
||||
interface Props {
|
||||
@@ -43,7 +43,7 @@ export function EdgeScriptForm({ edgeKey, edgeId }: Props) {
|
||||
hideIdGetter={edgeId !== undefined}
|
||||
/>
|
||||
|
||||
<ScriptTabs
|
||||
<Scripts
|
||||
values={edgeProperties}
|
||||
agentVersion={agentVersion}
|
||||
edgeKey={edgeKey}
|
||||
|
||||
@@ -49,7 +49,7 @@ interface Props {
|
||||
onPlatformChange(platform: Platform): void;
|
||||
}
|
||||
|
||||
export function ScriptTabs({
|
||||
export function Scripts({
|
||||
agentVersion,
|
||||
values,
|
||||
edgeKey,
|
||||
@@ -134,7 +134,7 @@ function buildLinuxStandaloneCommand(
|
||||
)
|
||||
);
|
||||
|
||||
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}\
|
||||
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}
|
||||
docker run -d \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||
@@ -168,7 +168,7 @@ function buildWindowsStandaloneCommand(
|
||||
|
||||
return `${
|
||||
edgeIdScript ? `$Env:PORTAINER_EDGE_ID = "@(${edgeIdScript})" \n\n` : ''
|
||||
}\
|
||||
}
|
||||
docker run -d \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
@@ -199,7 +199,7 @@ function buildLinuxSwarmCommand(
|
||||
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||
]);
|
||||
|
||||
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}\
|
||||
return `${edgeIdScript ? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n` : ''}
|
||||
docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_agent_network;
|
||||
@@ -270,11 +270,13 @@ function buildKubernetesCommand(
|
||||
const idEnvVar = edgeIdScript
|
||||
? `PORTAINER_EDGE_ID=$(${edgeIdScript}) \n\n`
|
||||
: '';
|
||||
const envVarsTrimmed = envVars.trim();
|
||||
const edgeIdVar = !edgeIdScript && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
|
||||
const selfSigned = allowSelfSignedCerts ? '1' : '0';
|
||||
|
||||
return `${idEnvVar}curl https://downloads.portainer.io/ce${agentShortVersion}/portainer-edge-agent-setup.sh | bash -s -- "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${agentSecret}" "${envVarsTrimmed}"`;
|
||||
return `${idEnvVar}curl https://downloads.portainer.io/ce${agentShortVersion}/portainer-edge-agent-setup.sh |
|
||||
bash -s -- "${edgeIdVar}" \\
|
||||
"${edgeKey}" \\
|
||||
"${selfSigned}" "${agentSecret}" "${envVars}"`;
|
||||
}
|
||||
|
||||
function buildDefaultEnvVars(
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'lodash-es';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
export class EdgeGroupFormController {
|
||||
/* @ngInject */
|
||||
@@ -18,7 +17,6 @@ export class EdgeGroupFormController {
|
||||
};
|
||||
|
||||
this.associateEndpoint = this.associateEndpoint.bind(this);
|
||||
this.dissociateEndpointAsync = this.dissociateEndpointAsync.bind(this);
|
||||
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
|
||||
this.getDynamicEndpointsAsync = this.getDynamicEndpointsAsync.bind(this);
|
||||
this.getDynamicEndpoints = this.getDynamicEndpoints.bind(this);
|
||||
@@ -41,29 +39,6 @@ export class EdgeGroupFormController {
|
||||
}
|
||||
|
||||
dissociateEndpoint(endpoint) {
|
||||
return this.$async(this.dissociateEndpointAsync, endpoint);
|
||||
}
|
||||
|
||||
async dissociateEndpointAsync(endpoint) {
|
||||
const confirmed = await confirmAsync({
|
||||
title: 'Confirm action',
|
||||
message: 'Removing the environment from this group will remove its corresponding edge stacks',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.Endpoints = _.filter(this.model.Endpoints, (id) => id !== endpoint.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,9 @@
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td>
|
||||
<span ng-if="ctrl.application.ApplicationOwner" style="margin-right: 5px" data-cy="k8sAppDetail-owner">
|
||||
<i class="fas fa-user"></i> {{ ctrl.application.ApplicationOwner }}
|
||||
</span>
|
||||
<span ng-if="ctrl.application.ApplicationOwner" style="margin-right: 5px"> <i class="fas fa-user"></i> {{ ctrl.application.ApplicationOwner }} </span>
|
||||
<span> <i class="fas fa-clock"></i> {{ ctrl.application.CreationDate | getisodate }}</span>
|
||||
<span ng-if="ctrl.application.ApplicationOwner" data-cy="k8sAppDetail-creationMethod">
|
||||
<span ng-if="ctrl.application.ApplicationOwner">
|
||||
<i class="fa fa-file-code space-left space-right" aria-hidden="true"></i> Deployed from {{ ctrl.state.appType }}</span
|
||||
>
|
||||
</td>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.display-text {
|
||||
|
||||
@@ -21,12 +21,13 @@ export function useCopy(copyText: string, fadeDelay = 1000) {
|
||||
navigator.clipboard.writeText(copyText);
|
||||
} else {
|
||||
// https://stackoverflow.com/a/57192718
|
||||
const inputEl = document.createElement('textarea');
|
||||
const inputEl = document.createElement('input');
|
||||
inputEl.value = copyText;
|
||||
inputEl.type = 'text';
|
||||
document.body.appendChild(inputEl);
|
||||
inputEl.select();
|
||||
document.execCommand('copy');
|
||||
inputEl.hidden = true;
|
||||
inputEl.type = 'hidden';
|
||||
document.body.removeChild(inputEl);
|
||||
}
|
||||
setCopiedSuccessfully(true);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.code {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 20px;
|
||||
padding: 16px 90px;
|
||||
}
|
||||
|
||||
.root {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function DetailsRow({ label, children }: Props) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{label}</td>
|
||||
{children && <td data-cy={`detailsTable-${label}Value`}>{children}</td>}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
|
||||
import { DetailsTable } from './DetailsTable';
|
||||
import { DetailsRow } from './DetailsRow';
|
||||
|
||||
type Args = {
|
||||
key1: string;
|
||||
val1: string;
|
||||
key2: string;
|
||||
val2: string;
|
||||
};
|
||||
|
||||
export default {
|
||||
component: DetailsTable,
|
||||
title: 'Components/Tables/DetailsTable',
|
||||
} as Meta;
|
||||
|
||||
function Template({ key1, val1, key2, val2 }: Args) {
|
||||
return (
|
||||
<DetailsTable>
|
||||
<DetailsRow label={key1}>{val1}</DetailsRow>
|
||||
<DetailsRow label={key2}>{val2}</DetailsRow>
|
||||
</DetailsTable>
|
||||
);
|
||||
}
|
||||
|
||||
export const Default: Story<Args> = Template.bind({});
|
||||
Default.args = {
|
||||
key1: 'Name',
|
||||
val1: 'My Cool App',
|
||||
key2: 'Id',
|
||||
val2: 'dmsjs1532',
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { render } from '@/react-tools/test-utils';
|
||||
|
||||
import { DetailsTable } from './index';
|
||||
|
||||
// should display child row elements
|
||||
test('should display child row elements', () => {
|
||||
const person = {
|
||||
name: 'Bob',
|
||||
id: 'dmsjs1532',
|
||||
};
|
||||
|
||||
const { queryByText } = render(
|
||||
<DetailsTable>
|
||||
<DetailsTable.Row label="Name">{person.name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">{person.id}</DetailsTable.Row>
|
||||
</DetailsTable>
|
||||
);
|
||||
|
||||
const nameRow = queryByText(person.name);
|
||||
expect(nameRow).toBeVisible();
|
||||
|
||||
const idRow = queryByText(person.id);
|
||||
expect(idRow).toBeVisible();
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type Props = {
|
||||
headers?: string[];
|
||||
dataCy?: string;
|
||||
};
|
||||
|
||||
export function DetailsTable({
|
||||
headers = [],
|
||||
dataCy,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<table className="table" data-cy={dataCy}>
|
||||
{headers.length > 0 && (
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header) => (
|
||||
<th key={header}>{header}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { DetailsTable as MainComponent } from './DetailsTable';
|
||||
import { DetailsRow } from './DetailsRow';
|
||||
|
||||
interface DetailsTableSubcomponents {
|
||||
Row: typeof DetailsRow;
|
||||
}
|
||||
|
||||
const DetailsTable = MainComponent as typeof MainComponent &
|
||||
DetailsTableSubcomponents;
|
||||
|
||||
DetailsTable.Row = DetailsRow;
|
||||
|
||||
export { DetailsTable };
|
||||
@@ -45,19 +45,4 @@
|
||||
:global :root[theme='dark'] :local .root :global .selector__option:active,
|
||||
:global :root[theme='dark'] :local .root :global .selector__option--is-focused {
|
||||
background-color: var(--blue-2);
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
.root :global .selector__option--is-selected {
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
:global :root[theme='highcontrast'] :local .root :global .selector__single-value,
|
||||
:global :root[theme='dark'] :local .root :global .selector__single-value {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
:global :root[theme='highcontrast'] :local .root :global .selector__input-container,
|
||||
:global :root[theme='dark'] :local .root :global .selector__input-container {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
.edit-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 5px;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
||||
</span>
|
||||
</span>
|
||||
{groupName && (
|
||||
<span className="small space-right">
|
||||
<span className="small">
|
||||
<span>Group: </span>
|
||||
<span>{groupName}</span>
|
||||
</span>
|
||||
@@ -148,7 +148,7 @@ function useEnvironmentTagNames(tagIds?: TagId[]) {
|
||||
);
|
||||
});
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
if (tags) {
|
||||
return tags.join(', ');
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
display: inline-block;
|
||||
border: 0px;
|
||||
padding: 10px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
|
||||
@@ -133,7 +133,6 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
||||
sort: sortByFilter,
|
||||
order: sortByDescending ? 'desc' : 'asc',
|
||||
edgeDeviceFilter: 'none',
|
||||
tagsPartialMatch: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
@@ -58,18 +58,14 @@ export function parseAxiosError(
|
||||
if ('isAxiosError' in err) {
|
||||
const { error, details } = parseError(err as AxiosError);
|
||||
resultErr = error;
|
||||
if (msg && details) {
|
||||
resultMsg = `${msg}: ${details}`;
|
||||
} else {
|
||||
resultMsg = msg || details;
|
||||
}
|
||||
resultMsg = msg ? `${msg}: ${details}` : details;
|
||||
}
|
||||
|
||||
return new PortainerError(resultMsg, resultErr);
|
||||
}
|
||||
|
||||
function defaultErrorParser(axiosError: AxiosError) {
|
||||
const message = axiosError.response?.data.message || '';
|
||||
const message = axiosError.response?.data.message;
|
||||
const details = axiosError.response?.data.details || message;
|
||||
const error = new Error(message);
|
||||
return { error, details };
|
||||
|
||||
@@ -23,20 +23,11 @@ const validation = yup.object({
|
||||
EdgePortainerUrl: yup
|
||||
.string()
|
||||
.test(
|
||||
'url',
|
||||
'URL should be a valid URI and cannot include localhost',
|
||||
(value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return !!url.hostname && url.hostname !== 'localhost';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
'not-local',
|
||||
'Cannot use localhost as environment URL',
|
||||
(value) => !value?.includes('localhost')
|
||||
)
|
||||
.url('URL should be a valid URI')
|
||||
.required('URL is required'),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
import { Settings } from '../settings.service';
|
||||
|
||||
import { EdgeComputeSettings } from './EdgeComputeSettings';
|
||||
import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
|
||||
import { Settings } from './types';
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
|
||||
@@ -42,9 +42,9 @@ export interface Settings {
|
||||
AgentSecret: string;
|
||||
EdgePortainerUrl: string;
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EdgePingInterval: number;
|
||||
EdgeSnapshotInterval: number;
|
||||
EdgeCommandInterval: number;
|
||||
EdgePingInterval: string;
|
||||
EdgeSnapshotInterval: string;
|
||||
EdgeCommandInterval: string;
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
|
||||
@@ -5,8 +5,6 @@ import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||
import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
||||
import EndpointHelper from '@/portainer/helpers/endpointHelper';
|
||||
import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { isEdgeEnvironment } from '@/portainer/environments/utils';
|
||||
|
||||
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
||||
|
||||
@@ -107,7 +105,7 @@ function EndpointController(
|
||||
});
|
||||
}
|
||||
|
||||
$scope.updateEndpoint = async function () {
|
||||
$scope.updateEndpoint = function () {
|
||||
var endpoint = $scope.endpoint;
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
@@ -115,27 +113,6 @@ function EndpointController(
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
|
||||
if (isEdgeEnvironment(endpoint.Type) && _.difference($scope.initialTagIds, endpoint.TagIds).length > 0) {
|
||||
let confirmed = await confirmAsync({
|
||||
title: 'Confirm action',
|
||||
message: 'Removing tags from this environment will remove the corresponding edge stacks when dynamic grouping is being used',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var payload = {
|
||||
Name: endpoint.Name,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
@@ -252,7 +229,6 @@ function EndpointController(
|
||||
}
|
||||
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.initialTagIds = endpoint.TagIds.slice();
|
||||
$scope.groups = groups;
|
||||
$scope.availableTags = tags;
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ssl-certificate-settings ng-show="state.showHTTPS"></ssl-certificate-settings>
|
||||
<ssl-certificate-settings ng-show="$ctrl.showHTTPS"></ssl-certificate-settings>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
|
||||
@@ -4,9 +4,7 @@ services:
|
||||
portainer:
|
||||
image: ${DESKTOP_PLUGIN_IMAGE}
|
||||
command: ['--admin-password', '$$$$2y$$$$05$$$$bsb.XmF.r2DU6/9oVUaDxu3.Lxhmg1R8M0NMLK6JJKUiqUcaNjvdu']
|
||||
restart: always
|
||||
environment:
|
||||
- DOCKER_EXTENSION=1
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "Portainer",
|
||||
"icon": "portainer.svg",
|
||||
"vm": {
|
||||
"composefile": "docker-compose.yml",
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
FROM portainer/base
|
||||
|
||||
LABEL org.opencontainers.image.title="Portainer" \
|
||||
org.opencontainers.image.description="Docker container management made simple, with the world’s most popular GUI-based container management platform." \
|
||||
org.opencontainers.image.description="Rich container management experience using Portainer." \
|
||||
org.opencontainers.image.vendor="Portainer.io" \
|
||||
com.docker.desktop.extension.api.version=">= 0.2.2" \
|
||||
com.docker.desktop.extension.icon="https://portainer-io-assets.sfo2.cdn.digitaloceanspaces.com/logos/portainer.png" \
|
||||
com.docker.extension.screenshots="[{\"alt\": \"screenshot one\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-1.png\"},{\"alt\": \"screenshot two\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-2.png\"},{\"alt\": \"screenshot three\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-3.png\"},{\"alt\": \"screenshot four\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-4.png\"},{\"alt\": \"screenshot five\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-5.png\"},{\"alt\": \"screenshot six\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-6.png\"},{\"alt\": \"screenshot seven\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-7.png\"},{\"alt\": \"screenshot eight\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-8.png\"},{\"alt\": \"screenshot nine\", \"url\": \"https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/docker-extension-9.png\"}]" \
|
||||
com.docker.extension.detailed-description="<p data-renderer-start-pos=\"226\">Portainer’s Docker Desktop extension gives you access to all of Portainer’s rich management functionality within your docker desktop experience.</p><h2 data-renderer-start-pos=\"374\">With Portainer you can:</h2><ul><li>See all your running containers</li><li>Easily view all of your container logs</li><li>Console into containers</li><li>Easily deploy your code into containers using a simple form</li><li>Turn your YAML into custom templates for easy reuse</li></ul><h2 data-renderer-start-pos=\"660\">About Portainer </h2><p data-renderer-start-pos=\"680\">Portainer is the worlds’ most popular universal container management platform with more than 650,000 active monthly users. Portainer can be used to manage Docker Standalone, Kubernetes, Docker Swarm and Nomad environments through a single common interface. It includes a simple GitOps automation engine and a Kube API. </p><p data-renderer-start-pos=\"1006\">Portainer Business Edition is our fully supported commercial grade product for business-wide use. It includes all the functionality that businesses need to manage containers at scale. Visit <a class=\"sc-jKJlTe dPfAtb\" href=\"http://portainer.io/\" title=\"http://Portainer.io\" data-renderer-mark=\"true\">Portainer.io</a> to learn more about Portainer Business and <a class=\"sc-jKJlTe dPfAtb\" href=\"http://portainer.io/take5?utm_campaign=DockerCon&utm_source=Docker%20Desktop\" title=\"http://portainer.io/take5?utm_campaign=DockerCon&utm_source=Docker%20Desktop\" data-renderer-mark=\"true\">get 5 free nodes.</a></p>" \
|
||||
com.docker.extension.publisher-url="https://www.portainer.io" \
|
||||
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
|
||||
com.docker.desktop.extension.icon=https://portainer-io-assets.sfo2.cdn.digitaloceanspaces.com/logos/portainer.png
|
||||
|
||||
COPY dist /
|
||||
COPY build/docker-extension /
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -6566,7 +6566,7 @@ css-tree@^1.1.2, css-tree@^1.1.3:
|
||||
mdn-data "2.0.14"
|
||||
source-map "^0.6.1"
|
||||
|
||||
css-what@^5.0.0:
|
||||
css-what@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
|
||||
integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
|
||||
@@ -7197,7 +7197,7 @@ domexception@^2.0.1:
|
||||
dependencies:
|
||||
webidl-conversions "^5.0.0"
|
||||
|
||||
domhandler@^4.0.0:
|
||||
domhandler@^4.0.0, domhandler@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626"
|
||||
integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==
|
||||
@@ -7211,7 +7211,7 @@ domhandler@^4.2.0:
|
||||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
domutils@^2.5.2, domutils@^2.6.0:
|
||||
domutils@^2.5.2, domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
|
||||
@@ -11300,7 +11300,7 @@ jsesc@^2.5.1:
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
|
||||
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
|
||||
|
||||
json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
|
||||
json-parse-better-errors@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
||||
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
|
||||
@@ -11677,7 +11677,7 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
lodash-es@^4.17.15, lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
@@ -12356,11 +12356,6 @@ nanoid@^3.1.23:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
||||
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
||||
|
||||
nanoid@^3.1.30:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
|
||||
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
|
||||
|
||||
nanoid@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
|
||||
@@ -12631,7 +12626,7 @@ npmlog@^5.0.1:
|
||||
gauge "^3.0.0"
|
||||
set-blocking "^2.0.0"
|
||||
|
||||
nth-check@^2.0.0:
|
||||
nth-check@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
|
||||
integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==
|
||||
|
||||
Reference in New Issue
Block a user