Compare commits

...

78 Commits

Author SHA1 Message Date
Chaim Lev-Ari
8ab739adfd refactor(docker/services): convert service tasks table to react [EE-4337]
close [EE-4337]
2023-08-28 14:40:58 +02:00
Chaim Lev-Ari
0ee6c5c6e9 refactor(ui/datatables): allow to control selected state from parent 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
1e2dbd7778 refactor(ui/tables): remove temp type 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
92dd6ed7bc refactor(ui/datatables): allow for not sort 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
f780207b82 feat(ui/datatables): support meta for expandable table 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
86a848d927 feat(ui/datatables): fix filter style 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
ca4130b221 fix(ui/datatables): simplify getRowId 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
bb7c6077d5 feat(ui/datatables): allow more global filters 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
bccab06abb refactor(ui/datatables): use object type for table data 2023-08-27 12:32:31 +02:00
Chaim Lev-Ari
531f88b947 chore(tests): clean tests output [EE-5758] (#9215) 2023-08-27 12:30:45 +02:00
Dakota Walsh
2953848b9a feat(gpu): remove GPU lightbubble EE-5254 (#10096) 2023-08-25 15:32:08 +12:00
Dakota Walsh
c0ba221021 fix(registry): ecr secret fix [EE-5673] (#10107) 2023-08-25 13:12:41 +12:00
andres-portainer
be85d34c4b fix(logging): enable colored logging EE-5512 (#10097) 2023-08-24 18:40:52 -03:00
cmeng
7125ef81f3 fix(stack): pass registries to unpacker to start stack EE-4797 (#10095) 2023-08-24 13:01:49 +12:00
cmeng
1aae2e27f4 chore(chisel): bump chisel to 1.9.0 EE-5976 (#10093) 2023-08-24 11:06:33 +12:00
cmeng
3237e1990c fix(waiting-room): search endpoints by dynamic edge group name EE-5965 (#10090) 2023-08-24 09:18:59 +12:00
Ali
1e61f7e305 fix(ingress): handle system resources [EE-4775] (#9972)
* fix(ingress): handle system resources [EE-4775]
2023-08-23 09:13:35 +12:00
Chaim Lev-Ari
5586910e9d fix(ui/datatables): sync page count with filtering [EE-5890] (#10010) 2023-08-22 09:36:31 +03:00
Prabhat Khera
bb646162d1 fix wrong error message for secrets (#10073) 2023-08-21 08:05:57 +12:00
Chaim Lev-Ari
cfe0d3092d feat(ui): add confirmation to delete actions [EE-4612] (#10003) 2023-08-19 19:19:02 +03:00
cmeng
6fde4195f8 fix(migrator): prevent duplicated migration EE-5777 (#10077) 2023-08-18 21:40:48 +12:00
Chaim Lev-Ari
36b8c849b3 feat(edge/stacks): reload edge stacks from server [EE-5970] (#10061) 2023-08-17 14:09:41 +03:00
Ali
0f6607e703 refactor(app): migrate the yaml inspector to react [EE-5356] (#10058)
Co-authored-by: testa113 <testa113>
2023-08-17 22:01:10 +12:00
Ali
23295d2736 feat(app): migrate app containers to react [EE-5353] (#9992) 2023-08-17 22:00:25 +12:00
cmeng
6290e9facc fix(waiting-room): search endpoints by edge group name EE-5965 (#10072) 2023-08-17 14:47:09 +12:00
cmeng
95424c322d fix(datatable): image page not loading image list EE-5978 (#10071) 2023-08-17 09:53:28 +12:00
Chaim Lev-Ari
a1e610a39a fix(edge/groups): filter selected environments [EE-5891] (#10050) 2023-08-16 12:24:37 +03:00
Chaim Lev-Ari
a27cc6c0e5 fix(edge/stacks): show pending envs [EE-5913] (#10052) 2023-08-16 10:22:41 +03:00
Ali
2b4cb1b7b4 fix(ingress): empty initial selection + fixes [EE-5852] (#10066)
Co-authored-by: testa113 <testa113>
2023-08-16 18:07:46 +12:00
Ali
26074437ca fix(environments): fix env table [EE-5971] (#10059)
Co-authored-by: testa113 <testa113>
2023-08-16 13:21:23 +12:00
Prabhat Khera
665a25e448 fix edit namespace resource quota issue (#10064) 2023-08-16 10:25:01 +12:00
Chaim Lev-Ari
4a91e947ed feat(edge/configs): add context help [EE-5963] (#10055) 2023-08-15 18:46:58 +03:00
Chaim Lev-Ari
d514eeec86 fix(edge/devices): search waiting room devices [EE-5895] (#10014) 2023-08-15 06:05:10 +03:00
matias-portainer
0ef4aad79a fix(authentication): allow whitespaces when loading AD OU name EE-5206 (#9977) 2023-08-14 12:18:07 -03:00
matias-portainer
8355d449c5 fix(edge/stacks): add pagination to environments list EE-5908 (#10042) 2023-08-14 12:17:00 -03:00
Chaim Lev-Ari
fd7e8a629e feat(edge/stacks): info for old agent status [EE-5792] (#10013) 2023-08-14 16:04:24 +03:00
Ali
7757bf7a84 fix(r2a): remove withUserProvider [EE-5355] (#10048)
Co-authored-by: testa113 <testa113>
2023-08-14 19:01:31 +12:00
Ali
5862aa5dd8 fix(app): use correct withCurrentUser wrapper [EE-5928] (#10040)
Co-authored-by: testa113 <testa113>
2023-08-14 16:53:28 +12:00
cmeng
925a0d0a9a fix(stack): fail to start swarm stack with private image EE-4797 (#10047) 2023-08-14 16:13:12 +12:00
Ali
2a7a96f498 fix(microk8s): PO ui fixes [EE-5900] (#10031)
Co-authored-by: testa113 <testa113>
2023-08-14 12:34:58 +12:00
Ali
c472fe9c18 refactor(app): app events datatable [EE-5355] (#10024) 2023-08-14 05:09:40 +12:00
andres-portainer
0eaf296e1b fix(unpacker): implement unpacker error parsing EE-5779 (#10005) 2023-08-10 10:25:59 -03:00
Oscar Zhou
598b8d0f28 fix(stagger): introduce stack version into DeploymentInfo struct (#10011) 2023-08-10 11:58:40 +12:00
matias-portainer
e1a3010bc7 fix(edge/stacks): fix UI issues EE-5844 (#10021) 2023-08-09 10:09:08 -03:00
cmeng
2de4863532 fix(edge-stack): detaching swarm stack from git repository EE-5812 (#9998) 2023-08-07 10:33:04 +12:00
Oscar Zhou
8cf54cd0df fix(react/datatable): override getColumnCanGlobalFilter method (#9990) 2023-08-07 10:30:38 +12:00
cmeng
1ef1953d7d fix(edge-stack): detaching from git repository EE-5812 (#9989) 2023-08-04 15:17:46 +12:00
cmeng
5b033abaa4 fix(registry): registry login failure for regular stack EE-5832 (#9986) 2023-08-04 15:16:55 +12:00
Ali
5865f1ca77 fix(app): update summary with ingresses [EE-5847] (#9973)
Co-authored-by: testa113 <testa113>
2023-08-04 13:48:21 +12:00
Chaim Lev-Ari
f59573f306 fix(home): empty default sort [EE-5822] (#9951) 2023-08-03 16:21:09 -03:00
Chaim Lev-Ari
1cecbd7177 fix(docker/images): show empty size cell [EE-5823] (#9954) 2023-08-03 16:19:58 -03:00
Ali
acf9203580 fix(ingress): ingress ui feedback [EE-5852] (#9982)
Co-authored-by: testa113 <testa113>
2023-08-03 23:03:09 +12:00
cmeng
9845518aa9 fix(edge-stack): unable to edit edge stack EE-5845 (#9981) 2023-08-03 17:21:01 +12:00
matias-portainer
d7e83aad26 fix(endpoints): fix nil pointer dereference EE-5843 (#9969) 2023-08-02 11:06:34 -03:00
Matt Hook
df47f3d8a8 show kube icon for custom template (#9968) 2023-08-02 09:43:54 +12:00
Ali
d0ecf6c16b fix(ingress): loading and ui fixes [EE-5132] (#9959) 2023-08-01 19:31:35 +12:00
Matt Hook
e400c4dfc6 bump compose to 2.20.2 (#9964) 2023-08-01 12:27:21 +12:00
Matt Hook
721457b71d bump version to 2.20 (#9963) 2023-08-01 09:20:51 +12:00
Ali
b19800681f fix(app): improve perceived ingress load time [EE-5805] (#9946)
Co-authored-by: testa113 <testa113>
2023-07-31 20:18:45 +12:00
cmeng
6a4e44ee0a fix(stack): update gitops updates tooltip EE-5827 (#9962) 2023-07-31 18:46:00 +12:00
Chaim Lev-Ari
37ece734f0 refactor(kube/apps): convert placement table to react [EE-4662] (#8938) 2023-07-29 17:08:41 +02:00
Prabhat Khera
bf79ef7d89 fix(security): upgrade helm binary to v3.12.2 [EE-5801] (#9263) 2023-07-28 15:08:45 +12:00
James Carppe
883ef2578f fix indentation in bug report template (#9944) 2023-07-28 13:05:43 +12:00
Matt Hook
a585f34106 workding change (#9266) 2023-07-28 07:53:33 +12:00
Ali
b128139b69 fix(UI): PO review tweaks [EE-5776] (#9245)
Co-authored-by: testa113 <testa113>
2023-07-28 07:50:53 +12:00
James Carppe
4c425a7af8 Discussions updates (#9730)
* Update bug template: versions to dropdown, add license types to editions, set render on command used

* Update docs URL in help template
2023-07-27 10:27:32 +05:30
Dakota Walsh
400d95c1a5 fix(metrics): node chart race condition EE-5447 (#9249) 2023-07-27 11:46:38 +12:00
Dakota Walsh
ca617e2ac9 fix(jwt): replace deprecated gorilla/securecookie [EE-5153] (#9247) 2023-07-27 09:34:16 +12:00
samdulam
4a90b8a3f7 Fix links in Discussions and Issues Templates (#9258)
* Fix Links

* Fix links for discussions
2023-07-26 12:34:15 +05:30
samdulam
43ad3face2 Fix Links (#9257) 2023-07-26 12:11:06 +05:30
samdulam
69e61be474 file type changes (#9256) 2023-07-26 12:07:06 +05:30
samdulam
a4ea7a3709 Changes to issues templates now that Discussions are enabled (#9255)
* Discussions Enabled and Templates

* Discussions - Ideas Template
2023-07-26 12:02:38 +05:30
samdulam
c5ecf8a66d Change Issues so we can move to discussions 2023-07-26 12:00:41 +05:30
samdulam
c2c0631495 Add Discussion Templates (#9254)
* Discussions Enabled and Templates

* Discussions - Ideas Template
2023-07-26 11:57:59 +05:30
samdulam
4ff3cee72e Add workflow_dispatch so we can run manually (#9253) 2023-07-26 09:33:54 +05:30
Matt Hook
c4e8251e52 post po review changes (#9244) 2023-07-26 11:36:02 +12:00
andres-portainer
21b00c267d fix(docker): use version negotiation for the Docker client EE-5797 (#9250) 2023-07-25 19:00:21 -03:00
samdulam
86ec058347 Change stabot action version as it stopped working (#9246) 2023-07-25 14:47:07 +05:30
254 changed files with 3717 additions and 2293 deletions

11
.github/DISCUSSION_TEMPLATE/help.yaml vendored Normal file
View File

@@ -0,0 +1,11 @@
body:
- type: markdown
attributes:
value: |
Before asking a question, make sure it hasn't been already asked and answered. You can search our [discussions](https://github.com/orgs/portainer/discussions) and [bug reports](https://github.com/portainer/portainer/issues) in GitHub. Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io/) first.
- type: textarea
attributes:
label: Ask a Question!
validations:
required: true

38
.github/DISCUSSION_TEMPLATE/ideas.yaml vendored Normal file
View File

@@ -0,0 +1,38 @@
body:
- type: markdown
attributes:
value: |
# Welcome!
Thanks for suggesting an idea for Portainer!
Before opening a new idea or feature request, make sure that we do not have any duplicates already open. You can ensure this by [searching this discussion cagetory](https://github.com/orgs/portainer/discussions/categories/ideas). If there is a duplicate, please add a comment to the existing idea instead.
Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io) as they may point you toward a solution.
**DO NOT FILE DUPLICATE REQUESTS.**
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: Short list of what the feature request aims to address.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

View File

@@ -1,54 +0,0 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug/need-confirmation, kind/bug
assignees: ''
---
<!--
Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Bug description**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
**Steps to reproduce the issue:**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Technical details:**
- Portainer version:
- Docker version (managed by Portainer):
- Kubernetes version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Browser:
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
- Have you reviewed our technical documentation and knowledge base? Yes/No
**Additional context**
Add any other context about the problem here.

164
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,164 @@
name: Bug Report
description: Create a report to help us improve.
labels: kind/bug,bug/need-confirmation
body:
- type: markdown
attributes:
value: |
# Welcome!
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
- type: checkboxes
id: terms
attributes:
label: Before you start please confirm the following.
options:
- label: Yes, I've searched similar issues on [GitHub](https://github.com/portainer/portainer/issues).
required: true
- label: Yes, I've checked whether this issue is covered in the Portainer [documentation](https://docs.portainer.io) or [knowledge base](https://portal.portainer.io/knowledge).
required: true
- type: markdown
attributes:
value: |
# About your issue
Tell us a bit about the issue you're having.
How to write a good bug report:
- Respect the issue template as much as possible.
- Summarize the issue so that we understand what is going wrong.
- Describe what you would have expected to have happened, and what actually happened instead.
- Provide easy to follow steps to reproduce the issue.
- Remain clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown).
- type: textarea
attributes:
label: Problem Description
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: A clear and concise description of what actually happens.
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Please be as detailed as possible when providing steps to reproduce.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Portainer logs or screenshots
description: Provide Portainer container logs or any screenshots related to the issue.
validations:
required: false
- type: markdown
attributes:
value: |
# About your environment
Tell us a bit about your Portainer environment.
- type: dropdown
attributes:
label: Portainer version
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.18.4'
- '2.18.3'
- '2.18.2'
- '2.18.1'
- '2.17.1'
- '2.17.0'
- '2.16.2'
- '2.16.1'
- '2.16.0'
- '2.15.1'
- '2.15.0'
validations:
required: true
- type: dropdown
attributes:
label: Portainer Edition
multiple: false
options:
- 'Business Edition (BE/EE) with 5NF / 3NF license'
- 'Business Edition (BE/EE) with Home & Student license'
- 'Business Edition (BE/EE) with Starter license'
- 'Business Edition (BE/EE) with Professional or Enterprise license'
- 'Community Edition (CE)'
validations:
required: true
- type: input
attributes:
label: Platform and Version
description: |
Enter your container management platform (Docker | Swarm | Kubernetes) along with the version.
Example: Docker 24.0.3 | Docker Swarm 24.0.3 | Kubernetes 1.26
You can find our supported platforms [in our documentation](https://docs.portainer.io/start/requirements-and-prerequisites).
validations:
required: true
- type: input
attributes:
label: OS and Architecture
description: |
Enter your Operating System, Version and Architecture. Example: Ubuntu 22.04, AMD64 | Raspbian OS, ARM64
validations:
required: true
- type: input
attributes:
label: Browser
description: |
Enter your browser and version. Example: Google Chrome 114.0
validations:
required: false
- type: textarea
attributes:
label: What command did you use to deploy Portainer?
description: |
Example: `docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest`
If you deployed Portainer using a compose file or manifest you can provide this here as well.
render: bash
validations:
required: false
- type: textarea
attributes:
label: Additional Information
description: Any additional information about your environment, the bug, or anything else you think might be helpful.
validations:
required: false

View File

@@ -1,5 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Portainer Business Edition - Get 3 nodes free
url: https://www.portainer.io/take-3
- name: Question
url: https://github.com/orgs/portainer/discussions/new?category=help
about: Ask us a question about Portainer usage or deployment.
- name: Idea or Feature Request
url: https://github.com/orgs/portainer/discussions/new?category=ideas
about: Suggest an idea or feature/enhancement that should be added in Portainer.
- name: Portainer Business Edition - Get 3 Nodes Free
url: https://www.portainer.io/take-3
about: Portainer Business Edition has more features, more support and you can now get 3 nodes free for as long as you want.

View File

@@ -1,7 +1,8 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
- cron: '0 12 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
@@ -9,7 +10,7 @@ jobs:
issues: write
steps:
- uses: actions/stale@v4.0.0
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,7 +13,7 @@ jobs:
- run: yarn --frozen-lockfile
- name: Run tests
run: yarn jest --maxWorkers=2
run: make test-client ARGS="--maxWorkers=2"
test-server:
runs-on: ubuntu-latest
steps:

View File

@@ -65,7 +65,7 @@ clean: ## Remove all build and download artifacts
test: test-server test-client ## Run all tests
test-client: ## Run client tests
yarn test
yarn test $(ARGS)
test-server: ## Run server tests
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...

View File

@@ -1,9 +1,6 @@
package apikey
import (
"crypto/rand"
"io"
portainer "github.com/portainer/portainer/api"
)
@@ -18,13 +15,3 @@ type APIKeyService interface {
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
InvalidateUserKeyCache(userId portainer.UserID) bool
}
// generateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func generateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

View File

@@ -3,6 +3,7 @@ package apikey
import (
"testing"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/stretchr/testify/assert"
)
@@ -33,7 +34,7 @@ func Test_generateRandomKey(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateRandomKey(tt.wantLenth)
got := securecookie.GenerateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
@@ -41,7 +42,7 @@ func Test_generateRandomKey(t *testing.T) {
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 100; i++ {
key := generateRandomKey(8)
key := securecookie.GenerateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true

View File

@@ -8,6 +8,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/pkg/errors"
)
@@ -39,7 +40,7 @@ func (a *apiKeyService) HashRaw(rawKey string) []byte {
// GenerateApiKey generates a raw API key for a user (for one-time display).
// The generated API key is stored in the cache and database.
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
randKey := generateRandomKey(32)
randKey := securecookie.GenerateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey

View File

@@ -126,8 +126,8 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
}
config := &chserver.Config{
Reverse: true,
PrivateKeyFile: privateKeyFile,
Reverse: true,
KeyFile: privateKeyFile,
}
chiselServer, err := chserver.NewServer(config)

View File

@@ -39,9 +39,9 @@ func setLoggingMode(mode string) {
case "PRETTY":
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
NoColor: true,
TimeFormat: "2006/01/02 03:04PM",
FormatMessage: formatMessage})
FormatMessage: formatMessage,
})
case "JSON":
log.Logger = log.Output(os.Stderr)
}
@@ -51,5 +51,6 @@ func formatMessage(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("%s |", i)
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/docker"
dockerclient "github.com/portainer/portainer/api/docker/client"
@@ -119,11 +120,15 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
log.Fatal().Err(err).Msg("failed generating instance id")
}
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{})
migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()
// from MigrateData
v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
InstanceID: instanceId.String(),
MigratorCount: migratorCount,
}
store.VersionService.UpdateVersion(&v)

View File

@@ -50,7 +50,7 @@ func (store *Store) MigrateData() error {
if err != nil {
err = errors.Wrap(err, "failed to migrate database")
log.Warn().Msg("migration failed, restoring database to previous version")
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
if err != nil {
return errors.Wrap(err, "failed to restore database")

View File

@@ -115,10 +115,16 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
}
if environmentStatus.Details.Ok {
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
Type: portainer.EdgeStackStatusRunning,
Time: time.Now().Unix(),
})
statusArray = append(statusArray,
portainer.EdgeStackDeploymentStatus{
Type: portainer.EdgeStackStatusDeploymentReceived,
Time: time.Now().Unix(),
},
portainer.EdgeStackDeploymentStatus{
Type: portainer.EdgeStackStatusRunning,
Time: time.Now().Unix(),
},
)
}
if environmentStatus.Details.ImagesPulled {

View File

@@ -148,6 +148,17 @@ func (m *Migrator) LatestMigrations() Migrations {
return m.migrations[len(m.migrations)-1]
}
func (m *Migrator) GetMigratorCountOfCurrentAPIVersion() int {
migratorCount := 0
latestMigrations := m.LatestMigrations()
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
migratorCount = len(latestMigrations.MigrationFuncs)
}
return migratorCount
}
// !NOTE: Migration funtions should ideally be idempotent.
// ! Which simply means the function can run over the same data many times but only transform it once.
// ! In practice this really just means an extra check or two to ensure we're not destroying valid data.

View File

@@ -944,6 +944,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":3,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -57,20 +57,20 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(dockerClientVersion),
client.WithAPIVersionNegotiation(),
)
}
func CreateClientFromEnv() (*client.Client, error) {
return client.NewClientWithOpts(
client.FromEnv,
client.WithVersion(dockerClientVersion),
client.WithAPIVersionNegotiation(),
)
}
func CreateSimpleClient() (*client.Client, error) {
return client.NewClientWithOpts(
client.WithVersion(dockerClientVersion),
client.WithAPIVersionNegotiation(),
)
}
@@ -82,7 +82,7 @@ func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*cli
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(dockerClientVersion),
client.WithAPIVersionNegotiation(),
client.WithHTTPClient(httpCli),
)
}
@@ -116,7 +116,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
return client.NewClientWithOpts(
client.WithHost(endpointURL),
client.WithVersion(dockerClientVersion),
client.WithAPIVersionNegotiation(),
client.WithHTTPClient(httpCli),
client.WithHTTPHeaders(headers),
)
@@ -144,7 +144,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(dockerClientVersion),
client.WithAPIVersionNegotiation(),
client.WithHTTPClient(httpCli),
client.WithHTTPHeaders(headers),
)

View File

@@ -15,6 +15,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
)
// SwarmStackManager represents a service for managing stacks.
@@ -64,16 +65,35 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
if registry.Authentication {
err = registryutils.EnsureRegTokenValid(manager.dataStore, &registry)
if err != nil {
return err
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
continue
}
username, password, err := registryutils.GetRegEffectiveCredential(&registry)
if err != nil {
return err
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
continue
}
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
if err != nil {
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to login.")
}
}
}

View File

@@ -27,11 +27,10 @@ require (
github.com/google/go-cmp v0.5.9
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.4.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/jpillora/chisel v1.9.0
github.com/json-iterator/go v1.1.12
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/opencontainers/go-digest v1.0.0
@@ -49,11 +48,11 @@ require (
github.com/stretchr/testify v1.8.2
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.7.0
golang.org/x/crypto v0.12.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/mod v0.9.0
golang.org/x/oauth2 v0.6.0
golang.org/x/sync v0.1.0
golang.org/x/sync v0.3.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.27.4
@@ -110,7 +109,7 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/ansi v1.0.2 // indirect
github.com/jpillora/ansi v1.0.3 // indirect
github.com/jpillora/requestlog v1.0.0 // indirect
github.com/jpillora/sizestr v1.0.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
@@ -144,10 +143,10 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
@@ -162,6 +161,3 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
// Remove below line when the "determinstic key" patch for Chisel merged
replace github.com/jpillora/chisel => github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25

View File

@@ -203,8 +203,6 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -229,9 +227,10 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s=
github.com/jpillora/ansi v1.0.2/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jpillora/ansi v1.0.3 h1:nn4Jzti0EmRfDxm7JtEs5LzCbNwd5sv+0aE+LdS9/ZQ=
github.com/jpillora/ansi v1.0.3/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
github.com/jpillora/chisel v1.9.0 h1:pGZuxCZZ3W56Y2wX5bcXUvtB3r6wdaXRruJLAev8xzk=
github.com/jpillora/chisel v1.9.0/go.mod h1:qvgGfFR9ZhiDoYJM4IM1omX1HLbQSkZag8miP9u4SsQ=
github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA=
github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
@@ -312,8 +311,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25 h1:OfU9WGqE8bYdKB1dH3jgQpM2tP1+l5wGdNLO8Kk7nww=
github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25/go.mod h1:jhzGKO7NT6pNc/qto8YrNBGnuWZdqswvY6+n4zwE/Zc=
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/libhttp v0.0.0-20230615144939-a999f666d9a9 h1:Jq8g/pDcFL1Z/DnZgn6DyaWu29y9+RiB5aOJ/Xw4960=
@@ -400,9 +397,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@@ -425,9 +421,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
@@ -437,9 +432,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -455,8 +449,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -469,19 +461,17 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -50,7 +50,7 @@ func (handler *Handler) storeStackFile(stack *portainer.EdgeStack, deploymentTyp
entryPoint = stack.ManifestPath
}
_, err := handler.FileService.StoreEdgeStackFileFromBytesByVersion(stackFolder, entryPoint, stack.Version, config)
_, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, entryPoint, config)
if err != nil {
return fmt.Errorf("unable to persist updated Compose file with version on disk: %w", err)
}

View File

@@ -66,6 +66,11 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
}
edgeGroups, err := handler.DataStore.EdgeGroup().ReadAll()
if err != nil {
return httperror.InternalServerError("Unable to retrieve edge groups from the database", err)
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
@@ -88,7 +93,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, edgeGroups, settings)
if err != nil {
return httperror.InternalServerError("Unable to filter endpoints", err)
}

View File

@@ -294,7 +294,7 @@ func shouldReloadTLSConfiguration(endpoint *portainer.Endpoint, payload *endpoin
// When updating Docker API environment, as long as TLS is true and TLSSkipVerify is false,
// we assume that new TLS files have been uploaded and we need to reload the TLS configuration.
if endpoint.Type != portainer.DockerEnvironment ||
!strings.HasPrefix(*payload.URL, "tcp://") ||
(payload.URL != nil && !strings.HasPrefix(*payload.URL, "tcp://")) ||
payload.TLS == nil || !*payload.TLS {
return false
}

View File

@@ -12,6 +12,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/slices"
"github.com/portainer/portainer/api/internal/unique"
@@ -34,6 +35,7 @@ type EnvironmentsQuery struct {
edgeCheckInPassedSeconds int
edgeStackId portainer.EdgeStackID
edgeStackStatus *portainer.EdgeStackStatusType
excludeIds []portainer.EndpointID
}
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
@@ -69,6 +71,11 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
return EnvironmentsQuery{}, err
}
excludeIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "excludeIds")
if err != nil {
return EnvironmentsQuery{}, err
}
agentVersions := getArrayQueryParameter(r, "agentVersions")
name, _ := request.RetrieveQueryParameter(r, "name", true)
@@ -97,6 +104,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
types: endpointTypes,
tagIds: tagIDs,
endpointIds: endpointIDs,
excludeIds: excludeIDs,
tagsPartialMatch: tagsPartialMatch,
groupIds: groupIDs,
status: status,
@@ -111,13 +119,25 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
}, nil
}
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
func (handler *Handler) filterEndpointsByQuery(
filteredEndpoints []portainer.Endpoint,
query EnvironmentsQuery,
groups []portainer.EndpointGroup,
edgeGroups []portainer.EdgeGroup,
settings *portainer.Settings,
) ([]portainer.Endpoint, int, error) {
totalAvailableEndpoints := len(filteredEndpoints)
if len(query.endpointIds) > 0 {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
}
if len(query.excludeIds) > 0 {
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !slices.Contains(query.excludeIds, endpoint.ID)
})
}
if len(query.groupIds) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
}
@@ -177,7 +197,7 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, edgeGroups, tagsMap, query.search)
}
if len(query.types) > 0 {
@@ -208,9 +228,12 @@ func endpointStatusInStackMatchesFilter(edgeStackStatus map[portainer.EndpointID
status, ok := edgeStackStatus[envId]
// consider that if the env has no status in the stack it is in Pending state
// workaround because Stack.Status[EnvId].Details.Pending is never set to True in the codebase
if !ok && statusFilter == portainer.EdgeStackStatusPending {
return true
if statusFilter == portainer.EdgeStackStatusPending {
return !ok || len(status.Status) == 0
}
if !ok {
return false
}
return slices.ContainsFunc(status.Status, func(s portainer.EdgeStackDeploymentStatus) bool {
@@ -269,7 +292,13 @@ func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs
return endpoints[:n]
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
func filterEndpointsBySearchCriteria(
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
edgeGroups []portainer.EdgeGroup,
tagsMap map[portainer.TagID]string,
searchCriteria string,
) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
@@ -283,6 +312,15 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
endpoints[n] = endpoint
n++
continue
}
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpoints, endpointGroups) {
endpoints[n] = endpoint
n++
continue
}
}
@@ -362,6 +400,29 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
return false
}
// search endpoint's related edgegroups
func edgeGroupMatchSearchCriteria(
endpoint *portainer.Endpoint,
edgeGroups []portainer.EdgeGroup,
searchCriteria string,
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
) bool {
for _, edgeGroup := range edgeGroups {
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, endpoints, endpointGroups)
for _, endpointID := range relatedEndpointIDs {
if endpointID == endpoint.ID {
if strings.Contains(strings.ToLower(edgeGroup.Name), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {

View File

@@ -5,6 +5,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/slices"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
@@ -124,6 +125,28 @@ func Test_Filter_edgeFilter(t *testing.T) {
runTests(tests, t, handler, endpoints)
}
func Test_Filter_excludeIDs(t *testing.T) {
ids := []portainer.EndpointID{1, 2, 3, 4, 5, 6, 7, 8, 9}
environments := slices.Map(ids, func(id portainer.EndpointID) portainer.Endpoint {
return portainer.Endpoint{ID: id, GroupID: 1, Type: portainer.DockerEnvironment}
})
handler := setupFilterTest(t, environments)
tests := []filterTest{
{
title: "should exclude IDs 2,5,8",
expected: []portainer.EndpointID{1, 3, 4, 6, 7, 9},
query: EnvironmentsQuery{
excludeIds: []portainer.EndpointID{2, 5, 8},
},
},
}
runTests(tests, t, handler, environments)
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
@@ -135,7 +158,13 @@ func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []po
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
is := assert.New(t)
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
filteredEndpoints, _, err := handler.filterEndpointsByQuery(
endpoints,
test.query,
[]portainer.EndpointGroup{},
[]portainer.EdgeGroup{},
&portainer.Settings{},
)
is.NoError(err)

View File

@@ -84,7 +84,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.19.0
// @version 2.20.0
// @description.markdown api-description.md
// @termsOfService

View File

@@ -117,7 +117,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
stack.AutoUpdate.JobID = jobID
}
err = handler.startStack(stack, endpoint)
err = handler.startStack(stack, endpoint, securityContext)
if err != nil {
return httperror.InternalServerError("Unable to start stack", err)
}
@@ -136,13 +136,29 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return response.JSON(w, stack)
}
func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (handler *Handler) startStack(
stack *portainer.Stack,
endpoint *portainer.Endpoint,
securityContext *security.RestrictedRequestContext,
) error {
user, err := handler.DataStore.User().Read(securityContext.UserID)
if err != nil {
return fmt.Errorf("unable to load user information from the database: %w", err)
}
registries, err := handler.DataStore.Registry().ReadAll()
if err != nil {
return fmt.Errorf("unable to retrieve registries from the database: %w", err)
}
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
switch stack.Type {
case portainer.DockerComposeStack:
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
if stackutils.IsGitStack(stack) {
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint)
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint, filteredRegistries)
}
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
@@ -150,10 +166,10 @@ func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.E
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
if stackutils.IsGitStack(stack) {
return handler.StackDeployer.StartRemoteSwarmStack(stack, endpoint)
return handler.StackDeployer.StartRemoteSwarmStack(stack, endpoint, filteredRegistries)
}
return handler.SwarmStackManager.Deploy(stack, true, true, endpoint)
return handler.StackDeployer.DeploySwarmStack(stack, endpoint, filteredRegistries, true, true)
}
return nil

View File

@@ -198,6 +198,11 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
stack.Env = payload.Env
if stack.GitConfig != nil {
// detach from git
stack.GitConfig = nil
}
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.UpdateStoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
@@ -263,6 +268,11 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
stack.Env = payload.Env
if stack.GitConfig != nil {
// detach from git
stack.GitConfig = nil
}
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.UpdateStoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {

View File

@@ -6,7 +6,7 @@ import (
func (transport *baseTransport) proxyDeploymentsRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
switch request.Method {
case http.MethodPost, http.MethodPatch:
case http.MethodPost, http.MethodPatch, http.MethodPut:
transport.refreshRegistry(request, namespace)
}

View File

@@ -0,0 +1,16 @@
package securecookie
import (
"crypto/rand"
"io"
)
// GenerateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

View File

@@ -63,3 +63,12 @@ func RemoveIndex[T any](s []T, index int) []T {
s[index] = s[len(s)-1]
return s[:len(s)-1]
}
// Map applies the given function to each element of the slice and returns a new slice with the results
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}

View File

@@ -90,7 +90,10 @@ func (service *service) upgradeDocker(licenseKey, version, envType string) error
}
func (service *service) checkImageForDocker(ctx context.Context, image string, skipPullImage bool) error {
cli, err := client.NewClientWithOpts(client.FromEnv)
cli, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
if err != nil {
return errors.Wrap(err, "failed to create docker client")
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/securecookie"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/rs/zerolog/log"
)

View File

@@ -301,6 +301,8 @@ type (
// StackDeploymentInfo records the information of a deployed stack
StackDeploymentInfo struct {
// Version is the version of the stack and also is the deployed version in edge agent
Version int `json:"Version"`
// FileVersion is the version of the stack file, used to detect changes
FileVersion int `json:"FileVersion"`
// ConfigHash is the commit hash of the git repository used for deploying the stack
@@ -1557,7 +1559,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.19.0"
APIVersion = "2.20.0"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -100,6 +100,7 @@ func buildComposeStartCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions
cmd = appendSkipTLSVerifyIfNeeded(cmd, stack)
cmd = append(cmd, "-k")
cmd = append(cmd, env...)
cmd = append(cmd, registries...)
cmd = append(cmd, stack.GitConfig.URL)
cmd = append(cmd, stack.GitConfig.ReferenceName)
cmd = append(cmd, stack.Name)
@@ -162,6 +163,7 @@ func buildSwarmStartCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions,
cmd = append(cmd, UnpackerCmdSwarmDeploy, "-f", "-r", "-k")
cmd = appendSkipTLSVerifyIfNeeded(cmd, stack)
cmd = append(cmd, getEnv(stack.Env)...)
cmd = append(cmd, registries...)
cmd = append(cmd, stack.GitConfig.URL)
cmd = append(cmd, stack.GitConfig.ReferenceName)
cmd = append(cmd, stack.Name)

View File

@@ -35,7 +35,7 @@ func (s *noopDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoint
func (s *noopDeployer) UndeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
return nil
}
func (s *noopDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (s *noopDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
return nil
}
func (s *noopDeployer) StopRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
@@ -47,7 +47,7 @@ func (s *noopDeployer) DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *
func (s *noopDeployer) UndeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
return nil
}
func (s *noopDeployer) StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (s *noopDeployer) StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
return nil
}
func (s *noopDeployer) StopRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {

View File

@@ -1,7 +1,9 @@
package deployments
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
@@ -12,6 +14,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stdcopy"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
@@ -30,22 +33,29 @@ type RemoteStackDeployer interface {
// compose
DeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error
UndeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
StopRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
// swarm
DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
UndeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
StopRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
}
// Deploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
func (d *stackDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error {
func (d *stackDeployer) DeployRemoteComposeStack(
stack *portainer.Stack,
endpoint *portainer.Endpoint,
registries []portainer.Registry,
forcePullImage bool,
forceRecreate bool,
) error {
d.lock.Lock()
defer d.lock.Unlock()
d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint)
// --force-recreate doesn't pull updated images
if forcePullImage {
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
@@ -54,9 +64,14 @@ func (d *stackDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoin
}
}
return d.remoteStack(stack, endpoint, OperationDeploy, unpackerCmdBuilderOptions{
registries: registries,
})
return d.remoteStack(
stack,
endpoint,
OperationDeploy,
unpackerCmdBuilderOptions{
registries: registries,
},
)
}
// Undeploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
@@ -68,8 +83,19 @@ func (d *stackDeployer) UndeployRemoteComposeStack(stack *portainer.Stack, endpo
}
// Start a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
func (d *stackDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
return d.remoteStack(stack, endpoint, OperationComposeStart, unpackerCmdBuilderOptions{})
func (d *stackDeployer) StartRemoteComposeStack(
stack *portainer.Stack,
endpoint *portainer.Endpoint,
registries []portainer.Registry,
) error {
return d.remoteStack(
stack,
endpoint,
OperationComposeStart,
unpackerCmdBuilderOptions{
registries: registries,
},
)
}
// Stop a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
@@ -78,7 +104,13 @@ func (d *stackDeployer) StopRemoteComposeStack(stack *portainer.Stack, endpoint
}
// Deploy a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
func (d *stackDeployer) DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
func (d *stackDeployer) DeployRemoteSwarmStack(
stack *portainer.Stack,
endpoint *portainer.Endpoint,
registries []portainer.Registry,
prune bool,
pullImage bool,
) error {
d.lock.Lock()
defer d.lock.Unlock()
@@ -102,8 +134,19 @@ func (d *stackDeployer) UndeployRemoteSwarmStack(stack *portainer.Stack, endpoin
}
// Start a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
func (d *stackDeployer) StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
return d.remoteStack(stack, endpoint, OperationSwarmStart, unpackerCmdBuilderOptions{})
func (d *stackDeployer) StartRemoteSwarmStack(
stack *portainer.Stack,
endpoint *portainer.Endpoint,
registries []portainer.Registry,
) error {
return d.remoteStack(
stack,
endpoint,
OperationSwarmStart,
unpackerCmdBuilderOptions{
registries: registries,
},
)
}
// Stop a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
@@ -184,16 +227,18 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer.
case <-statusCh:
}
stdErr := &bytes.Buffer{}
out, err := cli.ContainerLogs(ctx, unpackerContainer.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
if err != nil {
log.Error().Err(err).Msg("unable to get logs from unpacker container")
} else {
outputBytes, err := io.ReadAll(out)
_, err = stdcopy.StdCopy(io.Discard, stdErr, out)
if err != nil {
log.Error().Err(err).Msg("unable to parse logs from unpacker container")
log.Warn().Err(err).Msg("unable to parse logs from unpacker container")
} else {
log.Info().
Str("output", string(outputBytes)).
Str("output", stdErr.String()).
Msg("Stack deployment output")
}
}
@@ -204,6 +249,26 @@ func (d *stackDeployer) remoteStack(stack *portainer.Stack, endpoint *portainer.
}
if status.State.ExitCode != 0 {
dec := json.NewDecoder(stdErr)
for {
errorStruct := struct {
Level string
Error string
}{}
if err := dec.Decode(&errorStruct); errors.Is(err, io.EOF) {
break
} else if err != nil {
log.Warn().Err(err).Msg("unable to parse logs from unpacker container")
continue
}
if errorStruct.Level == "error" {
return fmt.Errorf("an error occurred while running unpacker container with exit code %d: %s", status.State.ExitCode, errorStruct.Error)
}
}
return fmt.Errorf("an error occurred while running unpacker container with exit code %d", status.State.ExitCode)
}

View File

@@ -1,2 +0,0 @@
export default 'SvgrURL';
export const ReactComponent = 'div';

8
app/__mocks__/svg.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { forwardRef } from 'react';
const SvgrMock = forwardRef<HTMLSpanElement>((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<span ref={ref} {...props} />
));
export default SvgrMock;

View File

@@ -1,109 +0,0 @@
<div class="inner-datatable">
<table class="table-condensed table-hover nowrap-cells table">
<thead>
<tr>
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open" class="w-[10%]">
<div class="flex">
<table-column-header
col-title="'Status'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Status.State'"
is-sorted-desc="$ctrl.state.orderBy === 'Status.State' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Status.State')"
></table-column-header>
<span class="space-left">
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled"
>Filter
<pr-icon icon="'filter'"></pr-icon>
</span>
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled"
>Filter
<pr-icon icon="'check'"></pr-icon>
</span>
</span>
<div class="dropdown-menu" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Filter by state </div>
<div class="menuContent">
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
<input id="filter_state_{{ $ctrl.serviceId }}_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
<label for="filter_state_{{ $ctrl.serviceId }}_{{ $index }}">{{ filter.label }}</label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
</div>
</div>
</div>
</div>
</th>
<th style="width: 22%">Task</th>
<th>Actions</th>
<th>
<table-column-header
col-title="'Slot'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Slot'"
is-sorted-desc="$ctrl.state.orderBy === 'Slot' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Slot')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Node'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'NodeId'"
is-sorted-desc="$ctrl.state.orderBy === 'NodeId' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('NodeId')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Last Update'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Updated'"
is-sorted-desc="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Updated')"
></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))"
>
<td class="text-center">
<span class="label label-{{ item.Status.State | taskstatusbadge }} space-right">{{ item.Status.State }}</span>
</td>
<td>
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a>
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{
item.Id
}}</a>
</td>
<td>
<container-quick-actions
ng-if="!$ctrl.agentProxy || !item.Container"
container-id="item.ContainerId"
task-id="item.Id"
status="item.Status.State"
state="$ctrl.state"
></container-quick-actions>
<container-quick-actions
ng-if="$ctrl.agentProxy && item.Container"
container-id="item.Container.Id"
node-name="item.Container.NodeName"
status="item.Status.State"
state="$ctrl.state"
></container-quick-actions>
</td>
<td>{{ item.Slot ? item.Slot : '-' }}</td>
<td>{{ item.NodeId | tasknodename : $ctrl.nodes }}</td>
<td>{{ item.Updated | getisodate }}</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-muted text-center">No task matching filter.</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -1,15 +0,0 @@
angular.module('portainer.docker').component('serviceTasksDatatable', {
templateUrl: './serviceTasksDatatable.html',
controller: 'ServiceTasksDatatableController',
bindings: {
dataset: '<',
serviceId: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
nodes: '<',
agentProxy: '<',
textFilter: '=',
showTaskLogsButton: '<',
},
});

View File

@@ -1,94 +0,0 @@
import _ from 'lodash-es';
angular.module('portainer.docker').controller('ServiceTasksDatatableController', [
'$scope',
'$controller',
'DatatableService',
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;
this.state = Object.assign(this.state, {
showQuickActionStats: true,
showQuickActionLogs: true,
showQuickActionConsole: true,
showQuickActionInspect: true,
showQuickActionExec: true,
showQuickActionAttach: false,
});
this.filters = {
state: {
open: false,
enabled: false,
values: [],
},
};
this.applyFilters = function (item) {
var filters = ctrl.filters;
for (var i = 0; i < filters.state.values.length; i++) {
var filter = filters.state.values[i];
if (item.Status.State === filter.label && filter.display) {
return true;
}
}
return false;
};
this.onStateFilterChange = function () {
var filters = this.filters.state.values;
var filtered = false;
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
if (!filter.display) {
filtered = true;
}
}
this.filters.state.enabled = filtered;
};
this.prepareTableFromDataset = function () {
var availableStateFilters = [];
for (var i = 0; i < this.dataset.length; i++) {
var item = this.dataset[i];
availableStateFilters.push({ label: item.Status.State, display: true });
}
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
};
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View File

@@ -231,16 +231,7 @@
<tr dir-paginate-end ng-show="item.Expanded">
<td></td>
<td colspan="8">
<service-tasks-datatable
dataset="item.Tasks"
service-id="item.Id"
table-key="service-tasks"
order-by="Status.State"
nodes="$ctrl.nodes"
agent-proxy="$ctrl.agentProxy"
show-task-logs-button="$ctrl.showTaskLogsButton"
text-filter="$ctrl.state.textFilter"
></service-tasks-datatable>
<docker-service-tasks-datatable dataset="item.Tasks" search="$ctrl.state.textFilter"></docker-service-tasks-datatable>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">

View File

@@ -89,13 +89,7 @@
>
</td>
<td>
<container-quick-actions
ng-if="!$ctrl.agentProxy || !item.Container"
container-id="item.ContainerId"
task-id="item.Id"
status="item.Status.State"
state="$ctrl.state"
></container-quick-actions>
<task-table-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" task-id="item.Id" state="$ctrl.state"></task-table-quick-actions>
<container-quick-actions
ng-if="$ctrl.agentProxy && item.Container"
container-id="item.Container.Id"

View File

@@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { joinCommand, trimSHA } from './utils';
import { joinCommand, taskStatusBadge, trimSHA } from './utils';
function includeString(text, values) {
return values.some(function (val) {
@@ -49,22 +49,7 @@ angular
})
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
var labelStyle = 'default';
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) {
labelStyle = 'info';
} else if (includeString(status, ['pending'])) {
labelStyle = 'warning';
} else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) {
labelStyle = 'danger';
} else if (includeString(status, ['complete'])) {
labelStyle = 'primary';
} else if (includeString(status, ['running'])) {
labelStyle = 'success';
}
return labelStyle;
};
return taskStatusBadge;
})
.filter('taskhaslogs', function () {
'use strict';

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import { TaskState } from 'docker-types/generated/1.41';
export function trimSHA(imageName: string) {
if (!imageName) {
@@ -17,3 +18,38 @@ export function joinCommand(command: null | Array<string> = []) {
return command.join(' ');
}
export function taskStatusBadge(text?: TaskState) {
const status = _.toLower(text);
if (
[
'new',
'allocated',
'assigned',
'accepted',
'preparing',
'ready',
'starting',
'remove',
].includes(status)
) {
return 'info';
}
if (['pending'].includes(status)) {
return 'warning';
}
if (['shutdown', 'failed', 'rejected', 'orphaned'].includes(status)) {
return 'danger';
}
if (['complete'].includes(status)) {
return 'primary';
}
if (['running'].includes(status)) {
return 'success';
}
return 'default';
}

View File

@@ -1,14 +0,0 @@
export function TaskViewModel(data) {
this.Id = data.ID;
this.Created = data.CreatedAt;
this.Updated = data.UpdatedAt;
this.Slot = data.Slot;
this.Spec = data.Spec;
this.Status = data.Status;
this.DesiredState = data.DesiredState;
this.ServiceId = data.ServiceID;
this.NodeId = data.NodeID;
if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) {
this.ContainerId = data.Status.ContainerStatus.ContainerID;
}
}

36
app/docker/models/task.ts Normal file
View File

@@ -0,0 +1,36 @@
import { Task, TaskSpec, TaskState } from 'docker-types/generated/1.41';
export class TaskViewModel {
Id: string;
Created: string;
Updated: string;
Slot: number;
Spec?: TaskSpec;
Status: Task['Status'];
DesiredState: TaskState;
ServiceId: string;
NodeId: string;
ContainerId: string = '';
constructor(data: Task) {
this.Id = data.ID || '';
this.Created = data.CreatedAt || '';
this.Updated = data.UpdatedAt || '';
this.Slot = data.Slot || 0;
this.Spec = data.Spec;
this.Status = data.Status;
this.DesiredState = data.DesiredState || 'pending';
this.ServiceId = data.ServiceID || '';
this.NodeId = data.NodeID || '';
this.ContainerId = data.Status?.ContainerStatus?.ContainerID || '';
}
}

View File

@@ -13,7 +13,6 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
import { InsightsBox } from '@/react/components/InsightsBox';
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
import { ImagesDatatable } from '@/react/docker/images/ListView/ImagesDatatable/ImagesDatatable';
@@ -22,8 +21,10 @@ import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatab
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
import { servicesModule } from './services';
const ngModule = angular
.module('portainer.docker.react.components', [])
.module('portainer.docker.react.components', [servicesModule])
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
.component(
@@ -33,7 +34,6 @@ const ngModule = angular
'nodeName',
'state',
'status',
'taskId',
])
)
.component('templateListDropdown', TemplateListDropdownAngular)
@@ -71,7 +71,6 @@ const ngModule = angular
])
)
.component('betaAlert', r2a(BetaAlert, ['className', 'message', 'isHtml']))
.component('gpusInsights', r2a(GpusInsights, []))
.component(
'dockerImagesDatatable',
r2a(withUIRouter(withCurrentUser(ImagesDatatable)), [

View File

@@ -0,0 +1,21 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { TasksDatatable } from '@/react/docker/services/ListView/ServicesDatatable/TasksDatatable';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions';
export const servicesModule = angular
.module('portainer.docker.react.components.services', [])
.component(
'dockerServiceTasksDatatable',
r2a(withUIRouter(withCurrentUser(TasksDatatable)), ['dataset', 'search'])
)
.component(
'dockerTaskTableQuickActions',
r2a(withUIRouter(withCurrentUser(TaskTableQuickActions)), [
'state',
'taskId',
])
).name;

View File

@@ -1,6 +1,7 @@
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { confirmImageExport } from '@/react/docker/images/common/ConfirmExportModal';
import { confirmDelete } from '@@/modals/confirm';
angular.module('portainer.docker').controller('ImageController', [
'$async',
@@ -120,30 +121,42 @@ angular.module('portainer.docker').controller('ImageController', [
}
$scope.removeTag = function (repository) {
ImageService.deleteImage(repository, false)
.then(function success() {
if ($scope.image.RepoTags.length === 1) {
Notifications.success('Image successfully deleted', repository);
$state.go('docker.images', {}, { reload: true });
} else {
Notifications.success('Tag successfully deleted', repository);
$state.go('docker.images.image', { id: $transition$.params().id }, { reload: true });
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
});
return $async(async () => {
if (!(await confirmDelete('Are you sure you want to delete this tag?'))) {
return;
}
ImageService.deleteImage(repository, false)
.then(function success() {
if ($scope.image.RepoTags.length === 1) {
Notifications.success('Image successfully deleted', repository);
$state.go('docker.images', {}, { reload: true });
} else {
Notifications.success('Tag successfully deleted', repository);
$state.go('docker.images.image', { id: $transition$.params().id }, { reload: true });
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
});
});
};
$scope.removeImage = function (id) {
ImageService.deleteImage(id, false)
.then(function success() {
Notifications.success('Image successfully deleted', id);
$state.go('docker.images', {}, { reload: true });
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
});
return $async(async () => {
if (!(await confirmDelete('Deleting this image will also delete all associated tags. Are you sure you want to delete this image?'))) {
return;
}
ImageService.deleteImage(id, false)
.then(function success() {
Notifications.success('Image successfully deleted', id);
$state.go('docker.images', {}, { reload: true });
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
});
});
};
function exportImage(image) {

View File

@@ -57,7 +57,8 @@ angular.module('portainer.docker').controller('ImagesController', [
function confirmImageForceRemoval() {
return confirmDestructive({
title: 'Are you sure?',
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
message:
"Forcing removal of an image will remove it even if it's used by stopped containers, and delete all associated tags. Are you sure you want to remove the selected image(s)?",
confirmButton: buildConfirmButton('Remove the image', 'danger'),
});
}
@@ -65,7 +66,7 @@ angular.module('portainer.docker').controller('ImagesController', [
function confirmRegularRemove() {
return confirmDestructive({
title: 'Are you sure?',
message: 'Removing the image will remove all tags associated to that image. Are you sure you want to remove the image?',
message: 'Removing an image will also delete all associated tags. Are you sure you want to remove the selected image(s)?',
confirmButton: buildConfirmButton('Remove the image', 'danger'),
});
}

View File

@@ -72,6 +72,11 @@ angular
component: 'editEdgeStackView',
},
},
params: {
status: {
dynamic: true,
},
},
};
const edgeJobs = {

View File

@@ -92,7 +92,6 @@ export const componentsModule = angular
'query',
'title',
'data-cy',
'hideEnvironmentIds',
])
)
.component(

View File

@@ -1,4 +1,5 @@
import _ from 'lodash-es';
import { confirmDelete } from '@@/modals/confirm';
export class EdgeGroupsController {
/* @ngInject */
@@ -26,6 +27,10 @@ export class EdgeGroupsController {
}
async removeActionAsync(selectedItems) {
if (!(await confirmDelete('Do you want to remove the selected Edge Group(s)?'))) {
return;
}
for (let item of selectedItems) {
try {
await this.EdgeGroupService.remove(item.Id);

View File

@@ -15,7 +15,7 @@ export class EdgeJobsViewController {
}
removeAction(selectedItems) {
confirmDelete('Do you want to remove the selected edge job(s)?').then((confirmed) => {
confirmDelete('Do you want to remove the selected Edge job(s)?').then((confirmed) => {
if (!confirmed) {
return;
}

View File

@@ -1,209 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'server'"></pr-icon>
</div>
<span>
{{ $ctrl.titleText }}
</span>
</div>
<div class="searchBar">
<pr-icon icon="'search'" class="vertical-center" size="'sm'"></pr-icon>
<input
type="text"
class="searchInput ml-1"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for an application..."
ng-model-options="{ debounce: 300 }"
/>
</div>
<div ng-if="$ctrl.refreshCallback" class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><pr-icon icon="'more-vertical'"></pr-icon></span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" style="display: none" icon="'check'" mode="'success'"></pr-icon>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table" data-cy="k8sAppDetail-containerTable">
<thead>
<tr>
<th ng-if="!$ctrl.isPod">
<table-column-header
col-title="'Pod'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'PodName'"
is-sorted-desc="$ctrl.state.orderBy === 'PodName' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('PodName')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Name'"
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Image'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Image'"
is-sorted-desc="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Image')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Image Pull Policy'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'ImagePullPolicy'"
is-sorted-desc="$ctrl.state.orderBy === 'ImagePullPolicy' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('ImagePullPolicy')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Status'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Status'"
is-sorted-desc="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Status')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Node'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Node'"
is-sorted-desc="$ctrl.state.orderBy === 'Node' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Node')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Pod IP'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'IP'"
is-sorted-desc="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('IP')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Creation date'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'CreationDate'"
is-sorted-desc="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('CreationDate')"
></table-column-header>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
pagination-id="$ctrl.tableKey"
>
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
<td>{{ item.Name }}</td>
<td title="{{ item.Image }}">{{ item.Image | truncate : 64 }}</td>
<td>{{ item.ImagePullPolicy }}</td>
<td
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td
>
<td>
<span ng-if="item.Node">
<a ui-sref="kubernetes.cluster.node({ name: item.Node })">
{{ item.Node }}
</a>
</span>
<span ng-if="!item.Node">-</span>
</td>
<td>{{ item.PodIP }}</td>
<td>{{ item.CreationDate | getisodate }}</td>
<td>
<a
ng-if="item.Status === 'Running' && $ctrl.useServerMetrics"
ui-sref="kubernetes.applications.application.stats({ pod: item.PodName, container: item.Name })"
class="vertical-center mr-1"
>
<pr-icon icon="'bar-chart'"></pr-icon>Stats
</a>
<a ui-sref="kubernetes.applications.application.logs({ pod: item.PodName, container: item.Name })" class="vertical-center mr-1">
<pr-icon icon="'file-text'"></pr-icon>Logs
</a>
<a
ng-if="item.Status === 'Running'"
ui-sref="kubernetes.applications.application.console({ pod: item.PodName, container: item.Name })"
class="vertical-center mr-1"
>
<pr-icon icon="'terminal'"></pr-icon>Console
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="7" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="7" class="text-muted text-center">No pod available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -1,14 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesContainersDatatable', {
templateUrl: './containersDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
isPod: '<',
useServerMetrics: '<',
},
});

View File

@@ -14,9 +14,13 @@
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Switch to advanced mode to copy and paste multiple key/values
</div>
<div class="col-sm-12 small text-muted vertical-center" ng-if="!$ctrl.formValues.IsSimple">
<div class="col-sm-12 small text-muted vertical-center" ng-if="!$ctrl.formValues.IsSimple && $ctrl.type === 'configmap'">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Generate a configuration entry per line, use YAML format
Generate a ConfigMap entry per line, use YAML format
</div>
<div class="col-sm-12 small text-muted vertical-center" ng-if="!$ctrl.formValues.IsSimple && $ctrl.type === 'secret'">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Generate a Secret entry per line, use YAML format
</div>
</div>

View File

@@ -8,5 +8,6 @@ angular.module('portainer.kubernetes').component('kubernetesConfigurationData',
isValid: '=',
isCreation: '=',
isEditorDirty: '=',
type: '<',
},
});

View File

@@ -1,34 +0,0 @@
<div>
<web-editor-form
identifier="application-details-yaml"
value="$ctrl.data"
yml="true"
placeholder="Define or paste the content of your manifest here"
read-only="true"
hide-title="true"
height="{{ $ctrl.expanded ? '800px' : '500px' }}"
>
</web-editor-form>
<div class="py-5">
<span class="btn btn-light btn-sm" ng-click="$ctrl.copyYAML()">
<pr-icon class="vertical-center" icon="'copy'"></pr-icon>
Copy to clipboard
</span>
<span class="btn btn-light btn-sm space-left !ml-0" ng-click="$ctrl.toggleYAMLInspectorExpansion()">
<pr-icon class="vertical-center" icon="'minus'" size="'sm'" ng-if="$ctrl.expanded"></pr-icon>
<pr-icon class="vertical-center" icon="'plus'" size="'sm'" ng-if="!$ctrl.expanded"></pr-icon>
{{ $ctrl.expanded ? 'Collapse' : 'Expand' }}
</span>
<span id="copyNotificationYAML" style="display: none" class="small vertical-center ml-1">
<pr-icon class="vertical-center" icon="'check'" size="'md'" mode="'success'"></pr-icon> copied
</span>
<be-teaser-button
class="float-right"
feature-id="$ctrl.limitedFeature"
message="'Applies any changes that you make in the YAML editor by calling the Kubernetes API to patch the relevant resources. Any resource removals or unexpected resource additions that you make in the YAML will be ignored. Note that editing is disabled for resources in namespaces marked as system.'"
heading="'Apply YAML changes'"
button-text="'Apply changes'"
></be-teaser-button>
</div>
</div>

View File

@@ -1,8 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesYamlInspector', {
templateUrl: './yamlInspector.html',
controller: 'KubernetesYamlInspectorController',
bindings: {
key: '@',
data: '<',
},
});

View File

@@ -1,46 +0,0 @@
import angular from 'angular';
import YAML from 'yaml';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
class KubernetesYamlInspectorController {
/* @ngInject */
constructor(clipboard) {
this.clipboard = clipboard;
this.expanded = false;
}
cleanYamlUnwantedFields(yml) {
try {
const ymls = yml.split('---');
const cleanYmls = ymls.map((yml) => {
const y = YAML.parse(yml);
if (y.metadata) {
delete y.metadata.managedFields;
delete y.metadata.resourceVersion;
}
return YAML.stringify(y);
});
return cleanYmls.join('---\n');
} catch (e) {
return yml;
}
}
copyYAML() {
this.clipboard.copyText(this.data);
$('#copyNotificationYAML').show().fadeOut(2500);
}
toggleYAMLInspectorExpansion() {
this.expanded = !this.expanded;
}
$onInit() {
this.data = this.cleanYamlUnwantedFields(this.data);
this.limitedFeature = FeatureId.K8S_EDIT_YAML;
}
}
export default KubernetesYamlInspectorController;
angular.module('portainer.kubernetes').controller('KubernetesYamlInspectorController', KubernetesYamlInspectorController);

View File

@@ -0,0 +1,33 @@
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from '../pod/models';
export function nodeAffinityValues(
values: string | string[],
operator: KubernetesPodNodeAffinityNodeSelectorRequirementOperators
) {
if (
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN ||
operator ===
KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN
) {
return values;
}
if (
operator ===
KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS ||
operator ===
KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST
) {
return '';
}
if (
operator ===
KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN ||
operator ===
KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN
) {
return values[0];
}
return '';
}

View File

@@ -1,7 +1,7 @@
import _ from 'lodash-es';
import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models';
import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import { nodeAffinityValues } from './application';
angular
.module('portainer.kubernetes')
@@ -65,22 +65,7 @@ angular
})
.filter('kubernetesApplicationConstraintNodeAffinityValue', function () {
'use strict';
return function (values, operator) {
if (operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN || operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN) {
return values;
} else if (
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS ||
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST
) {
return '';
} else if (
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN ||
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN
) {
return values[0];
}
return '';
};
return nodeAffinityValues;
})
.filter('kubernetesNodeLabelHumanReadbleText', function () {
'use strict';

View File

@@ -76,12 +76,6 @@ class KubernetesApplicationHelper {
return containers;
}
static associateAllContainersAndApplication(app) {
const containers = _.flatMap(_.map(app.Pods, 'Containers'));
KubernetesApplicationHelper.associateContainerPersistedFoldersAndConfigurations(app, containers);
return containers;
}
static portMappingsFromApplications(applications) {
const res = _.reduce(
applications,

View File

@@ -4,9 +4,11 @@ class KubernetesFormValidationHelper {
static getInvalidKeys(names) {
const res = {};
_.forEach(names, (name, index) => {
const valid = /^[-._a-zA-Z0-9]+$/.test(name);
if (!valid) {
res[index] = true;
if (name) {
const valid = /^[-._a-zA-Z0-9]+$/.test(name);
if (!valid) {
res[index] = true;
}
}
});
return res;

View File

@@ -1,52 +0,0 @@
import _ from 'lodash-es';
angular
.module('portainer.kubernetes')
.filter('kubernetesPodStatusColor', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
switch (status) {
case 'running':
return 'success';
case 'waiting':
return 'warning';
case 'terminated':
return 'info';
default:
return 'danger';
}
};
})
.filter('kubernetesPodConditionStatusText', function () {
'use strict';
return function (status, type) {
switch (type) {
case 'Unschedulable':
switch (status) {
case 'True':
return 'Alert';
case 'False':
return 'OK';
case 'Unknown':
return 'Warning';
}
break;
case 'PodScheduled':
case 'Ready':
case 'Initialized':
case 'ContainersReady':
switch (status) {
case 'True':
return 'Ok';
case 'False':
return 'Alert';
case 'Unknown':
return 'Warning';
}
break;
default:
return 'Unknown';
}
};
});

View File

@@ -1,11 +1,11 @@
export const KubernetesPodNodeAffinityNodeSelectorRequirementOperators = Object.freeze({
IN: 'In',
NOT_IN: 'NotIn',
EXISTS: 'Exists',
DOES_NOT_EXIST: 'DoesNotExist',
GREATER_THAN: 'Gt',
LOWER_THAN: 'Lt',
});
export enum KubernetesPodNodeAffinityNodeSelectorRequirementOperators {
IN = 'In',
NOT_IN = 'NotIn',
EXISTS = 'Exists',
DOES_NOT_EXIST = 'DoesNotExist',
GREATER_THAN = 'Gt',
LOWER_THAN = 'Lt',
}
/**
* KubernetesPodAffinity Model

View File

@@ -15,10 +15,13 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import {
ApplicationSummaryWidget,
ApplicationDetailsWidget,
ApplicationEventsDatatable,
} from '@/react/kubernetes/applications/DetailsView';
import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { ApplicationContainersDatatable } from '@/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable';
import { withFormValidation } from '@/react-tools/withFormValidation';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { PlacementsDatatable } from '@/react/kubernetes/applications/ItemView/PlacementsDatatable';
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
export const ngModule = angular
.module('portainer.kubernetes.react.components', [])
@@ -94,6 +97,13 @@ export const ngModule = angular
'supportGlobalDeployment',
])
)
.component(
'kubeYamlInspector',
r2a(withUIRouter(withReactQuery(withCurrentUser(YAMLInspector))), [
'identifier',
'data',
])
)
.component(
'applicationSummaryWidget',
r2a(
@@ -102,11 +112,34 @@ export const ngModule = angular
)
)
.component(
'applicationDetailsWidget',
'applicationContainersDatatable',
r2a(
withUIRouter(withReactQuery(withUserProvider(ApplicationDetailsWidget))),
withUIRouter(
withReactQuery(withCurrentUser(ApplicationContainersDatatable))
),
[]
)
)
.component(
'applicationDetailsWidget',
r2a(
withUIRouter(withReactQuery(withCurrentUser(ApplicationDetailsWidget))),
[]
)
)
.component(
'applicationEventsDatatable',
r2a(
withUIRouter(withReactQuery(withCurrentUser(ApplicationEventsDatatable))),
[]
)
)
.component(
'kubernetesApplicationPlacementsDatatable',
r2a(withUIRouter(withCurrentUser(PlacementsDatatable)), [
'dataset',
'onRefresh',
])
);
export const componentsModule = ngModule.name;

View File

@@ -352,7 +352,7 @@
<!-- #region CONFIGMAPS -->
<div class="form-group">
<div class="col-sm-12 vertical-center">
<label class="control-label !pt-0 text-left">ConfigMap</label>
<label class="control-label !pt-0 text-left">ConfigMaps</label>
</div>
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.ConfigMaps.length">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
@@ -503,7 +503,7 @@
<!-- #region SECRETS -->
<div class="form-group">
<div class="col-sm-12 vertical-center pt-2.5">
<label class="control-label !pt-0 text-left">Secret</label>
<label class="control-label !pt-0 text-left">Secrets</label>
</div>
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.Secrets.length">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>

View File

@@ -40,15 +40,11 @@
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
The placement component helps you understand whether or not this application can be deployed on a specific node.
</div>
<kubernetes-application-placements-datatable
title-text="Placement constraints/preferences"
title-icon="minimize-2"
ng-if="ctrl.placements"
dataset="ctrl.placements"
table-key="kubernetes.application.placements"
order-by="Name"
reverse-order="false"
loading="ctrl.state.dataLoading"
refresh-callback="ctrl.getApplication"
on-refresh="(ctrl.getApplication)"
></kubernetes-application-placements-datatable>
</uib-tab>
@@ -60,22 +56,13 @@
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
title-icon="history"
dataset="ctrl.events"
table-key="kubernetes.application.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
></kubernetes-events-datatable>
<application-events-datatable />
</uib-tab>
<uib-tab index="3" ng-if="ctrl.application.Yaml" select="ctrl.showEditor()" classes="btn-sm">
<uib-tab-heading> <pr-icon icon="'code'"></pr-icon> YAML </uib-tab-heading>
<div class="px-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="application-yaml" data="ctrl.application.Yaml"></kubernetes-yaml-inspector>
<kube-yaml-inspector identifier="'application-yaml'" data="ctrl.application.Yaml" />
</div>
</uib-tab>
</uib-tabset>
@@ -91,17 +78,6 @@
</div>
<div class="row">
<div class="col-sm-12">
<kubernetes-containers-datatable
title-text="Application containers"
title-icon="server"
dataset="ctrl.allContainers"
table-key="kubernetes.application.containers"
is-pod="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD"
order-by="{{ ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD ? 'Name' : 'PodName' }}"
use-server-metrics="ctrl.state.useServerMetrics"
>
</kubernetes-containers-datatable>
</div>
<application-containers-datatable />
</div>
</div>

View File

@@ -10,7 +10,6 @@ import {
KubernetesDeploymentTypes,
} from 'Kubernetes/models/application/models';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index';
@@ -199,7 +198,6 @@ class KubernetesApplicationController {
this.KubernetesNodeService.get(),
]);
this.application = application;
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
this.placements = computePlacements(nodes, this.application);
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
@@ -238,7 +236,6 @@ class KubernetesApplicationController {
eventWarningCount: 0,
placementWarning: false,
expandedNote: false,
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
publicUrl: this.endpoint.PublicURL,
};

View File

@@ -1,71 +0,0 @@
import _ from 'lodash-es';
angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],
expandAll: false,
});
this.expandItem = function (item, expanded) {
if (!this.itemCanExpand(item)) {
return;
}
item.Expanded = expanded;
if (!expanded) {
item.Highlighted = false;
}
};
this.itemCanExpand = function (item) {
return !item.AcceptsApplication;
};
this.hasExpandableItems = function () {
return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
};
this.expandAll = function () {
this.state.expandAll = !this.state.expandAll;
_.forEach(this.state.filteredDataSet, (item) => {
if (this.itemCanExpand(item)) {
this.expandItem(item, this.state.expandAll);
}
});
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
});

View File

@@ -1,15 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesApplicationPlacementsDatatable', {
templateUrl: './template.html',
controller: 'KubernetesApplicationPlacementsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
refreshCallback: '<',
loading: '<',
removeAction: '<',
},
});

View File

@@ -1,191 +0,0 @@
<div class="datatable">
<div class="toolBar">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon"></pr-icon>
</div>
<span class="vertical-center">
{{ $ctrl.titleText }}
</span>
</div>
<div class="searchBar">
<pr-icon icon="'search'" class="vertical-center"></pr-icon>
<input
type="text"
class="searchInput ml-1"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for a node..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><pr-icon icon="'more-vertical'"></pr-icon></span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" style="display: none" icon="'check'" mode="'success'"></pr-icon>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="table-responsive border-none">
<table class="table-hover nowrap-cells table">
<thead>
<tr>
<th style="width: 2%">
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
<pr-icon icon="'chevron-down'" ng-if="$ctrl.state.expandAll"></pr-icon>
<pr-icon icon="'chevron-up'" ng-if="!$ctrl.state.expandAll"></pr-icon>
</a>
</th>
<th style="width: 98%">
<table-column-header
col-title="'Node'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Node'"
is-sorted-desc="$ctrl.state.orderBy === 'Node' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Node')"
></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter: $ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
ng-class="{ active: item.Checked, 'datatable-highlighted': item.Highlighted }"
ng-click="$ctrl.expandItem(item, !item.Expanded)"
pagination-id="$ctrl.tableKey"
>
<td>
<a ng-if="$ctrl.itemCanExpand(item)">
<pr-icon icon="'chevron-down'" class="mr-1" ng-if="item.Expanded"></pr-icon>
<pr-icon icon="'chevron-up'" class="mr-1" ng-if="!item.Expanded"></pr-icon>
</a>
<pr-icon icon="'check'" ng-if="item.AcceptsApplication" mode="'success'"></pr-icon>
<pr-icon icon="'x'" ng-if="!item.AcceptsApplication" mode="'error'"></pr-icon>
</td>
<td>
{{ item.Name }}
</td>
</tr>
<!-- ADMIN + UNMET TAINTS -->
<tr
ng-if="$ctrl.isAdmin"
ng-show="item.Expanded"
ng-repeat="taint in item.UnmetTaints"
ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }"
>
<td colspan="2">
This application is missing a toleration for the taint <code>{{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.Effect }}</code>
</td>
</tr>
<!-- !ADMIN + UNMET TAINTS -->
<!-- USER + UNMET TAINTS -->
<tr
ng-if="!$ctrl.isAdmin && item.UnmetTaints.length"
ng-show="item.Expanded"
ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }"
>
<td colspan="2"> Placement constraint not respected for that node. </td>
</tr>
<!-- ! USER + UNMET TAINTS -->
<!-- ADMIN + UNMET NODE SELECTOR LABELS -->
<tr
ng-if="$ctrl.isAdmin"
ng-show="item.Expanded"
ng-repeat="label in item.UnmatchedNodeSelectorLabels"
ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }"
>
<td colspan="2">
This application can only be scheduled on a node where the label <code>{{ label.key }}</code> is set to <code>{{ label.value }}</code>
</td>
</tr>
<!-- ! ADMIN + UNMET NODE SELECTOR LABELS -->
<!-- USER + UNMET NODE SELECTOR LABELS || UNMET NODE AFFINITIES -->
<tr
ng-if="!$ctrl.isAdmin && (item.UnmatchedNodeSelectorLabels.length || item.UnmatchedNodeAffinities.length)"
ng-show="item.Expanded"
ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }"
>
<td colspan="2"> Placement label not respected for that node. </td>
</tr>
<!-- ! USER + UNMET NODE SELECTOR LABELS || UNMET NODE AFFINITIES -->
<!-- ADMIN + UNMET NODE AFFINITIES -->
<tr
ng-if="$ctrl.isAdmin"
ng-show="item.Expanded && item.UnmatchedNodeAffinities.length"
ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }"
>
<td colspan="2"> This application can only be scheduled on nodes respecting one of the following labels combination: </td>
</tr>
<tr
dir-paginate-end
ng-if="$ctrl.isAdmin"
ng-show="item.Expanded"
ng-repeat="aff in item.UnmatchedNodeAffinities"
ng-class="{ 'datatable-highlighted': item.Highlighted, 'datatable-unhighlighted': !item.Highlighted }"
>
<td></td>
<td>
<code ng-repeat-start="term in aff track by $index">
{{ term.key }} {{ term.operator }} {{ term.values | kubernetesApplicationConstraintNodeAffinityValue : term.operator }}
</code>
<span ng-repeat-end>{{ $last ? '' : ' + ' }}</span>
</td>
</tr>
<!-- ! ADMIN + UNMET NODE AFFINITIES -->
<tr ng-if="$ctrl.loading">
<td colspan="2" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="!$ctrl.loading && (!$ctrl.dataset || $ctrl.state.filteredDataSet.length === 0)">
<td colspan="2" class="text-muted text-center">No node available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</div>

View File

@@ -26,95 +26,93 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve container metrics">
<span class="small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
Portainer was unable to retrieve any metrics associated to that container. Please contact your administrator to ensure that the Kubernetes metrics feature is properly
configured.
</span>
</information-panel>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-md-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'info'"></pr-icon>
</div>
<span class="vertical-center"> About statistics </span>
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve container metrics">
<span class="small text-warning vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
Portainer was unable to retrieve any metrics associated to that container. Please contact your administrator to ensure that the Kubernetes metrics feature is properly
configured.
</span>
</information-panel>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-md-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'info'"></pr-icon>
</div>
<span class="vertical-center"> About statistics </span>
</div>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-warning">
This view displays real-time statistics about the container <b>{{ ctrl.state.transition.containerName | trimcontainername }}</b
>.
</span>
</div>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left"> Refresh rate </label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" size="'sm'"></pr-icon>
</div>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-warning">
This view displays real-time statistics about the container <b>{{ ctrl.state.transition.containerName | trimcontainername }}</b
>.
</span>
</div>
<div class="form-group" ng-if="ctrl.state.networkStatsUnavailable">
<div class="col-sm-12">
<span class="small text-muted">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
Network stats are unavailable for this container.
</span>
</div>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left"> Refresh rate </label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'svg-memory'"></pr-icon>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" size="'sm'"></pr-icon>
</span>
</div>
<div class="form-group" ng-if="ctrl.state.networkStatsUnavailable">
<div class="col-sm-12">
<span class="small text-muted">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
Network stats are unavailable for this container.
</span>
</div>
<span class="vertical-center"> Memory usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-container" style="position: relative">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-sm-12" ng-if="!ctrl.state.networkStatsUnavailable">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'cpu'"></pr-icon>
</div>
<span class="vertical-center"> CPU usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-container" style="position: relative">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'svg-memory'"></pr-icon>
</div>
<span class="vertical-center"> Memory usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-container" style="position: relative">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-sm-12" ng-if="!ctrl.state.networkStatsUnavailable">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'cpu'"></pr-icon>
</div>
<span class="vertical-center"> CPU usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-container" style="position: relative">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -19,6 +19,7 @@ class KubernetesApplicationStatsController {
this.ChartService = ChartService;
this.onInit = this.onInit.bind(this);
this.initCharts = this.initCharts.bind(this);
}
changeUpdateRepeater() {
@@ -68,17 +69,26 @@ class KubernetesApplicationStatsController {
}
initCharts() {
const cpuChartCtx = $('#cpuChart');
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
this.cpuChart = cpuChart;
const memoryChartCtx = $('#memoryChart');
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
this.memoryChart = memoryChart;
this.updateCPUChart();
this.updateMemoryChart();
this.setUpdateRepeater();
let i = 0;
const findCharts = setInterval(() => {
let cpuChartCtx = $('#cpuChart');
let memoryChartCtx = $('#memoryChart');
if (cpuChartCtx.length !== 0 && memoryChartCtx.length !== 0) {
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
this.cpuChart = cpuChart;
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
this.memoryChart = memoryChart;
this.updateCPUChart();
this.updateMemoryChart();
this.setUpdateRepeater();
clearInterval(findCharts);
return;
}
i++;
if (i >= 10) {
clearInterval(findCharts);
}
}, 200);
}
getStats() {

View File

@@ -247,7 +247,7 @@
<div class="flex-center gap-1"> <pr-icon icon="'code'" size="'sm'"></pr-icon> YAML </div>
</uib-tab-heading>
<div style="padding-right: 25px" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="node-yaml" data="ctrl.node.Yaml"> </kubernetes-yaml-inspector>
<kube-yaml-inspector identifier="'node-yaml'" data="ctrl.node.Yaml" />
</div>
</uib-tab>
</uib-tabset>

View File

@@ -15,86 +15,84 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve node metrics">
<span class="small text-muted vertical-center">
<pr-icon icon="'alert-triangle'" mode="'primary'"></pr-icon>
Portainer was unable to retrieve any metrics associated to that node. Please contact your administrator to ensure that the Kubernetes metrics feature is properly configured.
</span>
</information-panel>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-md-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'info'"></pr-icon>
</div>
<span class="vertical-center"> About statistics </span>
<information-panel ng-if="!ctrl.state.getMetrics" title-text="Unable to retrieve node metrics">
<span class="small text-muted vertical-center">
<pr-icon icon="'alert-triangle'" mode="'primary'"></pr-icon>
Portainer was unable to retrieve any metrics associated to that node. Please contact your administrator to ensure that the Kubernetes metrics feature is properly configured.
</span>
</information-panel>
<div class="row" ng-if="ctrl.state.getMetrics">
<div class="col-md-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'info'"></pr-icon>
</div>
<span class="vertical-center"> About statistics </span>
</div>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
This view displays real-time statistics about the node <b>{{ ctrl.state.transition.nodeName }}</b
>.
</span>
</div>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left"> Refresh rate </label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</div>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
This view displays real-time statistics about the node <b>{{ ctrl.state.transition.nodeName }}</b
>.
</span>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-show="ctrl.state.getMetrics">
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'svg-memory'"></pr-icon>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left"> Refresh rate </label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="ctrl.state.refreshRate" ng-change="ctrl.changeUpdateRepeater()" class="form-control">
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<span class="vertical-center"> Memory usage </span>
<span>
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
</span>
</div>
</div>
<rd-widget-body>
<div class="chart-node" style="position: relative">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'cpu'"></pr-icon>
</div>
<span class="vertical-center"> CPU usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-node" style="position: relative">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-show="ctrl.state.getMetrics">
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'svg-memory'"></pr-icon>
</div>
<span class="vertical-center"> Memory usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-node" style="position: relative">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-sm-12">
<rd-widget>
<div class="toolBar px-5 pt-5">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'cpu'"></pr-icon>
</div>
<span class="vertical-center"> CPU usage </span>
</div>
</div>
<rd-widget-body>
<div class="chart-node" style="position: relative">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -17,6 +17,7 @@ class KubernetesNodeStatsController {
this.ChartService = ChartService;
this.onInit = this.onInit.bind(this);
this.initCharts = this.initCharts.bind(this);
}
changeUpdateRepeater() {
@@ -63,17 +64,20 @@ class KubernetesNodeStatsController {
}
initCharts() {
const cpuChartCtx = $('#cpuChart');
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
this.cpuChart = cpuChart;
const memoryChartCtx = $('#memoryChart');
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
this.memoryChart = memoryChart;
this.updateCPUChart();
this.updateMemoryChart();
this.setUpdateRepeater();
const findCharts = setInterval(() => {
let cpuChartCtx = $('#cpuChart');
let memoryChartCtx = $('#memoryChart');
if (cpuChartCtx.length !== 0 && memoryChartCtx.length !== 0) {
const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
this.cpuChart = cpuChart;
const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
this.memoryChart = memoryChart;
this.updateCPUChart();
this.updateMemoryChart();
this.setUpdateRepeater();
clearInterval(findCharts);
}
}, 200);
}
getStats() {
@@ -84,7 +88,7 @@ class KubernetesNodeStatsController {
const memory = filesizeParser(stats.usage.memory);
const cpu = KubernetesResourceReservationHelper.parseCPU(stats.usage.cpu);
this.stats = {
read: stats.creationTimestamp,
read: stats.metadata.creationTimestamp,
MemoryUsage: memory,
CPUUsage: (cpu / this.nodeCPU) * 100,
};
@@ -118,12 +122,6 @@ class KubernetesNodeStatsController {
this.nodeCPU = node.CPU || 1;
await this.getStats();
if (this.state.getMetrics) {
this.$document.ready(() => {
this.initCharts();
});
}
} else {
this.state.getMetrics = false;
}
@@ -132,6 +130,11 @@ class KubernetesNodeStatsController {
this.Notifications.error('Failure', err, 'Unable to retrieve node stats');
} finally {
this.state.viewReady = true;
if (this.state.getMetrics) {
this.$document.ready(() => {
this.initCharts();
});
}
}
}

View File

@@ -88,6 +88,7 @@
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="true"
type="'configmap'"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>
</div>

View File

@@ -75,7 +75,7 @@
YAML
</uib-tab-heading>
<div class="px-5 !pt-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"></kubernetes-yaml-inspector>
<kube-yaml-inspector identifier="'configuration-yaml'" data="ctrl.configuration.Yaml" />
</div>
</uib-tab>
</uib-tabset>
@@ -100,6 +100,7 @@
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="false"
type="'configmap'"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>

View File

@@ -87,7 +87,7 @@
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
<span>
More information about types of secret can be found in the official
<a class="hyperlink" href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types" target="_blank">kubernetes documentation</a>.
<a class="hyperlink" href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types" target="_blank">Kubernetes documentation</a>.
</span>
</div>
</div>
@@ -186,6 +186,7 @@
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="true"
type="'secret'"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>

View File

@@ -82,7 +82,7 @@
YAML
</uib-tab-heading>
<div class="px-5 !pt-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"></kubernetes-yaml-inspector>
<kube-yaml-inspector identifier="'secret-yaml'" data="ctrl.configuration.Yaml" />
</div>
</uib-tab>
</uib-tabset>
@@ -107,6 +107,7 @@
is-valid="ctrl.state.isDataValid"
on-change-validation="ctrl.isFormValid()"
is-creation="false"
type="'secret'"
is-editor-dirty="ctrl.state.isEditorDirty"
></kubernetes-configuration-data>

View File

@@ -22,7 +22,7 @@
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesClusterSetupForm">
<div class="col-sm-12 form-section-title"> Networking </div>
<div class="col-sm-12 form-section-title"> Networking - Services </div>
<div class="form-group">
<div class="col-sm-12 text-muted small">
@@ -41,6 +41,8 @@
</div>
</div>
<div class="col-sm-12 form-section-title"> Networking - Ingresses </div>
<ingress-class-datatable
on-change-controllers="(ctrl.onChangeControllers)"
allow-none-ingress-class="ctrl.formValues.AllowNoneIngressClass"
@@ -51,47 +53,57 @@
view="'cluster'"
></ingress-class-datatable>
<label htmlFor="foldingButtonIngControllerSettings" class="col-sm-12 form-section-title flex cursor-pointer items-center">
<button
id="foldingButtonIngControllerSettings"
type="button"
class="mx-2 !ml-0 inline-flex w-2 items-center justify-center border-0 bg-transparent"
ng-click="ctrl.toggleAdvancedIngSettings($event)"
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="ctrl.formValues.AllowNoneIngressClass"
name="'allowNoIngressClass'"
label="'Allow ingress class to be set to &quot;none&quot;'"
tooltip="'This allows users setting up ingresses to select &quot;none&quot; as the ingress class.'"
on-change="(ctrl.onToggleAllowNoneIngressClass)"
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
switch-class="'col-sm-8'"
>
</por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="ctrl.formValues.IngressAvailabilityPerNamespace"
name="'ingressAvailabilityPerNamespace'"
label="'Configure ingress controller availability per namespace'"
tooltip="'This allows an administrator to configure, in each namespace, which ingress controllers will be available for users to select when setting up ingresses for applications.'"
on-change="(ctrl.onToggleIngressAvailabilityPerNamespace)"
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
switch-class="'col-sm-8'"
>
</por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="ctrl.formValues.RestrictStandardUserIngressW"
name="'restrictStandardUserIngressW'"
label="'Only allow admins to deploy ingresses'"
tooltip="'Enforces only allowing admins to deploy ingresses (and disallows standard users from doing so).'"
on-change="(ctrl.onToggleRestrictStandardUserIngressW)"
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
switch-class="'col-sm-8 text-muted'"
data-cy="kubeSetup-restrictStandardUserIngressWToggle"
disabled="!ctrl.isRBACEnabled"
>
</por-switch-field>
</div>
</div>
<div class="mb-4 !inline-flex gap-1 !align-top">
<div class="icon icon-sm"><pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon></div>
<div class="text-muted small"
>You may set up ingress defaults (hostnames and annotations) via Create/Edit ingress. Users may then select them via the hostname dropdown in Create/Edit
application.</div
>
<pr-icon ng-if="!ctrl.state.isIngToggleSectionExpanded" icon="'chevron-right'"></pr-icon>
<pr-icon ng-if="ctrl.state.isIngToggleSectionExpanded" icon="'chevron-down'"></pr-icon>
</button>
More settings
</label>
<div ng-if="ctrl.state.isIngToggleSectionExpanded" class="ml-4">
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="ctrl.formValues.AllowNoneIngressClass"
name="'allowNoIngressClass'"
label="'Allow ingress class to be set to &quot;none&quot;'"
tooltip="'This allows users setting up ingresses to select &quot;none&quot; as the ingress class.'"
on-change="(ctrl.onToggleAllowNoneIngressClass)"
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
switch-class="'col-sm-8'"
>
</por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
checked="ctrl.formValues.IngressAvailabilityPerNamespace"
name="'ingressAvailabilityPerNamespace'"
label="'Configure ingress controller availability per namespace'"
tooltip="'This allows an administrator to configure, in each namespace, which ingress controllers will be available for users to select when setting up ingresses for applications.'"
on-change="(ctrl.onToggleIngressAvailabilityPerNamespace)"
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
switch-class="'col-sm-8'"
>
</por-switch-field>
</div>
</div>
</div>
<!-- auto update window -->
@@ -161,19 +173,6 @@
>
</por-switch-field>
</div>
<div class="col-sm-12 mt-5">
<por-switch-field
name="'restrictStandardUserIngressW'"
label="'Only allow admins to deploy ingresses'"
tooltip="'Enforces only allowing admins to deploy ingresses (and disallows standard users from doing so).'"
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
switch-class="'col-sm-8 text-muted'"
data-cy="kubeSetup-restrictStandardUserIngressWToggle"
feature-id="ctrl.limitedFeatureIngressDeploy"
disabled="!ctrl.isRBACEnabled"
></por-switch-field>
</div>
</div>
<!-- #endregion -->

View File

@@ -189,7 +189,7 @@ class KubernetesConfigureController {
await getMetricsForAllNodes(this.endpoint.Id);
this.state.metrics.isServerRunning = true;
this.state.metrics.pending = false;
this.state.metrics.userClick = false;
this.state.metrics.userClick = true;
this.formValues.UseServerMetrics = true;
} catch (_) {
this.state.metrics.isServerRunning = false;

View File

@@ -305,7 +305,7 @@
<uib-tab index="2" ng-if="ctrl.pool.Yaml" select="ctrl.showEditor()" classes="btn-sm">
<uib-tab-heading class="vertical-center"><pr-icon icon="'code'"></pr-icon> YAML </uib-tab-heading>
<div class="px-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="resource-pool-yaml" data="ctrl.pool.Yaml"></kubernetes-yaml-inspector>
<kube-yaml-inspector identifier="'namespace-yaml'" data="ctrl.pool.Yaml" />
</div>
</uib-tab>
</uib-tabset>

View File

@@ -16,7 +16,7 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { updateIngressControllerClassMap, getIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
import { confirmUpdate } from '@@/modals/confirm';
import { confirmUpdateNamespace } from '@/react/kubernetes/namespaces/ItemView/ConfirmUpdateNamespace';
import { getMetricsForAllNodes, getMetricsForAllPods } from '@/react/kubernetes/services/service.ts';
import { getMetricsForAllPods } from '@/react/kubernetes/services/service.ts';
class KubernetesResourcePoolController {
/* #region CONSTRUCTOR */
@@ -36,7 +36,8 @@ class KubernetesResourcePoolController {
KubernetesApplicationService,
KubernetesIngressService,
KubernetesVolumeService,
KubernetesNamespaceService
KubernetesNamespaceService,
KubernetesNodeService
) {
Object.assign(this, {
$async,
@@ -54,6 +55,7 @@ class KubernetesResourcePoolController {
KubernetesIngressService,
KubernetesVolumeService,
KubernetesNamespaceService,
KubernetesNodeService,
});
this.IngressClassTypes = KubernetesIngressClassTypes;
@@ -366,7 +368,7 @@ class KubernetesResourcePoolController {
const name = this.$state.params.id;
const [nodes, pools] = await Promise.all([getMetricsForAllNodes, this.KubernetesResourcePoolService.get('', { getQuota: true })]);
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get('', { getQuota: true })]);
this.ingressControllers = [];
if (this.state.ingressAvailabilityPerNamespace) {

View File

@@ -20,7 +20,7 @@ const { CREATE, UPDATE, DELETE } = KubernetesResourceActions;
* Get summary of Kubernetes resources to be created, updated or deleted
* @param {KubernetesApplicationFormValues} formValues
*/
export default function (formValues, oldFormValues = {}) {
export function getApplicationResources(formValues, oldFormValues = {}) {
if (oldFormValues instanceof KubernetesApplicationFormValues) {
const resourceSummary = getUpdatedApplicationResources(oldFormValues, formValues);
return resourceSummary;
@@ -139,9 +139,9 @@ function getUpdatedApplicationResources(oldFormValues, newFormValues) {
}
// Ingress
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
const newServicePorts = newFormValues.Services.flatMap((service) => service.Ports);
const oldServicePorts = oldFormValues.Services.flatMap((service) => service.Ports);
const oldIngresses = generateNewIngressesFromFormPaths(oldFormValues.OriginalIngresses, oldServicePorts, oldServicePorts);
const newServicePorts = newFormValues.Services.flatMap((service) => service.Ports);
const newIngresses = generateNewIngressesFromFormPaths(newFormValues.OriginalIngresses, newServicePorts, oldServicePorts);
resources.push(...getIngressUpdateSummary(oldIngresses, newIngresses));
} else if (!oldService && newService) {
@@ -190,7 +190,7 @@ function getApplicationResourceType(app) {
function getIngressUpdateSummary(oldIngresses, newIngresses) {
const ingressesSummaries = newIngresses
.map((newIng) => {
const oldIng = _.find(oldIngresses, { Name: newIng.Name });
const oldIng = oldIngresses.find((oldIng) => oldIng.Name === newIng.Name);
return getIngressUpdateResourceSummary(oldIng, newIng);
})
.filter((s) => s); // remove nulls

View File

@@ -3,7 +3,7 @@ import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configurati
import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues';
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
import { KubernetesResourceActions, KubernetesResourceTypes } from 'Kubernetes/models/resource-types/models';
import getApplicationResources from './resources/applicationResources';
import { getApplicationResources } from './resources/applicationResources';
import getNamespaceResources from './resources/namespaceResources';
import getConfigurationResources from './resources/configurationResources';

View File

@@ -164,7 +164,7 @@
<uib-tab index="2" ng-if="ctrl.volume.PersistentVolumeClaim.Yaml" select="ctrl.showEditor()" classes="btn-sm">
<uib-tab-heading class="vertical-center" data-cy="k8sVolDetail-volYamlTab"> <pr-icon icon="'code'"></pr-icon> YAML </uib-tab-heading>
<div class="px-5" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="volume-yaml" data="ctrl.volume.PersistentVolumeClaim.Yaml"></kubernetes-yaml-inspector>
<kube-yaml-inspector identifier="'volume-yaml'" data="ctrl.volume.PersistentVolumeClaim.Yaml" />
</div>
</uib-tab>
</uib-tabset>

View File

@@ -14,14 +14,19 @@
{{ $ctrl.model.Title }}
</span>
<div class="space-left blocklist-item-subtitle inline-flex items-center">
<pr-icon ng-if="$ctrl.model.Platform === 1 || $ctrl.model.Platform === 'linux' || !$ctrl.model.Platform" icon="'svg-linux'" class="mr-1"></pr-icon>
<span ng-if="!$ctrl.model.Platform"> &amp; </span>
<pr-icon
icon="'svg-microsoft'"
ng-if="$ctrl.model.Platform === 2 || $ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"
class-name="'[&>*]:flex [&>*]:items-center'"
size="'lg'"
></pr-icon>
<div ng-if="$ctrl.typeLabel !== 'manifest'" class="vertical-center gap-1">
<pr-icon ng-if="$ctrl.model.Platform === 1 || $ctrl.model.Platform === 'linux' || !$ctrl.model.Platform" icon="'svg-linux'" class="mr-1"></pr-icon>
<pr-icon
icon="'svg-microsoft'"
ng-if="$ctrl.model.Platform === 2 || $ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"
class-name="'[&>*]:flex [&>*]:items-center'"
size="'lg'"
></pr-icon>
</div>
<!-- currently only kubernetes uses the typeLabel of 'manifest' -->
<div ng-if="$ctrl.typeLabel === 'manifest'" class="vertical-center">
<pr-icon icon="'svg-kubernetes'" size="'lg'" class="align-bottom" class-name="'[&>*]:flex [&>*]:items-center'"></pr-icon>
</div>
{{ $ctrl.typeLabel }}
</div>
</div>

View File

@@ -41,7 +41,7 @@ export default class LdapSettingsBaseDnBuilderController {
}
getOUValues(dn, domainSuffix = '') {
const regex = /(\w+)=(\w*),?/;
const regex = /(\w+)=([a-zA-Z0-9_ ]*),?/;
let ouValues = [];
let left = dn;
let match = left.match(regex);

View File

@@ -211,11 +211,6 @@
</div>
</div>
<!-- !open-amt info -->
<!-- gpus info -->
<div class="mb-4" ng-if="isDockerStandaloneEnv">
<gpus-insights></gpus-insights>
</div>
<!-- gpus info -->
<div class="form-group">
<div class="col-sm-12">
<button

View File

@@ -117,8 +117,8 @@ export function createMockEnvironment(): Environment {
StartTime: '',
},
StatusMessage: {
Detail: '',
Summary: '',
detail: '',
summary: '',
},
};
}

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