Compare commits

..

2 Commits

Author SHA1 Message Date
Matt Hook
29f528b6e9 improve code and add comments 2021-10-29 13:08:01 +13:00
Matt Hook
c836fc9734 fix(migration): bubble up recovered panic in new error EE-1971 2021-10-29 11:41:54 +13:00
199 changed files with 3221 additions and 14459 deletions

13
.babelrc Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": ["lodash", "angularjs-annotate"],
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry",
"corejs": "2"
}
]
]
}

View File

@@ -17,75 +17,12 @@ 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',
{
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'
- files:
- app/**/*.test.*
extends:
- 'plugin:jest/recommended'
- 'plugin:jest/style'
env:
'jest/globals': true
no-useless-escape: off
import/order: error

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- 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.
- 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?

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@ bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json

View File

@@ -1 +0,0 @@
dist

View File

@@ -4,20 +4,10 @@
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": [
"*.html"
],
"files": ["*.html"],
"options": {
"parser": "angular"
}
},
{
"files": [
"*.{j,t}sx"
],
"options": {
"printWidth": 80,
}
}
]
}
}

View File

@@ -1,31 +0,0 @@
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;
},
};

View File

@@ -1,11 +0,0 @@
import '../app/assets/css';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

View File

@@ -75,7 +75,7 @@ The feature request process is similar to the bug report process but has an extr
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build and run Portainer locally
## Build Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
@@ -85,7 +85,7 @@ Install dependencies with yarn:
$ yarn
```
Then build and run the project in a Docker container:
Then build and run the project:
```sh
$ yarn start
@@ -95,16 +95,6 @@ Portainer can now be accessed at <https://localhost:9443>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
### Build customisation
By default, `yarn start` will use `/tmp/portainer/` for the data store, so it won't persist over reboots.
You can customise the following settings:
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default `/tmp/portainer`)
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`)
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`)
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:

View File

@@ -2,15 +2,13 @@
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
</p>
**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 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 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 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.
**Portainer** allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a super-simple graphical interface.
- [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)
A fully supported version of Portainer is available for business use. Visit http://www.portainer.io to learn more
## Demo
@@ -22,11 +20,12 @@ 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.9.x**. Portainer is on version 2, the second number denotes the month of release.
**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.
## Getting started
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
- [Documentation](https://documentation.portainer.io)
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
@@ -42,7 +41,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/community_help)
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
@@ -52,15 +51,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.
- 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!
## 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/or visit our [careers page](https://portainer.io/careers).
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.
## Privacy

View File

@@ -301,7 +301,7 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.9.1, 2.9.2
// Portainer 2.9.1
if m.currentDBVersion < 33 {
err := m.migrateDBVersionToDB33()
if err != nil {
@@ -316,13 +316,6 @@ 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")

View File

@@ -100,32 +100,6 @@ 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
@@ -244,12 +218,8 @@ 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, createTime)
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
resourceControl, ok := volumeResourceControls[oldResourceID]
if ok {

View File

@@ -1,11 +0,0 @@
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
}

View File

@@ -1,108 +0,0 @@
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")
}

View File

@@ -80,7 +80,16 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
waitForAgentToConnect := 2 * time.Duration(endpoint.EdgeCheckinInterval)
for waitForAgentToConnect >= 0 {
waitForAgentToConnect--
time.Sleep(time.Second)
tunnel = service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
break
}
}
}
tunnel = service.GetTunnelDetails(endpoint.ID)

View File

@@ -36,7 +36,6 @@ 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(),

View File

@@ -1,12 +1,11 @@
package cli
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"fmt"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
)
type pairList []portainer.Pair

View File

@@ -1,45 +0,0 @@
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
}

View File

@@ -2,23 +2,21 @@ package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/libhelm"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/hostmanagement/openamt"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
@@ -239,49 +237,6 @@ 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 {
@@ -468,8 +423,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
gitService := initGitService()
openAMTService := openamt.NewService()
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
@@ -539,11 +492,6 @@ 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)
@@ -558,7 +506,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)
}
@@ -625,7 +573,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeConfigService: kubeConfigService,

View File

@@ -1,114 +0,0 @@
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))
})
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/pkg/errors"
@@ -15,7 +16,6 @@ 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 := stackutils.GetStackFilePaths(stack)
filePaths := getStackFiles(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 := stackutils.GetStackFilePaths(stack)
filePaths := getStackFiles(stack)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
return errors.Wrap(err, "failed to remove a stack")
}
@@ -115,3 +115,27 @@ 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
}

View File

