Compare commits
100 Commits
fork_branc
...
fix/EE-197
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd786c5041 | ||
|
|
16e5ba3f88 | ||
|
|
6255e8d4b5 | ||
|
|
830286c332 | ||
|
|
9ad626b36e | ||
|
|
a598b2d72d | ||
|
|
6be1ff4d9c | ||
|
|
c0a4727114 | ||
|
|
cea634a7aa | ||
|
|
5f2e3452e4 | ||
|
|
aa15b34add | ||
|
|
06d25d1491 | ||
|
|
8e83a95996 | ||
|
|
17a20cb2c6 | ||
|
|
b596d0febd | ||
|
|
33871eb447 | ||
|
|
183304853e | ||
|
|
0042c7c1d9 | ||
|
|
80af93afec | ||
|
|
988069df56 | ||
|
|
0ee403c1b2 | ||
|
|
b280eb6997 | ||
|
|
761e102b2f | ||
|
|
5bd157f8fc | ||
|
|
bcaf20caca | ||
|
|
1a6af5d58f | ||
|
|
41993ad378 | ||
|
|
6b91a813f0 | ||
|
|
d64cab0c50 | ||
|
|
048613a0c5 | ||
|
|
606fe54321 | ||
|
|
22b72fb6e3 | ||
|
|
7d92aa1971 | ||
|
|
9e9a4ca4cc | ||
|
|
a2886115b8 | ||
|
|
cc3b1face2 | ||
|
|
1157849b70 | ||
|
|
98b8d6d0b2 | ||
|
|
e126f63965 | ||
|
|
f2113a515b | ||
|
|
d0ecbc49c8 | ||
|
|
dee1428bfb | ||
|
|
af0d637414 | ||
|
|
ebfabe6c47 | ||
|
|
85a6a80722 | ||
|
|
b285219a58 | ||
|
|
3fb8a232b8 | ||
|
|
28f71e486a | ||
|
|
c763219f74 | ||
|
|
8f4589e535 | ||
|
|
0caf5ca59e | ||
|
|
cec8f34ae9 | ||
|
|
71de07bbea | ||
|
|
76ced401f0 | ||
|
|
33001a8654 | ||
|
|
f738af0f34 | ||
|
|
5c85c563e1 | ||
|
|
db00390cd2 | ||
|
|
32756f9e1b | ||
|
|
5ba80c3a44 | ||
|
|
77f73378ea | ||
|
|
734f077861 | ||
|
|
b5ec8c52fb | ||
|
|
988efe6b02 | ||
|
|
40a6645e23 | ||
|
|
cf60235696 | ||
|
|
65cc5342a7 | ||
|
|
90a18b5ded | ||
|
|
b29961e01e | ||
|
|
d17e7c8160 | ||
|
|
d3cc1a24cc | ||
|
|
fb7cdacbaa | ||
|
|
ec24826228 | ||
|
|
f0efc4f904 | ||
|
|
d18c8d0e88 | ||
|
|
4f350ab6f5 | ||
|
|
623079442f | ||
|
|
1ff5f25e40 | ||
|
|
ff87e687ec | ||
|
|
d4fd295c86 | ||
|
|
62f418836f | ||
|
|
ce5ea28727 | ||
|
|
00c7464c25 | ||
|
|
5eced421d5 | ||
|
|
006634e007 | ||
|
|
3cde10bcac | ||
|
|
9dcd5651e8 | ||
|
|
ba1f0f4018 | ||
|
|
41999e149f | ||
|
|
dfe0b3f69d | ||
|
|
f544d4447c | ||
|
|
8383bc05c5 | ||
|
|
0200a668df | ||
|
|
dcd1e902cd | ||
|
|
c93ec8d08c | ||
|
|
b7841e7fc3 | ||
|
|
8096c5e8bc | ||
|
|
551d287982 | ||
|
|
885ae16278 | ||
|
|
9c279e7fae |
13
.babelrc
13
.babelrc
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"plugins": ["lodash", "angularjs-annotate"],
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": "2"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -17,12 +17,76 @@ plugins:
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
sourceType: module
|
||||
project: './tsconfig.json'
|
||||
ecmaFeatures:
|
||||
modules: true
|
||||
|
||||
rules:
|
||||
no-control-regex: off
|
||||
no-control-regex: 'off'
|
||||
no-empty: warn
|
||||
no-empty-function: warn
|
||||
no-useless-escape: off
|
||||
import/order: error
|
||||
no-useless-escape: 'off'
|
||||
import/order:
|
||||
[
|
||||
'error',
|
||||
{
|
||||
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroupsExcludedImportTypes: ['internal'],
|
||||
},
|
||||
]
|
||||
|
||||
settings:
|
||||
'import/resolver':
|
||||
alias:
|
||||
map:
|
||||
- ['@', './app']
|
||||
extensions: ['.js', '.ts', '.tsx']
|
||||
|
||||
overrides:
|
||||
- files:
|
||||
- app/**/*.ts{,x}
|
||||
parserOptions:
|
||||
project: './tsconfig.json'
|
||||
parser: '@typescript-eslint/parser'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
extends:
|
||||
- airbnb
|
||||
- airbnb-typescript
|
||||
- 'plugin:eslint-comments/recommended'
|
||||
- 'plugin:react-hooks/recommended'
|
||||
- 'plugin:react/jsx-runtime'
|
||||
- 'plugin:@typescript-eslint/recommended'
|
||||
- 'plugin:@typescript-eslint/eslint-recommended'
|
||||
- 'plugin:promise/recommended'
|
||||
- prettier # should be last
|
||||
settings:
|
||||
react:
|
||||
version: 'detect'
|
||||
rules:
|
||||
import/order:
|
||||
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
|
||||
func-style: [error, 'declaration']
|
||||
import/prefer-default-export: off
|
||||
no-use-before-define: ['error', { functions: false }]
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
|
||||
no-shadow: 'off'
|
||||
'@typescript-eslint/no-shadow': off
|
||||
jsx-a11y/no-autofocus: warn
|
||||
react/forbid-prop-types: off
|
||||
react/require-default-props: off
|
||||
react/no-array-index-key: off
|
||||
react/jsx-filename-extension: [0]
|
||||
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
|
||||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
- 'plugin:jest/recommended'
|
||||
- 'plugin:jest/style'
|
||||
env:
|
||||
'jest/globals': true
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Portainer Business
|
||||
url: https://www.portainer.io/portainerbusiness
|
||||
about: Would you and your co-workers benefit from our enterprise edition which provides functionality to deploy Portainer at scale?
|
||||
- name: Portainer Business Edition - Get 5 nodes free
|
||||
url: https://portainer.io/pricing/take5
|
||||
about: Portainer Business Edition has more features, more support and you can now get 5 nodes free for as long as you want.
|
||||
|
||||
53
.github/workflows/validate-openapi-spec.yml
vendored
Normal file
53
.github/workflows/validate-openapi-spec.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Validate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup Go v1.17.3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17.3'
|
||||
|
||||
- name: Prebuild docs
|
||||
run: yarn prebuild:docs
|
||||
|
||||
- name: Build OpenAPI 2.0 Spec
|
||||
run: yarn build:docs
|
||||
|
||||
# Install dependencies globally to bypass installing all frontend deps
|
||||
- name: Install swagger2openapi and swagger-cli
|
||||
run: yarn global add swagger2openapi @apidevtools/swagger-cli
|
||||
|
||||
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
|
||||
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
|
||||
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
|
||||
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
|
||||
run: yarn validate:docs
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,9 +3,11 @@ bower_components
|
||||
dist
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer*
|
||||
storybook-static
|
||||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
__debug_bin
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
dist
|
||||
14
.prettierrc
14
.prettierrc
@@ -4,10 +4,20 @@
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.{j,t}sx"
|
||||
],
|
||||
"options": {
|
||||
"printWidth": 80,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
31
.storybook/main.js
Normal file
31
.storybook/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
cssLoaderOptions: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[path][name]__[local]',
|
||||
auto: true,
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: (config) => {
|
||||
config.resolve.plugins = [
|
||||
...(config.resolve.plugins || []),
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: config.resolve.extensions,
|
||||
}),
|
||||
];
|
||||
return config;
|
||||
},
|
||||
};
|
||||
11
.storybook/preview.js
Normal file
11
.storybook/preview.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import '../app/assets/css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
21
README.md
21
README.md
@@ -2,13 +2,15 @@
|
||||
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
|
||||
</p>
|
||||
|
||||
**Portainer CE** is a lightweight ‘universal’ management GUI that can be used to **easily** manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as **simple** to deploy as it is to use.
|
||||
**Portainer Community Edition** is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as simple to deploy as it is to use. The application allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a ‘smart’ GUI and/or an extensive API.
|
||||
|
||||
Portainer consists of a single container that can run on any cluster. It can be deployed as a Linux container or a Windows native container.
|
||||
|
||||
**Portainer** allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a super-simple graphical interface.
|
||||
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
|
||||
|
||||
A fully supported version of Portainer is available for business use. Visit http://www.portainer.io to learn more
|
||||
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
|
||||
- [Take5 – get 5 free nodes of Portainer Business for as long as you want them](https://portainer.io/pricing/take5)
|
||||
- [Portainer BE install guide](https://install.portainer.io)
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -20,12 +22,11 @@ Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
Portainer CE is updated regularly. We aim to do an update release every couple of months.
|
||||
|
||||
**The latest version of Portainer is 2.6.x** And you can find the release notes [here.](https://www.portainer.io/blog/new-portainer-ce-2.6.0-release)
|
||||
Portainer is on version 2, the second number denotes the month of release.
|
||||
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
|
||||
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
|
||||
|
||||
@@ -41,7 +42,7 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
|
||||
|
||||
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
|
||||
|
||||
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
|
||||
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
|
||||
@@ -51,15 +52,15 @@ You can join the Portainer Community by visiting community.portainer.io. This wi
|
||||
## Reporting bugs and contributing
|
||||
|
||||
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request.
|
||||
|
||||
## Security
|
||||
|
||||
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
|
||||
|
||||
## WORK FOR US
|
||||
## Work for us
|
||||
|
||||
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and we will be in touch.
|
||||
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and/or visit our [careers page](https://portainer.io/careers).
|
||||
|
||||
## Privacy
|
||||
|
||||
|
||||
@@ -18,13 +18,16 @@ const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
||||
var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) error {
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
migrateLog.Info(fmt.Sprintf("Error during migration, recovering [%v]", err))
|
||||
if e := recover(); e != nil {
|
||||
store.Rollback(true)
|
||||
err = fmt.Errorf("%v", e)
|
||||
}
|
||||
}()
|
||||
|
||||
// !Important: we must use a named return value in the function definition and not a local
|
||||
// !variable referenced from the closure or else the return value will be incorrectly set
|
||||
return migrator.Migrate()
|
||||
}
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.1
|
||||
// Portainer 2.9.1, 2.9.2
|
||||
if m.currentDBVersion < 33 {
|
||||
err := m.migrateDBVersionToDB33()
|
||||
if err != nil {
|
||||
@@ -316,6 +316,13 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
|
||||
if m.currentDBVersion < 35 {
|
||||
if err := m.migrateDBVersionToDB35(); err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB35")
|
||||
}
|
||||
}
|
||||
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
|
||||
@@ -100,6 +100,32 @@ func (m *Migrator) updateDockerhubToDB32() error {
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
}
|
||||
|
||||
// The following code will make this function idempotent.
|
||||
// i.e. if run again, it will not change the data. It will ensure that
|
||||
// we only have one migrated registry entry. Duplicates will be removed
|
||||
// if they exist and which has been happening due to earlier migration bugs
|
||||
migrated := false
|
||||
registries, _ := m.registryService.Registries()
|
||||
for _, r := range registries {
|
||||
if r.Type == registry.Type &&
|
||||
r.Name == registry.Name &&
|
||||
r.URL == registry.URL &&
|
||||
r.Authentication == registry.Authentication {
|
||||
|
||||
if !migrated {
|
||||
// keep this one entry
|
||||
migrated = true
|
||||
} else {
|
||||
// delete subsequent duplicates
|
||||
m.registryService.DeleteRegistry(portainer.RegistryID(r.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if migrated {
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -218,8 +244,12 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
|
||||
if !nameExist {
|
||||
continue
|
||||
}
|
||||
createTime, createTimeExist := volume["CreatedAt"].(string)
|
||||
if !createTimeExist {
|
||||
continue
|
||||
}
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
|
||||
if ok {
|
||||
|
||||
11
api/bolt/migrator/migrate_dbversion34.go
Normal file
11
api/bolt/migrator/migrate_dbversion34.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB35() error {
|
||||
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
|
||||
// calling it again will now fix the issue as the function has been repaired.
|
||||
err := m.updateDockerhubToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
108
api/bolt/migrator/migrate_dbversion34_test.go
Normal file
108
api/bolt/migrator/migrate_dbversion34_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||
"github.com/portainer/portainer/api/bolt/endpoint"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
db35TestFile = "portainer-mig-35.db"
|
||||
username = "portainer"
|
||||
password = "password"
|
||||
)
|
||||
|
||||
func setupDB35Test(t *testing.T) *Migrator {
|
||||
is := assert.New(t)
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
is.NoError(err, "failed to init testing DB connection")
|
||||
|
||||
// Create an old style dockerhub authenticated account
|
||||
dockerhubService, err := dockerhub.NewService(&internal.DbConnection{DB: dbConn})
|
||||
is.NoError(err, "failed to init testing registry service")
|
||||
err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password})
|
||||
is.NoError(err, "failed to create dockerhub account")
|
||||
|
||||
registryService, err := registry.NewService(&internal.DbConnection{DB: dbConn})
|
||||
is.NoError(err, "failed to init testing registry service")
|
||||
|
||||
endpointService, err := endpoint.NewService(&internal.DbConnection{DB: dbConn})
|
||||
is.NoError(err, "failed to init endpoint service")
|
||||
|
||||
m := &Migrator{
|
||||
db: dbConn,
|
||||
dockerhubService: dockerhubService,
|
||||
registryService: registryService,
|
||||
endpointService: endpointService,
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// TestUpdateDockerhubToDB32 tests a normal upgrade
|
||||
func TestUpdateDockerhubToDB32(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
m := setupDB35Test(t)
|
||||
defer m.db.Close()
|
||||
defer os.Remove(db35TestFile)
|
||||
|
||||
if err := m.updateDockerhubToDB32(); err != nil {
|
||||
t.Errorf("failed to update settings: %v", err)
|
||||
}
|
||||
|
||||
// Verify we have a single registry were created
|
||||
registries, err := m.registryService.Registries()
|
||||
is.NoError(err, "failed to read registries from the RegistryService")
|
||||
is.Equal(len(registries), 1, "only one migrated registry expected")
|
||||
}
|
||||
|
||||
// TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration
|
||||
// created a large number of duplicate "dockerhub migrated" registry entries.
|
||||
func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
m := setupDB35Test(t)
|
||||
defer m.db.Close()
|
||||
defer os.Remove(db35TestFile)
|
||||
|
||||
// Create lots of duplicate entries...
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
Name: "Dockerhub (authenticated - migrated)",
|
||||
URL: "docker.io",
|
||||
Authentication: true,
|
||||
Username: "portainer",
|
||||
Password: "password",
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
}
|
||||
|
||||
for i := 1; i < 150; i++ {
|
||||
err := m.registryService.CreateRegistry(registry)
|
||||
assert.NoError(t, err, "create registry failed")
|
||||
}
|
||||
|
||||
// Verify they were created
|
||||
registries, err := m.registryService.Registries()
|
||||
is.NoError(err, "failed to read registries from the RegistryService")
|
||||
is.Condition(func() bool {
|
||||
return len(registries) > 1
|
||||
}, "expected multiple duplicate registry entries")
|
||||
|
||||
// Now run the migrator
|
||||
if err := m.updateDockerhubToDB32(); err != nil {
|
||||
t.Errorf("failed to update settings: %v", err)
|
||||
}
|
||||
|
||||
// Verify we have a single registry were created
|
||||
registries, err = m.registryService.Registries()
|
||||
is.NoError(err, "failed to read registries from the RegistryService")
|
||||
is.Equal(len(registries), 1, "only one migrated registry expected")
|
||||
}
|
||||
@@ -77,6 +77,31 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
||||
return stack, err
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks with same name
|
||||
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var t portainer.Stack
|
||||
err := internal.UnmarshalObject(v, &t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Name == name {
|
||||
stacks = append(stacks, t)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks.
|
||||
func (service *Service) Stacks() ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
@@ -192,8 +217,8 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
var stack portainer.Stack
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
stack := portainer.Stack{}
|
||||
err := internal.UnmarshalObject(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -32,6 +33,7 @@ type Service struct {
|
||||
snapshotService portainer.SnapshotService
|
||||
chiselServer *chserver.Server
|
||||
shutdownCtx context.Context
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
@@ -215,18 +217,13 @@ func (service *Service) checkTunnels() {
|
||||
}
|
||||
}
|
||||
|
||||
if len(tunnel.Jobs) > 0 {
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
} else {
|
||||
service.tunnelDetailsMap.Remove(item.Key)
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,12 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
// update the LastActivity
|
||||
service.SetTunnelStatusToActive(endpoint.ID)
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
err := service.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
@@ -74,9 +80,9 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
|
||||
}
|
||||
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
return tunnel, nil
|
||||
@@ -112,6 +118,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
|
||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||
|
||||
@@ -36,6 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
||||
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"fmt"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type pairList []portainer.Pair
|
||||
|
||||
45
api/cli/pairlistbool.go
Normal file
45
api/cli/pairlistbool.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"strings"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type pairListBool []portainer.Pair
|
||||
|
||||
// Set implementation for a list of portainer.Pair
|
||||
func (l *pairListBool) Set(value string) error {
|
||||
p := new(portainer.Pair)
|
||||
|
||||
// default to true. example setting=true is equivalent to setting
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
p.Name = parts[0]
|
||||
p.Value = "true"
|
||||
} else {
|
||||
p.Name = parts[0]
|
||||
p.Value = parts[1]
|
||||
}
|
||||
|
||||
*l = append(*l, *p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implementation for a list of pair
|
||||
func (l *pairListBool) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsCumulative implementation for a list of pair
|
||||
func (l *pairListBool) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func BoolPairs(s kingpin.Settings) (target *[]portainer.Pair) {
|
||||
target = new([]portainer.Pair)
|
||||
s.SetValue((*pairListBool)(target))
|
||||
return
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -237,6 +239,49 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
|
||||
return nil
|
||||
}
|
||||
|
||||
// enableFeaturesFromFlags turns on or off feature flags
|
||||
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
|
||||
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
|
||||
func enableFeaturesFromFlags(dataStore portainer.DataStore, flags *portainer.CLIFlags) error {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings == nil {
|
||||
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
||||
}
|
||||
|
||||
// loop through feature flags to check if they are supported
|
||||
for _, feat := range *flags.FeatureFlags {
|
||||
var correspondingFeature *portainer.Feature
|
||||
for i, supportedFeat := range portainer.SupportedFeatureFlags {
|
||||
if strings.EqualFold(feat.Name, string(supportedFeat)) {
|
||||
correspondingFeature = &portainer.SupportedFeatureFlags[i]
|
||||
}
|
||||
}
|
||||
|
||||
if correspondingFeature == nil {
|
||||
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
|
||||
}
|
||||
|
||||
featureState, err := strconv.ParseBool(feat.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
|
||||
}
|
||||
|
||||
if featureState {
|
||||
log.Printf("Feature %v : on", *correspondingFeature)
|
||||
} else {
|
||||
log.Printf("Feature %v : off", *correspondingFeature)
|
||||
}
|
||||
|
||||
settings.FeatureFlagSettings[*correspondingFeature] = featureState
|
||||
}
|
||||
|
||||
return dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
private, public, err := fileService.LoadKeyPair()
|
||||
if err != nil {
|
||||
@@ -467,6 +512,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
@@ -490,6 +537,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
}
|
||||
|
||||
err = enableFeaturesFromFlags(dataStore, flags)
|
||||
if err != nil {
|
||||
log.Fatalf("failed enabling feature flag: %v", err)
|
||||
}
|
||||
|
||||
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed loading edge jobs from database: %v", err)
|
||||
@@ -504,7 +556,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
|
||||
if err != nil {
|
||||
log.Fatalf("failed getting admin password file: %v", err)
|
||||
}
|
||||
|
||||
114
api/cmd/portainer/main_test.go
Normal file
114
api/cmd/portainer/main_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type mockKingpinSetting string
|
||||
|
||||
func (m mockKingpinSetting) SetValue(value kingpin.Value) {
|
||||
value.Set(string(m))
|
||||
}
|
||||
|
||||
func Test_enableFeaturesFromFlags(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
tests := []struct {
|
||||
featureFlag string
|
||||
isSupported bool
|
||||
}{
|
||||
{"test", false},
|
||||
{"openamt", false},
|
||||
{"open-amt", true},
|
||||
{"oPeN-amT", true},
|
||||
{"fdo", true},
|
||||
{"FDO", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(test.featureFlag)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
if test.isSupported {
|
||||
is.NoError(err)
|
||||
} else {
|
||||
is.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("passes for all supported feature flags", func(t *testing.T) {
|
||||
for _, flag := range portainer.SupportedFeatureFlags {
|
||||
mockKingpinSetting := mockKingpinSetting(flag)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const FeatTest portainer.Feature = "optional-test"
|
||||
|
||||
func optionalFunc(dataStore portainer.DataStore) string {
|
||||
|
||||
// TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
|
||||
// ideally, the `if` should look more like:
|
||||
// if featureflags.FlagEnabled(FeatTest) {}
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings[FeatTest] {
|
||||
return "enabled"
|
||||
}
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
func Test_optionalFeature(t *testing.T) {
|
||||
portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
|
||||
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
// Enable the test feature
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(FeatTest)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
is.Equal("enabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// Same store, so the feature flag should still be enabled
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
is.Equal("enabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// disable the test feature
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
is.Equal("disabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// Same store, so feature flag should still be disabled
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
is.Equal("disabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
}
|
||||
@@ -91,7 +91,11 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
tunnel, err := reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// ComposeStackManager is a wrapper for docker-compose binary
|
||||
@@ -58,7 +58,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
@@ -115,27 +115,3 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
|
||||
return "stack.env", nil
|
||||
}
|
||||
|
||||
// getStackFiles returns list of stack's confile file paths.
|
||||
// items in the list would be sanitized according to following criterias:
|
||||
// 1. no empty paths
|
||||
// 2. no "../xxx" paths that are trying to escape stack folder
|
||||
// 3. no dir paths
|
||||
// 4. root paths would be made relative
|
||||
func getStackFiles(stack *portainer.Stack) []string {
|
||||
paths := make([]string, 0, len(stack.AdditionalFiles)+1)
|
||||
|
||||
for _, p := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
p = `.` + p
|
||||
}
|
||||
|
||||
if p == `` || p == `.` || strings.HasPrefix(p, `..`) || strings.HasSuffix(p, string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -64,21 +64,3 @@ func Test_createEnvFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getStackFiles(t *testing.T) {
|
||||
stack := &portainer.Stack{
|
||||
EntryPoint: "./file", // picks entry point
|
||||
AdditionalFiles: []string{
|
||||
``, // ignores empty string
|
||||
`.`, // ignores .
|
||||
`..`, // ignores ..
|
||||
`./dir/`, // ignrores paths that end with trailing /
|
||||
`/with-root-prefix`, // replaces "root" based paths with relative
|
||||
`./relative`, // keeps relative paths
|
||||
`../escape`, // prevents dir escape
|
||||
},
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
assert.ElementsMatch(t, filePaths, []string{`./file`, `./with-root-prefix`, `./relative`})
|
||||
}
|
||||
|
||||
@@ -44,19 +44,26 @@ func NewSwarmStackManager(binaryPath, configPath string, signatureService portai
|
||||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) error {
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, "logout")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -64,7 +71,10 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if prune {
|
||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
|
||||
@@ -84,7 +94,10 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||
|
||||
// Remove executes the docker stack rm command.
|
||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -108,7 +121,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string, error) {
|
||||
// Assume Linux as a default
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
@@ -121,7 +134,10 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
tunnel, err := manager.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
@@ -141,7 +157,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
}
|
||||
}
|
||||
|
||||
return command, args
|
||||
return command, args, nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||
@@ -175,7 +191,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
|
||||
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
|
||||
var config map[string]interface{}
|
||||
|
||||
raw, err := manager.fileService.GetFileContent(path)
|
||||
raw, err := manager.fileService.GetFileContent(path, "")
|
||||
if err != nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -69,12 +69,31 @@ type Service struct {
|
||||
fileStorePath string
|
||||
}
|
||||
|
||||
// JoinPaths takes a trusted root path and a list of untrusted paths and joins
|
||||
// them together using directory separators while enforcing that the untrusted
|
||||
// paths cannot go higher up than the trusted root
|
||||
func JoinPaths(trustedRoot string, untrustedPaths ...string) string {
|
||||
if trustedRoot == "" {
|
||||
trustedRoot = "."
|
||||
}
|
||||
|
||||
p := filepath.Join(trustedRoot, filepath.Join(append([]string{"/"}, untrustedPaths...)...))
|
||||
|
||||
// avoid setting a volume name from the untrusted paths
|
||||
vnp := filepath.VolumeName(p)
|
||||
if filepath.VolumeName(trustedRoot) == "" && vnp != "" {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(p, vnp), `\`)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// NewService initializes a new service. It creates a data directory and a directory to store files
|
||||
// inside this directory if they don't exist.
|
||||
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
service := &Service{
|
||||
dataStorePath: dataStorePath,
|
||||
fileStorePath: path.Join(dataStorePath, fileStorePath),
|
||||
fileStorePath: JoinPaths(dataStorePath, fileStorePath),
|
||||
}
|
||||
|
||||
err := os.MkdirAll(dataStorePath, 0755)
|
||||
@@ -112,12 +131,12 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
|
||||
// GetBinaryFolder returns the full path to the binary store on the filesystem
|
||||
func (service *Service) GetBinaryFolder() string {
|
||||
return path.Join(service.fileStorePath, BinaryStorePath)
|
||||
return JoinPaths(service.fileStorePath, BinaryStorePath)
|
||||
}
|
||||
|
||||
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
|
||||
func (service *Service) GetDockerConfigPath() string {
|
||||
return path.Join(service.fileStorePath, DockerConfigPath)
|
||||
return JoinPaths(service.fileStorePath, DockerConfigPath)
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
@@ -128,7 +147,7 @@ func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
// GetStackProjectPath returns the absolute path on the FS for a stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
||||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
return JoinPaths(service.wrapFileStore(ComposeStorePath), stackIdentifier)
|
||||
}
|
||||
|
||||
// Copy copies the file on fromFilePath to toFilePath
|
||||
@@ -194,13 +213,13 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
|
||||
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
@@ -208,25 +227,25 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
}
|
||||
|
||||
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
|
||||
return path.Join(service.fileStorePath, EdgeStackStorePath, edgeStackIdentifier)
|
||||
return JoinPaths(service.wrapFileStore(EdgeStackStorePath), edgeStackIdentifier)
|
||||
}
|
||||
|
||||
// StoreEdgeStackFileFromBytes creates a subfolder in the EdgeStackStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
|
||||
stackStorePath := JoinPaths(EdgeStackStorePath, edgeStackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
@@ -234,20 +253,20 @@ func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileNam
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
}
|
||||
|
||||
// StoreRegistryManagementFileFromBytes creates a subfolder in the
|
||||
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
|
||||
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
|
||||
extensionStorePath := JoinPaths(ExtensionRegistryManagementStorePath, folder)
|
||||
err := service.createDirectoryInStore(extensionStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
file := path.Join(extensionStorePath, fileName)
|
||||
file := JoinPaths(extensionStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(file, r)
|
||||
@@ -255,13 +274,13 @@ func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName st
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, file), nil
|
||||
return service.wrapFileStore(file), nil
|
||||
}
|
||||
|
||||
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
|
||||
// It returns the path to the newly created file.
|
||||
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
|
||||
storePath := path.Join(TLSStorePath, folder)
|
||||
storePath := JoinPaths(TLSStorePath, folder)
|
||||
err := service.createDirectoryInStore(storePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -279,13 +298,13 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.
|
||||
return "", ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := path.Join(storePath, fileName)
|
||||
tlsFilePath := JoinPaths(storePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path.Join(service.fileStorePath, tlsFilePath), nil
|
||||
return service.wrapFileStore(tlsFilePath), nil
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an environment(endpoint).
|
||||
@@ -301,17 +320,13 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
|
||||
default:
|
||||
return "", ErrUndefinedTLSFileType
|
||||
}
|
||||
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||
return JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName), nil
|
||||
}
|
||||
|
||||
// DeleteTLSFiles deletes a folder in the TLS store path.
|
||||
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||
err := os.RemoveAll(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
storePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder)
|
||||
return os.RemoveAll(storePath)
|
||||
}
|
||||
|
||||
// DeleteTLSFile deletes a specific TLS file from a folder.
|
||||
@@ -328,20 +343,19 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
|
||||
return ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
|
||||
filePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName)
|
||||
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// GetFileContent returns the content of a file as bytes.
|
||||
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
|
||||
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if filePath == "" {
|
||||
filePath = trustedRoot
|
||||
}
|
||||
return nil, fmt.Errorf("could not get the contents of the file '%s'", filePath)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
@@ -359,7 +373,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, jsonContent, 0644)
|
||||
return os.WriteFile(path, jsonContent, 0644)
|
||||
}
|
||||
|
||||
// FileExists checks for the existence of the specified file.
|
||||
@@ -369,23 +383,17 @@ func (service *Service) FileExists(filePath string) (bool, error) {
|
||||
|
||||
// KeyPairFilesExist checks for the existence of the key files.
|
||||
func (service *Service) KeyPairFilesExist() (bool, error) {
|
||||
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
|
||||
privateKeyPath := JoinPaths(service.dataStorePath, PrivateKeyFile)
|
||||
exists, err := service.FileExists(privateKeyPath)
|
||||
if err != nil {
|
||||
if err != nil || !exists {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
|
||||
publicKeyPath := JoinPaths(service.dataStorePath, PublicKeyFile)
|
||||
exists, err = service.FileExists(publicKeyPath)
|
||||
if err != nil {
|
||||
if err != nil || !exists {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -397,12 +405,7 @@ func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, p
|
||||
return err
|
||||
}
|
||||
|
||||
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
}
|
||||
|
||||
// LoadKeyPair retrieve the content of both key files on disk.
|
||||
@@ -422,13 +425,13 @@ func (service *Service) LoadKeyPair() ([]byte, []byte, error) {
|
||||
|
||||
// createDirectoryInStore creates a new directory in the file store
|
||||
func (service *Service) createDirectoryInStore(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
path := service.wrapFileStore(name)
|
||||
return os.MkdirAll(path, 0700)
|
||||
}
|
||||
|
||||
// createFile creates a new file in the file store with the content from r.
|
||||
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
path := service.wrapFileStore(filePath)
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
@@ -437,15 +440,11 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
path := service.wrapFileStore(filePath)
|
||||
block := &pem.Block{Type: fileType, Bytes: content}
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
@@ -454,18 +453,13 @@ func (service *Service) createPEMFileInStore(content []byte, fileType, filePath
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
err = pem.Encode(out, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return pem.Encode(out, block)
|
||||
}
|
||||
|
||||
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
path := service.wrapFileStore(filePath)
|
||||
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -477,19 +471,19 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
// GetCustomTemplateProjectPath returns the absolute path on the FS for a custom template based
|
||||
// on its identifier.
|
||||
func (service *Service) GetCustomTemplateProjectPath(identifier string) string {
|
||||
return path.Join(service.fileStorePath, CustomTemplateStorePath, identifier)
|
||||
return JoinPaths(service.wrapFileStore(CustomTemplateStorePath), identifier)
|
||||
}
|
||||
|
||||
// StoreCustomTemplateFileFromBytes creates a subfolder in the CustomTemplateStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) {
|
||||
customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier)
|
||||
customTemplateStorePath := JoinPaths(CustomTemplateStorePath, identifier)
|
||||
err := service.createDirectoryInStore(customTemplateStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templateFilePath := path.Join(customTemplateStorePath, fileName)
|
||||
templateFilePath := JoinPaths(customTemplateStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(templateFilePath, r)
|
||||
@@ -497,32 +491,32 @@ func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName st
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, customTemplateStorePath), nil
|
||||
return service.wrapFileStore(customTemplateStorePath), nil
|
||||
}
|
||||
|
||||
// GetEdgeJobFolder returns the absolute path on the filesystem for an Edge job based
|
||||
// on its identifier.
|
||||
func (service *Service) GetEdgeJobFolder(identifier string) string {
|
||||
return path.Join(service.fileStorePath, EdgeJobStorePath, identifier)
|
||||
return JoinPaths(service.wrapFileStore(EdgeJobStorePath), identifier)
|
||||
}
|
||||
|
||||
// StoreEdgeJobFileFromBytes creates a subfolder in the EdgeJobStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreEdgeJobFileFromBytes(identifier string, data []byte) (string, error) {
|
||||
edgeJobStorePath := path.Join(EdgeJobStorePath, identifier)
|
||||
edgeJobStorePath := JoinPaths(EdgeJobStorePath, identifier)
|
||||
err := service.createDirectoryInStore(edgeJobStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := path.Join(edgeJobStorePath, createEdgeJobFileName(identifier))
|
||||
filePath := JoinPaths(edgeJobStorePath, createEdgeJobFileName(identifier))
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, filePath), nil
|
||||
return service.wrapFileStore(filePath), nil
|
||||
}
|
||||
|
||||
func createEdgeJobFileName(identifier string) string {
|
||||
@@ -532,20 +526,14 @@ func createEdgeJobFileName(identifier string) string {
|
||||
// ClearEdgeJobTaskLogs clears the Edge job task logs
|
||||
func (service *Service) ClearEdgeJobTaskLogs(edgeJobID string, taskID string) error {
|
||||
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
|
||||
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// GetEdgeJobTaskLogFileContent fetches the Edge job task logs
|
||||
func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID string) (string, error) {
|
||||
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
|
||||
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -555,20 +543,15 @@ func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID st
|
||||
|
||||
// StoreEdgeJobTaskLogFileFromBytes stores the log file
|
||||
func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error {
|
||||
edgeJobStorePath := path.Join(EdgeJobStorePath, edgeJobID)
|
||||
edgeJobStorePath := JoinPaths(EdgeJobStorePath, edgeJobID)
|
||||
err := service.createDirectoryInStore(edgeJobStorePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := path.Join(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
|
||||
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return service.createFileInStore(filePath, r)
|
||||
}
|
||||
|
||||
func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string {
|
||||
@@ -582,7 +565,7 @@ func (service *Service) GetTemporaryPath() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
|
||||
return JoinPaths(service.wrapFileStore(TempPath), uid.String()), nil
|
||||
}
|
||||
|
||||
// GetDataStorePath returns path to data folder
|
||||
@@ -591,12 +574,12 @@ func (service *Service) GetDatastorePath() string {
|
||||
}
|
||||
|
||||
func (service *Service) wrapFileStore(filepath string) string {
|
||||
return path.Join(service.fileStorePath, filepath)
|
||||
return JoinPaths(service.fileStorePath, filepath)
|
||||
}
|
||||
|
||||
func defaultCertPathUnderFileStore() (string, string) {
|
||||
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
|
||||
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
|
||||
70
api/filesystem/filesystem_linux_test.go
Normal file
70
api/filesystem/filesystem_linux_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package filesystem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
var ts = []struct {
|
||||
trusted string
|
||||
untrusted string
|
||||
expected string
|
||||
}{
|
||||
{"", "", "."},
|
||||
{"", ".", "."},
|
||||
{"", "d/e/f", "d/e/f"},
|
||||
{"", "./d/e/f", "d/e/f"},
|
||||
{"", "../d/e/f", "d/e/f"},
|
||||
{"", "/d/e/f", "d/e/f"},
|
||||
{"", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{".", "", "."},
|
||||
{".", ".", "."},
|
||||
{".", "d/e/f", "d/e/f"},
|
||||
{".", "./d/e/f", "d/e/f"},
|
||||
{".", "../d/e/f", "d/e/f"},
|
||||
{".", "/d/e/f", "d/e/f"},
|
||||
{".", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{"./", "", "."},
|
||||
{"./", ".", "."},
|
||||
{"./", "d/e/f", "d/e/f"},
|
||||
{"./", "./d/e/f", "d/e/f"},
|
||||
{"./", "../d/e/f", "d/e/f"},
|
||||
{"./", "/d/e/f", "d/e/f"},
|
||||
{"./", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{"a/b/c", "", "a/b/c"},
|
||||
{"a/b/c", ".", "a/b/c"},
|
||||
{"a/b/c", "d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "./d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "../d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
|
||||
|
||||
{"/a/b/c", "", "/a/b/c"},
|
||||
{"/a/b/c", ".", "/a/b/c"},
|
||||
{"/a/b/c", "d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "./d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "../d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "../../../etc/shadow", "/a/b/c/etc/shadow"},
|
||||
|
||||
{"./a/b/c", "", "a/b/c"},
|
||||
{"./a/b/c", ".", "a/b/c"},
|
||||
{"./a/b/c", "d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "./d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "../d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
|
||||
|
||||
{"../a/b/c", "", "../a/b/c"},
|
||||
{"../a/b/c", ".", "../a/b/c"},
|
||||
{"../a/b/c", "d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "./d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "../d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "../../../etc/shadow", "../a/b/c/etc/shadow"},
|
||||
}
|
||||
|
||||
for _, c := range ts {
|
||||
r := JoinPaths(c.trusted, c.untrusted)
|
||||
if r != c.expected {
|
||||
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
api/filesystem/filesystem_windows_test.go
Normal file
120
api/filesystem/filesystem_windows_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package filesystem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
var ts = []struct {
|
||||
trusted string
|
||||
untrusted string
|
||||
expected string
|
||||
}{
|
||||
{"", "", "."},
|
||||
{"", ".", "."},
|
||||
{"", "d/e/f", `d\e\f`},
|
||||
{"", "./d/e/f", `d\e\f`},
|
||||
{"", "../d/e/f", `d\e\f`},
|
||||
{"", "/d/e/f", `d\e\f`},
|
||||
{"", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{"", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{"", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{".", "", "."},
|
||||
{".", ".", "."},
|
||||
{".", "d/e/f", `d\e\f`},
|
||||
{".", "./d/e/f", `d\e\f`},
|
||||
{".", "../d/e/f", `d\e\f`},
|
||||
{".", "/d/e/f", `d\e\f`},
|
||||
{".", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{".", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{".", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{".", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{".", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{"./", "", "."},
|
||||
{"./", ".", "."},
|
||||
{"./", "d/e/f", `d\e\f`},
|
||||
{"./", "./d/e/f", `d\e\f`},
|
||||
{"./", "../d/e/f", `d\e\f`},
|
||||
{"./", "/d/e/f", `d\e\f`},
|
||||
{"./", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{"./", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"./", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"./", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{"./", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{"a/b/c", "", `a\b\c`},
|
||||
{"a/b/c", ".", `a\b\c`},
|
||||
{"a/b/c", "d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "./d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "../d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
|
||||
{"a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
{"a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
|
||||
{"/a/b/c", "", `\a\b\c`},
|
||||
{"/a/b/c", ".", `\a\b\c`},
|
||||
{"/a/b/c", "d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "./d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "../d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "../../../windows/system.ini", `\a\b\c\windows\system.ini`},
|
||||
{"/a/b/c", `C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
|
||||
{"/a/b/c", `..\..\..\..\C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
|
||||
{"/a/b/c", `\\server\a\b\c`, `\a\b\c\server\a\b\c`},
|
||||
{"/a/b/c", `..\..\..\..\\server\a\b\c`, `\a\b\c\server\a\b\c`},
|
||||
|
||||
{"./a/b/c", "", `a\b\c`},
|
||||
{"./a/b/c", ".", `a\b\c`},
|
||||
{"./a/b/c", "d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "./d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "../d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
|
||||
{"./a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"./a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"./a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
{"./a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
|
||||
{"../a/b/c", "", `..\a\b\c`},
|
||||
{"../a/b/c", ".", `..\a\b\c`},
|
||||
{"../a/b/c", "d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "./d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "../d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "../../../windows/system.ini", `..\a\b\c\windows\system.ini`},
|
||||
{"../a/b/c", `C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
|
||||
{"../a/b/c", `..\..\..\..\C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
|
||||
{"../a/b/c", `\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
|
||||
{"../a/b/c", `..\..\..\..\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
|
||||
|
||||
{"C:/a/b/c", "", `C:\a\b\c`},
|
||||
{"C:/a/b/c", ".", `C:\a\b\c`},
|
||||
{"C:/a/b/c", "d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "./d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "../d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "../../../windows/system.ini", `C:\a\b\c\windows\system.ini`},
|
||||
{"C:/a/b/c", `C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
|
||||
{"C:/a/b/c", `..\..\..\..\C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
|
||||
{"C:/a/b/c", `\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
|
||||
{"C:/a/b/c", `..\..\..\..\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
|
||||
|
||||
{`\\server\a\b\c`, "", `\\server\a\b\c`},
|
||||
{`\\server\a\b\c`, ".", `\\server\a\b\c`},
|
||||
{`\\server\a\b\c`, "d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "./d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "../d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "../../../windows/system.ini", `\\server\a\b\c\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `..\..\..\C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
|
||||
{`\\server\a\b\c`, `..\..\..\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
|
||||
}
|
||||
|
||||
for _, c := range ts {
|
||||
r := JoinPaths(c.trusted, c.untrusted)
|
||||
if r != c.expected {
|
||||
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,13 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.7.3
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
|
||||
33
api/go.sum
33
api/go.sum
@@ -41,8 +41,6 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@@ -64,9 +62,7 @@ github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5
|
||||
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
@@ -320,19 +316,11 @@ github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
|
||||
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
||||
github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ=
|
||||
github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
@@ -456,8 +444,6 @@ github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs=
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
@@ -503,8 +489,6 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -600,14 +584,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 h1:0ZGSu3Atz7RHMDsoITHV676igRfsb51mlgELGo37ELU=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19 h1:tG2gU4mkm5yElj35XpU3lgllOYQxN3kaM1Jab7AqTDs=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc h1:vxVN9srGND+iA9oBmyFgtbtOvnmOCLmxw20ncYCJ5HA=
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc/go.mod h1:nyQA6IahOruIvENCcBk54aaUvV2WHFdXkvBjIutg+SY=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -648,7 +634,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
@@ -691,8 +676,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/swaggo/swag v1.7.3 h1:ucB7irEdRrhjmW+Z1Ss4GjO68oPKQFjSgOR8BCAvcbU=
|
||||
github.com/swaggo/swag v1.7.3/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
@@ -706,7 +689,6 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
@@ -790,7 +772,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -827,7 +808,6 @@ golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
|
||||
@@ -908,7 +888,6 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -926,7 +905,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -973,8 +951,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1078,7 +1054,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -2,6 +2,7 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -270,6 +271,23 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
|
||||
|
||||
exists, err := handler.FileService.FileExists(entryPath)
|
||||
if err != nil || !exists {
|
||||
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
|
||||
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
|
||||
}
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -41,7 +40,7 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(path.Join(customTemplate.ProjectPath, customTemplate.EntryPoint))
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.EntryPoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom template file from disk", err}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type decoratedEdgeGroup struct {
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeGroup{HasEdgeStack=bool} "EdgeGroups"
|
||||
// @success 200 {array} decoratedEdgeGroup "EdgeGroups"
|
||||
// @failure 500
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_groups [get]
|
||||
|
||||
@@ -39,7 +39,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath)
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent("", edgeJob.ScriptPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -45,7 +44,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
|
||||
fileName = stack.ManifestPath
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, fileName))
|
||||
stackFileContent, err := handler.FileService.GetFileContent(stack.ProjectPath, fileName)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package edgestacks
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -56,7 +55,7 @@ func (handler *Handler) convertAndStoreKubeManifestIfNeeded(edgeStack *portainer
|
||||
return nil
|
||||
}
|
||||
|
||||
composeConfig, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
|
||||
composeConfig, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, edgeStack.EntryPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve Compose file from disk: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package endpointedge
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -75,7 +74,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, fileName))
|
||||
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ package endpointproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -37,22 +35,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ package endpointproxy
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -37,22 +35,9 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
|
||||
|
||||
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -131,7 +131,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
Version: job.Version,
|
||||
}
|
||||
|
||||
file, err := handler.FileService.GetFileContent(job.ScriptPath)
|
||||
file, err := handler.FileService.GetFileContent("", job.ScriptPath)
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}
|
||||
|
||||
@@ -60,7 +60,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/dockerhub/{registryId}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}/extensions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/ssl"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/storybook"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
@@ -63,6 +64,7 @@ type Handler struct {
|
||||
SSLHandler *ssl.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
@@ -74,7 +76,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.9.1
|
||||
// @version 2.9.3
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -227,6 +229,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/webhooks"):
|
||||
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/storybook"):
|
||||
http.StripPrefix("/storybook", h.StorybookHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/"):
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -32,21 +32,23 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
||||
authorizationService: authorizationService,
|
||||
}
|
||||
|
||||
kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter()
|
||||
|
||||
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
|
||||
kubeRouter.Use(bouncer.AuthenticatedAccess)
|
||||
kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
kubeRouter.Use(kubeOnlyMiddleware)
|
||||
|
||||
kubeRouter.PathPrefix("/config").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet)
|
||||
kubeRouter.PathPrefix("/nodes_limits").Handler(
|
||||
|
||||
// endpoints
|
||||
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
endpointRouter.Use(kubeOnlyMiddleware)
|
||||
|
||||
endpointRouter.PathPrefix("/nodes_limits").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).Methods(http.MethodGet)
|
||||
|
||||
// namespaces
|
||||
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
|
||||
// to keep it simple, we've decided to leave it like this.
|
||||
namespaceRouter := kubeRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
kcli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
@@ -22,88 +24,184 @@ import (
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param ids query []int false "will include only these environments(endpoints)"
|
||||
// @param excludeIds query []int false "will exclude these environments(endpoints)"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Environment(Endpoint) or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /kubernetes/{id}/config [get]
|
||||
// @router /kubernetes/config [get]
|
||||
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
|
||||
}
|
||||
|
||||
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
||||
endpoints, handlerErr := handler.filterUserKubeEndpoints(r)
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "empty endpoints list", errors.New("empty endpoints list")}
|
||||
}
|
||||
|
||||
apiServerURL := getProxyUrl(r, endpointID)
|
||||
|
||||
config, err := cli.GetKubeConfig(r.Context(), apiServerURL, bearerToken, tokenData)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to generate Kubeconfig", err}
|
||||
config, handlerErr := handler.buildConfig(r, tokenData, bearerToken, endpoints)
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, endpoint.Name)
|
||||
contentAcceptHeader := r.Header.Get("Accept")
|
||||
if contentAcceptHeader == "text/yaml" {
|
||||
return writeFileContent(w, r, endpoints, tokenData, config)
|
||||
}
|
||||
|
||||
func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.Endpoint, *httperror.HandlerError) {
|
||||
var endpointIDs []portainer.EndpointID
|
||||
_ = request.RetrieveJSONQueryParameter(r, "ids", &endpointIDs, true)
|
||||
|
||||
var excludeEndpointIDs []portainer.EndpointID
|
||||
_ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true)
|
||||
|
||||
if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 {
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters")}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.dataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
|
||||
}
|
||||
|
||||
if len(endpointIDs) > 0 {
|
||||
var endpoints []portainer.Endpoint
|
||||
for _, endpointID := range endpointIDs {
|
||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err}
|
||||
}
|
||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
continue
|
||||
}
|
||||
endpoints = append(endpoints, *endpoint)
|
||||
}
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
return filteredEndpoints, nil
|
||||
}
|
||||
|
||||
var kubeEndpoints []portainer.Endpoint
|
||||
endpoints, err := handler.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
if !endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
continue
|
||||
}
|
||||
kubeEndpoints = append(kubeEndpoints, endpoint)
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(kubeEndpoints, endpointGroups, securityContext)
|
||||
if len(excludeEndpointIDs) > 0 {
|
||||
filteredEndpoints = endpointutils.FilterByExcludeIDs(filteredEndpoints, excludeEndpointIDs)
|
||||
}
|
||||
return filteredEndpoints, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenData, bearerToken string, endpoints []portainer.Endpoint) (*clientV1.Config, *httperror.HandlerError) {
|
||||
configClusters := make([]clientV1.NamedCluster, len(endpoints))
|
||||
configContexts := make([]clientV1.NamedContext, len(endpoints))
|
||||
var configAuthInfos []clientV1.NamedAuthInfo
|
||||
authInfosSet := make(map[string]bool)
|
||||
|
||||
for idx, endpoint := range endpoints {
|
||||
cli, err := handler.kubernetesClientFactory.GetKubeClient(&endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
||||
}
|
||||
|
||||
serviceAccount, err := cli.GetServiceAccount(tokenData)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username), err}
|
||||
}
|
||||
|
||||
configClusters[idx] = buildCluster(r, endpoint)
|
||||
configContexts[idx] = buildContext(serviceAccount.Name, endpoint)
|
||||
if !authInfosSet[serviceAccount.Name] {
|
||||
configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccount.Name, bearerToken))
|
||||
authInfosSet[serviceAccount.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return &clientV1.Config{
|
||||
APIVersion: "v1",
|
||||
Kind: "Config",
|
||||
Clusters: configClusters,
|
||||
Contexts: configContexts,
|
||||
CurrentContext: configContexts[0].Name,
|
||||
AuthInfos: configAuthInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||
proxyURL := fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpoint.ID)
|
||||
return clientV1.NamedCluster{
|
||||
Name: buildClusterName(endpoint.Name),
|
||||
Cluster: clientV1.Cluster{
|
||||
Server: proxyURL,
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildClusterName(endpointName string) string {
|
||||
return fmt.Sprintf("portainer-cluster-%s", endpointName)
|
||||
}
|
||||
|
||||
func buildContext(serviceAccountName string, endpoint portainer.Endpoint) clientV1.NamedContext {
|
||||
contextName := fmt.Sprintf("portainer-ctx-%s", endpoint.Name)
|
||||
return clientV1.NamedContext{
|
||||
Name: contextName,
|
||||
Context: clientV1.Context{
|
||||
AuthInfo: serviceAccountName,
|
||||
Cluster: buildClusterName(endpoint.Name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildAuthInfo(serviceAccountName string, bearerToken string) clientV1.NamedAuthInfo {
|
||||
return clientV1.NamedAuthInfo{
|
||||
Name: serviceAccountName,
|
||||
AuthInfo: clientV1.AuthInfo{
|
||||
Token: bearerToken,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portainer.Endpoint, tokenData *portainer.TokenData, config *clientV1.Config) *httperror.HandlerError {
|
||||
filenameSuffix := "kubeconfig"
|
||||
if len(endpoints) == 1 {
|
||||
filenameSuffix = endpoints[0].Name
|
||||
}
|
||||
filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, filenameSuffix)
|
||||
|
||||
if r.Header.Get("Accept") == "text/yaml" {
|
||||
yaml, err := kcli.GenerateYAML(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to generate Kubeconfig", err}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase))
|
||||
return YAML(w, yaml)
|
||||
return response.YAML(w, yaml)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase))
|
||||
return response.JSON(w, config)
|
||||
}
|
||||
|
||||
// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server
|
||||
func getProxyUrl(r *http.Request, endpointID int) string {
|
||||
return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID)
|
||||
}
|
||||
|
||||
// YAML writes yaml response as string to writer. Returns a pointer to a HandlerError if encoding fails.
|
||||
// This could be moved to a more useful place; but that place is most likely not in this project.
|
||||
// It should actually go in https://github.com/portainer/libhttp - since that is from where we use response.JSON.
|
||||
// We use `data interface{}` as parameter - since im trying to keep it as close to (or the same as) response.JSON method signature:
|
||||
// https://github.com/portainer/libhttp/blob/d20481a3da823c619887c440a22fdf4fa8f318f2/response/response.go#L13
|
||||
func YAML(rw http.ResponseWriter, data interface{}) *httperror.HandlerError {
|
||||
rw.Header().Set("Content-Type", "text/yaml")
|
||||
|
||||
strData, ok := data.(string)
|
||||
if !ok {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to write YAML response",
|
||||
Err: errors.New("failed to convert input to string"),
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprint(rw, strData)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
func hideFields(settings *portainer.Settings) {
|
||||
settings.LDAPSettings.Password = ""
|
||||
settings.OAuthSettings.ClientSecret = ""
|
||||
settings.OAuthSettings.KubeSecretKey = nil
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle settings operations.
|
||||
|
||||
@@ -16,6 +16,8 @@ type publicSettingsResponse struct {
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
|
||||
// Whether edge compute features are enabled
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
|
||||
// Supported feature flags
|
||||
Features map[portainer.Feature]bool `json:"Features"`
|
||||
// The URL used for oauth login
|
||||
OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"`
|
||||
// The URL used for oauth logout
|
||||
@@ -52,6 +54,7 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: appSettings.EnableTelemetry,
|
||||
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
||||
Features: appSettings.FeatureFlagSettings,
|
||||
}
|
||||
//if OAuth authentication is on, compose the related fields from application settings
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
|
||||
|
||||
@@ -54,11 +54,8 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
|
||||
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid Helm repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) {
|
||||
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.UserSessionTimeout != nil {
|
||||
_, err := time.ParseDuration(*payload.UserSessionTimeout)
|
||||
@@ -114,7 +111,18 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.HelmRepositoryURL != nil {
|
||||
settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
|
||||
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Helm repository URL. Must correspond to a valid URL format", err}
|
||||
}
|
||||
}
|
||||
|
||||
settings.HelmRepositoryURL = newHelmRepo
|
||||
} else {
|
||||
settings.HelmRepositoryURL = ""
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
@@ -140,8 +148,13 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
if clientSecret == "" {
|
||||
clientSecret = settings.OAuthSettings.ClientSecret
|
||||
}
|
||||
kubeSecret := payload.OAuthSettings.KubeSecretKey
|
||||
if kubeSecret == nil {
|
||||
kubeSecret = settings.OAuthSettings.KubeSecretKey
|
||||
}
|
||||
settings.OAuthSettings = *payload.OAuthSettings
|
||||
settings.OAuthSettings.ClientSecret = clientSecret
|
||||
settings.OAuthSettings.KubeSecretKey = kubeSecret
|
||||
}
|
||||
|
||||
if payload.EnableEdgeComputeFeatures != nil {
|
||||
|
||||
@@ -2,8 +2,8 @@ package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
type composeStackFromFileContentPayload struct {
|
||||
@@ -36,6 +37,36 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID, stack *portainer.Stack) error {
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// stop scheduler updates of the stack before removal
|
||||
if stack.AutoUpdate != nil {
|
||||
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
||||
}
|
||||
|
||||
err = handler.DataStore.Stack().DeleteStack(stack.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [Stack] Unable to remove the associated resource control from the database for stack: [%+v].", stack)
|
||||
}
|
||||
}
|
||||
|
||||
if exists, _ := handler.FileService.FileExists(stack.ProjectPath); exists {
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
log.Printf("Unable to remove stack files from disk for stack: [%+v].", stack)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload composeStackFromFileContentPayload
|
||||
@@ -50,8 +81,22 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return stackExistsError(payload.Name)
|
||||
stacks, err := handler.DataStore.Stack().StacksByName(payload.Name)
|
||||
if err != nil {
|
||||
return stackExistsError(payload.Name)
|
||||
}
|
||||
for _, stack := range stacks {
|
||||
if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID {
|
||||
err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
} else {
|
||||
return stackExistsError(payload.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
@@ -155,8 +200,22 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return stackExistsError(payload.Name)
|
||||
stacks, err := handler.DataStore.Stack().StacksByName(payload.Name)
|
||||
if err != nil {
|
||||
return stackExistsError(payload.Name)
|
||||
}
|
||||
for _, stack := range stacks {
|
||||
if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID {
|
||||
err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
} else {
|
||||
return stackExistsError(payload.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//make sure the webhook ID is unique
|
||||
@@ -284,8 +343,22 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return stackExistsError(payload.Name)
|
||||
stacks, err := handler.DataStore.Stack().StacksByName(payload.Name)
|
||||
if err != nil {
|
||||
return stackExistsError(payload.Name)
|
||||
}
|
||||
for _, stack := range stacks {
|
||||
if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID {
|
||||
err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
} else {
|
||||
return stackExistsError(payload.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
@@ -389,10 +462,9 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
|
||||
path := path.Join(config.stack.ProjectPath, file)
|
||||
stackContent, err := handler.FileService.GetFileContent(path)
|
||||
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get stack file content `%q`", path)
|
||||
return errors.Wrapf(err, "failed to get stack file content `%q`", file)
|
||||
}
|
||||
|
||||
err = handler.isValidStackFile(stackContent, securitySettings)
|
||||
|
||||
@@ -54,9 +54,6 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return errors.New("Invalid stack file content")
|
||||
}
|
||||
if govalidator.IsNull(payload.Namespace) {
|
||||
return errors.New("Invalid namespace")
|
||||
}
|
||||
if govalidator.IsNull(payload.StackName) {
|
||||
return errors.New("Invalid stack name")
|
||||
}
|
||||
@@ -64,9 +61,6 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
|
||||
}
|
||||
|
||||
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Namespace) {
|
||||
return errors.New("Invalid namespace")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package stacks
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -399,8 +398,7 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
|
||||
path := path.Join(config.stack.ProjectPath, file)
|
||||
stackContent, err := handler.FileService.GetFileContent(path)
|
||||
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get stack file content")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -198,8 +197,8 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
for _, fileName := range fileNames {
|
||||
manifestFilePath := path.Join(tmpDir, fileName)
|
||||
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
|
||||
manifestFilePath := filesystem.JoinPaths(tmpDir, fileName)
|
||||
manifestContent, err := handler.FileService.GetFileContent(stack.ProjectPath, fileName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read manifest file")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -82,7 +81,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
stackFileContent, err := handler.FileService.GetFileContent(stack.ProjectPath, stack.EntryPoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ type stackListOperationFilters struct {
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags stacks
|
||||
// @security jwt
|
||||
// @param filters query string false "Filters to process on the stack list. Encoded as JSON (a map[string]string). For example, {"SwarmID": "jpofkc0i9uo9wtx1zesuk649w"} will only return stacks that are part of the specified Swarm cluster. Available filters: EndpointID, SwarmID."
|
||||
// @param filters query string false "Filters to process on the stack list. Encoded as JSON (a map[string]string). For example, {'SwarmID': 'jpofkc0i9uo9wtx1zesuk649w'} will only return stacks that are part of the specified Swarm cluster. Available filters: EndpointID, SwarmID."
|
||||
// @success 200 {array} portainer.Stack "Success"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
|
||||
@@ -136,7 +136,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
stack.UpdatedBy = user.Username
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
|
||||
stack.GitConfig.Authentication = nil
|
||||
if payload.RepositoryAuthentication {
|
||||
password := payload.RepositoryPassword
|
||||
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||
@@ -146,6 +145,12 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
_, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository", Err: err}
|
||||
}
|
||||
} else {
|
||||
stack.GitConfig.Authentication = nil
|
||||
}
|
||||
|
||||
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
@@ -74,6 +73,10 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
_, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository", Err: err}
|
||||
}
|
||||
} else {
|
||||
stack.GitConfig.Authentication = nil
|
||||
}
|
||||
@@ -104,7 +107,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
tempFileDir, _ := ioutil.TempDir("", "kub_file_content")
|
||||
defer os.RemoveAll(tempFileDir)
|
||||
|
||||
if err := filesystem.WriteToFile(path.Join(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil {
|
||||
if err := filesystem.WriteToFile(filesystem.JoinPaths(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to persist deployment file in a temp directory", Err: err}
|
||||
}
|
||||
|
||||
|
||||
23
api/http/handler/storybook/handler.go
Normal file
23
api/http/handler/storybook/handler.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package storybook
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Handler represents an HTTP API handler for managing static files.
|
||||
type Handler struct {
|
||||
http.Handler
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to serve static files.
|
||||
func NewHandler(assetsPath string) *Handler {
|
||||
h := &Handler{
|
||||
http.FileServer(http.Dir(path.Join(assetsPath, "storybook"))),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
handler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -68,9 +67,7 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(projectPath, payload.ComposeFilePathInRepository)
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
fileContent, err := handler.FileService.GetFileContent(projectPath, payload.ComposeFilePathInRepository)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed loading file content", err}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Handler struct {
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
// NewHandler creates a handler to manage webhooks operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
|
||||
@@ -73,11 +73,17 @@ func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *p
|
||||
}
|
||||
|
||||
service.Spec.TaskTemplate.ForceUpdate++
|
||||
|
||||
var imageName = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0]
|
||||
|
||||
if imageTag != "" {
|
||||
service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, ":")[0] + ":" + imageTag
|
||||
var tagIndex = strings.LastIndex(imageName, ":")
|
||||
if tagIndex == -1 {
|
||||
tagIndex = len(imageName)
|
||||
}
|
||||
service.Spec.TaskTemplate.ContainerSpec.Image = imageName[:tagIndex] + ":" + imageTag
|
||||
} else {
|
||||
service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0]
|
||||
service.Spec.TaskTemplate.ContainerSpec.Image = imageName
|
||||
}
|
||||
|
||||
_, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, dockertypes.ServiceUpdateOptions{QueryRegistry: true})
|
||||
|
||||
@@ -20,7 +20,6 @@ type webhookListOperationFilters struct {
|
||||
// @tags webhooks
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body webhookCreatePayload true "Webhook data"
|
||||
// @param filters query webhookListOperationFilters false "Filters"
|
||||
// @success 200 {array} portainer.Webhook
|
||||
// @failure 400
|
||||
|
||||
@@ -12,7 +12,10 @@ import (
|
||||
)
|
||||
|
||||
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID)
|
||||
tunnel, err := handler.ReverseTunnelService.GetActiveTunnel(params.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port))
|
||||
if err != nil {
|
||||
|
||||
@@ -122,17 +122,11 @@ func (transport *Transport) createPrivateResourceControl(resourceIdentifier stri
|
||||
}
|
||||
|
||||
func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resourceIdentifier, nodeName string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
client := transport.dockerClient
|
||||
|
||||
if nodeName != "" {
|
||||
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
client = dockerClient
|
||||
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
switch resourceType {
|
||||
case portainer.ContainerResourceControl:
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
@@ -33,7 +32,6 @@ type (
|
||||
dataStore portainer.DataStore
|
||||
signatureService portainer.DigitalSignatureService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
dockerClient *client.Client
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
@@ -63,11 +61,6 @@ type (
|
||||
|
||||
// NewTransport returns a pointer to a new Transport instance.
|
||||
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport) (*Transport, error) {
|
||||
dockerClient, err := parameters.DockerClientFactory.CreateClient(parameters.Endpoint, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := &Transport{
|
||||
endpoint: parameters.Endpoint,
|
||||
dataStore: parameters.DataStore,
|
||||
@@ -75,7 +68,6 @@ func NewTransport(parameters *TransportParameters, httpTransport *http.Transport
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
dockerClientFactory: parameters.DockerClientFactory,
|
||||
HTTPTransport: httpTransport,
|
||||
dockerClient: dockerClient,
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
|
||||
@@ -132,16 +132,12 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt
|
||||
volumeID := request.Header.Get("X-Portainer-VolumeName")
|
||||
|
||||
if volumeID != "" {
|
||||
cli := transport.dockerClient
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
if agentTargetHeader != "" {
|
||||
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
cli = dockerClient
|
||||
cli, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.VolumeInspect(context.Background(), volumeID)
|
||||
if err == nil {
|
||||
@@ -223,10 +219,13 @@ func (transport *Transport) getDockerID() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
cli := transport.dockerClient
|
||||
defer cli.Close()
|
||||
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
info, err := cli.Info(context.Background())
|
||||
info, err := client.Info(context.Background())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
|
||||
|
||||
func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
rawURL := fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||
rawURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
endpointURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
|
||||
@@ -67,9 +67,9 @@ func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Hand
|
||||
// DeleteEndpointProxy deletes the proxy associated to a key
|
||||
// and cleans the k8s environment(endpoint) client cache. DeleteEndpointProxy
|
||||
// is currently only called for edge connection clean up.
|
||||
func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
|
||||
manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID))
|
||||
manager.k8sClientFactory.RemoveKubeClient(endpoint)
|
||||
func (manager *Manager) DeleteEndpointProxy(endpointID portainer.EndpointID) {
|
||||
manager.endpointProxies.Remove(fmt.Sprint(endpointID))
|
||||
manager.k8sClientFactory.RemoveKubeClient(endpointID)
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||
|
||||
@@ -81,7 +81,7 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
|
||||
}
|
||||
|
||||
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized environments(endpoints) (can be inherited via endoint groups).
|
||||
// Non administrator users only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||
filteredEndpoints := endpoints
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
sslhandler "github.com/portainer/portainer/api/http/handler/ssl"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/storybook"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
@@ -213,6 +214,8 @@ func (server *Server) Start() error {
|
||||
stackHandler.ComposeStackManager = server.ComposeStackManager
|
||||
stackHandler.StackDeployer = server.StackDeployer
|
||||
|
||||
var storybookHandler = storybook.NewHandler(server.AssetsPath)
|
||||
|
||||
var tagHandler = tags.NewHandler(requestBouncer)
|
||||
tagHandler.DataStore = server.DataStore
|
||||
|
||||
@@ -271,6 +274,7 @@ func (server *Server) Start() error {
|
||||
SSLHandler: sslHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
StorybookHandler: storybookHandler,
|
||||
TagHandler: tagHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
|
||||
@@ -45,3 +45,60 @@ func Test_IsKubernetesEndpoint(t *testing.T) {
|
||||
assert.Equal(t, test.expected, ans)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterByExcludeIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputArray []portainer.Endpoint
|
||||
inputExcludeIDs []portainer.EndpointID
|
||||
asserts func(*testing.T, []portainer.Endpoint)
|
||||
}{
|
||||
{
|
||||
name: "filter endpoints",
|
||||
inputArray: []portainer.Endpoint{
|
||||
{ID: portainer.EndpointID(1)},
|
||||
{ID: portainer.EndpointID(2)},
|
||||
{ID: portainer.EndpointID(3)},
|
||||
{ID: portainer.EndpointID(4)},
|
||||
},
|
||||
inputExcludeIDs: []portainer.EndpointID{
|
||||
portainer.EndpointID(2),
|
||||
portainer.EndpointID(3),
|
||||
},
|
||||
asserts: func(t *testing.T, output []portainer.Endpoint) {
|
||||
assert.Contains(t, output, portainer.Endpoint{ID: portainer.EndpointID(1)})
|
||||
assert.NotContains(t, output, portainer.Endpoint{ID: portainer.EndpointID(2)})
|
||||
assert.NotContains(t, output, portainer.Endpoint{ID: portainer.EndpointID(3)})
|
||||
assert.Contains(t, output, portainer.Endpoint{ID: portainer.EndpointID(4)})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
inputArray: []portainer.Endpoint{},
|
||||
inputExcludeIDs: []portainer.EndpointID{
|
||||
portainer.EndpointID(2),
|
||||
},
|
||||
asserts: func(t *testing.T, output []portainer.Endpoint) {
|
||||
assert.Equal(t, 0, len(output))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no filter",
|
||||
inputArray: []portainer.Endpoint{
|
||||
{ID: portainer.EndpointID(1)},
|
||||
{ID: portainer.EndpointID(2)},
|
||||
},
|
||||
inputExcludeIDs: []portainer.EndpointID{},
|
||||
asserts: func(t *testing.T, output []portainer.Endpoint) {
|
||||
assert.Equal(t, 2, len(output))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := FilterByExcludeIDs(tt.inputArray, tt.inputExcludeIDs)
|
||||
tt.asserts(t, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ func IsLocalEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
|
||||
}
|
||||
|
||||
// IsKubernetesEndpoint returns true if this is a kubernetes endpoint
|
||||
// IsKubernetesEndpoint returns true if this is a kubernetes environment(endpoint)
|
||||
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
|
||||
@@ -29,3 +29,24 @@ func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
|
||||
// FilterByExcludeIDs receives an environment(endpoint) array and returns a filtered array using an excludeIds param
|
||||
func FilterByExcludeIDs(endpoints []portainer.Endpoint, excludeIds []portainer.EndpointID) []portainer.Endpoint {
|
||||
if len(excludeIds) == 0 {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
idsSet := make(map[portainer.EndpointID]bool)
|
||||
for _, id := range excludeIds {
|
||||
idsSet[id] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if !idsSet[endpoint.ID] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package stackutils
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -20,7 +19,7 @@ func ResourceControlID(endpointID portainer.EndpointID, name string) string {
|
||||
func GetStackFilePaths(stack *portainer.Stack) []string {
|
||||
var filePaths []string
|
||||
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
filePaths = append(filePaths, path.Join(stack.ProjectPath, file))
|
||||
filePaths = append(filePaths, filesystem.JoinPaths(stack.ProjectPath, file))
|
||||
}
|
||||
return filePaths
|
||||
}
|
||||
@@ -37,8 +36,8 @@ func CreateTempK8SDeploymentFiles(stack *portainer.Stack, kubeDeployer portainer
|
||||
}
|
||||
|
||||
for _, fileName := range fileNames {
|
||||
manifestFilePath := path.Join(tmpDir, fileName)
|
||||
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
|
||||
manifestFilePath := filesystem.JoinPaths(tmpDir, fileName)
|
||||
manifestContent, err := ioutil.ReadFile(filesystem.JoinPaths(stack.ProjectPath, fileName))
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "failed to read manifest file")
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@ package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gorilla/securecookie"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// scope represents JWT scopes that are supported in JWT claims.
|
||||
type scope string
|
||||
|
||||
// Service represents a service for managing JWT tokens.
|
||||
type Service struct {
|
||||
secret []byte
|
||||
secrets map[scope][]byte
|
||||
userSessionTimeout time.Duration
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
@@ -23,6 +24,7 @@ type claims struct {
|
||||
UserID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
Scope scope `json:"scope"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
@@ -31,6 +33,11 @@ var (
|
||||
errInvalidJWTToken = errors.New("Invalid JWT token")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultScope = scope("default")
|
||||
kubeConfigScope = scope("kubeconfig")
|
||||
)
|
||||
|
||||
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
|
||||
func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Service, error) {
|
||||
userSessionTimeout, err := time.ParseDuration(userSessionDuration)
|
||||
@@ -43,73 +50,122 @@ func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Ser
|
||||
return nil, errSecretGeneration
|
||||
}
|
||||
|
||||
kubeSecret, err := getOrCreateKubeSecret(dataStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
secret,
|
||||
map[scope][]byte{
|
||||
defaultScope: secret,
|
||||
kubeConfigScope: kubeSecret,
|
||||
},
|
||||
userSessionTimeout,
|
||||
dataStore,
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (service *Service) defaultExpireAt() (int64) {
|
||||
func getOrCreateKubeSecret(dataStore portainer.DataStore) ([]byte, error) {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubeSecret := settings.OAuthSettings.KubeSecretKey
|
||||
if kubeSecret == nil {
|
||||
kubeSecret = securecookie.GenerateRandomKey(32)
|
||||
if kubeSecret == nil {
|
||||
return nil, errSecretGeneration
|
||||
}
|
||||
settings.OAuthSettings.KubeSecretKey = kubeSecret
|
||||
err = dataStore.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return kubeSecret, nil
|
||||
}
|
||||
|
||||
func (service *Service) defaultExpireAt() int64 {
|
||||
return time.Now().Add(service.userSessionTimeout).Unix()
|
||||
}
|
||||
|
||||
// GenerateToken generates a new JWT token.
|
||||
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||
return service.generateSignedToken(data, service.defaultExpireAt())
|
||||
return service.generateSignedToken(data, service.defaultExpireAt(), defaultScope)
|
||||
}
|
||||
|
||||
// GenerateTokenForOAuth generates a new JWT for OAuth login
|
||||
// token expiry time from the OAuth provider is considered
|
||||
// GenerateTokenForOAuth generates a new JWT token for OAuth login
|
||||
// token expiry time response from OAuth provider is considered
|
||||
func (service *Service) GenerateTokenForOAuth(data *portainer.TokenData, expiryTime *time.Time) (string, error) {
|
||||
expireAt := service.defaultExpireAt()
|
||||
if expiryTime != nil && !expiryTime.IsZero() {
|
||||
expireAt = expiryTime.Unix()
|
||||
}
|
||||
return service.generateSignedToken(data, expireAt)
|
||||
return service.generateSignedToken(data, expireAt, defaultScope)
|
||||
}
|
||||
|
||||
// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||
func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, error) {
|
||||
scope := parseScope(token)
|
||||
secret := service.secrets[scope]
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
msg := fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
return nil, msg
|
||||
}
|
||||
return service.secret, nil
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err == nil && parsedToken != nil {
|
||||
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
|
||||
tokenData := &portainer.TokenData{
|
||||
return &portainer.TokenData{
|
||||
ID: portainer.UserID(cl.UserID),
|
||||
Username: cl.Username,
|
||||
Role: portainer.UserRole(cl.Role),
|
||||
}
|
||||
return tokenData, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errInvalidJWTToken
|
||||
}
|
||||
|
||||
// parse a JWT token, fallback to defaultScope if no scope is present in the JWT
|
||||
func parseScope(token string) scope {
|
||||
unverifiedToken, _, _ := new(jwt.Parser).ParseUnverified(token, &claims{})
|
||||
if unverifiedToken != nil {
|
||||
if cl, ok := unverifiedToken.Claims.(*claims); ok {
|
||||
if cl.Scope == kubeConfigScope {
|
||||
return kubeConfigScope
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultScope
|
||||
}
|
||||
|
||||
// SetUserSessionDuration sets the user session duration
|
||||
func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration) {
|
||||
service.userSessionTimeout = userSessionDuration
|
||||
}
|
||||
|
||||
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64) (string, error) {
|
||||
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64, scope scope) (string, error) {
|
||||
secret, found := service.secrets[scope]
|
||||
if !found {
|
||||
return "", fmt.Errorf("invalid scope: %v", scope)
|
||||
}
|
||||
|
||||
cl := claims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
Scope: scope,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||
|
||||
signedToken, err := token.SignedString(service.secret)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||
signedToken, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ func (service *Service) GenerateTokenForKubeconfig(data *portainer.TokenData) (s
|
||||
expiryAt = 0
|
||||
}
|
||||
|
||||
return service.generateSignedToken(data, expiryAt)
|
||||
return service.generateSignedToken(data, expiryAt, kubeConfigScope)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestService_GenerateTokenForKubeconfig(t *testing.T) {
|
||||
}
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(got, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return service.secret, nil
|
||||
return service.secrets[kubeConfigScope], nil
|
||||
})
|
||||
assert.NoError(t, err, "failed to parse generated token")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +11,8 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateSignedToken(t *testing.T) {
|
||||
svc, err := NewService("24h", nil)
|
||||
dataStore := i.NewDatastore(i.WithSettingsService(&portainer.Settings{}))
|
||||
svc, err := NewService("24h", dataStore)
|
||||
assert.NoError(t, err, "failed to create a copy of service")
|
||||
|
||||
token := &portainer.TokenData{
|
||||
@@ -20,11 +22,11 @@ func TestGenerateSignedToken(t *testing.T) {
|
||||
}
|
||||
expiresAt := time.Now().Add(1 * time.Hour).Unix()
|
||||
|
||||
generatedToken, err := svc.generateSignedToken(token, expiresAt)
|
||||
generatedToken, err := svc.generateSignedToken(token, expiresAt, defaultScope)
|
||||
assert.NoError(t, err, "failed to generate a signed token")
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(generatedToken, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return svc.secret, nil
|
||||
return svc.secrets[defaultScope], nil
|
||||
})
|
||||
assert.NoError(t, err, "failed to parse generated token")
|
||||
|
||||
@@ -36,3 +38,20 @@ func TestGenerateSignedToken(t *testing.T) {
|
||||
assert.Equal(t, int(token.Role), tokenClaims.Role)
|
||||
assert.Equal(t, expiresAt, tokenClaims.ExpiresAt)
|
||||
}
|
||||
|
||||
func TestGenerateSignedToken_InvalidScope(t *testing.T) {
|
||||
dataStore := i.NewDatastore(i.WithSettingsService(&portainer.Settings{}))
|
||||
svc, err := NewService("24h", dataStore)
|
||||
assert.NoError(t, err, "failed to create a copy of service")
|
||||
|
||||
token := &portainer.TokenData{
|
||||
Username: "Joe",
|
||||
ID: 1,
|
||||
Role: 1,
|
||||
}
|
||||
expiresAt := time.Now().Add(1 * time.Hour).Unix()
|
||||
|
||||
_, err = svc.generateSignedToken(token, expiresAt, "testing")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "invalid scope: testing", err.Error())
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -45,8 +45,8 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||
}
|
||||
|
||||
// Remove the cached kube client so a new one can be created
|
||||
func (factory *ClientFactory) RemoveKubeClient(endpoint *portainer.Endpoint) {
|
||||
factory.endpointClients.Remove(strconv.Itoa(int(endpoint.ID)))
|
||||
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
|
||||
factory.endpointClients.Remove(strconv.Itoa(int(endpointID)))
|
||||
}
|
||||
|
||||
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
||||
@@ -123,7 +123,6 @@ func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*ku
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed activating tunnel")
|
||||
}
|
||||
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
|
||||
|
||||
return factory.createRemoteClient(endpointURL)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
)
|
||||
|
||||
// GetKubeConfig returns kubeconfig for the current user based on:
|
||||
// - portainer server url
|
||||
// - portainer user bearer token
|
||||
// - portainer token data - which maps to k8s service account
|
||||
func (kcl *KubeClient) GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *portainer.TokenData) (*clientV1.Config, error) {
|
||||
serviceAccount, err := kcl.GetServiceAccount(tokenData)
|
||||
if err != nil {
|
||||
errText := fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username)
|
||||
return nil, fmt.Errorf("%s; err=%w", errText, err)
|
||||
}
|
||||
|
||||
kubeconfig := generateKubeconfig(apiServerURL, bearerToken, serviceAccount.Name)
|
||||
|
||||
return kubeconfig, nil
|
||||
}
|
||||
|
||||
// generateKubeconfig will generate and return kubeconfig resource - usable by `kubectl` cli
|
||||
// which will allow the client to connect directly to k8s server environment(endpoint) via portainer (proxy)
|
||||
func generateKubeconfig(apiServerURL, bearerToken, serviceAccountName string) *clientV1.Config {
|
||||
const (
|
||||
KubeConfigPortainerContext = "portainer-ctx"
|
||||
KubeConfigPortainerCluster = "portainer-cluster"
|
||||
)
|
||||
|
||||
return &clientV1.Config{
|
||||
APIVersion: "v1",
|
||||
Kind: "Config",
|
||||
CurrentContext: KubeConfigPortainerContext,
|
||||
Contexts: []clientV1.NamedContext{
|
||||
{
|
||||
Name: KubeConfigPortainerContext,
|
||||
Context: clientV1.Context{
|
||||
AuthInfo: serviceAccountName,
|
||||
Cluster: KubeConfigPortainerCluster,
|
||||
},
|
||||
},
|
||||
},
|
||||
Clusters: []clientV1.NamedCluster{
|
||||
{
|
||||
Name: KubeConfigPortainerCluster,
|
||||
Cluster: clientV1.Cluster{
|
||||
Server: apiServerURL,
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthInfos: []clientV1.NamedAuthInfo{
|
||||
{
|
||||
Name: serviceAccountName,
|
||||
AuthInfo: clientV1.AuthInfo{
|
||||
Token: bearerToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kfake "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func Test_GetKubeConfig(t *testing.T) {
|
||||
|
||||
t.Run("returns error if SA non-existent", func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: 1,
|
||||
Role: portainer.AdministratorRole,
|
||||
Username: portainerClusterAdminServiceAccountName,
|
||||
}
|
||||
|
||||
_, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData)
|
||||
|
||||
if err == nil {
|
||||
t.Error("GetKubeConfig should fail as service account does not exist")
|
||||
}
|
||||
if k8sErr := errors.Unwrap(err); !k8serrors.IsNotFound(k8sErr) {
|
||||
t.Error("GetKubeConfig should fail with service account not found k8s error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successfully obtains kubeconfig for cluster admin", func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
Role: portainer.AdministratorRole,
|
||||
Username: portainerClusterAdminServiceAccountName,
|
||||
}
|
||||
serviceAccount := &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: tokenData.Username},
|
||||
}
|
||||
|
||||
k.cli.CoreV1().ServiceAccounts(portainerNamespace).Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
||||
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
||||
|
||||
_, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetKubeConfig should succeed; err=%s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successfully obtains kubeconfig for standard user", func(t *testing.T) {
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: 1,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
nonAdminUserName := userServiceAccountName(int(tokenData.ID), k.instanceID)
|
||||
serviceAccount := &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: nonAdminUserName},
|
||||
}
|
||||
|
||||
k.cli.CoreV1().ServiceAccounts(portainerNamespace).Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
||||
defer k.cli.CoreV1().ServiceAccounts(portainerNamespace).Delete(context.Background(), serviceAccount.Name, metav1.DeleteOptions{})
|
||||
|
||||
_, err := k.GetKubeConfig(context.Background(), "localhost", "abc", tokenData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetKubeConfig should succeed; err=%s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_generateKubeconfig(t *testing.T) {
|
||||
apiServerURL, bearerToken, serviceAccountName := "localhost", "test-token", "test-user"
|
||||
|
||||
t.Run("generates Config resource kind", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
want := "Config"
|
||||
if config.Kind != want {
|
||||
t.Errorf("generateKubeconfig resource kind should be %s", want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates v1 version", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
want := "v1"
|
||||
if config.APIVersion != want {
|
||||
t.Errorf("generateKubeconfig api version should be %s", want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates single entry context cluster and authinfo", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
if len(config.Contexts) != 1 {
|
||||
t.Error("generateKubeconfig should generate single context configuration")
|
||||
}
|
||||
if len(config.Clusters) != 1 {
|
||||
t.Error("generateKubeconfig should generate single cluster configuration")
|
||||
}
|
||||
if len(config.AuthInfos) != 1 {
|
||||
t.Error("generateKubeconfig should generate single user configuration")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets default context appropriately", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
want := "portainer-ctx"
|
||||
if config.CurrentContext != want {
|
||||
t.Errorf("generateKubeconfig set cluster to be %s", want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates cluster with InsecureSkipTLSVerify to be set to true", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
if config.Clusters[0].Cluster.InsecureSkipTLSVerify != true {
|
||||
t.Error("generateKubeconfig default cluster InsecureSkipTLSVerify should be true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should contain passed in value", func(t *testing.T) {
|
||||
config := generateKubeconfig(apiServerURL, bearerToken, serviceAccountName)
|
||||
if config.Clusters[0].Cluster.Server != apiServerURL {
|
||||
t.Errorf("generateKubeconfig default cluster server url should be %s", apiServerURL)
|
||||
}
|
||||
|
||||
if config.AuthInfos[0].Name != serviceAccountName {
|
||||
t.Errorf("generateKubeconfig default authinfo name should be %s", serviceAccountName)
|
||||
}
|
||||
|
||||
if config.AuthInfos[0].AuthInfo.Token != bearerToken {
|
||||
t.Errorf("generateKubeconfig default authinfo user token should be %s", bearerToken)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||
return []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"list"},
|
||||
Verbs: []string{"list", "get"},
|
||||
Resources: []string{"namespaces", "nodes"},
|
||||
APIGroups: []string{""},
|
||||
},
|
||||
@@ -21,8 +21,8 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||
APIGroups: []string{"storage.k8s.io"},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"list"},
|
||||
Resources: []string{"namespaces", "pods"},
|
||||
Verbs: []string{"list", "get"},
|
||||
Resources: []string{"namespaces", "pods", "nodes"},
|
||||
APIGroups: []string{"metrics.k8s.io"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -50,6 +49,7 @@ type (
|
||||
AdminPasswordFile *string
|
||||
Assets *string
|
||||
Data *string
|
||||
FeatureFlags *[]Pair
|
||||
EnableEdgeComputeFeatures *bool
|
||||
EndpointURL *string
|
||||
Labels *[]Pair
|
||||
@@ -388,6 +388,9 @@ type (
|
||||
// ExtensionID represents a extension identifier
|
||||
ExtensionID int
|
||||
|
||||
// Feature represents a feature that can be enabled or disabled via feature flags
|
||||
Feature string
|
||||
|
||||
// GitlabRegistryData represents data required for gitlab registry to work
|
||||
GitlabRegistryData struct {
|
||||
ProjectID int `json:"ProjectId"`
|
||||
@@ -544,6 +547,7 @@ type (
|
||||
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||
SSO bool `json:"SSO"`
|
||||
LogoutURI string `json:"LogoutURI"`
|
||||
KubeSecretKey []byte `json:"KubeSecretKey"`
|
||||
}
|
||||
|
||||
// Pair defines a key/value string pair
|
||||
@@ -703,6 +707,7 @@ type (
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings" example:""`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings" example:""`
|
||||
FeatureFlagSettings map[Feature]bool `json:"FeatureFlagSettings" example:""`
|
||||
// The interval in which environment(endpoint) snapshots are created
|
||||
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
|
||||
// URL to the templates that will be displayed in the UI when navigating to App Templates
|
||||
@@ -1212,7 +1217,7 @@ type (
|
||||
// FileService represents a service for managing files
|
||||
FileService interface {
|
||||
GetDockerConfigPath() string
|
||||
GetFileContent(filePath string) ([]byte, error)
|
||||
GetFileContent(trustedRootPath, filePath string) ([]byte, error)
|
||||
Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error
|
||||
Rename(oldPath, newPath string) error
|
||||
RemoveDirectory(directoryPath string) error
|
||||
@@ -1280,7 +1285,6 @@ type (
|
||||
DeleteRegistrySecret(registry *Registry, namespace string) error
|
||||
CreateRegistrySecret(registry *Registry, namespace string) error
|
||||
IsRegistrySecret(namespace, secretName string) (bool, error)
|
||||
GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *TokenData) (*clientV1.Config, error)
|
||||
ToggleSystemState(namespace string, isSystem bool) error
|
||||
}
|
||||
|
||||
@@ -1373,6 +1377,7 @@ type (
|
||||
StackService interface {
|
||||
Stack(ID StackID) (*Stack, error)
|
||||
StackByName(name string) (*Stack, error)
|
||||
StacksByName(name string) ([]Stack, error)
|
||||
Stacks() ([]Stack, error)
|
||||
CreateStack(stack *Stack) error
|
||||
UpdateStack(ID StackID, stack *Stack) error
|
||||
@@ -1392,7 +1397,7 @@ type (
|
||||
|
||||
// SwarmStackManager represents a service to manage Swarm stacks
|
||||
SwarmStackManager interface {
|
||||
Login(registries []Registry, endpoint *Endpoint)
|
||||
Login(registries []Registry, endpoint *Endpoint) error
|
||||
Logout(endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
|
||||
Remove(stack *Stack, endpoint *Endpoint) error
|
||||
@@ -1470,9 +1475,9 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.9.1"
|
||||
APIVersion = "2.9.3"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 32
|
||||
DBVersion = 35
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
@@ -1514,6 +1519,18 @@ const (
|
||||
WebSocketKeepAlive = 1 * time.Hour
|
||||
)
|
||||
|
||||
// Supported feature flags
|
||||
const (
|
||||
FeatOpenAMT Feature = "open-amt"
|
||||
FeatFDO Feature = "fdo"
|
||||
)
|
||||
|
||||
// List of supported features
|
||||
var SupportedFeatureFlags = []Feature{
|
||||
FeatOpenAMT,
|
||||
FeatFDO,
|
||||
}
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
// AuthenticationInternal represents the internal authentication method (authentication against Portainer API)
|
||||
|
||||
1
app/__mocks__/fileMock.js
Normal file
1
app/__mocks__/fileMock.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'test-file-stub';
|
||||
1
app/__mocks__/styleMock.js
Normal file
1
app/__mocks__/styleMock.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@@ -2,6 +2,7 @@ import './assets/css';
|
||||
import '@babel/polyfill';
|
||||
|
||||
import angular from 'angular';
|
||||
import { UI_ROUTER_REACT_HYBRID } from '@uirouter/react-hybrid';
|
||||
|
||||
import './matomo-setup';
|
||||
import analyticsModule from './angulartics.matomo';
|
||||
@@ -15,6 +16,7 @@ import './portainer/__module';
|
||||
angular.module('portainer', [
|
||||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
UI_ROUTER_REACT_HYBRID,
|
||||
'ui.select',
|
||||
'isteven-multi-select',
|
||||
'ngSanitize',
|
||||
@@ -44,7 +46,10 @@ angular.module('portainer', [
|
||||
|
||||
if (require) {
|
||||
var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im);
|
||||
req.keys().forEach(function (key) {
|
||||
req(key);
|
||||
});
|
||||
req
|
||||
.keys()
|
||||
.filter((path) => !path.includes('.test'))
|
||||
.forEach(function (key) {
|
||||
req(key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
angular.module('portainer').run([
|
||||
'$rootScope',
|
||||
@@ -49,7 +50,7 @@ angular.module('portainer').run([
|
||||
|
||||
function ping(EndpointProvider, SystemService) {
|
||||
let endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined && endpoint.Type === 4) {
|
||||
if (endpoint !== undefined && endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) {
|
||||
SystemService.ping(endpoint.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,25 +129,6 @@ a[ng-click] {
|
||||
background-color: var(--bg-service-datatable-tbody);
|
||||
}
|
||||
|
||||
.tooltip.portainer-tooltip .tooltip-inner {
|
||||
font-family: Montserrat;
|
||||
background-color: var(--bg-tooltip-color);
|
||||
padding: 0.833em 1em;
|
||||
color: var(--text-tooltip-color);
|
||||
border: 1px solid var(--border-tooltip-color);
|
||||
border-radius: 0.14285714rem;
|
||||
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
|
||||
}
|
||||
|
||||
.tooltip.portainer-tooltip .tooltip-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fa.tooltip-icon {
|
||||
margin-left: 5px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.fa.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
@@ -326,7 +307,6 @@ a[ng-click] {
|
||||
.custom-header-ico {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-responsive {
|
||||
@@ -460,10 +440,25 @@ a[ng-click] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bootbox-form .visible {
|
||||
position: initial !important;
|
||||
display: initial !important;
|
||||
margin-left: 10px !important;
|
||||
margin-top: -2px !important;
|
||||
}
|
||||
|
||||
.bootbox-form label {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.bootbox-checkbox-list {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--white-color);
|
||||
border: 1px solid var(--grey-48);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.small-select {
|
||||
display: inline-block;
|
||||
padding: 0px 6px;
|
||||
@@ -600,7 +595,7 @@ a[ng-click] {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
.tag:not(.token) {
|
||||
padding: 2px 6px;
|
||||
color: white;
|
||||
background-color: var(--blue-2);
|
||||
@@ -841,6 +836,10 @@ json-tree .branch-preview {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.space-y-8 > * + * {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.my-8 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
import 'ui-select/dist/select.css';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import '@fortawesome/fontawesome-free/css/brands.css';
|
||||
import '@fortawesome/fontawesome-free/css/solid.css';
|
||||
import '@fortawesome/fontawesome-free/css/fontawesome.css';
|
||||
import 'toastr/build/toastr.css';
|
||||
import 'xterm/dist/xterm.css';
|
||||
import 'angularjs-slider/dist/rzslider.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/addon/lint/lint.css';
|
||||
import 'angular-json-tree/dist/angular-json-tree.css';
|
||||
import 'angular-loading-bar/build/loading-bar.css';
|
||||
import 'angular-moment-picker/dist/angular-moment-picker.min.css';
|
||||
import 'angular-multiselect/isteven-multi-select.css';
|
||||
import 'spinkit/spinkit.min.css';
|
||||
|
||||
import './rdash.css';
|
||||
import './app.css';
|
||||
|
||||
import './theme.css';
|
||||
import './vendor-override.css';
|
||||
|
||||
@@ -217,7 +217,7 @@ html {
|
||||
--border-table-color: var(--grey-19);
|
||||
--border-table-top-color: var(--grey-19);
|
||||
--border-datatable-top-color: var(--grey-10);
|
||||
--border-blocklist-color: var(--grey-44) ccc;
|
||||
--border-blocklist-color: var(--grey-44);
|
||||
--border-input-group-addon-color: var(--grey-44);
|
||||
--border-btn-default-color: var(--grey-44);
|
||||
--border-boxselector-color: var(--grey-6);
|
||||
@@ -571,6 +571,7 @@ html {
|
||||
--border-pre-color: var(--grey-3);
|
||||
--border-codemirror-cursor-color: var(--white-color);
|
||||
--border-modal: 1px solid var(--white-color);
|
||||
--border-blocklist-color: var(--white-color);
|
||||
|
||||
--hover-sidebar-color: var(--blue-9);
|
||||
--hover-sidebar-color: var(--black-color);
|
||||
|
||||
@@ -269,6 +269,7 @@ json-tree .branch-preview {
|
||||
|
||||
.panel {
|
||||
border: 1px solid var(--border-panel-color);
|
||||
background-color: var(--bg-panel-body-color);
|
||||
}
|
||||
|
||||
.theme-information .col-sm-12 {
|
||||
|
||||
@@ -28,7 +28,16 @@ export function ContainerGroupViewModel(data) {
|
||||
this.Name = data.name;
|
||||
this.Location = data.location;
|
||||
this.IPAddress = data.properties.ipAddress ? data.properties.ipAddress.ip : '';
|
||||
this.Ports = addressPorts.length ? addressPorts.map((binding, index) => ({ container: containerPorts[index].port, host: binding.port, protocol: binding.protocol })) : [];
|
||||
this.Ports = addressPorts.length
|
||||
? addressPorts.map((binding, index) => {
|
||||
const port = (containerPorts[index] && containerPorts[index].port) || undefined;
|
||||
return {
|
||||
container: port,
|
||||
host: binding.port,
|
||||
protocol: binding.protocol,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
this.Image = container.properties.image || '';
|
||||
this.OSType = data.properties.osType;
|
||||
this.AllocatePublicIP = data.properties.ipAddress && data.properties.ipAddress.type === 'Public';
|
||||
|
||||
@@ -32,3 +32,5 @@ angular
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const BROWSER_OS_PLATFORM = navigator.userAgent.indexOf('Windows NT') > -1 ? 'win' : 'lin';
|
||||
export const NEW_LINE_BREAKER = BROWSER_OS_PLATFORM === 'win' ? '\r\n' : '\n';
|
||||
|
||||
@@ -77,13 +77,19 @@ class porImageRegistryController {
|
||||
async reloadRegistries() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
|
||||
this.registries = _.concat(this.defaultRegistry, registries);
|
||||
let showDefaultRegistry = false;
|
||||
this.registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
|
||||
|
||||
// hide default(anonymous) dockerhub registry if user has an authenticated one
|
||||
if (!this.registries.some((registry) => registry.Type === RegistryTypes.DOCKERHUB)) {
|
||||
showDefaultRegistry = true;
|
||||
this.registries.push(this.defaultRegistry);
|
||||
}
|
||||
|
||||
const id = this.model.Registry.Id;
|
||||
const registry = _.find(this.registries, { Id: id });
|
||||
if (!registry) {
|
||||
this.model.Registry = this.defaultRegistry;
|
||||
this.model.Registry = showDefaultRegistry ? this.defaultRegistry : this.registries[0];
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<select
|
||||
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
|
||||
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
|
||||
ng-model="$ctrl.model.Registry"
|
||||
id="image_registry"
|
||||
class="form-control"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash-es';
|
||||
import { NEW_LINE_BREAKER } from '@/constants';
|
||||
|
||||
angular.module('portainer.docker').controller('LogViewerController', [
|
||||
'clipboard',
|
||||
@@ -23,13 +24,13 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
||||
};
|
||||
|
||||
this.copy = function () {
|
||||
clipboard.copyText(this.state.filteredLogs.map((log) => log.line));
|
||||
clipboard.copyText(this.state.filteredLogs.map((log) => log.line).join(NEW_LINE_BREAKER));
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(2000);
|
||||
};
|
||||
|
||||
this.copySelection = function () {
|
||||
clipboard.copyText(this.state.selectedLines);
|
||||
clipboard.copyText(this.state.selectedLines.join(NEW_LINE_BREAKER));
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(2000);
|
||||
};
|
||||
@@ -48,7 +49,9 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
||||
};
|
||||
|
||||
this.downloadLogs = function () {
|
||||
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log.line, '')]);
|
||||
// To make the feature of downloading container logs working both on Windows and Linux,
|
||||
// we need to use correct new line breakers on corresponding OS.
|
||||
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + log.line + NEW_LINE_BREAKER, '')]);
|
||||
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
|
||||
};
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ angular.module('portainer.docker').factory('LogHelper', [
|
||||
|
||||
function stripHeaders(logs) {
|
||||
logs = logs.substring(8);
|
||||
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||
logs = logs.replace(/\r?\n(.{8})/g, '\n');
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export function ContainerStatsViewModel(data) {
|
||||
}
|
||||
}
|
||||
this.Networks = _.values(data.networks);
|
||||
if (data.blkio_stats !== undefined) {
|
||||
if (data.blkio_stats !== undefined && data.blkio_stats.io_service_bytes_recursive !== null) {
|
||||
//TODO: take care of multiple block devices
|
||||
var readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Read');
|
||||
if (readData === undefined) {
|
||||
|
||||
@@ -69,6 +69,8 @@ function createEventDetails(event) {
|
||||
details = 'Exec instance created';
|
||||
} else if (event.Action.indexOf('exec_start') === 0) {
|
||||
details = 'Exec instance started';
|
||||
} else if (event.Action.indexOf('exec_die') === 0) {
|
||||
details = 'Exec instance exited ';
|
||||
} else {
|
||||
details = 'Unsupported event';
|
||||
}
|
||||
|
||||
@@ -90,22 +90,11 @@ angular.module('portainer.docker').factory('ContainerService', [
|
||||
};
|
||||
|
||||
service.updateRestartPolicy = updateRestartPolicy;
|
||||
service.updateLimits = updateLimits;
|
||||
|
||||
function updateRestartPolicy(id, restartPolicy, maximumRetryCounts) {
|
||||
return Container.update({ id: id }, { RestartPolicy: { Name: restartPolicy, MaximumRetryCount: maximumRetryCounts } }).$promise;
|
||||
}
|
||||
|
||||
function updateLimits(id, config) {
|
||||
return Container.update({ id: id },
|
||||
{
|
||||
//MemorySwap: must be set, -1: non limits, 0: treated as unset(cause update error).
|
||||
MemoryReservation: config.HostConfig.MemoryReservation, "Memory": config.HostConfig.Memory, "MemorySwap": -1,
|
||||
NanoCpus: config.HostConfig.NanoCpus
|
||||
}
|
||||
).$promise;
|
||||
}
|
||||
|
||||
service.createContainer = function (configuration) {
|
||||
var deferred = $q.defer();
|
||||
Container.create(configuration)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user