@@ -64,3 +64,21 @@ 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`})
}

View File

@@ -191,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
}

View File

@@ -6,14 +6,14 @@ import (
"encoding/pem"
"errors"
"fmt"
"path/filepath"
"strings"
"io/ioutil"
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"io"
"os"
"path"
)
const (
@@ -69,31 +69,12 @@ 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: JoinPaths(dataStorePath, fileStorePath),
fileStorePath: path.Join(dataStorePath, fileStorePath),
}
err := os.MkdirAll(dataStorePath, 0755)
@@ -131,12 +112,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 JoinPaths(service.fileStorePath, BinaryStorePath)
return path.Join(service.fileStorePath, BinaryStorePath)
}
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
func (service *Service) GetDockerConfigPath() string {
return JoinPaths(service.fileStorePath, DockerConfigPath)
return path.Join(service.fileStorePath, DockerConfigPath)
}
// RemoveDirectory removes a directory on the filesystem.
@@ -147,7 +128,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 JoinPaths(service.wrapFileStore(ComposeStorePath), stackIdentifier)
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
}
// Copy copies the file on fromFilePath to toFilePath
@@ -213,13 +194,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 := JoinPaths(ComposeStorePath, stackIdentifier)
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := JoinPaths(stackStorePath, fileName)
composeFilePath := path.Join(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
@@ -227,25 +208,25 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
return "", err
}
return service.wrapFileStore(stackStorePath), nil
return path.Join(service.fileStorePath, 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 JoinPaths(service.wrapFileStore(EdgeStackStorePath), edgeStackIdentifier)
return path.Join(service.fileStorePath, 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 := JoinPaths(EdgeStackStorePath, edgeStackIdentifier)
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := JoinPaths(stackStorePath, fileName)
composeFilePath := path.Join(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
@@ -253,20 +234,20 @@ func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileNam
return "", err
}
return service.wrapFileStore(stackStorePath), nil
return path.Join(service.fileStorePath, 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 := JoinPaths(ExtensionRegistryManagementStorePath, folder)
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
err := service.createDirectoryInStore(extensionStorePath)
if err != nil {
return "", err
}
file := JoinPaths(extensionStorePath, fileName)
file := path.Join(extensionStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(file, r)
@@ -274,13 +255,13 @@ func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName st
return "", err
}
return service.wrapFileStore(file), nil
return path.Join(service.fileStorePath, 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 := JoinPaths(TLSStorePath, folder)
storePath := path.Join(TLSStorePath, folder)
err := service.createDirectoryInStore(storePath)
if err != nil {
return "", err
@@ -298,13 +279,13 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.
return "", ErrUndefinedTLSFileType
}
tlsFilePath := JoinPaths(storePath, fileName)
tlsFilePath := path.Join(storePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return "", err
}
return service.wrapFileStore(tlsFilePath), nil
return path.Join(service.fileStorePath, tlsFilePath), nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an environment(endpoint).
@@ -320,13 +301,17 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
default:
return "", ErrUndefinedTLSFileType
}
return JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName), nil
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
}
// DeleteTLSFiles deletes a folder in the TLS store path.
func (service *Service) DeleteTLSFiles(folder string) error {
storePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder)
return os.RemoveAll(storePath)
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
err := os.RemoveAll(storePath)
if err != nil {
return err
}
return nil
}
// DeleteTLSFile deletes a specific TLS file from a folder.
@@ -343,19 +328,20 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
return ErrUndefinedTLSFileType
}
filePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName)
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
return os.Remove(filePath)
err := os.Remove(filePath)
if err != nil {
return err
}
return nil
}
// GetFileContent returns the content of a file as bytes.
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
if filePath == "" {
filePath = trustedRoot
}
return nil, fmt.Errorf("could not get the contents of the file '%s'", filePath)
return nil, err
}
return content, nil
@@ -373,7 +359,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
return err
}
return os.WriteFile(path, jsonContent, 0644)
return ioutil.WriteFile(path, jsonContent, 0644)
}
// FileExists checks for the existence of the specified file.
@@ -383,17 +369,23 @@ func (service *Service) FileExists(filePath string) (bool, error) {
// KeyPairFilesExist checks for the existence of the key files.
func (service *Service) KeyPairFilesExist() (bool, error) {
privateKeyPath := JoinPaths(service.dataStorePath, PrivateKeyFile)
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
exists, err := service.FileExists(privateKeyPath)
if err != nil || !exists {
if err != nil {
return false, err
}
if !exists {
return false, nil
}
publicKeyPath := JoinPaths(service.dataStorePath, PublicKeyFile)
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
exists, err = service.FileExists(publicKeyPath)
if err != nil || !exists {
if err != nil {
return false, err
}
if !exists {
return false, nil
}
return true, nil
}
@@ -405,7 +397,12 @@ func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, p
return err
}
return service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
if err != nil {
return err
}
return nil
}
// LoadKeyPair retrieve the content of both key files on disk.
@@ -425,13 +422,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 := service.wrapFileStore(name)
path := path.Join(service.fileStorePath, 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 := service.wrapFileStore(filePath)
path := path.Join(service.fileStorePath, filePath)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
@@ -440,11 +437,15 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
defer out.Close()
_, err = io.Copy(out, r)
return err
if err != nil {
return err
}
return nil
}
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
path := service.wrapFileStore(filePath)
path := path.Join(service.fileStorePath, filePath)
block := &pem.Block{Type: fileType, Bytes: content}
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
@@ -453,13 +454,18 @@ func (service *Service) createPEMFileInStore(content []byte, fileType, filePath
}
defer out.Close()
return pem.Encode(out, block)
err = pem.Encode(out, block)
if err != nil {
return err
}
return nil
}
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
path := service.wrapFileStore(filePath)
path := path.Join(service.fileStorePath, filePath)
fileContent, err := os.ReadFile(path)
fileContent, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
@@ -471,19 +477,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 JoinPaths(service.wrapFileStore(CustomTemplateStorePath), identifier)
return path.Join(service.fileStorePath, 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 := JoinPaths(CustomTemplateStorePath, identifier)
customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier)
err := service.createDirectoryInStore(customTemplateStorePath)
if err != nil {
return "", err
}
templateFilePath := JoinPaths(customTemplateStorePath, fileName)
templateFilePath := path.Join(customTemplateStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(templateFilePath, r)
@@ -491,32 +497,32 @@ func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName st
return "", err
}
return service.wrapFileStore(customTemplateStorePath), nil
return path.Join(service.fileStorePath, 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 JoinPaths(service.wrapFileStore(EdgeJobStorePath), identifier)
return path.Join(service.fileStorePath, 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 := JoinPaths(EdgeJobStorePath, identifier)
edgeJobStorePath := path.Join(EdgeJobStorePath, identifier)
err := service.createDirectoryInStore(edgeJobStorePath)
if err != nil {
return "", err
}
filePath := JoinPaths(edgeJobStorePath, createEdgeJobFileName(identifier))
filePath := path.Join(edgeJobStorePath, createEdgeJobFileName(identifier))
r := bytes.NewReader(data)
err = service.createFileInStore(filePath, r)
if err != nil {
return "", err
}
return service.wrapFileStore(filePath), nil
return path.Join(service.fileStorePath, filePath), nil
}
func createEdgeJobFileName(identifier string) string {
@@ -526,14 +532,20 @@ 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)
return os.Remove(path)
err := os.Remove(path)
if err != nil {
return err
}
return nil
}
// GetEdgeJobTaskLogFileContent fetches the Edge job task logs
func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID string) (string, error) {
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
fileContent, err := os.ReadFile(path)
fileContent, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
@@ -543,15 +555,20 @@ func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID st
// StoreEdgeJobTaskLogFileFromBytes stores the log file
func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error {
edgeJobStorePath := JoinPaths(EdgeJobStorePath, edgeJobID)
edgeJobStorePath := path.Join(EdgeJobStorePath, edgeJobID)
err := service.createDirectoryInStore(edgeJobStorePath)
if err != nil {
return err
}
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
filePath := path.Join(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
r := bytes.NewReader(data)
return service.createFileInStore(filePath, r)
err = service.createFileInStore(filePath, r)
if err != nil {
return err
}
return nil
}
func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string {
@@ -565,7 +582,7 @@ func (service *Service) GetTemporaryPath() (string, error) {
return "", err
}
return JoinPaths(service.wrapFileStore(TempPath), uid.String()), nil
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
}
// GetDataStorePath returns path to data folder
@@ -574,12 +591,12 @@ func (service *Service) GetDatastorePath() string {
}
func (service *Service) wrapFileStore(filepath string) string {
return JoinPaths(service.fileStorePath, filepath)
return path.Join(service.fileStorePath, filepath)
}
func defaultCertPathUnderFileStore() (string, string) {
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
return certPath, keyPath
}

View File

@@ -1,70 +0,0 @@
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)
}
}
}

View File

@@ -1,120 +0,0 @@
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)
}
}
}

View File

@@ -37,7 +37,7 @@ require (
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.4
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
@@ -47,5 +47,4 @@ require (
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)

View File

@@ -691,8 +691,8 @@ 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.4 h1:up+ixy8yOqJKiFcuhMgkuYuF4xnevuhnFAXXF8OSfNg=
github.com/swaggo/swag v1.7.4/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI=
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=
@@ -790,6 +790,7 @@ 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=
@@ -1148,5 +1149,3 @@ sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZa
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg=

View File

@@ -1,49 +0,0 @@
package openamt
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type authenticationResponse struct {
Token string `json:"token"`
}
func (service *Service) executeAuthenticationRequest(configuration portainer.OpenAMTConfiguration) (*authenticationResponse, error) {
loginURL := fmt.Sprintf("https://%v/mps/login/api/v1/authorize", configuration.MPSURL)
payload := map[string]string{
"username": configuration.Credentials.MPSUser,
"password": configuration.Credentials.MPSPassword,
}
jsonValue, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer(jsonValue))
req.Header.Set("Content-Type", "application/json")
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
var token authenticationResponse
err = json.Unmarshal(responseBody, &token)
if err != nil {
return nil, err
}
return &token, nil
}

View File

@@ -1,140 +0,0 @@
package openamt
import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
)
type CIRAConfig struct {
ConfigName string `json:"configName"`
MPSServerAddress string `json:"mpsServerAddress"`
ServerAddressFormat int `json:"serverAddressFormat"`
CommonName string `json:"commonName"`
MPSPort int `json:"mpsPort"`
Username string `json:"username"`
MPSRootCertificate string `json:"mpsRootCertificate"`
RegeneratePassword bool `json:"regeneratePassword"`
AuthMethod int `json:"authMethod"`
}
func (service *Service) createOrUpdateCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
ciraConfig, err := service.getCIRAConfig(configuration, configName)
if err != nil {
return nil, err
}
method := http.MethodPost
if ciraConfig != nil {
method = http.MethodPatch
}
ciraConfig, err = service.saveCIRAConfig(method, configuration, configName)
if err != nil {
return nil, err
}
return ciraConfig, nil
}
func (service *Service) getCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/ciraconfigs/%v", configuration.MPSURL, configName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result CIRAConfig
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveCIRAConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/ciraconfigs", configuration.MPSURL)
certificate, err := service.getCIRACertificate(configuration)
if err != nil {
return nil, err
}
addressFormat, err := addressFormat(configuration.MPSURL)
if err != nil {
return nil, err
}
config := CIRAConfig{
ConfigName: configName,
MPSServerAddress: configuration.MPSURL,
CommonName: configuration.MPSURL,
ServerAddressFormat: addressFormat,
MPSPort: 4433,
Username: "admin",
MPSRootCertificate: certificate,
RegeneratePassword: false,
AuthMethod: 2,
}
payload, _ := json.Marshal(config)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result CIRAConfig
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func addressFormat(url string) (int, error) {
ip := net.ParseIP(url)
if ip == nil {
return 201, nil // FQDN
}
if strings.Contains(url, ".") {
return 3, nil // IPV4
}
if strings.Contains(url, ":") {
return 4, nil // IPV6
}
return 0, fmt.Errorf("could not determine server address format for %v", url)
}
func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfiguration) (string, error) {
loginURL := fmt.Sprintf("https://%v/mps/api/v1/ciracert", configuration.MPSURL)
req, err := http.NewRequest(http.MethodGet, loginURL, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", configuration.Credentials.MPSToken))
response, err := service.httpsClient.Do(req)
if err != nil {
return "", err
}
if response.StatusCode != http.StatusOK {
return "", errors.New(fmt.Sprintf("unexpected status code %v", response.Status))
}
certificate, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
block, _ := pem.Decode(certificate)
return base64.StdEncoding.EncodeToString(block.Bytes), nil
}

View File

@@ -1,81 +0,0 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
Domain struct {
DomainName string `json:"profileName"`
DomainSuffix string `json:"domainSuffix"`
ProvisioningCert string `json:"provisioningCert"`
ProvisioningCertPassword string `json:"provisioningCertPassword"`
ProvisioningCertStorageFormat string `json:"provisioningCertStorageFormat"`
}
)
func (service *Service) createOrUpdateDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
domain, err := service.getDomain(configuration)
if err != nil {
return nil, err
}
method := http.MethodPost
if domain != nil {
method = http.MethodPatch
}
domain, err = service.saveDomain(method, configuration)
if err != nil {
return nil, err
}
return domain, nil
}
func (service *Service) getDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/domains/%v", configuration.MPSURL, configuration.DomainConfiguration.DomainName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result Domain
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveDomain(method string, configuration portainer.OpenAMTConfiguration) (*Domain, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/domains", configuration.MPSURL)
profile := Domain{
DomainName: configuration.DomainConfiguration.DomainName,
DomainSuffix: configuration.DomainConfiguration.DomainName,
ProvisioningCert: configuration.DomainConfiguration.CertFileText,
ProvisioningCertPassword: configuration.DomainConfiguration.CertPassword,
ProvisioningCertStorageFormat: "string",
}
payload, _ := json.Marshal(profile)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result Domain
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -1,104 +0,0 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
Profile struct {
ProfileName string `json:"profileName"`
Activation string `json:"activation"`
CIRAConfigName *string `json:"ciraConfigName"`
GenerateRandomAMTPassword bool `json:"generateRandomPassword"`
AMTPassword string `json:"amtPassword"`
GenerateRandomMEBxPassword bool `json:"generateRandomMEBxPassword"`
MEBXPassword string `json:"mebxPassword"`
Tags []string `json:"tags"`
DHCPEnabled bool `json:"dhcpEnabled"`
TenantId string `json:"tenantId"`
WIFIConfigs []ProfileWifiConfig `json:"wifiConfigs"`
}
ProfileWifiConfig struct {
Priority int `json:"priority"`
ProfileName string `json:"profileName"`
}
)
func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
profile, err := service.getAMTProfile(configuration, profileName)
if err != nil {
return nil, err
}
method := http.MethodPost
if profile != nil {
method = http.MethodPatch
}
profile, err = service.saveAMTProfile(method, configuration, profileName, ciraConfigName, wirelessConfig)
if err != nil {
return nil, err
}
return profile, nil
}
func (service *Service) getAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string) (*Profile, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/profiles/%v", configuration.MPSURL, profileName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result Profile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveAMTProfile(method string, configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/profiles", configuration.MPSURL)
profile := Profile{
ProfileName: profileName,
Activation: "acmactivate",
GenerateRandomAMTPassword: false,
GenerateRandomMEBxPassword: false,
AMTPassword: configuration.Credentials.MPSPassword,
MEBXPassword: configuration.Credentials.MPSPassword,
CIRAConfigName: &ciraConfigName,
Tags: []string{},
DHCPEnabled: true,
}
if wirelessConfig != "" {
profile.WIFIConfigs = []ProfileWifiConfig{
{
Priority: 1,
ProfileName: DefaultWirelessConfigName,
},
}
}
payload, _ := json.Marshal(profile)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result Profile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -1,91 +0,0 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
portainer "github.com/portainer/portainer/api"
)
type (
WirelessProfile struct {
ProfileName string `json:"profileName"`
AuthenticationMethod int `json:"authenticationMethod"`
EncryptionMethod int `json:"encryptionMethod"`
SSID string `json:"ssid"`
PSKPassphrase string `json:"pskPassphrase"`
}
)
func (service *Service) createOrUpdateWirelessConfig(configuration portainer.OpenAMTConfiguration, wirelessConfigName string) (*WirelessProfile, error) {
wirelessConfig, err := service.getWirelessConfig(configuration, wirelessConfigName)
if err != nil {
return nil, err
}
method := http.MethodPost
if wirelessConfig != nil {
method = http.MethodPatch
}
wirelessConfig, err = service.saveWirelessConfig(method, configuration, wirelessConfigName)
if err != nil {
return nil, err
}
return wirelessConfig, nil
}
func (service *Service) getWirelessConfig(configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
url := fmt.Sprintf("https://%v/rps/api/v1/admin/wirelessconfigs/%v", configuration.MPSURL, configName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result WirelessProfile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveWirelessConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
parsedAuthenticationMethod, err := strconv.Atoi(configuration.WirelessConfiguration.AuthenticationMethod)
if err != nil {
return nil, fmt.Errorf("error parsing wireless authentication method: %v", err.Error())
}
parsedEncryptionMethod, err := strconv.Atoi(configuration.WirelessConfiguration.EncryptionMethod)
if err != nil {
return nil, fmt.Errorf("error parsing wireless encryption method: %v", err.Error())
}
url := fmt.Sprintf("https://%v/rps/api/v1/admin/wirelessconfigs", configuration.MPSURL)
config := WirelessProfile{
ProfileName: configName,
AuthenticationMethod: parsedAuthenticationMethod,
EncryptionMethod: parsedEncryptionMethod,
SSID: configuration.WirelessConfiguration.SSID,
PSKPassphrase: configuration.WirelessConfiguration.PskPass,
}
payload, _ := json.Marshal(config)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result WirelessProfile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -1,148 +0,0 @@
package openamt
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
portainer "github.com/portainer/portainer/api"
)
const (
DefaultCIRAConfigName = "ciraConfigDefault"
DefaultWirelessConfigName = "wirelessProfileDefault"
DefaultProfileName = "profileAMTDefault"
)
// Service represents a service for managing an OpenAMT server.
type Service struct {
httpsClient *http.Client
}
// NewService initializes a new service.
func NewService() *Service {
return &Service{
httpsClient:
&http.Client{
Timeout: time.Second * time.Duration(5),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
}
}
type openAMTError struct {
ErrorMsg string `json:"message"`
Errors []struct {
ErrorMsg string `json:"msg"`
} `json:"errors"`
}
func parseError(responseBody []byte) error {
var errorResponse openAMTError
err := json.Unmarshal(responseBody, &errorResponse)
if err != nil {
return err
}
if len(errorResponse.Errors) > 0 {
return errors.New(errorResponse.Errors[0].ErrorMsg)
}
if errorResponse.ErrorMsg != "" {
return errors.New(errorResponse.ErrorMsg)
}
return nil
}
func (service *Service) ConfigureDefault(configuration portainer.OpenAMTConfiguration) error {
token, err := service.executeAuthenticationRequest(configuration)
if err != nil {
return err
}
configuration.Credentials.MPSToken = token.Token
ciraConfig, err := service.createOrUpdateCIRAConfig(configuration, DefaultCIRAConfigName)
if err != nil {
return err
}
wirelessConfigName := ""
if configuration.WirelessConfiguration != nil {
wirelessConfig, err := service.createOrUpdateWirelessConfig(configuration, DefaultWirelessConfigName)
if err != nil {
return err
}
wirelessConfigName = wirelessConfig.ProfileName
}
_, err = service.createOrUpdateAMTProfile(configuration, DefaultProfileName, ciraConfig.ConfigName, wirelessConfigName)
if err != nil {
return err
}
_, err = service.createOrUpdateDomain(configuration)
if err != nil {
return err
}
return nil
}
func (service *Service) executeSaveRequest(method string, url string, token string, payload []byte) ([]byte, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
if response.StatusCode < 200 || response.StatusCode > 300 {
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
return nil, errors.New(fmt.Sprintf("unexpected status code %v", response.Status))
}
return responseBody, nil
}
func (service *Service) executeGetRequest(url string, token string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
if response.StatusCode < 200 || response.StatusCode > 300 {
if response.StatusCode == http.StatusNotFound {
return nil, nil
}
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
return nil, errors.New(fmt.Sprintf("unexpected status code %v", response.Status))
}
return responseBody, nil
}

View File

@@ -2,7 +2,6 @@ package customtemplates
import (
"errors"
"log"
"net/http"
"regexp"
"strconv"
@@ -271,23 +270,6 @@ 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
}

View File

@@ -2,6 +2,7 @@ package customtemplates
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -40,7 +41,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(customTemplate.ProjectPath, customTemplate.EntryPoint)
fileContent, err := handler.FileService.GetFileContent(path.Join(customTemplate.ProjectPath, customTemplate.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom template file from disk", err}
}

View File

@@ -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}
}

View File

@@ -2,6 +2,7 @@ package edgestacks
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -44,7 +45,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
fileName = stack.ManifestPath
}
stackFileContent, err := handler.FileService.GetFileContent(stack.ProjectPath, fileName)
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, fileName))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}

View File

@@ -3,6 +3,7 @@ package edgestacks
import (
"fmt"
"net/http"
"path"
"strconv"
"github.com/gorilla/mux"
@@ -55,7 +56,7 @@ func (handler *Handler) convertAndStoreKubeManifestIfNeeded(edgeStack *portainer
return nil
}
composeConfig, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, edgeStack.EntryPoint)
composeConfig, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
if err != nil {
return fmt.Errorf("unable to retrieve Compose file from disk: %w", err)
}

View File

@@ -3,6 +3,7 @@ package endpointedge
import (
"errors"
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -74,7 +75,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
}
}
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, fileName))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}

View File

@@ -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}

View File

@@ -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.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
bouncer.RestrictedAccess(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}",

View File

@@ -17,7 +17,6 @@ import (
"github.com/portainer/portainer/api/http/handler/endpoints"
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/motd"
@@ -28,7 +27,6 @@ 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,10 +61,8 @@ type Handler struct {
RoleHandler *roles.Handler
SettingsHandler *settings.Handler
SSLHandler *ssl.Handler
OpenAMTHandler *openamt.Handler
StackHandler *stacks.Handler
StatusHandler *status.Handler
StorybookHandler *storybook.Handler
TagHandler *tags.Handler
TeamMembershipHandler *teammemberships.Handler
TeamHandler *teams.Handler
@@ -78,7 +74,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.9.3
// @version 2.9.2
// @description.markdown api-description.md
// @termsOfService
@@ -223,8 +219,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/ssl"):
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/teams"):
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
@@ -233,8 +227,6 @@ 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)
}

View File

@@ -1,197 +0,0 @@
package openamt
import (
"context"
"io"
"io/ioutil"
"log"
"net/http"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
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/sirupsen/logrus"
)
type OpenAMTHostInfo struct {
Endpoint portainer.EndpointID
Text string
}
const (
// TODO: this should get extracted to some configurable - don't assume Docker Hub is everyone's global namespace, or that they're allowed to pull images from the internet
rpcGoImageName = "ptrrd/openamt:rpc-go"
rpcGoContainerName = "openamt-rpc-go"
)
// @id OpenAMTHostInfo
// @summary Request OpenAMT info from a node
// @description Request OpenAMT info from a node
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @accept json
// @produce json
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @router /manage/{id}/info [get]
func (handler *Handler) OpenAMTHostInfo(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}
}
logrus.WithField("endpointID", endpointID).Info("OpenAMTHostInfo")
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
}
ctx := context.TODO()
// pull the image so we can check if there's a new one
// TODO: these should be able to be over-ridden (don't hardcode the assuption that secure users can access Docker Hub, or that its even the orchestrator's "global namespace")
cmdLine := []string{"amtinfo", "--json"}
output, err := handler.PullAndRunContainer(ctx, endpoint, rpcGoImageName, rpcGoContainerName, cmdLine)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: output, Err: err}
}
amtInfo := OpenAMTHostInfo{
Endpoint: portainer.EndpointID(endpointID),
Text: output,
}
return response.JSON(w, amtInfo)
}
func (handler *Handler) PullAndRunContainer(ctx context.Context, endpoint *portainer.Endpoint, imageName, containerName string, cmdLine []string) (output string, err error) {
// TODO: this should not be Docker specific
// TODO: extract from this Handler into something global.
// TODO: start
// docker run --rm -it --privileged ptrrd/openamt:rpc-go amtinfo
// on the Docker standalone node (one per env :)
// and later, on the specified node in the swarm, or kube.
nodeName := ""
docker, err := handler.DockerClientFactory.CreateClient(endpoint, nodeName)
if err != nil {
return "Unable to create Docker Client connection", err
}
defer docker.Close()
if err := pullImage(ctx, docker, imageName); err != nil {
return "Could not pull image from registry", err
}
output, err = runContainer(ctx, docker, imageName, containerName, cmdLine)
if err != nil {
return "Could not run container", err
}
return output, nil
}
// TODO: ideally, pullImage and runContainer will become a simple version of the use compose abstraction that can be called from withing Portainer.
// TODO: the idea being that if we have an internal struct of a parsed compose file, we can also populate that struct programatically, and run it to get the result I'm getting here.
// TODO: likley an upgrade and absrtaction of DeployComposeStack/DeploySwarmStack/DeployKubernetesStack
// pullImage will pull the image to the specified environment
// TODO: add k8s implemenation
// TODO: work out registry auth
func pullImage(ctx context.Context, docker *client.Client, imageName string) error {
r, err := docker.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
logrus.WithError(err).WithField("imageName", imageName).Error("Could not pull image from registry")
return err
}
// yeah, swiped this, need to figure out a good way to wait til its done...
b := make([]byte, 8)
for {
_, err := r.Read(b)
// TODO: should convert json text to a struct and show just the text messages
//if n > 0 {
//fmt.Printf(string(b))
//}
if err == io.EOF {
break
}
}
r.Close()
return nil
}
// TODO: ideally, pullImage and runContainer will become a simple version of the use compose abstraction that can be called from withing Portainer.
// runContainer should be used to run a short command that returns information to stdout
// TODO: add k8s support
func runContainer(ctx context.Context, docker *client.Client, imageName, containerName string, cmdLine []string) (output string, err error) {
envs := []string{}
create, err := docker.ContainerCreate(
ctx,
&container.Config{
Image: imageName,
Cmd: cmdLine,
Env: envs,
Tty: true,
OpenStdin: true,
AttachStdout: true,
AttachStderr: true,
},
&container.HostConfig{
Privileged: true,
},
&network.NetworkingConfig{},
nil,
containerName)
if err != nil {
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("creating container")
return "", err
}
err = docker.ContainerStart(ctx, create.ID, types.ContainerStartOptions{})
if err != nil {
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("starting container")
return "", err
}
log.Printf("%s container created and started\n", containerName)
statusCh, errCh := docker.ContainerWait(ctx, create.ID, container.WaitConditionNotRunning)
var statusCode int64
select {
case err := <-errCh:
if err != nil {
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("starting container")
return "", err
}
case status := <-statusCh:
statusCode = status.StatusCode
}
logrus.WithField("status", statusCode).Debug("container wait status")
out, err := docker.ContainerLogs(ctx, create.ID, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("getting container log")
return "", err
}
err = docker.ContainerRemove(ctx, create.ID, types.ContainerRemoveOptions{})
if err != nil {
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("removing container")
return "", err
}
outputBytes, err := ioutil.ReadAll(out)
if err != nil {
logrus.WithError(err).WithField("imagename", imageName).WithField("containername", containerName).Error("read container output")
return "", err
}
return string(outputBytes), nil
}

View File

@@ -1,42 +0,0 @@
package openamt
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle OpenAMT operations.
type Handler struct {
*mux.Router
OpenAMTService portainer.OpenAMTService
DataStore portainer.DataStore
// used by OpenAMTHostInfo
DockerClientFactory *docker.ClientFactory
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore) (*Handler, error) {
h := &Handler{
Router: mux.NewRouter(),
}
settings, err := dataStore.Settings().Settings()
if err != nil {
return nil, err
}
featureEnabled, _ := settings.FeatureFlagSettings[portainer.FeatOpenAMT]
if featureEnabled {
h.Handle("/open_amt", bouncer.AdminAccess(httperror.LoggerHandler(h.openAMTConfigureDefault))).Methods(http.MethodPost)
h.Handle("/open-amt/{id}/info", bouncer.AdminAccess(httperror.LoggerHandler(h.OpenAMTHostInfo))).Methods(http.MethodGet)
}
return h, nil
}

View File

@@ -1,218 +0,0 @@
package openamt
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
"github.com/sirupsen/logrus"
"software.sslmate.com/src/go-pkcs12"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type openAMTConfigureDefaultPayload struct {
EnableOpenAMT bool
MPSURL string
MPSUser string
MPSPassword string
CertFileText string
CertPassword string
DomainName string
UseWirelessConfig bool
WifiAuthenticationMethod string
WifiEncryptionMethod string
WifiSSID string
WifiPskPass string
}
func (payload *openAMTConfigureDefaultPayload) Validate(r *http.Request) error {
if payload.EnableOpenAMT {
if payload.MPSURL == "" {
return errors.New("MPS Url must be provided")
}
if payload.MPSUser == "" {
return errors.New("MPS User must be provided")
}
if payload.MPSPassword == "" {
return errors.New("MPS Password must be provided")
}
if payload.DomainName == "" {
return errors.New("domain name must be provided")
}
if payload.CertFileText == "" {
return errors.New("certificate file must be provided")
}
if payload.CertPassword == "" {
return errors.New("certificate password must be provided")
}
if payload.UseWirelessConfig {
if payload.WifiAuthenticationMethod == "" {
return errors.New("wireless authentication method must be provided")
}
if payload.WifiEncryptionMethod == "" {
return errors.New("wireless encryption method must be provided")
}
if payload.WifiSSID == "" {
return errors.New("wireless config SSID must be provided")
}
if payload.WifiPskPass == "" {
return errors.New("wireless config PSK passphrase must be provided")
}
}
}
return nil
}
// @id OpenAMTConfigureDefault
// @summary Enable OpenAMT capabilities
// @description Enable OpenAMT capabilities
// @description **Access policy**: administrator
// @tags intel
// @security jwt
// @accept json
// @produce json
// @param body body openAMTConfigureDefaultPayload true "OpenAMT Settings"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings"
// @failure 500 "Server error"
// @router /open_amt [post]
func (handler *Handler) openAMTConfigureDefault(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload openAMTConfigureDefaultPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
logrus.WithError(err).Error("Invalid request payload")
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
if payload.EnableOpenAMT {
certificateErr := validateCertificate(payload.CertFileText, payload.CertPassword)
if certificateErr != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error validating certificate", Err: certificateErr}
}
err = handler.enableOpenAMT(payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error enabling OpenAMT", Err: err}
}
return response.Empty(w)
}
err = handler.disableOpenAMT()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Error disabling OpenAMT", Err: err}
}
return response.Empty(w)
}
func validateCertificate(certificateRaw string, certificatePassword string) error {
certificateData, err := base64.StdEncoding.Strict().DecodeString(certificateRaw)
if err != nil {
return err
}
_, certificate, _, err := pkcs12.DecodeChain(certificateData, certificatePassword)
if err != nil {
return err
}
if certificate == nil {
return errors.New("certificate could not be decoded")
}
issuer := certificate.Issuer.CommonName
if !isValidIssuer(issuer) {
return fmt.Errorf("certificate issuer is invalid: %v", issuer)
}
return nil
}
func isValidIssuer(issuer string) bool {
formattedIssuer := strings.ToLower(strings.ReplaceAll(issuer, " ", ""))
return strings.Contains(formattedIssuer, "comodo") ||
strings.Contains(formattedIssuer, "digicert") ||
strings.Contains(formattedIssuer, "entrust") ||
strings.Contains(formattedIssuer, "godaddy")
}
func (handler *Handler) enableOpenAMT(configurationPayload openAMTConfigureDefaultPayload) error {
configuration := portainer.OpenAMTConfiguration{
Enabled: true,
MPSURL: configurationPayload.MPSURL,
Credentials: portainer.MPSCredentials{
MPSUser: configurationPayload.MPSUser,
MPSPassword: configurationPayload.MPSPassword,
},
DomainConfiguration: portainer.DomainConfiguration{
CertFileText: configurationPayload.CertFileText,
CertPassword: configurationPayload.CertPassword,
DomainName: configurationPayload.DomainName,
},
}
if configurationPayload.UseWirelessConfig {
configuration.WirelessConfiguration = &portainer.WirelessConfiguration{
AuthenticationMethod: configurationPayload.WifiAuthenticationMethod,
EncryptionMethod: configurationPayload.WifiEncryptionMethod,
SSID: configurationPayload.WifiSSID,
PskPass: configurationPayload.WifiPskPass,
}
}
err := handler.OpenAMTService.ConfigureDefault(configuration)
if err != nil {
logrus.WithError(err).Error("error configuring OpenAMT server")
return err
}
err = handler.saveConfiguration(configuration)
if err != nil {
logrus.WithError(err).Error("error updating OpenAMT configurations")
return err
}
logrus.Info("OpenAMT successfully enabled")
return nil
}
func (handler *Handler) saveConfiguration(configuration portainer.OpenAMTConfiguration) error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
configuration.Credentials.MPSToken = ""
settings.OpenAMTConfiguration = configuration
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
}
return nil
}
func (handler *Handler) disableOpenAMT() error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
settings.OpenAMTConfiguration.Enabled = false
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
}
logrus.Info("OpenAMT successfully disabled")
return nil
}

View File

@@ -12,7 +12,6 @@ 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.

View File

@@ -16,8 +16,6 @@ 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
@@ -54,7 +52,6 @@ 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 {

View File

@@ -121,8 +121,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
}
settings.HelmRepositoryURL = newHelmRepo
} else {
settings.HelmRepositoryURL = ""
}
if payload.BlackListedLabels != nil {
@@ -148,13 +146,8 @@ 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 {

View File

@@ -3,6 +3,7 @@ package stacks
import (
"fmt"
"net/http"
"path"
"strconv"
"time"
@@ -388,9 +389,10 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
if err != nil {
return errors.Wrapf(err, "failed to get stack file content `%q`", file)
return errors.Wrapf(err, "failed to get stack file content `%q`", path)
}
err = handler.isValidStackFile(stackContent, securitySettings)

View File

@@ -3,6 +3,7 @@ package stacks
import (
"fmt"
"net/http"
"path"
"strconv"
"time"
@@ -398,7 +399,8 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
if err != nil {
return errors.WithMessage(err, "failed to get stack file content")
}

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"github.com/pkg/errors"
@@ -197,8 +198,8 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St
defer os.RemoveAll(tmpDir)
for _, fileName := range fileNames {
manifestFilePath := filesystem.JoinPaths(tmpDir, fileName)
manifestContent, err := handler.FileService.GetFileContent(stack.ProjectPath, fileName)
manifestFilePath := path.Join(tmpDir, fileName)
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
if err != nil {
return errors.Wrap(err, "failed to read manifest file")
}

View File

@@ -2,6 +2,7 @@ package stacks
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -81,7 +82,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
}
}
stackFileContent, err := handler.FileService.GetFileContent(stack.ProjectPath, stack.EntryPoint)
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}

View File

@@ -136,6 +136,7 @@ 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 {
@@ -149,8 +150,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
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 != "" {

View File

@@ -5,6 +5,7 @@ import (
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"github.com/asaskevich/govalidator"
@@ -107,7 +108,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
tempFileDir, _ := ioutil.TempDir("", "kub_file_content")
defer os.RemoveAll(tempFileDir)
if err := filesystem.WriteToFile(filesystem.JoinPaths(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil {
if err := filesystem.WriteToFile(path.Join(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}
}

View File

@@ -1,23 +0,0 @@
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)
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"path"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@@ -67,7 +68,9 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}
fileContent, err := handler.FileService.GetFileContent(projectPath, payload.ComposeFilePathInRepository)
composeFilePath := path.Join(projectPath, payload.ComposeFilePathInRepository)
fileContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed loading file content", err}
}

View File

@@ -28,7 +28,6 @@ import (
"github.com/portainer/portainer/api/http/handler/endpoints"
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/motd"
@@ -39,7 +38,6 @@ 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"
@@ -76,7 +74,6 @@ type Server struct {
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
OpenAMTService portainer.OpenAMTService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
@@ -205,14 +202,6 @@ func (server *Server) Start() error {
var sslHandler = sslhandler.NewHandler(requestBouncer)
sslHandler.SSLService = server.SSLService
openAMTHandler, err := openamt.NewHandler(requestBouncer, server.DataStore)
if err != nil {
return err
}
openAMTHandler.OpenAMTService = server.OpenAMTService
openAMTHandler.DataStore = server.DataStore
openAMTHandler.DockerClientFactory = server.DockerClientFactory
var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.DataStore = server.DataStore
stackHandler.DockerClientFactory = server.DockerClientFactory
@@ -224,8 +213,6 @@ 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
@@ -278,14 +265,12 @@ func (server *Server) Start() error {
HelmTemplatesHandler: helmTemplatesHandler,
KubernetesHandler: kubernetesHandler,
MOTDHandler: motdHandler,
OpenAMTHandler: openAMTHandler,
RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler,
SettingsHandler: settingsHandler,
SSLHandler: sslHandler,
StatusHandler: statusHandler,
StackHandler: stackHandler,
StorybookHandler: storybookHandler,
TagHandler: tagHandler,
TeamHandler: teamHandler,
TeamMembershipHandler: teamMembershipHandler,

View File

@@ -3,6 +3,7 @@ package stackutils
import (
"fmt"
"io/ioutil"
"path"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
@@ -19,7 +20,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, filesystem.JoinPaths(stack.ProjectPath, file))
filePaths = append(filePaths, path.Join(stack.ProjectPath, file))
}
return filePaths
}
@@ -36,8 +37,8 @@ func CreateTempK8SDeploymentFiles(stack *portainer.Stack, kubeDeployer portainer
}
for _, fileName := range fileNames {
manifestFilePath := filesystem.JoinPaths(tmpDir, fileName)
manifestContent, err := ioutil.ReadFile(filesystem.JoinPaths(stack.ProjectPath, fileName))
manifestFilePath := path.Join(tmpDir, fileName)
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
if err != nil {
return nil, "", errors.Wrap(err, "failed to read manifest file")
}

View File

@@ -2,20 +2,19 @@ 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 {
secrets map[scope][]byte
secret []byte
userSessionTimeout time.Duration
dataStore portainer.DataStore
}
@@ -24,7 +23,6 @@ type claims struct {
UserID int `json:"id"`
Username string `json:"username"`
Role int `json:"role"`
Scope scope `json:"scope"`
jwt.StandardClaims
}
@@ -33,11 +31,6 @@ 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)
@@ -50,97 +43,54 @@ func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Ser
return nil, errSecretGeneration
}
kubeSecret, err := getOrCreateKubeSecret(dataStore)
if err != nil {
return nil, err
}
service := &Service{
map[scope][]byte{
defaultScope: secret,
kubeConfigScope: kubeSecret,
},
secret,
userSessionTimeout,
dataStore,
}
return service, nil
}
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 {
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(), defaultScope)
return service.generateSignedToken(data, service.defaultExpireAt())
}
// GenerateTokenForOAuth generates a new JWT token for OAuth login
// token expiry time response from OAuth provider is considered
// GenerateTokenForOAuth generates a new JWT for OAuth login
// token expiry time from the 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, defaultScope)
return service.generateSignedToken(data, expireAt)
}
// 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 secret, nil
return service.secret, nil
})
if err == nil && parsedToken != nil {
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
return &portainer.TokenData{
tokenData := &portainer.TokenData{
ID: portainer.UserID(cl.UserID),
Username: cl.Username,
Role: portainer.UserRole(cl.Role),
}, 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 tokenData, nil
}
}
return defaultScope
return nil, errInvalidJWTToken
}
// SetUserSessionDuration sets the user session duration
@@ -148,24 +98,18 @@ func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration
service.userSessionTimeout = userSessionDuration
}
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)
}
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64) (string, error) {
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(secret)
signedToken, err := token.SignedString(service.secret)
if err != nil {
return "", err
}

View File

@@ -22,5 +22,5 @@ func (service *Service) GenerateTokenForKubeconfig(data *portainer.TokenData) (s
expiryAt = 0
}
return service.generateSignedToken(data, expiryAt, kubeConfigScope)
return service.generateSignedToken(data, expiryAt)
}

View File

@@ -66,7 +66,7 @@ func TestService_GenerateTokenForKubeconfig(t *testing.T) {
}
parsedToken, err := jwt.ParseWithClaims(got, &claims{}, func(token *jwt.Token) (interface{}, error) {
return service.secrets[kubeConfigScope], nil
return service.secret, nil
})
assert.NoError(t, err, "failed to parse generated token")

View File

@@ -1,7 +1,6 @@
package jwt
import (
i "github.com/portainer/portainer/api/internal/testhelpers"
"testing"
"time"
@@ -11,8 +10,7 @@ import (
)
func TestGenerateSignedToken(t *testing.T) {
dataStore := i.NewDatastore(i.WithSettingsService(&portainer.Settings{}))
svc, err := NewService("24h", dataStore)
svc, err := NewService("24h", nil)
assert.NoError(t, err, "failed to create a copy of service")
token := &portainer.TokenData{
@@ -22,11 +20,11 @@ func TestGenerateSignedToken(t *testing.T) {
}
expiresAt := time.Now().Add(1 * time.Hour).Unix()
generatedToken, err := svc.generateSignedToken(token, expiresAt, defaultScope)
generatedToken, err := svc.generateSignedToken(token, expiresAt)
assert.NoError(t, err, "failed to generate a signed token")
parsedToken, err := jwt.ParseWithClaims(generatedToken, &claims{}, func(token *jwt.Token) (interface{}, error) {
return svc.secrets[defaultScope], nil
return svc.secret, nil
})
assert.NoError(t, err, "failed to parse generated token")
@@ -38,20 +36,3 @@ 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())
}

View File

@@ -40,34 +40,6 @@ type (
AuthenticationKey string `json:"AuthenticationKey" example:"cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="`
}
// OpenAMTConfiguration represents the credentials and configurations used to connect to an OpenAMT MPS server
OpenAMTConfiguration struct {
Enabled bool `json:"Enabled"`
MPSURL string `json:"MPSURL"`
Credentials MPSCredentials `json:"Credentials"`
DomainConfiguration DomainConfiguration `json:"DomainConfiguration"`
WirelessConfiguration *WirelessConfiguration `json:"WirelessConfiguration"`
}
MPSCredentials struct {
MPSUser string `json:"MPSUser"`
MPSPassword string `json:"MPSPassword"`
MPSToken string `json:"MPSToken"` // retrieved from API
}
DomainConfiguration struct {
CertFileText string `json:"CertFileText"`
CertPassword string `json:"CertPassword"`
DomainName string `json:"DomainName"`
}
WirelessConfiguration struct {
AuthenticationMethod string `json:"AuthenticationMethod"`
EncryptionMethod string `json:"EncryptionMethod"`
SSID string `json:"SSID"`
PskPass string `json:"PskPass"`
}
// CLIFlags represents the available flags on the CLI
CLIFlags struct {
Addr *string
@@ -78,7 +50,6 @@ type (
AdminPasswordFile *string
Assets *string
Data *string
FeatureFlags *[]Pair
EnableEdgeComputeFeatures *bool
EndpointURL *string
Labels *[]Pair
@@ -417,9 +388,6 @@ 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"`
@@ -576,7 +544,6 @@ type (
DefaultTeamID TeamID `json:"DefaultTeamID"`
SSO bool `json:"SSO"`
LogoutURI string `json:"LogoutURI"`
KubeSecretKey []byte `json:"KubeSecretKey"`
}
// Pair defines a key/value string pair
@@ -736,8 +703,6 @@ type (
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
LDAPSettings LDAPSettings `json:"LDAPSettings" example:""`
OAuthSettings OAuthSettings `json:"OAuthSettings" example:""`
OpenAMTConfiguration OpenAMTConfiguration `json:"OpenAMTConfiguration" 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
@@ -1247,7 +1212,7 @@ type (
// FileService represents a service for managing files
FileService interface {
GetDockerConfigPath() string
GetFileContent(trustedRootPath, filePath string) ([]byte, error)
GetFileContent(filePath string) ([]byte, error)
Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error
Rename(oldPath, newPath string) error
RemoveDirectory(directoryPath string) error
@@ -1286,11 +1251,6 @@ type (
LatestCommitID(repositoryURL, referenceName, username, password string) (string, error)
}
// OpenAMTService represents a service for managing OpenAMT
OpenAMTService interface {
ConfigureDefault(configuration OpenAMTConfiguration) error
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
HelmUserRepositoryByUserID(userID UserID) ([]HelmUserRepository, error)
@@ -1510,9 +1470,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.9.3"
APIVersion = "2.9.2"
// DBVersion is the version number of the Portainer database
DBVersion = 35
DBVersion = 33
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
@@ -1554,18 +1514,6 @@ 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)

View File

@@ -1 +0,0 @@
module.exports = 'test-file-stub';

View File

@@ -1 +0,0 @@
module.exports = {};

View File

@@ -2,7 +2,6 @@ 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';
@@ -16,7 +15,6 @@ import './portainer/__module';
angular.module('portainer', [
'ui.bootstrap',
'ui.router',
UI_ROUTER_REACT_HYBRID,
'ui.select',
'isteven-multi-select',
'ngSanitize',
@@ -46,10 +44,7 @@ angular.module('portainer', [
if (require) {
var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im);
req
.keys()
.filter((path) => !path.includes('.test'))
.forEach(function (key) {
req(key);
});
req.keys().forEach(function (key) {
req(key);
});
}

View File

@@ -129,6 +129,25 @@ 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;
}
@@ -307,6 +326,7 @@ a[ng-click] {
.custom-header-ico {
max-width: 32px;
max-height: 32px;
margin-right: 2px;
}
.btn-responsive {
@@ -580,7 +600,7 @@ a[ng-click] {
padding-top: 7px;
}
.tag:not(.token) {
.tag {
padding: 2px 6px;
color: white;
background-color: var(--blue-2);

View File

@@ -1,21 +1,4 @@
import './rdash.css';
import './app.css';
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 './theme.css';
import './vendor-override.css';

View File

@@ -269,7 +269,6 @@ json-tree .branch-preview {
.panel {
border: 1px solid var(--border-panel-color);
background-color: var(--bg-panel-body-color);
}
.theme-information .col-sm-12 {

View File

@@ -28,16 +28,7 @@ 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) => {
const port = (containerPorts[index] && containerPorts[index].port) || undefined;
return {
container: port,
host: binding.port,
protocol: binding.protocol,
};
})
: [];
this.Ports = addressPorts.length ? addressPorts.map((binding, index) => ({ container: containerPorts[index].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';

View File

@@ -69,8 +69,6 @@ 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';
}

View File

@@ -1,7 +1,8 @@
import _ from 'lodash-es';
import angular from 'angular';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import angular from 'angular';
class CreateConfigController {
/* @ngInject */
constructor($async, $state, $transition$, $window, ModalService, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {

View File

@@ -1,8 +1,8 @@
import _ from 'lodash-es';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '../../../models/container';

View File

@@ -1,5 +1,3 @@
import { HIDE_AUTO_UPDATE_WINDOW } from 'Portainer/feature-flags/feature-ids';
export default class DockerFeaturesConfigurationController {
/* @ngInject */
constructor($async, EndpointService, Notifications, StateManager) {
@@ -8,8 +6,6 @@ export default class DockerFeaturesConfigurationController {
this.Notifications = Notifications;
this.StateManager = StateManager;
this.limitedFeature = HIDE_AUTO_UPDATE_WINDOW;
this.formValues = {
enableHostManagementFeatures: false,
allowVolumeBrowserForRegularUsers: false,

View File

@@ -41,25 +41,6 @@
></por-switch-field>
</div>
</div>
<!-- auto update window -->
<div class="col-sm-12 form-section-title">
Change Window Setting
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.state.autoUpdateSettings.Enabled"
name="disableSysctlSettingForRegularUsers"
label="Enable Change Window"
label-class="col-sm-7 col-lg-4"
feature="$ctrl.limitedFeature"
tooltip="Specify a timeframe during which automatic updates can occur in this environment."
>
</por-switch-field>
</div>
</div>
<!-- security -->
<div class="col-sm-12 form-section-title">
Docker Security Settings

View File

@@ -1,72 +1,35 @@
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ImportImageController', [
'$scope',
'$state',
'ImageService',
'Notifications',
'HttpRequestHelper',
'Authentication',
'ImageHelper',
'endpoint',
function ($scope, $state, ImageService, Notifications, HttpRequestHelper, Authentication, ImageHelper, endpoint) {
function ($scope, $state, ImageService, Notifications, HttpRequestHelper) {
$scope.state = {
actionInProgress: false,
};
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
$scope.formValues = {
UploadFile: null,
NodeName: null,
RegistryModel: new PorImageRegistryModel(),
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullImageValidity = validity;
}
async function tagImage(id) {
const registryModel = $scope.formValues.RegistryModel;
if (registryModel.Image) {
const image = ImageHelper.createImageConfigForContainer(registryModel);
try {
await ImageService.tagImage(id, image.fromImage);
} catch (err) {
Notifications.error('Failure', err, 'Unable to tag image');
}
}
}
$scope.uploadImage = async function () {
$scope.uploadImage = function () {
$scope.state.actionInProgress = true;
var nodeName = $scope.formValues.NodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
var file = $scope.formValues.UploadFile;
try {
const { data } = await ImageService.uploadImage(file);
if (data.error) {
Notifications.error('Failure', data.error, 'Unable to upload image');
} else if (data.stream) {
var regex = /Loaded.*?: (.*?)\n$/g;
var imageIds = regex.exec(data.stream);
if (imageIds && imageIds.length == 2) {
await tagImage(imageIds[1]);
$state.go('docker.images.image', { id: imageIds[1] }, { reload: true });
}
ImageService.uploadImage(file)
.then(function success() {
Notifications.success('Images successfully uploaded');
} else {
Notifications.success('The uploaded tar file contained multiple images. The provided tag therefore has been ignored.');
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to upload image');
} finally {
$scope.state.actionInProgress = false;
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to upload image');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
},
]);

View File

@@ -37,25 +37,6 @@
<node-selector model="formValues.NodeName"> </node-selector>
<!-- !node-selection -->
</div>
<div class="row" authorization="DockerImageCreate">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tag" title-text="Tag the image"></rd-widget-header>
<rd-widget-body>
<!-- image-and-registry -->
<por-image-registry
model="formValues.RegistryModel"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
set-validity="setPullImageValidity"
check-rate-limits="true"
></por-image-registry>
</rd-widget-body>
</rd-widget>
</div>
</div>
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions

View File

@@ -1,7 +1,7 @@
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
require('./includes/update-restart.html');

View File

@@ -19,9 +19,10 @@ require('./includes/updateconfig.html');
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import * as envVarsUtils from '@/portainer/helpers/env-vars';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ServiceController', [
'$q',
'$scope',

8
app/global.d.ts vendored
View File

@@ -1,8 +0,0 @@
declare module '*.jpg' {
export default '' as string;
}
declare module '*.png' {
export default '' as string;
}
declare module '*.css';

View File

@@ -1,8 +1,8 @@
import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import _ from 'lodash-es';
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
'$scope',

View File

@@ -1,5 +1,5 @@
import _ from 'lodash-es';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import _ from 'lodash-es';
export default class HelmTemplatesController {
/* @ngInject */
@@ -98,9 +98,8 @@ export default class HelmTemplatesController {
try {
// fetch globally set helm repo and user helm repos (parallel)
const { GlobalRepository, UserRepositories } = await this.HelmService.getHelmRepositories(this.endpoint.Id);
this.state.globalRepository = GlobalRepository;
const userHelmReposUrls = UserRepositories.map((repo) => repo.URL);
const uniqueHelmRepos = [...new Set([GlobalRepository, ...userHelmReposUrls])].map((url) => url.toLowerCase()).filter((url) => url); // remove duplicates and blank, to lowercase
const uniqueHelmRepos = [...new Set([GlobalRepository, ...userHelmReposUrls])].map((url) => url.toLowerCase()); // remove duplicates, to lowercase
this.state.repos = uniqueHelmRepos;
return uniqueHelmRepos;
} catch (err) {
@@ -170,8 +169,6 @@ export default class HelmTemplatesController {
chartsLoading: false,
resourcePoolsLoading: false,
viewReady: false,
isAdmin: this.Authentication.isAdmin(),
globalRepository: undefined,
};
const helmRepos = await this.getHelmRepoURLs();

View File

@@ -8,11 +8,6 @@
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This is a first version for Helm charts, for more information see this <a href="https://www.portainer.io/blog/portainer-now-with-helm-support" target="_blank">blog post</a>.
</p>
<p ng-if="$ctrl.state.globalRepository === ''">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
<span>The Global Helm Repository is not configured.</span>
<a ng-if="$ctrl.state.isAdmin" ui-sref="portainer.settings">Configure Global Helm Repository in Settings</a>
</p>
</span>
</information-panel>

View File

@@ -28,13 +28,7 @@
Namespaces
</sidebar-menu-item>
<sidebar-menu-item
path="kubernetes.templates.helm"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-dharmachakra fa-fw"
class-name="sidebar-list"
data-cy="k8sSidebar-helm"
>
<sidebar-menu-item path="kubernetes.templates.helm" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-dharmachakra fa-fw" class-name="sidebar-list" data-cy="k8sSidebar-helm">
Helm
</sidebar-menu-item>
@@ -68,12 +62,12 @@
path="kubernetes.cluster"
path-params="{ endpointId: $ctrl.endpointId }"
is-sidebar-open="$ctrl.isSidebarOpen"
children-paths="['kubernetes.cluster', 'portainer.k8sendpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access']"
children-paths="['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access']"
>
<div ng-if="$ctrl.adminAccess">
<sidebar-menu-item
authorization="K8sClusterSetupRW"
path="portainer.k8sendpoint.kubernetesConfig"
path="portainer.endpoints.endpoint.kubernetesConfig"
path-params="{ id: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-setup"

View File

@@ -1,7 +1,7 @@
import _ from 'lodash-es';
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
import _ from 'lodash-es';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';

View File

@@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { KubernetesEndpoint, KubernetesEndpointAnnotationLeader, KubernetesEndpointSubset } from 'Kubernetes/endpoint/models';
import _ from 'lodash-es';
class KubernetesEndpointConverter {
static apiToEndpoint(data) {

View File

@@ -45,7 +45,7 @@ export function HelmService(HelmFactory) {
}
/**
* @description: Get a list of all the helm repositories available for the current user
* @description: Show values helm of a helm chart, this basically runs `helm show values`
* @returns {Promise} - Resolves with an object containing list of user helm repos and default/global settings helm repo
* @throws {PortainerError} - Rejects with error if helm show fails
*/

View File

@@ -1,7 +1,7 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import _ from 'lodash-es';
import YAML from 'yaml';
class KubernetesConfigurationHelper {
static getUsingApplications(config, applications) {

View File

@@ -1,12 +0,0 @@
import KubernetesStackHelper from './stackHelper';
describe('stacksFromApplications', () => {
const { stacksFromApplications } = KubernetesStackHelper;
test('should return an empty array when passed an empty array', () => {
expect(stacksFromApplications([])).toHaveLength(0);
});
test('should return an empty array when passed a list of applications without stacks', () => {
expect(stacksFromApplications([{ StackName: '' }, { StackName: '' }, { StackName: '' }, { StackName: '' }])).toHaveLength(0);
});
});

View File

@@ -1,10 +1,10 @@
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesNode, KubernetesNodeDetails, KubernetesNodeTaint, KubernetesNodeAvailabilities, KubernetesPortainerNodeDrainLabel } from 'Kubernetes/node/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import { KubernetesNodeFormValues, KubernetesNodeTaintFormValues, KubernetesNodeLabelFormValues } from 'Kubernetes/node/formValues';
import { KubernetesNodeCreatePayload, KubernetesNodeTaintPayload } from 'Kubernetes/node/payload';
import * as JsonPatch from 'fast-json-patch';
class KubernetesNodeConverter {
static apiToNode(data, res) {

View File

@@ -4,7 +4,7 @@ import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
class KubernetesApplicationsController {
/* @ngInject */
constructor($async, $state, Notifications, KubernetesApplicationService, HelmService, KubernetesConfigurationService, Authentication, ModalService, LocalStorage, StackService) {
@@ -81,17 +81,13 @@ class KubernetesApplicationsController {
await this.HelmService.uninstall(this.endpoint.Id, application);
} else {
await this.KubernetesApplicationService.delete(application);
if (application.Metadata.labels[KubernetesPortainerApplicationStackNameLabel]) {
// Update applications in stack
const stack = this.state.stacks.find((x) => x.Name === application.StackName);
const index = stack.Applications.indexOf(application);
stack.Applications.splice(index, 1);
// remove stack if no app left in the stack
if (stack.Applications.length === 0 && application.StackId) {
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
}
// Update applications in stack
const stack = this.state.stacks.find((x) => x.Name === application.StackName);
const index = stack.Applications.indexOf(application);
stack.Applications.splice(index, 1);
// remove stack if no app left in the stack
if (stack.Applications.length === 0 && application.StackId) {
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
}
}
this.Notifications.success('Application successfully removed', application.Name);

View File

@@ -1007,7 +1007,7 @@
</p>
<p ng-if="ctrl.isAdmin">
Server metrics features must be enabled in the
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: ctrl.endpoint.Id})" class="ctrl.isAdmin">environment configuration view</a>.
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})" class="ctrl.isAdmin">environment configuration view</a>.
</p>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<kubernetes-view-header title="Kubernetes features configuration" state="portainer.k8sendpoint.kubernetesConfig" view-ready="ctrl.state.viewReady">
<kubernetes-view-header title="Kubernetes features configuration" state="portainer.endpoints.endpoint.kubernetesConfig" view-ready="ctrl.state.viewReady">
<a ui-sref="portainer.endpoints">Environment</a> &gt; <a ui-sref="portainer.endpoints.endpoint({id: ctrl.endpoint.Id})">{{ ctrl.endpoint.Name }}</a> &gt; Kubernetes configuration
</kubernetes-view-header>
@@ -141,24 +141,6 @@
</span>
</div>
<!-- auto update window -->
<div class="col-sm-12 form-section-title">
Change Window Setting
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.state.autoUpdateSettings.Enabled"
name="disableSysctlSettingForRegularUsers"
label="Enable Change Window"
feature="ctrl.limitedFeatureAutoWindow"
tooltip="Specify a timeframe during which automatic updates can occur in this environment."
>
</por-switch-field>
</div>
</div>
<div class="col-sm-12 form-section-title">
Security
</div>

View File

@@ -7,7 +7,6 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { K8S_SETUP_DEFAULT } from '@/portainer/feature-flags/feature-ids';
import { HIDE_AUTO_UPDATE_WINDOW } from 'Portainer/feature-flags/feature-ids';
class KubernetesConfigureController {
/* #region CONSTRUCTOR */
@@ -40,7 +39,6 @@ class KubernetesConfigureController {
this.onInit = this.onInit.bind(this);
this.configureAsync = this.configureAsync.bind(this);
this.limitedFeature = K8S_SETUP_DEFAULT;
this.limitedFeatureAutoWindow = HIDE_AUTO_UPDATE_WINDOW;
}
/* #endregion */

View File

@@ -45,13 +45,7 @@
<div class="col-sm-12 form-section-title">
Deployment type
</div>
<box-selector
radio-name="deploy"
ng-model="ctrl.state.DeployType"
on-change="(ctrl.onDeployTypeChange)"
options="ctrl.deployOptions"
data-cy="k8sAppDeploy-deploymentSelector"
></box-selector>
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions" data-cy="k8sAppDeploy-deploymentSelector"></box-selector>
</div>
<!-- repository -->
@@ -64,7 +58,6 @@
show-auth-explanation="true"
path-text-title="Manifest path"
path-placeholder="deployment.yml"
deploy-method="{{ ctrl.DeployMethod }}"
></git-form>
<!-- !repository -->

View File

@@ -32,7 +32,6 @@ class KubernetesDeployController {
this.StackService = StackService;
this.WebhookHelper = WebhookHelper;
this.CustomTemplateService = CustomTemplateService;
this.DeployMethod = 'manifest';
this.deployOptions = [
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
@@ -80,7 +79,6 @@ class KubernetesDeployController {
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
this.onDeployTypeChange = this.onDeployTypeChange.bind(this);
}
buildAnalyticsProperties() {
@@ -282,14 +280,6 @@ class KubernetesDeployController {
return this.$async(this.getNamespacesAsync);
}
onDeployTypeChange(value) {
if (value == this.ManifestDeployTypes.COMPOSE) {
this.DeployMethod = 'compose';
} else {
this.DeployMethod = 'manifest';
}
}
async uiCanExit() {
if (this.formValues.EditorContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();

View File

@@ -210,8 +210,8 @@
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length === 0">
<div class="col-sm-12 small text-muted">
The ingress feature must be enabled in the
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: $ctrl.endpoint.Id})">environment configuration view</a> to be able to register ingresses inside this
namespace.
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpoint.Id})">environment configuration view</a> to be able to register ingresses inside
this namespace.
</div>
</div>

View File

@@ -164,8 +164,8 @@
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length === 0">
<div class="col-sm-12 small text-muted">
The ingress feature must be enabled in the
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: ctrl.endpoint.Id})">environment configuration view</a> to be able to register ingresses inside this
namespace.
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})">environment configuration view</a> to be able to register ingresses inside
this namespace.
</div>
</div>

View File

@@ -1,9 +1,9 @@
import angular from 'angular';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
import filesizeParser from 'filesize-parser';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesVolumeController {

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