Compare commits

..

24 Commits

Author SHA1 Message Date
Steven Kang 14b998d270 Set static DOCKER_VERSION for ppc64le and s390x (#7135)
Test / test-client (push) Has been cancelled
2022-06-28 11:38:12 +12:00
Chaim Lev-Ari 605ff8c1da fix(environments): hide async mode on deployment [EE-3380] (#7129)
fixes [EE-3380]
2022-06-28 10:23:07 +12:00
Chaim Lev-Ari 13f93f4262 fix(analytics): load public settings [EE-3590] (#7127) 2022-06-27 19:29:06 +03:00
Steven Kang 16be5ed329 feat(build): set static DOCKER_VERSION for ppc64le and s390x (#7124) 2022-06-27 09:54:04 +12:00
Chaim Lev-Ari c6612898f3 fix(api): add missing edge types [EE-3590] (#7117) 2022-06-26 08:38:20 +03:00
andres-portainer 564f34b0ba fix(wizard): replace the YAML file by the docker commands EE-3589 (#7112) 2022-06-24 14:59:00 -03:00
LP B 392fbdb4a7 fix(app/account): ensure newTransition exists in uiCanExit [EE-3336] (#7109) 2022-06-24 17:35:39 +02:00
Chaim Lev-Ari a826c78786 fix(edge): show heartbeat for async env [EE-3380] (#7096) 2022-06-22 20:11:42 +03:00
Matt Hook a35f0607f1 fix docker download path for mac platforms (#7101) 2022-06-22 10:06:28 +12:00
LP B 081d32af0d fix(app/account): create access token button (#7091)
* fix(app/account): create access token button

* fix(app/formcontrol): error message overlapping input on smaller screens
2022-06-20 14:14:41 +02:00
itsconquest 4cc0b1f567 fix(auth): track skips per user [EE-3318] (#7088) 2022-06-20 17:00:00 +12:00
Chaim Lev-Ari d4da7e1760 fix(docker/networks): show correct resource control data [EE-3401] (#7061) 2022-06-17 19:21:38 +03:00
itsconquest aced418880 fix(auth): clear skips when using new instance [EE-3331] (#7026) 2022-06-17 14:45:42 +12:00
Chaim Lev-Ari 614f42fe5a feat(custom-templates): hide variables [EE-2602] (#7069) 2022-06-16 08:32:43 +03:00
itsconquest 58736fe93b feat(auth): allow single char passwords [EE-3385] (#7049)
* feat(auth): allow single character passwords

* match weak password modal logic to slider
2022-06-16 12:31:39 +12:00
Matt Hook b78330b10d fix(swarm): don't stomp on the x-registry-auth header EE-3308 (#7037)
* don't stomp on the x-registry-auth header

* del header if empty json provided for registry auth
2022-06-16 09:54:06 +12:00
itsconquest eed4a92ca8 fix(auth): notify user password requirements [EE-3344] (#7041)
* fix(auth): notify user password requirements [EE-3344]

* fix angular code
2022-06-15 17:15:38 +12:00
Dmitry Salakhov 0e7468a1e8 fix: clarify password change error (#7020) 2022-06-15 15:44:54 +12:00
congs b807481f1c fix(teamleader): EE-3411 normal users get an unauthorized error (#7053) 2022-06-14 14:12:33 +12:00
Ali da27de2154 fix(wizard): return back to envs page EE-3419 (#7064) 2022-06-13 14:59:23 +12:00
congs 6743e4fbb2 fix(teamleader): EE-3383 allow teamleader promote member to teamleader (#7039) 2022-06-10 17:13:23 +12:00
Ali b489ffaa63 fix(wizard): show teasers for kaas and kubeconfig features [EE-3316] (#7033)
* fix(wizard): add nomad, kaas, kubeconfig teasers
2022-06-10 09:16:43 +12:00
congs 6e12499d61 fix(teamleader): EE-3332 hide name and leaders (#7032) 2022-06-09 14:22:42 +12:00
Ali f7acbe16ba fix(wizard): use 'New Environments' title EE-3329 (#7035) 2022-06-08 16:37:53 +12:00
1344 changed files with 22628 additions and 30281 deletions
+2 -17
View File
@@ -31,12 +31,7 @@ rules:
[
'error',
{
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
@@ -46,7 +41,6 @@ settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
@@ -58,7 +52,6 @@ overrides:
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- 'regex'
extends:
- airbnb
- airbnb-typescript
@@ -75,14 +68,7 @@ overrides:
version: 'detect'
rules:
import/order:
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
['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 }]
@@ -104,7 +90,6 @@ overrides:
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
- files:
- app/**/*.test.*
extends:
-1
View File
@@ -7,7 +7,6 @@ storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache
+1 -1
View File
@@ -22,7 +22,7 @@ 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.13.x**.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
## Getting started
-71
View File
@@ -1,71 +0,0 @@
package agent
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if tlsConfig != nil {
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
if err != nil {
return 0, "", err
}
parsedURL.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
if err != nil {
return 0, "", err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
version := resp.Header.Get(portainer.PortainerAgentHeader)
if version == "" {
return 0, "", errors.New("Version Header is missing")
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, "", errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, "", err
}
if agentPlatformNumber == 0 {
return 0, "", errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), version, nil
}
-9
View File
@@ -1,9 +0,0 @@
package build
// Variables to be set during the build time
var BuildNumber string
var ImageTag string
var NodejsVersion string
var YarnVersion string
var WebpackVersion string
var GoVersion string
+1 -10
View File
@@ -16,7 +16,6 @@ import (
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
@@ -744,15 +743,7 @@ func main() {
for {
server := buildServer(flags)
logrus.WithFields(logrus.Fields{
"Version": portainer.APIVersion,
"BuildNumber": build.BuildNumber,
"ImageTag": build.ImageTag,
"NodejsVersion": build.NodejsVersion,
"YarnVersion": build.YarnVersion,
"WebpackVersion": build.WebpackVersion,
"GoVersion": build.GoVersion},
).Print("[INFO] [cmd,main] Starting Portainer")
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
err := server.Start()
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
}
+1 -19
View File
@@ -103,26 +103,8 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
store.createBackupFolders()
options = store.setupOptions(options)
dbPath := store.databasePath()
if err := store.Close(); err != nil {
return options.BackupPath, fmt.Errorf(
"error closing datastore before creating backup: %v",
err,
)
}
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
return options.BackupPath, err
}
if _, err := store.Open(); err != nil {
return options.BackupPath, fmt.Errorf(
"error opening datastore after creating backup: %v",
err,
)
}
return options.BackupPath, nil
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
}
// RestoreWithOptions previously saved backup for the current Edition with options
-3
View File
@@ -103,9 +103,6 @@ func (m *Migrator) Migrate() error {
// Portainer 2.14
newMigration(50, m.migrateDBVersionToDB50),
// Portainer 2.15
newMigration(60, m.migrateDBVersionToDB60),
}
var lastDbVersion int
@@ -1,30 +0,0 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB60() error {
if err := m.addGpuInputFieldDB60(); err != nil {
return err
}
return nil
}
func (m *Migrator) addGpuInputFieldDB60() error {
migrateLog.Info("- add gpu input field")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpoint.Gpus = []portainer.Pair{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
@@ -27,9 +27,6 @@
],
"endpoints": [
{
"Agent": {
"Version": ""
},
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"AzureCredentials": {
@@ -46,7 +43,6 @@
},
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"Gpus": [],
"GroupId": 1,
"Id": 1,
"IsEdgeDevice": false,
@@ -179,8 +175,6 @@
}
},
"DockerVersion": "20.10.13",
"GpuUseAll": false,
"GpuUseList": null,
"HealthyContainerCount": 0,
"ImageCount": 9,
"NodeCount": 0,
@@ -796,7 +790,6 @@
"IsComposeFormat": false,
"Name": "alpine",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
"ResourceControl": null,
"Status": 1,
@@ -819,7 +812,6 @@
"IsComposeFormat": false,
"Name": "redis",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
"ResourceControl": null,
"Status": 1,
@@ -842,7 +834,6 @@
"IsComposeFormat": false,
"Name": "nginx",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
"ResourceControl": null,
"Status": 1,
@@ -919,7 +910,7 @@
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "60",
"DB_VERSION": "50",
"INSTANCE_ID": "null"
}
}
+1 -34
View File
@@ -7,10 +7,9 @@ import (
"time"
"github.com/docker/docker/api/types"
_container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
)
// Snapshotter represents a service used to create environment(endpoint) snapshots
@@ -155,35 +154,11 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
healthyContainers := 0
unhealthyContainers := 0
stacks := make(map[string]struct{})
gpuUseSet := make(map[string]struct{})
gpuUseAll := false
for _, container := range containers {
if container.State == "exited" {
stoppedContainers++
} else if container.State == "running" {
runningContainers++
// snapshot GPUs
response, err := cli.ContainerInspect(context.Background(), container.ID)
if err != nil {
return err
}
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
}
}
if gpuOptions != nil {
if gpuOptions.Count == -1 {
gpuUseAll = true
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
}
}
}
if strings.Contains(container.Status, "(healthy)") {
@@ -199,14 +174,6 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
}
}
gpuUseList := make([]string, 0, len(gpuUseSet))
for gpuUse := range gpuUseSet {
gpuUseList = append(gpuUseList, gpuUse)
}
snapshot.GpuUseAll = gpuUseAll
snapshot.GpuUseList = gpuUseList
snapshot.RunningContainerCount = runningContainers
snapshot.StoppedContainerCount = stoppedContainers
snapshot.HealthyContainerCount = healthyContainers
+183 -25
View File
@@ -6,6 +6,7 @@ import (
"io"
"os"
"path"
"regexp"
"strings"
"github.com/pkg/errors"
@@ -13,6 +14,7 @@ import (
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
"github.com/docker/cli/cli/compose/loader"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
@@ -54,13 +56,13 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
return errors.Wrap(err, "failed to deploy a stack")
}
@@ -74,14 +76,12 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
if err := updateNetworkEnvFile(stack); err != nil {
return err
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
return errors.Wrap(err, "failed to remove a stack")
}
@@ -103,42 +103,200 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
}
// createEnvFile creates a file that would hold both "in-place" and default environment variables.
// It will return the name of the file if the stack has "in-place" env vars, otherwise empty string.
func createEnvFile(stack *portainer.Stack) (string, error) {
// workaround for EE-1862. It will have to be removed when
// docker/compose upgraded to v2.x.
if err := createNetworkEnvFile(stack); err != nil {
return "", errors.Wrap(err, "failed to create network env file")
}
if stack.Env == nil || len(stack.Env) == 0 {
return "", nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
}
defer envfile.Close()
copyDefaultEnvFile(stack, envfile)
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
return "stack.env", nil
}
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
func copyDefaultEnvFile(stack *portainer.Stack, w io.Writer) {
defaultEnvFile, err := os.Open(path.Join(path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint)), ".env"))
if err != nil {
// If cannot open a default file, then don't need to copy it.
// We could as well stat it and check if it exists, but this is more efficient.
return
func fileNotExist(filePath string) bool {
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
return true
}
defer defaultEnvFile.Close()
if _, err = io.Copy(w, defaultEnvFile); err == nil {
io.WriteString(w, "\n")
}
// If couldn't copy the .env file, then ignore the error and try to continue
return false
}
func updateNetworkEnvFile(stack *portainer.Stack) error {
envFilePath := path.Join(stack.ProjectPath, ".env")
stackFilePath := path.Join(stack.ProjectPath, "stack.env")
if fileNotExist(envFilePath) {
if fileNotExist(stackFilePath) {
return nil
}
flags := os.O_WRONLY | os.O_SYNC | os.O_CREATE
envFile, err := os.OpenFile(envFilePath, flags, 0666)
if err != nil {
return err
}
defer envFile.Close()
stackFile, err := os.Open(stackFilePath)
if err != nil {
return err
}
defer stackFile.Close()
_, err = io.Copy(envFile, stackFile)
return err
}
return nil
}
func createNetworkEnvFile(stack *portainer.Stack) error {
networkNameSet := NewStringSet()
for _, filePath := range stackutils.GetStackFilePaths(stack) {
networkNames, err := extractNetworkNames(filePath)
if err != nil {
return errors.Wrap(err, "failed to extract network name")
}
if networkNames == nil || networkNames.Len() == 0 {
continue
}
networkNameSet.Union(networkNames)
}
for _, s := range networkNameSet.List() {
if _, ok := os.LookupEnv(s); ok {
networkNameSet.Remove(s)
}
}
if networkNameSet.Len() == 0 && stack.Env == nil {
return nil
}
envfile, err := os.OpenFile(path.Join(stack.ProjectPath, ".env"),
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errors.Wrap(err, "failed to open env file")
}
defer envfile.Close()
var scanEnvSettingFunc = func(name string) (string, bool) {
if stack.Env != nil {
for _, v := range stack.Env {
if name == v.Name {
return v.Value, true
}
}
}
return "", false
}
for _, s := range networkNameSet.List() {
if _, ok := scanEnvSettingFunc(s); !ok {
stack.Env = append(stack.Env, portainer.Pair{
Name: s,
Value: "None",
})
}
}
if stack.Env != nil {
for _, v := range stack.Env {
envfile.WriteString(
fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
}
return nil
}
func extractNetworkNames(filePath string) (StringSet, error) {
if info, err := os.Stat(filePath); errors.Is(err,
os.ErrNotExist) || info.IsDir() {
return nil, nil
}
stackFileContent, err := os.ReadFile(filePath)
if err != nil {
return nil, errors.Wrap(err, "failed to open yaml file")
}
config, err := loader.ParseYAML(stackFileContent)
if err != nil {
// invalid stack file
return nil, errors.Wrap(err, "invalid stack file")
}
var version string
if _, ok := config["version"]; ok {
version, _ = config["version"].(string)
}
var networks map[string]interface{}
if value, ok := config["networks"]; ok {
if value == nil {
return nil, nil
}
if networks, ok = value.(map[string]interface{}); !ok {
return nil, nil
}
} else {
return nil, nil
}
networkContent, err := loader.LoadNetworks(networks, version)
if err != nil {
return nil, nil // skip the error
}
re := regexp.MustCompile(`^\$\{?([^\}]+)\}?$`)
networkNames := NewStringSet()
for _, v := range networkContent {
matched := re.FindAllStringSubmatch(v.Name, -1)
if matched != nil && matched[0] != nil {
if strings.Contains(matched[0][1], ":-") {
continue
}
if strings.Contains(matched[0][1], "?") {
continue
}
if strings.Contains(matched[0][1], "-") {
continue
}
networkNames.Add(matched[0][1])
}
}
if networkNames.Len() == 0 {
return nil, nil
}
return networkNames, nil
}
+46 -12
View File
@@ -65,22 +65,56 @@ func Test_createEnvFile(t *testing.T) {
}
}
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
func Test_createNetworkEnvFile(t *testing.T) {
dir := t.TempDir()
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
stack := &portainer.Stack{
buf := []byte(`
version: '3.6'
services:
nginx-example:
image: nginx:latest
networks:
default:
name: ${test}
driver: bridge
`)
if err := ioutil.WriteFile(path.Join(dir,
"docker-compose.yml"), buf, 0644); err != nil {
t.Fatalf("Failed to create yaml file: %s", err)
}
stackWithoutEnv := &portainer.Stack{
ProjectPath: dir,
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{},
}
if err := createNetworkEnvFile(stackWithoutEnv); err != nil {
t.Fatalf("Failed to create network env file: %s", err)
}
content, err := ioutil.ReadFile(path.Join(dir, ".env"))
if err != nil {
t.Fatalf("Failed to read network env file: %s", err)
}
assert.Equal(t, "test=None\n", string(content))
stackWithEnv := &portainer.Stack{
ProjectPath: dir,
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{
{Name: "VAR1", Value: "NEW_VAL1"},
{Name: "VAR3", Value: "VAL3"},
{Name: "test", Value: "test-value"},
},
}
result, err := createEnvFile(stack)
assert.Equal(t, "stack.env", result)
assert.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env"))
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
if err := createNetworkEnvFile(stackWithEnv); err != nil {
t.Fatalf("Failed to create network env file: %s", err)
}
content, err = ioutil.ReadFile(path.Join(dir, ".env"))
if err != nil {
t.Fatalf("Failed to read network env file: %s", err)
}
assert.Equal(t, "test=test-value\n", string(content))
}
+8 -10
View File
@@ -1,6 +1,6 @@
module github.com/portainer/portainer/api
go 1.18
go 1.17
require (
github.com/Microsoft/go-winio v0.5.1
@@ -11,7 +11,7 @@ require (
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/docker/cli v20.10.9+incompatible
github.com/docker/docker v20.10.16+incompatible
github.com/docker/docker v20.10.9+incompatible
github.com/fvbommel/sortorder v1.0.2
github.com/fxamacker/cbor/v2 v2.3.0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
@@ -20,7 +20,7 @@ require (
github.com/go-playground/validator/v10 v10.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.5.8
github.com/google/go-cmp v0.5.6
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
@@ -32,8 +32,8 @@ require (
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
@@ -43,7 +43,6 @@ require (
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
@@ -62,6 +61,7 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/containerd/containerd v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
@@ -95,9 +95,6 @@ require (
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.15.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -115,11 +112,12 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
+832 -10
View File
File diff suppressed because it is too large Load Diff
@@ -1,86 +0,0 @@
package containers
import (
"net/http"
"strings"
containertypes "github.com/docker/docker/api/types/container"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"golang.org/x/exp/slices"
)
type containerGpusResponse struct {
Gpus string `json:"gpus"`
}
// @id dockerContainerGpusInspect
// @summary Fetch container gpus data
// @description
// @description **Access policy**:
// @tags docker
// @security jwt
// @accept json
// @produce json
// @param environmentId path int true "Environment identifier"
// @param containerId path int true "Container identifier"
// @success 200 {object} containerGpusResponse "Success"
// @failure 404 "Environment or container not found"
// @failure 400 "Bad request"
// @failure 500 "Internal server error"
// @router /docker/{environmentId}/containers/{containerId}/gpus [get]
func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
containerId, err := request.RetrieveRouteVariableValue(r, "containerId")
if err != nil {
return httperror.BadRequest("Invalid container identifier route variable", err)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return httperror.NotFound("Unable to find an environment on request context", err)
}
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
if err != nil {
return httperror.InternalServerError("Unable to connect to the Docker daemon", err)
}
container, err := cli.ContainerInspect(r.Context(), containerId)
if err != nil {
return httperror.NotFound("Unable to find the container", err)
}
if container.HostConfig == nil {
return httperror.NotFound("Unable to find the container host config", err)
}
gpuOptionsIndex := slices.IndexFunc(container.HostConfig.DeviceRequests, func(opt containertypes.DeviceRequest) bool {
if opt.Driver == "nvidia" {
return true
}
if len(opt.Capabilities) == 0 || len(opt.Capabilities[0]) == 0 {
return false
}
return opt.Capabilities[0][0] == "gpu"
})
if gpuOptionsIndex == -1 {
return response.JSON(w, containerGpusResponse{Gpus: "none"})
}
gpuOptions := container.HostConfig.DeviceRequests[gpuOptionsIndex]
gpu := "all"
if gpuOptions.Count != -1 {
gpu = "id:" + strings.Join(gpuOptions.DeviceIDs, ",")
}
return response.JSON(w, containerGpusResponse{Gpus: gpu})
}
@@ -1,31 +0,0 @@
package containers
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
)
type Handler struct {
*mux.Router
dockerClientFactory *docker.ClientFactory
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(routePrefix string, bouncer *security.RequestBouncer, dockerClientFactory *docker.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dockerClientFactory: dockerClientFactory,
}
router := h.PathPrefix(routePrefix).Subrouter()
router.Use(bouncer.AuthenticatedAccess)
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
return h
}
-63
View File
@@ -1,63 +0,0 @@
package docker
import (
"errors"
"net/http"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/handler/docker/containers"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
dataStore dataservices.DataStore
dockerClientFactory *docker.ClientFactory
authorizationService *authorization.Service
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
authorizationService: authorizationService,
dataStore: dataStore,
dockerClientFactory: dockerClientFactory,
}
// endpoints
endpointRouter := h.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(dockerOnlyMiddleware)
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dockerClientFactory)
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
return h
}
func dockerOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
return
}
if !endpointutils.IsDockerEndpoint(endpoint) {
errMessage := "environment is not a docker environment"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
return
}
next.ServeHTTP(rw, request)
})
}
@@ -77,16 +77,13 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
}
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
}
endpoint.Type = agentPlatform
}
endpoint.Type = agentPlatform
version := r.Header.Get(portainer.PortainerAgentHeader)
endpoint.Agent.Version = version
endpoint.LastCheckInDate = time.Now().Unix()
@@ -57,7 +57,7 @@ var endpointTestCases = []endpointTestCase{
portainer.EndpointRelation{
EndpointID: 2,
},
http.StatusForbidden,
http.StatusBadRequest,
},
{
portainer.Endpoint{
@@ -194,9 +194,7 @@ func TestWithEndpoints(t *testing.T) {
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -241,7 +239,6 @@ func TestLastCheckInDateIncreases(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -358,7 +355,6 @@ func TestEdgeStackStatus(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -428,7 +424,6 @@ func TestEdgeJobsResponse(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -1,50 +0,0 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/set"
)
// @id AgentVersions
// @summary List agent versions
// @description List all agent versions based on the current user authorizations and query parameters.
// @description **Access policy**: restricted
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} string "List of available agent versions"
// @failure 500 "Server error"
// @router /endpoints/agent_versions [get]
func (handler *Handler) agentVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
agentVersions := set.Set[string]{}
for _, endpoint := range filteredEndpoints {
if endpoint.Agent.Version != "" {
agentVersions[endpoint.Agent.Version] = true
}
}
return response.JSON(w, agentVersions.Keys())
}
+61 -31
View File
@@ -1,19 +1,20 @@
package endpoints
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
"time"
"github.com/gofrs/uuid"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/agent"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/edge"
@@ -24,7 +25,6 @@ type endpointCreatePayload struct {
URL string
EndpointCreationType endpointCreationEnum
PublicURL string
Gpus []portainer.Pair
GroupID int
TLS bool
TLSSkipVerify bool
@@ -142,13 +142,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
payload.PublicURL = publicURL
}
gpus := make([]portainer.Pair, 0)
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
if err != nil {
return errors.New("Invalid Gpus parameter")
}
payload.Gpus = gpus
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
payload.EdgeCheckinInterval = checkinInterval
@@ -244,7 +237,6 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
}
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
var err error
switch payload.EndpointCreationType {
case azureEnvironment:
return handler.createAzureEndpoint(payload)
@@ -257,22 +249,12 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
}
endpointType := portainer.DockerEnvironment
var agentVersion string
if payload.EndpointCreationType == agentEnvironment {
var tlsConfig *tls.Config
if payload.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
if err != nil {
return nil, httperror.InternalServerError("Unable to create TLS configuration", err)
}
}
agentPlatform, version, err := agent.GetAgentVersionAndPlatform(payload.URL, tlsConfig)
agentPlatform, err := handler.pingAndCheckPlatform(payload)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err}
}
agentVersion = version
if agentPlatform == portainer.AgentPlatformDocker {
endpointType = portainer.AgentOnDockerEnvironment
} else if agentPlatform == portainer.AgentPlatformKubernetes {
@@ -282,7 +264,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
}
if payload.TLS {
return handler.createTLSSecuredEndpoint(payload, endpointType, agentVersion)
return handler.createTLSSecuredEndpoint(payload, endpointType)
}
return handler.createUnsecuredEndpoint(payload)
}
@@ -308,7 +290,6 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
AzureCredentials: credentials,
@@ -342,7 +323,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
URL: portainerHost,
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -398,7 +378,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -433,7 +412,6 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
Type: portainer.KubernetesLocalEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
@@ -454,7 +432,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
return endpoint, nil
}
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
@@ -463,7 +441,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
@@ -477,8 +454,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
IsEdgeDevice: payload.IsEdgeDevice,
}
endpoint.Agent.Version = agentVersion
err := handler.storeTLSFiles(endpoint, payload)
if err != nil {
return nil, err
@@ -572,3 +547,58 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end
return nil
}
func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if payload.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
if err != nil {
return 0, err
}
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL))
if err != nil {
return 0, err
}
url.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
if err != nil {
return 0, err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, err
}
if agentPlatformNumber == 0 {
return 0, errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), nil
}
+335 -24
View File
@@ -4,14 +4,24 @@ import (
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/utils"
)
const (
EdgeDeviceFilterAll = "all"
EdgeDeviceFilterTrusted = "trusted"
EdgeDeviceFilterUntrusted = "untrusted"
EdgeDeviceFilterNone = "none"
)
const (
@@ -19,6 +29,8 @@ const (
EdgeDeviceIntervalAdd = 20
)
var endpointGroupNames map[portainer.EndpointGroupID]string
// @id EndpointList
// @summary List environments(endpoints)
// @description List all environments(endpoints) based on the current user authorizations. Will
@@ -30,20 +42,14 @@ const (
// @security jwt
// @produce json
// @param start query int false "Start searching from"
// @param limit query int false "Limit results to this value"
// @param sort query int false "Sort results by this value"
// @param order query int false "Order sorted results by desc/asc" Enum("asc", "desc")
// @param search query string false "Search query"
// @param groupIds query []int false "List environments(endpoints) of these groups"
// @param status query []int false "List environments(endpoints) by this status"
// @param groupId query int false "List environments(endpoints) of this group"
// @param limit query int false "Limit results to this value"
// @param types query []int false "List environments(endpoints) of this type"
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
// @param endpointIds query []int false "will return only these environments(endpoints)"
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
// @param agentVersions query []string false "will return only environments with on of these agent versions"
// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)"
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)"
// @param edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
// @param name query string false "will return only environments(endpoints) with this name"
// @success 200 {array} portainer.Endpoint "Endpoints"
// @failure 500 "Server error"
@@ -54,42 +60,103 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
start--
}
search, _ := request.RetrieveQueryParameter(r, "search", true)
if search != "" {
search = strings.ToLower(search)
}
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
var endpointTypes []int
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
var tagIDs []portainer.TagID
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
var endpointIDs []portainer.EndpointID
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
var statuses []int
request.RetrieveJSONQueryParameter(r, "status", &statuses, true)
var groupIDs []int
request.RetrieveJSONQueryParameter(r, "groupIds", &groupIDs, true)
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
query, err := parseQuery(r)
if err != nil {
return httperror.BadRequest("Invalid query parameters", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
totalAvailableEndpoints := len(filteredEndpoints)
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
if err != nil {
return httperror.InternalServerError("Unable to filter endpoints", err)
if groupID != 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
}
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
}
if len(groupIDs) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, groupIDs)
}
name, _ := request.RetrieveQueryParameter(r, "name", true)
if name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, name)
}
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
if edgeDeviceFilter != "" {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
}
if len(statuses) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
}
if search != "" {
tags, err := handler.DataStore.Tag().Tags()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
}
if endpointTypes != nil {
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, endpointTypes)
}
if tagIDs != nil {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
}
// Sort endpoints by field
sortEndpointsByField(filteredEndpoints, endpointGroups, sortField, sortOrder == "desc")
filteredEndpointCount := len(filteredEndpoints)
@@ -129,6 +196,64 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
return endpoints[start:end]
}
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []int) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if utils.Contains(endpointGroupIDs, int(endpoint.GroupID)) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
status = portainer.EndpointStatusDown // Offline
if isCheckValid {
status = portainer.EndpointStatusUp // Online
}
}
if utils.Contains(statuses, int(status)) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, sortField string, isSortDesc bool) {
switch sortField {
@@ -140,7 +265,7 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
}
case "Group":
endpointGroupNames := make(map[portainer.EndpointGroupID]string, 0)
endpointGroupNames = make(map[portainer.EndpointGroupID]string, 0)
for _, group := range endpointGroups {
endpointGroupNames[group.ID] = group.Name
}
@@ -169,6 +294,123 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
}
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
return true
}
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
return true
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {
typeSet[portainer.EndpointType(endpointType)] = true
}
for _, endpoint := range endpoints {
if typeSet[endpoint.Type] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
// none - return all endpoints that are not edge devices
if edgeDeviceFilter == EdgeDeviceFilterNone && !endpoint.IsEdgeDevice {
return true
}
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return false
}
switch edgeDeviceFilter {
case EdgeDeviceFilterAll:
return true
case EdgeDeviceFilterTrusted:
return endpoint.UserTrusted
case EdgeDeviceFilterUntrusted:
return !endpoint.UserTrusted
}
return false
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
var endpointGroup portainer.EndpointGroup
for _, group := range groups {
@@ -179,3 +421,72 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
}
return endpointGroup
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
if name == "" {
return endpoints
}
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Name == name {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
+40 -146
View File
@@ -16,147 +16,66 @@ import (
"github.com/stretchr/testify/assert"
)
type endpointListTest struct {
type endpointListEdgeDeviceTest struct {
title string
expected []portainer.EndpointID
filter string
}
func Test_EndpointList_AgentVersion(t *testing.T) {
version1Endpoint := portainer.Endpoint{
ID: 1,
GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{
Version: "1.0.0",
},
}
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1, Type: portainer.AgentOnDockerEnvironment, Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "2.0.0"}}
noVersionEndpoint := portainer.Endpoint{ID: 3, Type: portainer.AgentOnDockerEnvironment, GroupID: 1}
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
handler, teardown := setup(t, []portainer.Endpoint{
notAgentEnvironments,
version1Endpoint,
version2Endpoint,
noVersionEndpoint,
})
func Test_endpointList(t *testing.T) {
var err error
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
type endpointListAgentVersionTest struct {
endpointListTest
filter []string
}
tests := []endpointListAgentVersionTest{
{
endpointListTest{
"should show version 1 agent endpoints and non-agent endpoints",
[]portainer.EndpointID{version1Endpoint.ID, notAgentEnvironments.ID},
},
[]string{version1Endpoint.Agent.Version},
},
{
endpointListTest{
"should show version 2 endpoints and non-agent endpoints",
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID},
},
[]string{version2Endpoint.Agent.Version},
},
{
endpointListTest{
"should show version 1 and 2 endpoints and non-agent endpoints",
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID, version1Endpoint.ID},
},
[]string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
is := assert.New(t)
query := ""
for _, filter := range test.filter {
query += fmt.Sprintf("agentVersions[]=%s&", filter)
}
req := buildEndpointListRequest(query)
resp, err := doEndpointListRequest(req, handler, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
respIds := []portainer.EndpointID{}
for _, endpoint := range resp {
respIds = append(respIds, endpoint.ID)
}
is.ElementsMatch(test.expected, respIds)
})
}
}
func Test_endpointList_edgeDeviceFilter(t *testing.T) {
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment}
handler, teardown := setup(t, []portainer.Endpoint{
trustedEdgeDevice,
untrustedEdgeDevice,
endpoints := []portainer.Endpoint{
trustedEndpoint,
untrustedEndpoint,
regularUntrustedEdgeEndpoint,
regularTrustedEdgeEndpoint,
regularEndpoint,
})
defer teardown()
type endpointListEdgeDeviceTest struct {
endpointListTest
edgeDevice *bool
edgeDeviceUntrusted bool
}
for _, endpoint := range endpoints {
err = store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
h := NewHandler(bouncer, nil)
h.DataStore = store
h.ComposeStackManager = testhelpers.NewComposeStackManager()
tests := []endpointListEdgeDeviceTest{
{
endpointListTest: endpointListTest{
"should show all endpoints except of the untrusted devices",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID},
},
edgeDevice: nil,
"should show all edge endpoints",
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterAll,
},
{
endpointListTest: endpointListTest{
"should show only trusted edge devices and regular endpoints",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
},
edgeDevice: BoolAddr(true),
"should show only trusted edge devices",
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterTrusted,
},
{
endpointListTest: endpointListTest{
"should show only untrusted edge devices and regular endpoints",
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
},
edgeDevice: BoolAddr(true),
edgeDeviceUntrusted: true,
"should show only untrusted edge devices",
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
EdgeDeviceFilterUntrusted,
},
{
endpointListTest: endpointListTest{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
},
edgeDevice: BoolAddr(false),
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterNone,
},
}
@@ -164,13 +83,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
t.Run(test.title, func(t *testing.T) {
is := assert.New(t)
query := fmt.Sprintf("edgeDeviceUntrusted=%v&", test.edgeDeviceUntrusted)
if test.edgeDevice != nil {
query += fmt.Sprintf("edgeDevice=%v&", *test.edgeDevice)
}
req := buildEndpointListRequest(query)
resp, err := doEndpointListRequest(req, handler, is)
req := buildEndpointListRequest(test.filter)
resp, err := doEndpointListRequest(req, h, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
@@ -186,28 +100,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
}
}
func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
for _, endpoint := range endpoints {
err := store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
handler = NewHandler(bouncer, nil)
handler.DataStore = store
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
return handler, teardown
}
func buildEndpointListRequest(query string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?%s", query), nil)
func buildEndpointListRequest(filter string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
@@ -55,7 +55,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
@@ -47,7 +47,6 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
@@ -22,8 +22,6 @@ type endpointUpdatePayload struct {
// URL or IP address where exposed containers will be reachable.\
// Defaults to URL if not specified
PublicURL *string `example:"docker.mydomain.tld:2375"`
// GPUs information
Gpus []portainer.Pair
// Group identifier
GroupID *int `example:"1"`
// Require TLS to connect against this environment(endpoint)
@@ -112,10 +110,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.PublicURL = *payload.PublicURL
}
if payload.Gpus != nil {
endpoint.Gpus = payload.Gpus
}
if payload.EdgeCheckinInterval != nil {
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
}
@@ -271,7 +265,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if (payload.URL != nil && *payload.URL != endpoint.URL) || (payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) || endpoint.Type == portainer.AzureEnvironment {
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
-435
View File
@@ -1,435 +0,0 @@
package endpoints
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"golang.org/x/exp/slices"
)
type EnvironmentsQuery struct {
search string
types []portainer.EndpointType
tagIds []portainer.TagID
endpointIds []portainer.EndpointID
tagsPartialMatch bool
groupIds []portainer.EndpointGroupID
status []portainer.EndpointStatus
edgeDevice *bool
edgeDeviceUntrusted bool
name string
agentVersions []string
}
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
search, _ := request.RetrieveQueryParameter(r, "search", true)
if search != "" {
search = strings.ToLower(search)
}
status, err := getNumberArrayQueryParameter[portainer.EndpointStatus](r, "status")
if err != nil {
return EnvironmentsQuery{}, err
}
groupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "groupIds")
if err != nil {
return EnvironmentsQuery{}, err
}
endpointTypes, err := getNumberArrayQueryParameter[portainer.EndpointType](r, "types")
if err != nil {
return EnvironmentsQuery{}, err
}
tagIDs, err := getNumberArrayQueryParameter[portainer.TagID](r, "tagIds")
if err != nil {
return EnvironmentsQuery{}, err
}
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
endpointIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "endpointIds")
if err != nil {
return EnvironmentsQuery{}, err
}
agentVersions := getArrayQueryParameter(r, "agentVersions")
name, _ := request.RetrieveQueryParameter(r, "name", true)
edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true)
var edgeDevice *bool
if edgeDeviceParam != "" {
edgeDevice = BoolAddr(edgeDeviceParam == "true")
}
edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true)
return EnvironmentsQuery{
search: search,
types: endpointTypes,
tagIds: tagIDs,
endpointIds: endpointIDs,
tagsPartialMatch: tagsPartialMatch,
groupIds: groupIDs,
status: status,
edgeDevice: edgeDevice,
edgeDeviceUntrusted: edgeDeviceUntrusted,
name: name,
agentVersions: agentVersions,
}, nil
}
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
totalAvailableEndpoints := len(filteredEndpoints)
if len(query.endpointIds) > 0 {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
}
if len(query.groupIds) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
}
if query.name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
}
if query.edgeDevice != nil {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, *query.edgeDevice, query.edgeDeviceUntrusted)
} else {
// If the edgeDevice parameter is not set, we need to filter out the untrusted edge devices
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !endpoint.IsEdgeDevice || endpoint.UserTrusted
})
}
if len(query.status) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings)
}
if query.search != "" {
tags, err := handler.DataStore.Tag().Tags()
if err != nil {
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
}
if len(query.types) > 0 {
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, query.types)
}
if len(query.tagIds) > 0 {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, query.tagIds, groups, query.tagsPartialMatch)
}
if len(query.agentVersions) > 0 {
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !endpointutils.IsAgentEndpoint(&endpoint) || contains(query.agentVersions, endpoint.Agent.Version)
})
}
return filteredEndpoints, totalAvailableEndpoints, nil
}
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []portainer.EndpointGroupID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if slices.Contains(endpointGroupIDs, endpoint.GroupID) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portainer.EndpointStatus, settings *portainer.Settings) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
status = portainer.EndpointStatusDown // Offline
if isCheckValid {
status = portainer.EndpointStatusUp // Online
}
}
if slices.Contains(statuses, status) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
return true
}
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
return true
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {
typeSet[portainer.EndpointType(endpointType)] = true
}
for _, endpoint := range endpoints {
if typeSet[endpoint.Type] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool {
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return true
}
if !edgeDeviceParam {
return !endpoint.IsEdgeDevice
}
return endpoint.IsEdgeDevice && endpoint.UserTrusted == !untrustedParam
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
if name == "" {
return endpoints
}
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Name == name {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.Endpoint) bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if predicate(endpoint) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getArrayQueryParameter(r *http.Request, parameter string) []string {
list, exists := r.Form[fmt.Sprintf("%s[]", parameter)]
if !exists {
list = []string{}
}
return list
}
func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]T, error) {
list := getArrayQueryParameter(r, parameter)
if list == nil {
return []T{}, nil
}
var result []T
for _, item := range list {
number, err := strconv.Atoi(item)
if err != nil {
return nil, errors.Wrapf(err, "Unable to parse parameter %s", parameter)
}
result = append(result, T(number))
}
return result, nil
}
func contains(strings []string, param string) bool {
for _, str := range strings {
if str == param {
return true
}
}
return false
}
-177
View File
@@ -1,177 +0,0 @@
package endpoints
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
type filterTest struct {
title string
expected []portainer.EndpointID
query EnvironmentsQuery
}
func Test_Filter_AgentVersion(t *testing.T) {
version1Endpoint := portainer.Endpoint{ID: 1, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "1.0.0"}}
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "2.0.0"}}
noVersionEndpoint := portainer.Endpoint{ID: 3, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
}
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
endpoints := []portainer.Endpoint{
version1Endpoint,
version2Endpoint,
noVersionEndpoint,
notAgentEnvironments,
}
handler, teardown := setupFilterTest(t, endpoints)
defer teardown()
tests := []filterTest{
{
"should show version 1 endpoints",
[]portainer.EndpointID{version1Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version1Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
{
"should show version 2 endpoints",
[]portainer.EndpointID{version2Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version2Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
{
"should show version 1 and 2 endpoints",
[]portainer.EndpointID{version2Endpoint.ID, version1Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
}
runTests(tests, t, handler, endpoints)
}
func Test_Filter_edgeDeviceFilter(t *testing.T) {
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}
endpoints := []portainer.Endpoint{
trustedEdgeDevice,
untrustedEdgeDevice,
regularUntrustedEdgeEndpoint,
regularTrustedEdgeEndpoint,
regularEndpoint,
}
handler, teardown := setupFilterTest(t, endpoints)
defer teardown()
tests := []filterTest{
{
"should show all edge endpoints except of the untrusted devices",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EnvironmentsQuery{
types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment},
},
},
{
"should show only trusted edge devices and other regular endpoints",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(true),
},
},
{
"should show only untrusted edge devices and other regular endpoints",
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(true),
edgeDeviceUntrusted: true,
},
},
{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(false),
},
},
}
runTests(tests, t, handler, endpoints)
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
runTest(t, test, handler, endpoints)
})
}
}
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
is := assert.New(t)
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
is.NoError(err)
is.Equal(len(test.expected), len(filteredEndpoints))
respIds := []portainer.EndpointID{}
for _, endpoint := range filteredEndpoints {
respIds = append(respIds, endpoint.ID)
}
is.ElementsMatch(test.expected, respIds)
}
func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
for _, endpoint := range endpoints {
err := store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
handler = NewHandler(bouncer, nil)
handler.DataStore = store
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
return handler, teardown
}
-3
View File
@@ -67,9 +67,6 @@ func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/agent_versions",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
+2 -2
View File
@@ -39,8 +39,8 @@ func (e EndpointsByGroup) Less(i, j int) bool {
return false
}
groupA := e.endpointGroupNames[e.endpoints[i].GroupID]
groupB := e.endpointGroupNames[e.endpoints[j].GroupID]
groupA := endpointGroupNames[e.endpoints[i].GroupID]
groupB := endpointGroupNames[e.endpoints[j].GroupID]
return sortorder.NaturalLess(strings.ToLower(groupA), strings.ToLower(groupB))
}
-6
View File
@@ -1,6 +0,0 @@
package endpoints
func BoolAddr(b bool) *bool {
boolVar := b
return &boolVar
}
+1 -5
View File
@@ -7,7 +7,6 @@ import (
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates"
"github.com/portainer/portainer/api/http/handler/docker"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgejobs"
"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -46,7 +45,6 @@ type Handler struct {
AuthHandler *auth.Handler
BackupHandler *backup.Handler
CustomTemplatesHandler *customtemplates.Handler
DockerHandler *docker.Handler
EdgeGroupsHandler *edgegroups.Handler
EdgeJobsHandler *edgejobs.Handler
EdgeStacksHandler *edgestacks.Handler
@@ -82,7 +80,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.15.0
// @version 2.14.0
// @description.markdown api-description.md
// @termsOfService
@@ -181,8 +179,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/docker"):
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):
+2 -1
View File
@@ -2,6 +2,7 @@ package helm
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
@@ -107,7 +108,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = r.Host
hostURL = strings.Split(r.Host, ":")[0]
}
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -144,7 +145,8 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
}
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
hostURL := strings.Split(r.Host, ":")[0]
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{
+2 -4
View File
@@ -95,10 +95,8 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
}
}
//if LDAP authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
}
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
}
return publicSettings
}
-16
View File
@@ -7,8 +7,6 @@ import (
"strings"
"sync"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/docker/docker/api/types"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -135,20 +133,6 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
// if stack management is disabled for non admins and the user isn't an admin, then return false. Otherwise return true
func (handler *Handler) userCanManageStacks(securityContext *security.RestrictedRequestContext, endpoint *portainer.Endpoint) (bool, error) {
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpoint.ID))
if err != nil {
return false, fmt.Errorf("Failed to get user from the database: %w", err)
}
return canCreate, nil
}
return true, nil
}
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
@@ -82,22 +82,6 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
}
stack.EndpointID = portainer.EndpointID(endpointID)
stack.SwarmID = swarmId
+16 -11
View File
@@ -13,6 +13,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -75,18 +76,22 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
}
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
}
if !canCreate {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
-9
View File
@@ -103,15 +103,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack deletion is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
}
// stop scheduler updates of the stack before removal
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
+2 -12
View File
@@ -3,12 +3,11 @@ package stacks
import (
"net/http"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -60,15 +59,6 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
@@ -86,7 +76,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
}
}
+3 -12
View File
@@ -3,12 +3,12 @@ package stacks
import (
"net/http"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/http/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -55,15 +55,6 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
@@ -81,7 +72,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
}
if resourceControl != nil {
-9
View File
@@ -87,15 +87,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack migration is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
-9
View File
@@ -64,15 +64,6 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
-9
View File
@@ -75,15 +75,6 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if stack.Status == portainer.StackStatusInactive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
}
-9
View File
@@ -123,15 +123,6 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack editing is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
if updateError != nil {
return updateError
@@ -18,7 +18,6 @@ import (
type stackGitUpdatePayload struct {
AutoUpdate *portainer.StackAutoUpdate
Env []portainer.Pair
Prune bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
@@ -120,15 +119,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack editing is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
@@ -141,12 +131,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
if stack.Type == portainer.DockerSwarmStack {
stack.Option = &portainer.StackOption{
Prune: payload.Prune,
}
}
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
@@ -24,7 +24,6 @@ type stackGitRedployPayload struct {
RepositoryUsername string
RepositoryPassword string
Env []portainer.Pair
Prune bool
}
func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
@@ -111,15 +110,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
var payload stackGitRedployPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -128,11 +118,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.Env = payload.Env
if stack.Type == portainer.DockerSwarmStack {
stack.Option = &portainer.StackOption{
Prune: payload.Prune,
}
}
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
@@ -202,11 +187,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch stack.Type {
case portainer.DockerSwarmStack:
prune := false
if stack.Option != nil {
prune = stack.Option.Prune
}
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, prune)
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}
+1 -1
View File
@@ -27,7 +27,7 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demo
h.Handle("/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
h.Handle("/status/version",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
return h
}
@@ -0,0 +1,62 @@
package status
import (
"encoding/json"
"net/http"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
)
type inspectVersionResponse struct {
// Whether portainer has an update available
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
// The latest version available
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
}
type githubData struct {
TagName string `json:"tag_name"`
}
// @id StatusInspectVersion
// @summary Check for portainer updates
// @description Check if portainer has an update available
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags status
// @produce json
// @success 200 {object} inspectVersionResponse "Success"
// @router /status/version [get]
func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
var data githubData
err = json.Unmarshal(motd, &data)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
resp := inspectVersionResponse{
UpdateAvailable: false,
}
currentVersion := semver.New(portainer.APIVersion)
latestVersion := semver.New(data.TagName)
if currentVersion.LessThan(*latestVersion) {
resp.UpdateAvailable = true
resp.LatestVersion = data.TagName
}
response.JSON(w, &resp)
}
-105
View File
@@ -1,105 +0,0 @@
package status
import (
"encoding/json"
"net/http"
"strconv"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
log "github.com/sirupsen/logrus"
)
type versionResponse struct {
// Whether portainer has an update available
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
// The latest version available
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
ServerVersion string
DatabaseVersion string
Build BuildInfo
}
type BuildInfo struct {
BuildNumber string
ImageTag string
NodejsVersion string
YarnVersion string
WebpackVersion string
GoVersion string
}
// @id Version
// @summary Check for portainer updates
// @description Check if portainer has an update available
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags status
// @produce json
// @success 200 {object} versionResponse "Success"
// @router /status/version [get]
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
result := &versionResponse{
ServerVersion: portainer.APIVersion,
DatabaseVersion: strconv.Itoa(portainer.DBVersion),
Build: BuildInfo{
BuildNumber: build.BuildNumber,
ImageTag: build.ImageTag,
NodejsVersion: build.NodejsVersion,
YarnVersion: build.YarnVersion,
WebpackVersion: build.WebpackVersion,
GoVersion: build.GoVersion,
},
}
latestVersion := getLatestVersion()
if hasNewerVersion(portainer.APIVersion, latestVersion) {
result.UpdateAvailable = true
result.LatestVersion = latestVersion
}
response.JSON(w, &result)
}
func getLatestVersion() string {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
log.WithError(err).Debug("couldn't fetch latest Portainer release version")
return ""
}
var data struct {
TagName string `json:"tag_name"`
}
err = json.Unmarshal(motd, &data)
if err != nil {
log.WithError(err).Debug("couldn't parse latest Portainer version")
return ""
}
return data.TagName
}
func hasNewerVersion(currentVersion, latestVersion string) bool {
currentVersionSemver, err := semver.NewVersion(currentVersion)
if err != nil {
log.WithField("version", currentVersion).Debug("current Portainer version isn't a semver")
return false
}
latestVersionSemver, err := semver.NewVersion(latestVersion)
if err != nil {
log.WithField("version", latestVersion).Debug("latest Portainer version isn't a semver")
return false
}
return currentVersionSemver.LessThan(*latestVersionSemver)
}
+15 -2
View File
@@ -5,13 +5,14 @@ import (
"log"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/agent"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/url"
)
// ProxyServer provide an extended proxy with a local server to forward requests
@@ -33,7 +34,7 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.ParseURL(urlString)
endpointURL, err := parseURL(urlString)
if err != nil {
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
}
@@ -98,3 +99,15 @@ func (proxy *ProxyServer) Close() {
proxy.server.Close()
}
}
// parseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func parseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}
+3 -3
View File
@@ -5,13 +5,13 @@ import (
"io"
"log"
"net/http"
"net/url"
"strings"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/docker"
"github.com/portainer/portainer/api/internal/url"
)
func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
@@ -23,7 +23,7 @@ func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.
}
func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
endpointURL, err := url.ParseURL(endpoint.URL)
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
@@ -38,7 +38,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
rawURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.ParseURL(rawURL)
endpointURL, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
+4 -4
View File
@@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
}
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
filteredEndpoints := endpoints
if !context.IsAdmin {
if !context.IsAdmin && !context.IsTeamLeader {
filteredEndpoints = make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
@@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
}
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
// Non administrator users only have access to authorized environment(endpoint) groups.
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
filteredEndpointGroups := endpointGroups
if !context.IsAdmin {
if !context.IsAdmin && !context.IsTeamLeader {
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
for _, group := range endpointGroups {
-4
View File
@@ -21,7 +21,6 @@ import (
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates"
dockerhandler "github.com/portainer/portainer/api/http/handler/docker"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgejobs"
"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -185,8 +184,6 @@ func (server *Server) Start() error {
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled)
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
@@ -278,7 +275,6 @@ func (server *Server) Start() error {
AuthHandler: authHandler,
BackupHandler: backupHandler,
CustomTemplatesHandler: customTemplatesHandler,
DockerHandler: dockerHandler,
EdgeGroupsHandler: edgeGroupsHandler,
EdgeJobsHandler: edgeJobsHandler,
EdgeStacksHandler: edgeStacksHandler,
+1 -1
View File
@@ -37,7 +37,7 @@ func parseRegToken(registry *portainer.Registry) (username, password string, err
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
if registry.Type == portainer.EcrRegistry {
if isRegTokenValid(registry) {
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: current ECR token is still valid]")
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: curretn ECR token is still valid]")
} else {
err = doGetRegToken(dataStore, registry)
if err != nil {
-40
View File
@@ -1,40 +0,0 @@
package set
type SetKey interface {
~int | ~string
}
type Set[T SetKey] map[T]bool
func (s Set[T]) Add(key T) {
s[key] = true
}
func (s Set[T]) Contains(key T) bool {
_, ok := s[key]
return ok
}
func (s Set[T]) Remove(key T) {
delete(s, key)
}
func (s Set[T]) Len() int {
return len(s)
}
func (s Set[T]) IsEmpty() bool {
return len(s) == 0
}
func (s Set[T]) Keys() []T {
keys := make([]T, s.Len())
i := 0
for k := range s {
keys[i] = k
i++
}
return keys
}
-22
View File
@@ -2,14 +2,11 @@ package snapshot
import (
"context"
"crypto/tls"
"errors"
"log"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/agent"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/dataservices"
)
@@ -90,24 +87,6 @@ func SupportDirectSnapshot(endpoint *portainer.Endpoint) bool {
// SnapshotEndpoint will create a snapshot of the environment(endpoint) based on the environment(endpoint) type.
// If the snapshot is a success, it will be associated to the environment(endpoint).
func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error {
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment {
var err error
var tlsConfig *tls.Config
if endpoint.TLSConfig.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return err
}
}
_, version, err := agent.GetAgentVersionAndPlatform(endpoint.URL, tlsConfig)
if err != nil {
return err
}
endpoint.Agent.Version = version
}
switch endpoint.Type {
case portainer.AzureEnvironment:
return nil
@@ -196,7 +175,6 @@ func (service *Service) snapshotEndpoints() error {
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
-19
View File
@@ -1,19 +0,0 @@
package url
import (
"fmt"
"net/url"
"strings"
)
// ParseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func ParseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}
+1 -11
View File
@@ -11,7 +11,6 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
@@ -95,20 +94,11 @@ func (service *kubeClusterAccessService) IsSecure() bool {
// - pass down params to binaries
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
baseURL := service.baseURL
// When the api call is internal, the baseURL should not be used.
if hostURL == "localhost" {
hostURL = hostURL + service.httpsBindAddr
baseURL = "/"
}
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
logrus.Infof("[kubeconfig] [hostURL: %s, httpsBindAddr: %s, baseURL: %s]", hostURL, service.httpsBindAddr, baseURL)
clusterURL := hostURL + baseURL
clusterURL := hostURL + service.httpsBindAddr + baseURL
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
@@ -115,7 +115,7 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
clusterAccessDetails := kcs.GetData("mysite.com", 1)
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes",
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
CertificateAuthorityFile: "",
CertificateAuthorityData: "",
}
+37 -74
View File
@@ -3,18 +3,16 @@ package oauth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
log "github.com/sirupsen/logrus"
)
// Service represents a service used to authenticate users against an authorization server
@@ -31,39 +29,17 @@ func NewService() *Service {
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
token, err := getOAuthToken(code, configuration)
if err != nil {
log.Debugf("[internal,oauth] [message: failed retrieving oauth token: %v]", err)
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
return "", err
}
idToken, err := getIdToken(token)
username, err := getUsername(token.AccessToken, configuration)
if err != nil {
log.Debugf("[internal,oauth] [message: failed parsing id_token: %v]", err)
}
resource, err := getResource(token.AccessToken, configuration)
if err != nil {
log.Debugf("[internal,oauth] [message: failed retrieving resource: %v]", err)
return "", err
}
resource = mergeSecondIntoFirst(idToken, resource)
username, err := getUsername(resource, configuration)
if err != nil {
log.Debugf("[internal,oauth] [message: failed retrieving username: %v]", err)
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
return "", err
}
return username, nil
}
// mergeSecondIntoFirst merges the overlap map into the base overwriting any existing values.
func mergeSecondIntoFirst(base map[string]interface{}, overlap map[string]interface{}) map[string]interface{} {
for k, v := range overlap {
base[k] = v
}
return base
}
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
unescapedCode, err := url.QueryUnescape(code)
if err != nil {
@@ -79,55 +55,27 @@ func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2
return token, nil
}
// getIdToken retrieves parsed id_token from the OAuth token response.
// This is necessary for OAuth providers like Azure
// that do not provide information about user groups on the user resource endpoint.
func getIdToken(token *oauth2.Token) (map[string]interface{}, error) {
tokenData := make(map[string]interface{})
idToken := token.Extra("id_token")
if idToken == nil {
return tokenData, nil
}
jwtParser := jwt.Parser{
SkipClaimsValidation: true,
}
t, _, err := jwtParser.ParseUnverified(idToken.(string), jwt.MapClaims{})
if err != nil {
return tokenData, errors.Wrap(err, "failed to parse id_token")
}
if claims, ok := t.Claims.(jwt.MapClaims); ok {
for k, v := range claims {
tokenData[k] = v
}
}
return tokenData, nil
}
func getResource(token string, configuration *portainer.OAuthSettings) (map[string]interface{}, error) {
func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) {
req, err := http.NewRequest("GET", configuration.ResourceURI, nil)
if err != nil {
return nil, err
return "", err
}
client := &http.Client{}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return nil, err
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
return "", err
}
if resp.StatusCode != http.StatusOK {
return nil, &oauth2.RetrieveError{
return "", &oauth2.RetrieveError{
Response: resp,
Body: body,
}
@@ -135,32 +83,47 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
content, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return nil, err
return "", err
}
if content == "application/x-www-form-urlencoded" || content == "text/plain" {
values, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
return "", err
}
datamap := make(map[string]interface{})
for k, v := range values {
if len(v) == 0 {
datamap[k] = ""
} else {
datamap[k] = v[0]
username := values.Get(configuration.UserIdentifier)
if username == "" {
return username, &oauth2.RetrieveError{
Response: resp,
Body: body,
}
}
return datamap, nil
return username, nil
}
var datamap map[string]interface{}
if err = json.Unmarshal(body, &datamap); err != nil {
return nil, err
return "", err
}
return datamap, nil
username, ok := datamap[configuration.UserIdentifier].(string)
if ok && username != "" {
return username, nil
}
if !ok {
username, ok := datamap[configuration.UserIdentifier].(float64)
if ok && username != 0 {
return fmt.Sprint(int(username)), nil
}
}
return "", &oauth2.RetrieveError{
Response: resp,
Body: body,
}
}
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
@@ -174,6 +137,6 @@ func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
ClientSecret: configuration.ClientSecret,
Endpoint: endpoint,
RedirectURL: configuration.RedirectURI,
Scopes: strings.Split(configuration.Scopes, ","),
Scopes: []string{configuration.Scopes},
}
}
-24
View File
@@ -1,24 +0,0 @@
package oauth
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
)
func getUsername(datamap map[string]interface{}, configuration *portainer.OAuthSettings) (string, error) {
username, ok := datamap[configuration.UserIdentifier].(string)
if ok && username != "" {
return username, nil
}
if !ok {
username, ok := datamap[configuration.UserIdentifier].(float64)
if ok && username != 0 {
return fmt.Sprint(int(username)), nil
}
}
return "", errors.New("failed to extract username from oauth resource")
}
-80
View File
@@ -1,80 +0,0 @@
package oauth
import (
"testing"
portaineree "github.com/portainer/portainer/api"
)
func Test_getUsername(t *testing.T) {
t.Run("fails for non-matching user identifier", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"name": "john"}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if user identifier doesn't exist as key in oauth userinfo object")
}
})
t.Run("fails if username is empty string", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": ""}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object is empty string")
}
})
t.Run("fails if username is 0 int", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": 0}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object is 0 val int")
}
})
t.Run("fails if username is negative int", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": -1}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object is -1 (negative) int")
}
})
t.Run("succeeds if username is matched and is not empty", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": "john"}
_, err := getUsername(datamap, oauthSettings)
if err != nil {
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-empty")
}
})
// looks like a bug!?
t.Run("fails if username is matched and is positive int", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": 1}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object matched is positive int")
}
})
t.Run("succeeds if username is matched and is non-zero (or negative) float", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": 1.1}
_, err := getUsername(datamap, oauthSettings)
if err != nil {
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-zero (or negative)")
}
})
}
-145
View File
@@ -1,145 +0,0 @@
package oauth
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/oauth/oauthtest"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
func Test_getOAuthToken(t *testing.T) {
validCode := "valid-code"
srv, config := oauthtest.RunOAuthServer(validCode, &portainer.OAuthSettings{})
defer srv.Close()
t.Run("getOAuthToken fails upon invalid code", func(t *testing.T) {
code := ""
_, err := getOAuthToken(code, config)
if err == nil {
t.Errorf("getOAuthToken should fail upon providing invalid code; code=%v", code)
}
})
t.Run("getOAuthToken succeeds upon providing valid code", func(t *testing.T) {
code := validCode
token, err := getOAuthToken(code, config)
if token == nil || err != nil {
t.Errorf("getOAuthToken should successfully return access token upon providing valid code")
}
})
}
func Test_getIdToken(t *testing.T) {
verifiedToken := `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTM1NDA3MjksImV4cCI6MTY4NTA3NjcyOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkdyb3VwcyI6WyJGaXJzdCIsIlNlY29uZCJdfQ.GeU8XCV4Y4p5Vm-i63Aj7UP5zpb_0Zxb7-DjM2_z-s8`
nonVerifiedToken := `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTM1NDA3MjksImV4cCI6MTY4NTA3NjcyOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkdyb3VwcyI6WyJGaXJzdCIsIlNlY29uZCJdfQ.`
claims := map[string]interface{}{
"iss": "Online JWT Builder",
"iat": float64(1653540729),
"exp": float64(1685076729),
"aud": "www.example.com",
"sub": "john.doe@example.com",
"GivenName": "John",
"Surname": "Doe",
"Groups": []interface{}{"First", "Second"},
}
tests := []struct {
testName string
idToken string
expectedResult map[string]interface{}
expectedError error
}{
{
testName: "should return claims if token exists and is verified",
idToken: verifiedToken,
expectedResult: claims,
expectedError: nil,
},
{
testName: "should return claims if token exists but is not verified",
idToken: nonVerifiedToken,
expectedResult: claims,
expectedError: nil,
},
{
testName: "should return empty map if token does not exist",
idToken: "",
expectedResult: make(map[string]interface{}),
expectedError: nil,
},
}
for _, tc := range tests {
t.Run(tc.testName, func(t *testing.T) {
token := &oauth2.Token{}
if tc.idToken != "" {
token = token.WithExtra(map[string]interface{}{"id_token": tc.idToken})
}
result, err := getIdToken(token)
assert.Equal(t, err, tc.expectedError)
assert.Equal(t, result, tc.expectedResult)
})
}
}
func Test_getResource(t *testing.T) {
srv, config := oauthtest.RunOAuthServer("", &portainer.OAuthSettings{})
defer srv.Close()
t.Run("should fail upon missing Authorization Bearer header", func(t *testing.T) {
_, err := getResource("", config)
if err == nil {
t.Errorf("getResource should fail if access token is not provided in auth bearer header")
}
})
t.Run("should fail upon providing incorrect Authorization Bearer header", func(t *testing.T) {
_, err := getResource("incorrect-token", config)
if err == nil {
t.Errorf("getResource should fail if incorrect access token provided in auth bearer header")
}
})
t.Run("should succeed upon providing correct Authorization Bearer header", func(t *testing.T) {
_, err := getResource(oauthtest.AccessToken, config)
if err != nil {
t.Errorf("getResource should succeed if correct access token provided in auth bearer header")
}
})
}
func Test_Authenticate(t *testing.T) {
code := "valid-code"
authService := NewService()
t.Run("should fail if user identifier does not get matched in resource", func(t *testing.T) {
srv, config := oauthtest.RunOAuthServer(code, &portainer.OAuthSettings{})
defer srv.Close()
_, err := authService.Authenticate(code, config)
if err == nil {
t.Error("Authenticate should fail to extract username from resource if incorrect UserIdentifier provided")
}
})
t.Run("should succeed if user identifier does get matched in resource", func(t *testing.T) {
config := &portainer.OAuthSettings{UserIdentifier: "username"}
srv, config := oauthtest.RunOAuthServer(code, config)
defer srv.Close()
username, err := authService.Authenticate(code, config)
if err != nil {
t.Errorf("Authenticate should succeed to extract username from resource if correct UserIdentifier provided; UserIdentifier=%s", config.UserIdentifier)
}
want := "test-oauth-user"
if username != want {
t.Errorf("Authenticate should return correct username; got=%s, want=%s", username, want)
}
})
}
-96
View File
@@ -1,96 +0,0 @@
package oauthtest
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/gorilla/mux"
portainer "github.com/portainer/portainer/api"
)
const (
AccessToken = "test-token"
)
// OAuthRoutes is an OAuth 2.0 compliant handler
func OAuthRoutes(code string, config *portainer.OAuthSettings) http.Handler {
router := mux.NewRouter()
router.HandleFunc(
"/authorize",
func(w http.ResponseWriter, req *http.Request) {
location := fmt.Sprintf("%s?code=%s&state=%s", config.RedirectURI, code, "anything")
// w.Header().Set("Location", location)
// w.WriteHeader(http.StatusFound)
http.Redirect(w, req, location, http.StatusFound)
},
).Methods(http.MethodGet)
router.HandleFunc(
"/access_token",
func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := req.ParseForm(); err != nil {
fmt.Fprintf(w, "ParseForm() err: %v", err)
return
}
reqCode := req.FormValue("code")
if reqCode != code {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"token_type": "Bearer",
"expires_in": 86400,
"access_token": AccessToken,
"scope": "groups",
})
},
).Methods(http.MethodPost)
router.HandleFunc(
"/user",
func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
authHeader := req.Header.Get("Authorization")
splitToken := strings.Split(authHeader, "Bearer ")
if len(splitToken) < 2 || splitToken[1] != AccessToken {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"username": "test-oauth-user",
"groups": "testing",
})
},
).Methods(http.MethodGet)
return router
}
// RunOAuthServer is a barebones OAuth 2.0 compliant test server which can be used to test OAuth 2 functionality
func RunOAuthServer(code string, config *portainer.OAuthSettings) (*httptest.Server, *portainer.OAuthSettings) {
srv := httptest.NewUnstartedServer(http.DefaultServeMux)
addr := srv.Listener.Addr()
config.AuthorizationURI = fmt.Sprintf("http://%s/authorize", addr)
config.AccessTokenURI = fmt.Sprintf("http://%s/access_token", addr)
config.ResourceURI = fmt.Sprintf("http://%s/user", addr)
config.RedirectURI = fmt.Sprintf("http://%s/", addr)
srv.Config.Handler = OAuthRoutes(code, config)
srv.Start()
return srv, config
}
+2 -17
View File
@@ -199,8 +199,6 @@ type (
StackCount int `json:"StackCount"`
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
NodeCount int `json:"NodeCount"`
GpuUseAll bool `json:"GpuUseAll"`
GpuUseList []string `json:"GpuUseList"`
}
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
@@ -312,7 +310,6 @@ type (
GroupID EndpointGroupID `json:"GroupId" example:"1"`
// URL or IP address where exposed containers will be reachable
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
Gpus []Pair `json:"Gpus"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
// List of tag identifiers to which this environment(endpoint) is associated
@@ -359,10 +356,6 @@ type (
CommandInterval int `json:"CommandInterval" example:"60"`
}
Agent struct {
Version string `example:"1.0.0"`
}
// Deprecated fields
// Deprecated in DBVersion == 4
TLS bool `json:"TLS,omitempty"`
@@ -929,8 +922,6 @@ type (
AdditionalFiles []string `json:"AdditionalFiles"`
// The auto update settings of a git stack
AutoUpdate *StackAutoUpdate `json:"AutoUpdate"`
// The stack deployment option
Option *StackOption `json:"Option"`
// The git config of this stack
GitConfig *gittypes.RepoConfig
// Whether the stack is from a app template
@@ -951,12 +942,6 @@ type (
JobID string `example:"15"`
}
// StackOption represents the options for stack deployment
StackOption struct {
// Prune services that are no longer referenced
Prune bool `example:"false"`
}
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
StackID int
@@ -1400,9 +1385,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.15.0"
APIVersion = "2.14.0"
// DBVersion is the version number of the Portainer database
DBVersion = 60
DBVersion = 50
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
+1 -5
View File
@@ -52,11 +52,7 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint)
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
if err != nil {
d.composeStackManager.Down(context.TODO(), stack, endpoint)
}
return err
return d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
}
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
-11
View File
@@ -693,12 +693,6 @@ definitions:
$ref: '#/definitions/portainer.DockerSnapshotRaw'
DockerVersion:
type: string
GpuUseAll:
type: boolean
GpuUseList:
items:
type: string
type: array
HealthyContainerCount:
type: integer
ImageCount:
@@ -855,11 +849,6 @@ definitions:
EdgeKey:
description: The key which is used to map the agent to Portainer
type: string
Gpus:
description: Endpoint Gpus information
items:
$ref: '#/definitions/portainer.Pair'
type: array
GroupId:
description: Endpoint group identifier
example: 1
-2
View File
@@ -1,2 +0,0 @@
export default 'SvgrURL';
export const ReactComponent = 'div';
@@ -1,4 +1,3 @@
<button ng-if="!$ctrl.state.uploadInProgress" type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn btn-light ng-scope">
<pr-icon icon="'upload'" feather="true"></pr-icon>
<button type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
<i style="margin: 0" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
</button>
<button ng-if="$ctrl.state.uploadInProgress" type="button" class="btn btn-sm btn-light" button-spinner="$ctrl.state.uploadInProgress"></button>
@@ -1,57 +1,45 @@
<div class="datatable">
<rd-widget>
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}">
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="($ctrl.onFileSelectedForUpload)"> </file-uploader>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'" feather="true" class-name="'searchIcon'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div>
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="($ctrl.onFileSelectedForUpload)"> </file-uploader>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Name'"
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<table-column-header
col-title="'Size'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Size'"
is-sorted-desc="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Size')"
></table-column-header>
<a ng-click="$ctrl.changeOrderBy('Size')">
Size
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<table-column-header
col-title="'Last modification'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'ModTime'"
is-sorted-desc="$ctrl.state.orderBy === 'ModTime' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('ModTime')"
></table-column-header>
<a ng-click="$ctrl.changeOrderBy('ModTime')">
Last modification
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ModTime' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ModTime' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th> Actions </th>
</tr>
@@ -59,12 +47,12 @@
<tbody>
<tr ng-if="!$ctrl.isRoot">
<td colspan="4">
<a ng-click="$ctrl.goToParent()"><pr-icon icon="'corner-left-up'" feather="true"></pr-icon>Go to parent</a>
<a ng-click="$ctrl.goToParent()"><i class="fa fa-level-up-alt space-right"></i>Go to parent</a>
</td>
</tr>
<tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))">
<td>
<span ng-if="item.edit" class="vertical-center">
<span ng-if="item.edit">
<input
class="input-sm"
type="text"
@@ -72,27 +60,27 @@
on-enter-key="$ctrl.rename({ name: item.Name, newName: item.newName }); item.edit = false"
auto-focus
/>
<a class="interactive" ng-click="item.edit = false;"><pr-icon icon="'x'" feather="true"></pr-icon></a>
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><pr-icon icon="'check'" feather="true"></pr-icon></a>
<a class="interactive" ng-click="item.edit = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><i class="fa fa-check-square"></i></a>
</span>
<span ng-if="!item.edit && item.Dir">
<a ng-click="$ctrl.browse({name: item.Name})" class="vertical-center"><pr-icon icon="'folder'" feather="true"></pr-icon>{{ item.Name }}</a>
<a ng-click="$ctrl.browse({name: item.Name})"><i class="fa fa-folder space-right" aria-hidden="true"></i>{{ item.Name }}</a>
</span>
<span ng-if="!item.edit && !item.Dir" class="vertical-center"><pr-icon icon="'file'" feather="true"></pr-icon>{{ item.Name }}</span>
<span ng-if="!item.edit && !item.Dir"> <i class="fa fa-file space-right" aria-hidden="true"></i>{{ item.Name }} </span>
</td>
<td>{{ item.Size | humansize }}</td>
<td>
{{ item.ModTime | getisodatefromtimestamp }}
</td>
<td>
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-secondary space-right" ng-click="$ctrl.download({ name: item.Name })" ng-if="!item.Dir">
<pr-icon icon="'download'" feather="true"></pr-icon> Download
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })" ng-if="!item.Dir">
<i class="fa fa-download" aria-hidden="true"></i> Download
</btn>
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-secondary space-right" ng-click="item.newName = item.Name; item.edit = true">
<pr-icon icon="'edit'" feather="true"></pr-icon> Rename
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true">
<i class="fa fa-edit" aria-hidden="true"></i> Rename
</btn>
<btn authorization="DockerAgentBrowseDelete" class="btn btn-xs btn-dangerlight" ng-click="$ctrl.delete({ name: item.Name })">
<pr-icon icon="'trash-2'" feather="true"></pr-icon> Delete
<btn authorization="DockerAgentBrowseDelete" class="btn btn-xs btn-danger" ng-click="$ctrl.delete({ name: item.Name })">
<i class="fa fa-trash" aria-hidden="true"></i> Delete
</btn>
</td>
</tr>
@@ -1,6 +1,6 @@
<files-datatable
title-text="Host browser - {{ $ctrl.getRelativePath() }}"
title-icon="file"
title-icon="fa-file"
dataset="$ctrl.files"
table-key="host_browser"
order-by="Dir"
@@ -1,6 +1,6 @@
<files-datatable
title-text="Volume browser"
title-icon="file"
title-icon="fa-file"
dataset="$ctrl.files"
table-key="volume_browser"
order-by="Dir"
+1 -8
View File
@@ -6,14 +6,7 @@ export function onStartupAngular($rootScope, $state, $interval, LocalStorage, En
EndpointProvider.initialize();
$rootScope.$state = $state;
const defaultTitle = document.title;
$transitions.onEnter({}, () => {
const endpoint = EndpointProvider.currentEndpoint();
if (endpoint) {
document.title = `${defaultTitle} | ${endpoint.Name}`;
}
});
$rootScope.defaultTitle = document.title;
// Workaround to prevent the loading bar from going backward
// https://github.com/chieffancypants/angular-loading-bar/issues/273
+43 -89
View File
@@ -2,27 +2,8 @@
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Inter';
src: url('../fonts/Inter-VariableFont.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select {
font-family: Inter, Arial, Helvetica, sans-serif;
}
}
html {
font-size: 16px;
overflow-y: scroll;
}
body {
background: var(--bg-body-color);
font-family: 'Inter';
color: var(--text-body-color) !important;
}
html,
@@ -40,16 +21,15 @@ body,
position: relative;
}
.white-space-normal {
white-space: normal !important;
}
.logo {
display: inline;
max-width: 155px;
max-height: 55px;
}
.white-space-normal {
white-space: normal !important;
}
.legend .title {
padding: 0 0.3em;
margin: 0.5em;
@@ -82,16 +62,16 @@ body,
font-size: 18px;
}
.form-section-title {
@apply text-gray-9;
@apply th-dark:text-gray-5;
@apply th-highcontrast:text-white;
.header_title_content {
margin-left: 5px;
}
.form-section-title {
border-bottom: 1px solid var(--border-form-section-title-color);
margin-top: 5px;
margin-bottom: 10px;
margin-bottom: 15px;
color: var(--text-form-section-title-color);
padding-left: 0;
font-weight: 500;
font-size: 16px;
}
.form-horizontal .control-label.text-left {
@@ -117,6 +97,10 @@ input[type='checkbox'] {
text-align: center;
}
a[ng-click] {
cursor: pointer;
}
.space-right {
margin-right: 5px;
}
@@ -147,12 +131,25 @@ input[type='checkbox'] {
background-color: var(--bg-item-highlighted-null-color);
}
.service-datatable {
background-color: var(--bg-item-highlighted-color);
padding: 2px;
}
.service-datatable thead {
background-color: var(--bg-service-datatable-thead) !important;
}
.service-datatable tbody {
background-color: var(--bg-service-datatable-tbody);
}
.fa.green-icon {
color: #23ae89;
}
.fa.red-icon {
color: #f04438;
color: #ae2323;
}
.fa.orange-icon {
@@ -224,12 +221,12 @@ input[type='checkbox'] {
}
.blocklist-item {
padding: 10px;
margin-bottom: 10px;
padding: 7px;
margin-bottom: 7px;
cursor: pointer;
border: 1px solid var(--border-blocklist);
border-radius: 8px;
margin-right: 10px;
border: 1px solid var(--border-blocklist-color);
border-radius: 2px;
box-shadow: var(--shadow-box-color);
}
.blocklist-item--disabled {
@@ -244,8 +241,6 @@ input[type='checkbox'] {
}
.blocklist-item:hover {
@apply border border-blue-7;
background-color: var(--bg-blocklist-hover-color);
color: var(--text-blocklist-hover-color);
}
@@ -266,6 +261,7 @@ input[type='checkbox'] {
.blocklist-item-logo {
width: 100%;
max-width: 60px;
height: 100%;
max-height: 60px;
}
@@ -383,13 +379,12 @@ input[type='checkbox'] {
}
.panel-body {
padding: 20px 25px;
background-color: var(--white-color);
border-radius: 8px;
padding-top: 30px;
background-color: var(--white-color) fff;
}
.user-box {
margin-right: 15px;
margin-right: 25px;
}
.select-endpoint {
@@ -477,7 +472,7 @@ input[type='checkbox'] {
:root[theme='dark'] .bootbox-checkbox-list,
:root[theme='highcontrast'] .bootbox-checkbox-list {
background-color: var(--bg-modal-content-color);
background-color: var(--black-color);
}
.small-select {
@@ -812,12 +807,11 @@ json-tree .branch-preview {
}
/* !spinkit override */
/* uib-typeahead override */
#scrollable-dropdown-menu .dropdown-menu {
max-height: 300px;
overflow-y: auto;
.kubectl-shell {
display: block;
text-align: center;
padding-bottom: 5px;
}
/* !uib-typeahead override */
.no-margin {
margin: 0 !important;
@@ -841,43 +835,3 @@ json-tree .branch-preview {
.form-check.radio {
margin-left: 15px;
}
.inline-text {
display: inline;
position: absolute;
font-family: 'Montserrat';
font-size: smaller;
margin-left: 5px;
margin-right: 5px;
}
.web-editor {
background-color: var(--bg-webeditor-color);
border-radius: 8px;
padding: 10px;
}
.web-editor a {
color: var(--text-link-color);
}
.web-editor a:hover {
color: var(--text-link-hover-color);
text-decoration-line: underline;
}
reach-portal > div {
z-index: 10;
}
input[style*='background-image: url("data:image/png'] + [data-cy='auth-passwordInputToggle'] {
right: 20px;
}
input[style*='background-image: url("data:image/png'] {
padding-right: 60px;
}
.web-editor .trancluded-item:empty {
display: none;
}
-409
View File
@@ -1,409 +0,0 @@
/* Label, Section Title */
.label {
border-radius: 5px;
}
.label-success {
background-color: var(--ui-success-7);
}
.label-danger {
background-color: var(--ui-error-6);
}
.control-label {
@apply inline-flex items-center;
@apply font-medium;
@apply text-gray-7;
@apply th-dark:text-gray-warm-3;
@apply th-highcontrast:text-white;
}
.vertical-center {
display: inline-flex;
align-items: center;
gap: 5px;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.blue {
background: var(--bg-dashboard-item) !important;
}
.form-control {
border-radius: 5px;
}
/* Input Group Addon */
.input-group-addon:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.input-group .form-control:not(:first-child):not(:last-child) {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.input-group-btn:last-child .btn {
margin-left: 5px;
border-radius: 5px;
}
/* Toggle switch */
.switch {
position: relative;
display: inline-block;
width: 42px;
height: 25px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch input[type='checkbox']:disabled + .slider {
background-color: var(--ui-gray-3);
@apply th-dark:before:bg-gray-warm-8;
@apply th-highcontrast:before:bg-gray-warm-8;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
}
.switch-values {
font-style: normal;
font-weight: 500;
margin-left: 5px;
}
/* Toggle */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-switch-box-color);
-webkit-transition: 0.4s;
transition: 0.4s;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
}
.slider:before {
position: absolute;
content: '';
height: 19px;
width: 19px;
left: 3px;
bottom: 3px;
background-color: var(--white-color);
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: var(--ui-blue-8);
@apply th-dark:bg-blue-9;
@apply th-highcontrast:bg-blue-9;
}
input:focus + .slider {
box-shadow: 0 0 1px var(--ui-blue-8);
}
input:checked + .slider:before {
-webkit-transform: translateX(17px);
-ms-transform: translateX(17px);
transform: translateX(17px);
}
.slider.round {
border-radius: 25px;
}
.slider.round:before {
border-radius: 50%;
}
/* Checkbox */
.md-checkbox input[type='checkbox']:enabled + label:before {
background-color: var(--bg-checkbox) !important;
border: 1px solid var(--border-checkbox) !important;
border-radius: 5px;
}
.md-checkbox input[type='checkbox']:disabled + label:before {
border-radius: 5px;
}
.md-checkbox input[type='checkbox']:checked + label:before {
background-color: var(--ui-blue-8) !important;
color: var(--ui-blue-8) !important;
border: 1px solid var(--ui-blue-8) !important;
}
.md-checkbox input[type='checkbox']:checked + .checkmark {
border-color: var(--grey-6);
background-color: var(--bg-checkbox);
}
/* Slider */
.rzslider .rz-pointer {
background-color: var(--white-color);
border: 3px solid var(--ui-blue-8);
width: 25px;
height: 25px;
top: -10px;
}
.rzslider .rz-bar {
background-color: var(--ui-gray-5);
height: 8px;
border-radius: 5px;
}
.rzslider .rz-selection {
background-color: var(--ui-blue-8);
}
.rzslider .rz-pointer:after {
display: none;
}
/* Widget */
.widget .widget-icon {
@apply text-lg !p-2 mr-1;
@apply bg-blue-3 text-blue-8;
@apply th-dark:bg-gray-9 th-dark:text-blue-3;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 1.5%;
}
.widget .widget-body table thead {
border-top: 1px solid var(--border-table-color);
}
/* Toaster */
#toast-container > .toast-success {
background-image: url(../images/icon-success.svg) !important;
background-size: 40px 40px;
background-position: top 12px left 12px;
}
#toast-container > .toast-error {
background-image: url(../images/icon-error.svg) !important;
background-size: 40px 40px;
background-position: top 12px left 12px;
}
#toast-container > .toast-warning {
background-image: url(../images/icon-warning.svg) !important;
background-size: 40px 40px;
background-position: top 12px left 12px;
}
.toast-success .toast-progress {
background-color: var(--ui-success-7);
}
.toast-warning .toast-progress {
background-color: var(--ui-warning-6);
}
.toast-error .toast-progress {
background-color: var(--ui-error-8);
}
#toast-container > div {
color: var(--ui-gray-7);
background-color: var(--white-color);
border-radius: 8px;
padding: 18px 20px 18px 68px;
width: 300px;
opacity: 1;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
filter: alpha(opacity=100);
}
#toast-container > div:hover {
-moz-box-shadow: 0 0 12px var(--ui-gray-7);
-webkit-box-shadow: 0 0 12px var(--ui-gray-7);
box-shadow: 0 0 12px var(--ui-gray-7);
}
.toast-close-button {
color: var(--black-color);
text-decoration: none;
margin-top: 5px;
cursor: pointer;
opacity: 0.4;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
}
.toast-close-button:hover,
.toast-close-button:focus {
color: var(--black-color);
text-decoration: none;
cursor: pointer;
opacity: 0.6;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60);
filter: alpha(opacity=60);
}
.toast-title {
font-weight: 500;
color: var(--black-color);
padding-right: 10px;
margin-bottom: 4px;
}
/* Modal */
.modal-dialog {
width: 450px;
}
.modal-content {
padding: 20px;
}
.background-error {
padding-top: 55px;
background-image: url(../images/icon-error.svg);
background-repeat: no-repeat;
background-position: top left;
}
.background-warning {
padding-top: 55px;
background-image: url(../images/icon-warning.svg);
background-repeat: no-repeat;
background-position: top left;
}
.modal-header {
margin-bottom: 10px;
padding: 0px;
border-bottom: none;
}
.modal-header .close {
margin-top: 0px;
}
.modal-header .modal-title {
font-weight: bold;
}
.modal-body {
padding: 10px 0px;
border-bottom: none;
}
.modal-body .bootbox-body {
font-size: 12px;
color: var(--text-bootbox);
}
.modal-footer {
padding: 10px 0px;
border-top: none;
display: flex;
}
.modal-footer .bootbox-cancel {
width: 100%;
}
.modal-footer .bootbox-accept {
width: 100%;
}
.bootbox-checkbox-list {
border: 0px;
}
/* Status Indicator Inside Table Section Label Style */
.table .label {
border-radius: 8px !important;
display: inline-flex;
align-items: center;
}
.table .label .label-danger {
background-color: var(--ui-error-8);
}
.table .label .label-warn {
background-color: var(--ui-warning-8);
}
.table .label .label-success {
background-color: var(--ui-success-7);
}
/* Required Label with asterisk */
.required:after {
content: '*';
color: var(--ui-error-9);
}
.progress {
height: 8px;
border-radius: 4px;
width: 50%;
display: inline-block;
margin-bottom: 0;
}
.progress .progress-bar {
background-color: var(--ui-blue-8);
}
.progress + span {
display: inline-block;
font-size: 85%;
margin-left: 10px;
}
/* Pagination */
.datatable .footer .paginationControls .pagination {
border: 1px solid var(--border-pagination-color);
}
.pagination li button {
color: var(--ui-gray-9) !important;
}
.pagination li:active button,
.pagination li:focus button {
border: 1px solid var(--ui-gray-5) !important;
}
.pagination li a {
text-decoration: none !important;
cursor: pointer;
color: var(--ui-gray-9) !important;
}
.widget-header {
font-size: 16px;
}
-161
View File
@@ -1,161 +0,0 @@
.btn {
@apply !outline-none;
@apply border border-solid border-transparent;
border-radius: 8px;
display: inline-flex;
justify-content: space-around;
align-items: center;
gap: 5px;
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
@apply opacity-40;
pointer-events: none;
touch-action: none;
}
.btn:hover {
color: var(--text-button-hover-color);
}
.btn.active {
box-shadow: none;
}
.btn.btn-primary {
@apply text-white bg-blue-8 border-blue-8;
@apply hover:text-white hover:bg-blue-9 hover:border-blue-9;
@apply th-dark:hover:bg-blue-7 th-dark:hover:border-blue-7;
}
.btn.btn-primary:active,
.btn.btn-primary.active,
.open > .dropdown-toggle.btn-primary {
@apply bg-blue-9 border-blue-5;
}
.nav-pills > li.active > a,
.nav-pills > li.active > a:hover,
.nav-pills > li.active > a:focus {
@apply bg-blue-8;
}
/* Button Secondary */
.btn.btn-secondary {
@apply border border-solid;
@apply text-blue-9 bg-blue-2 border-blue-8;
@apply hover:bg-blue-3;
@apply th-dark:text-blue-3 th-dark:bg-gray-10 th-dark:border-blue-7;
@apply th-dark:hover:bg-blue-11;
}
.btn.btn-danger {
@apply bg-error-8 border-error-8;
@apply hover:bg-error-7 hover:border-error-7 hover:text-white;
}
.btn.btn-danger:active,
.btn.btn-danger.active,
.open > .dropdown-toggle.btn-danger {
@apply bg-error-8 text-white border-blue-5;
}
.btn.btn-dangerlight {
@apply text-error-9 th-dark:text-white;
@apply bg-error-3 th-dark:bg-error-9;
@apply hover:bg-error-2 th-dark:hover:bg-error-11;
@apply border-error-5 th-dark:border-error-7 th-highcontrast:border-error-7;
@apply border border-solid;
}
.btn.btn-success {
background-color: var(--ui-success-7);
}
.btn.btn-success:hover {
color: var(--white-color);
}
/* secondary-grey */
.btn.btn-default,
.btn.btn-light {
@apply bg-white border-gray-5 text-gray-9;
@apply hover:bg-gray-3 hover:border-gray-5 hover:text-gray-10;
/* dark mode */
@apply th-dark:bg-gray-warm-10 th-dark:border-gray-warm-7 th-dark:text-gray-warm-4;
@apply th-dark:hover:bg-gray-warm-9 th-dark:hover:border-gray-6 th-dark:hover:text-gray-warm-4;
@apply th-highcontrast:bg-black th-highcontrast:border-gray-2 th-highcontrast:text-white;
@apply th-highcontrast:hover:bg-gray-9 th-highcontrast:hover:border-gray-6 th-highcontrast:hover:text-gray-warm-4;
}
.btn.btn-light:active,
.btn.btn-light.active,
.open > .dropdown-toggle.btn-light {
background-color: var(--ui-gray-3);
}
.btn.btn-link {
@apply text-blue-8 hover:text-blue-9 disabled:text-gray-5;
@apply th-dark:text-blue-8 th-dark:hover:text-blue-7;
@apply th-highcontrast:text-blue-8 th-highcontrast:hover:text-blue-7;
}
.btn-group {
display: inline-flex;
}
.input-group-btn .btn.active,
.btn-group .btn.active {
@apply bg-blue-2 text-blue-10 border-blue-5;
@apply th-dark:bg-blue-11 th-dark:text-blue-2 th-dark:border-blue-9;
}
/* focus */
.btn.btn-primary:focus,
.btn.btn-secondary:focus,
.btn.btn-light:focus {
@apply border-blue-5;
}
.btn.btn-danger:focus,
.btn.btn-dangerlight:focus {
@apply border-blue-6;
}
.btn.btn-primary:focus,
.btn.btn-secondary:focus,
.btn.btn-light:focus,
.btn.btn-danger:focus,
.btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-3);
box-shadow: 0px 0px 0px 4px var(--btn-focus-color);
}
[theme='dark'] .btn.btn-primary:focus,
[theme='dark'] .btn.btn-secondary:focus,
[theme='dark'] .btn.btn-light:focus,
[theme='dark'] .btn.btn-danger:focus,
[theme='dark'] .btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-11);
}
a.no-link,
a[ng-click] {
@apply text-current;
@apply hover:no-underline hover:text-current;
@apply focus:no-underline focus:text-current;
}
a,
a.hyperlink {
@apply text-blue-8 hover:text-blue-9;
@apply hover:underline cursor-pointer;
}
-355
View File
@@ -1,355 +0,0 @@
{
"black": "#000000",
"white": "#ffffff",
"gray": {
"1": "#fcfcfd",
"2": "#f9fafb",
"3": "#f2f4f7",
"4": "#eaecf0",
"5": "#d0d5dd",
"6": "#98a2b3",
"7": "#667085",
"8": "#475467",
"9": "#344054",
"10": "#1d2939",
"11": "#101828"
},
"blue": {
"1": "#f5fbff",
"2": "#f0f9ff",
"3": "#e0f2fe",
"4": "#b9e6fe",
"5": "#7cd4fd",
"6": "#36bffa",
"7": "#0ba5ec",
"8": "#0086c9",
"9": "#026aa2",
"10": "#065986",
"11": "#0b4a6f"
},
"error": {
"1": "#fffbfa",
"2": "#fef3f2",
"3": "#fee4e2",
"4": "#fecdca",
"5": "#fda29b",
"6": "#f97066",
"7": "#f04438",
"8": "#d92d20",
"9": "#b42318",
"10": "#912018",
"11": "#7a271a"
},
"warning": {
"1": "#fffcf5",
"2": "#fffaeb",
"3": "#fef0c7",
"4": "#fedf89",
"5": "#fec84b",
"6": "#fdb022",
"7": "#f79009",
"8": "#dc6803",
"9": "#b54708",
"10": "#93370d",
"11": "#7a2e0e"
},
"success": {
"1": "#f6fef9",
"2": "#ecfdf3",
"3": "#d1fadf",
"4": "#a6f4c5",
"5": "#6ce9a6",
"6": "#32d583",
"7": "#12b76a",
"8": "#039855",
"9": "#027a48",
"10": "#05603a",
"11": "#054f31"
},
"gray-blue": {
"1": "#fcfcfd",
"2": "#f8f9fc",
"3": "#eaecf5",
"4": "#d5d9eb",
"5": "#b3b8db",
"6": "#717bbc",
"7": "#4e5ba6",
"8": "#3e4784",
"9": "#363f72",
"10": "#293056",
"11": "#293056"
},
"gray-cool": {
"1": "#fcfcfd",
"2": "#f9f9fb",
"3": "#eff1f5",
"4": "#dcdfea",
"5": "#b9c0d4",
"6": "#7d89b0",
"7": "#5d6b98",
"8": "#4a5578",
"9": "#404968",
"10": "#30374f",
"11": "#111322"
},
"gray-modern": {
"1": "#fcfcfd",
"2": "#f8fafc",
"3": "#eef2f6",
"4": "#e3e8ef",
"5": "#cdd5df",
"6": "#9aa4b2",
"7": "#697586",
"8": "#4b5565",
"9": "#364152",
"10": "#202939",
"11": "#121926"
},
"gray-neutral": {
"1": "#fcfcfd",
"2": "#f9fafb",
"3": "#f3f4f6",
"4": "#e5e7eb",
"5": "#d2d6db",
"6": "#9da4ae",
"7": "#6c737f",
"8": "#4d5761",
"9": "#384250",
"10": "#1f2a37",
"11": "#111927"
},
"gray-iron": {
"1": "#fcfcfc",
"2": "#fafafa",
"3": "#f4f4f5",
"4": "#e4e4e7",
"5": "#d1d1d6",
"6": "#d1d1d6",
"7": "#70707b",
"8": "#51525c",
"9": "#3f3f46",
"10": "#26272b",
"11": "#18181b"
},
"gray-true": {
"1": "#fcfcfc",
"2": "#fafafa",
"3": "#f5f5f5",
"4": "#e5e5e5",
"5": "#d6d6d6",
"6": "#a3a3a3",
"7": "#737373",
"8": "#525252",
"9": "#424242",
"10": "#292929",
"11": "#141414"
},
"gray-warm": {
"1": "#fdfdfc",
"2": "#fafaf9",
"3": "#f5f5f4",
"4": "#e7e5e4",
"5": "#d7d3d0",
"6": "#a9a29d",
"7": "#79716b",
"8": "#57534e",
"9": "#44403c",
"10": "#292524",
"11": "#1c1917"
},
"moss": {
"1": "#fafdf7",
"2": "#f5fbee",
"3": "#e6f4d7",
"4": "#ceeab0",
"5": "#acdc79",
"6": "#86cb3c",
"7": "#669f2a",
"8": "#4f7a21",
"9": "#3f621a",
"10": "#335015",
"11": "#2b4212"
},
"green-light": {
"1": "#fafef5",
"2": "#f3fee7",
"3": "#e4fbcc",
"4": "#d0f8ab",
"5": "#a6ef67",
"6": "#85e13a",
"7": "#66c61c",
"8": "#4ca30d",
"9": "#3b7c0f",
"10": "#326212",
"11": "#2b5314"
},
"green": {
"1": "#f6fef9",
"2": "#edfcf2",
"3": "#d3f8df",
"4": "#aaf0c4",
"5": "#73e2a3",
"6": "#73e2a3",
"7": "#16b364",
"8": "#099250",
"9": "#087443",
"10": "#095c37",
"11": "#084c2e"
},
"teal": {
"1": "#f6fefc",
"2": "#f0fdf9",
"3": "#ccfbef",
"4": "#99f6e0",
"5": "#5fe9d0",
"6": "#2ed3b7",
"7": "#15b79e",
"8": "#0e9384",
"9": "#107569",
"10": "#125d56",
"11": "#134e48"
},
"cyan": {
"1": "#f5feff",
"2": "#ecfdff",
"3": "#cff9fe",
"4": "#a5f0fc",
"5": "#67e3f9",
"6": "#22ccee",
"7": "#06aed4",
"8": "#088ab2",
"9": "#0e7090",
"10": "#155b75",
"11": "#164c63"
},
"blue-dark": {
"1": "#f5f8ff",
"2": "#eff4ff",
"3": "#d1e0ff",
"4": "#b2ccff",
"5": "#84adff",
"6": "#528bff",
"7": "#2970ff",
"8": "#155eef",
"9": "#004eeb",
"10": "#0040c1",
"11": "#00359e"
},
"indigo": {
"1": "#f5f8ff",
"2": "#eef4ff",
"3": "#e0eaff",
"4": "#c7d7fe",
"5": "#a4bcfd",
"6": "#8098f9",
"7": "#8098f9",
"8": "#444ce7",
"9": "#3538cd",
"10": "#2d31a6",
"11": "#2d3282"
},
"violet": {
"1": "#fbfaff",
"2": "#f5f3ff",
"3": "#ece9fe",
"4": "#ddd6fe",
"5": "#c3b5fd",
"6": "#a48afb",
"7": "#875bf7",
"8": "#7839ee",
"9": "#6927da",
"10": "#5720b7",
"11": "#491c96"
},
"purple": {
"1": "#fafaff",
"2": "#f4f3ff",
"3": "#ebe9fe",
"4": "#d9d6fe",
"5": "#bdb4fe",
"6": "#9b8afb",
"7": "#7a5af8",
"8": "#6938ef",
"9": "#5925dc",
"10": "#4a1fb8",
"11": "#3e1c96"
},
"fuchsia": {
"1": "#fefaff",
"2": "#fdf4ff",
"3": "#fbe8ff",
"4": "#f6d0fe",
"5": "#eeaafd",
"6": "#e478fa",
"7": "#d444f1",
"8": "#ba24d5",
"9": "#9f1ab1",
"10": "#821890",
"11": "#6f1877"
},
"pink": {
"1": "#fef6fb",
"2": "#fdf2fa",
"3": "#fce7f6",
"4": "#fce7f6",
"5": "#faa7e0",
"6": "#f670c7",
"7": "#ee46bc",
"8": "#dd2590",
"9": "#c11574",
"10": "#9e165f",
"11": "#851651"
},
"rose": {
"1": "#fff5f6",
"2": "#fff1f3",
"3": "#ffe4e8",
"4": "#fecdd6",
"5": "#fea3b4",
"6": "#fd6f8e",
"7": "#f63d68",
"8": "#e31b54",
"9": "#c01048",
"10": "#a11043",
"11": "#89123e"
},
"orange-dark": {
"1": "#fff9f5",
"2": "#fff4ed",
"3": "#ffe6d5",
"4": "#ffd6ae",
"5": "#ff9c66",
"6": "#ff692e",
"7": "#ff4405",
"8": "#e62e05",
"9": "#bc1b06",
"10": "#97180c",
"11": "#771a0d"
},
"orange": {
"1": "#fefaf5",
"2": "#fef6ee",
"3": "#fdead7",
"4": "#f9dbaf",
"5": "#f7b27a",
"6": "#f38744",
"7": "#ef6820",
"8": "#e04f16",
"9": "#b93815",
"10": "#932f19",
"11": "#772917"
},
"yellow": {
"1": "#fefdf0",
"2": "#fefbe8",
"3": "#fef7c3",
"4": "#feee95",
"5": "#feee95",
"6": "#fac515",
"7": "#eaaa08",
"8": "#ca8504",
"9": "#a15c07",
"10": "#854a0e",
"11": "#713b12"
}
}
-19
View File
@@ -1,19 +0,0 @@
import colors from './colors.json';
const element = document.createElement('style');
element.innerHTML = `:root {
${Object.entries(colors)
.map(([color, hex]) => {
if (typeof hex === 'string') {
return `--ui-${color}: ${hex}`;
}
return Object.entries(hex)
.map(([key, value]) => `--ui-${color}-${key}: ${value}`)
.join(';\n');
})
.join(';\n')}
}`;
document.head.prepend(element);
-133
View File
@@ -1,133 +0,0 @@
.feather {
display: block;
height: 1em;
width: 1em;
color: inherit;
}
pr-icon {
display: inline-flex;
}
.icon {
color: currentColor;
margin: 0;
display: inline-block;
font-size: var(--icon-size);
height: var(--icon-size);
width: var(--icon-size);
--icon-size: 1em;
}
.icon-xs {
--icon-size: 10px;
}
.icon-sm {
--icon-size: 14px;
}
.icon-md {
--icon-size: 16px;
}
.icon-lg {
--icon-size: 22px;
}
.icon-xl {
--icon-size: 26px;
}
.icon.icon-alt {
fill: var(--black-color);
stroke: var(--white-color);
}
.icon-primary,
.icon-blue {
color: var(--ui-blue-8);
}
.icon.icon-primary-alt {
fill: var(--ui-blue-8);
stroke: var(--white-color);
}
.icon-secondary {
color: var(--ui-gray-8);
}
.icon.icon-secondary-alt {
fill: var(--ui-gray-8);
stroke: var(--black-color);
}
.icon-warning,
.icon-orange {
color: var(--ui-warning-8);
}
.icon.icon-warning-alt {
fill: var(--ui-warning-8);
stroke: var(--white-color);
}
.icon-danger {
color: var(--ui-error-9);
}
.icon.icon-danger-alt {
fill: var(--ui-error-9);
stroke: var(--white-color);
}
.icon-success {
color: var(--ui-success-6);
}
.icon.icon-success-alt {
fill: var(--ui-success-8);
stroke: var(--white-color);
}
.icon-badge {
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 1.5%;
}
.icon-nested-blue {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
width: 30px;
padding: 5px;
text-align: center;
border-radius: 50%;
background-color: var(--ui-blue-3);
margin-right: 5px;
}
.icon-nested-blue > svg {
height: 20px;
width: 20px;
}
.icon-container {
display: flex;
align-items: center;
}
.btn-only-icon {
padding: 6px;
}
.btn-only-icon pr-icon {
margin-top: 0;
}
-5
View File
@@ -15,14 +15,9 @@ import 'angular-multiselect/isteven-multi-select.css';
import 'spinkit/spinkit.min.css';
import '@reach/menu-button/styles.css';
import './colors';
import './rdash.css';
import './app.css';
import './theme.css';
import './vendor-override.css';
import '../fonts/nomad-icon.css';
import './bootstrap-override.css';
import './icon.css';
import './button.css';
+111 -10
View File
@@ -4,6 +4,49 @@
width: 100%;
height: auto;
}
@media only screen and (min-width: 561px) {
#page-wrapper.open {
padding-left: 250px;
}
}
@media only screen and (max-width: 560px) {
#page-wrapper.open {
padding-left: 70px;
}
}
/**
* Hamburg Menu
* When the class of 'hamburg' is applied to the body tag of the document,
* the sidebar changes it's style to attempt to mimic a menu on a phone app,
* where the content is overlaying the content, rather than push it.
*/
@media only screen and (max-width: 560px) {
body.hamburg #page-wrapper {
padding-left: 0;
}
body.hamburg #page-wrapper:not(.open) #sidebar-wrapper {
position: absolute;
left: -100px;
}
body.hamburg #page-wrapper:not(.open) ul.sidebar .sidebar-title.separator {
display: none;
}
body.hamburg #page-wrapper.open #sidebar-wrapper {
position: fixed;
}
body.hamburg #page-wrapper.open #sidebar-wrapper ul.sidebar li.sidebar-main {
margin-left: 0px;
}
body.hamburg #sidebar-wrapper ul.sidebar li.sidebar-main,
body.hamburg .row.header .meta {
margin-left: 70px;
}
body.hamburg #sidebar-wrapper ul.sidebar li.sidebar-main,
body.hamburg #page-wrapper.open #sidebar-wrapper ul.sidebar li.sidebar-main {
transition: margin-left 0.4s ease 0s;
}
}
.loading {
width: 40px;
@@ -49,6 +92,33 @@
}
}
/* Fonts */
@font-face {
font-family: 'Montserrat';
src: url('../fonts/montserrat-regular-webfont.eot');
src: url('../fonts/montserrat-regular-webfont.eot?#iefix') format('embedded-opentype'), url('../fonts/montserrat-regular-webfont.woff') format('woff'),
url('../fonts/montserrat-regular-webfont.ttf') format('truetype'), url('../fonts/montserrat-regular-webfont.svg#montserratregular') format('svg');
font-weight: normal;
font-style: normal;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@font-face {
font-family: 'Montserrat';
src: url('../fonts/montserrat-regular-webfont.svg') format('svg');
}
select {
font-family: Arial, Helvetica, sans-serif;
}
}
/* Base */
html {
overflow-y: scroll;
}
body {
background: var(--bg-body-color);
font-family: 'Montserrat';
color: var(--text-body-color) !important;
}
.row {
margin-left: 0 !important;
margin-right: 0 !important;
@@ -59,7 +129,22 @@
.alerts-container .alert:last-child {
margin-bottom: 0;
}
#page-wrapper {
padding-left: 70px;
height: 100%;
}
#sidebar-wrapper {
margin-left: -150px;
left: -30px;
width: 250px;
position: fixed;
height: 100%;
z-index: 999;
}
#page-wrapper,
#sidebar-wrapper {
transition: all 0.4s ease 0s;
}
.green {
background: #23ae89 !important;
}
@@ -87,8 +172,9 @@ div.input-mask {
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
background: var(--bg-widget-color);
border: 1px solid var(--border-widget);
border-radius: 8px;
border: 1px solid transparent;
border-radius: 2px;
border-color: var(--border-widget-color);
}
.widget .widget-header .pagination,
.widget .widget-footer .pagination {
@@ -96,23 +182,25 @@ div.input-mask {
}
.widget .widget-header {
color: var(--text-widget-header-color);
padding: 20px 20px 10px 20px;
background-color: var(--bg-widget-header-color);
padding: 10px 15px;
border-bottom: 1px solid var(--border-widget-color);
line-height: 30px;
font-weight: 500;
}
.widget .widget-header i {
margin-right: 5px;
}
.widget .widget-body {
padding: 20px;
border-radius: 8px;
}
.widget .widget-body table thead {
background: var(--bg-widget-table-color);
}
.widget .widget-body table thead * {
font-size: 14px;
font-size: 14px !important;
}
.widget .widget-body table tbody * {
font-size: 13px;
font-size: 13px !important;
}
.widget .widget-body .error {
color: #ff0000;
@@ -146,7 +234,20 @@ div.input-mask {
border-top: 1px solid #e9e9e9;
padding: 10px;
}
.widget .widget-icon {
background: #30426a;
width: 65px;
height: 65px;
border-radius: 50%;
text-align: center;
vertical-align: middle;
margin-right: 15px;
}
.widget .widget-icon i {
line-height: 66px;
color: #ffffff;
font-size: 30px;
}
.widget .widget-footer {
border-top: 1px solid #e9e9e9;
padding: 10px;
+139 -170
View File
@@ -1,12 +1,13 @@
/* Color Variable */
:root {
--black-color: var(--ui-black);
--white-color: var(--ui-white);
html {
--black-color: #000;
--white-color: #fff;
--grey-1: #212121;
--grey-2: #181818;
--grey-3: #383838;
--grey-4: #585858;
--grey-5: #323c48;
--grey-6: #333333;
--grey-7: #767676;
--grey-8: #aaa;
@@ -34,6 +35,7 @@
--grey-30: #444;
--grey-31: #868686;
--grey-32: #65798e;
--grey-34: #314252;
--grey-35: #546477;
--grey-36: #55637d;
--grey-37: #2d3e63;
@@ -51,12 +53,15 @@
--grey-49: rgba(0, 0, 0, 0.54);
--grey-50: rgba(161, 170, 166, 0.5);
--grey-51: rgba(0, 0, 0, 0.15);
--grey-52: rgba(255, 255, 255, 0.3);
--grey-53: rgba(255, 255, 255, 0.6);
--grey-54: rgb(54, 54, 54);
--grey-55: rgba(255, 255, 255, 0.8);
--grey-56: #b2bfdc;
--grey-57: #999;
--grey-58: #ebf4f8;
--grey-59: #e6e6e6;
--grey-60: #cacaca;
--grey-61: rgb(231, 231, 231);
--blue-1: #219;
@@ -71,6 +76,7 @@
--blue-10: #61b6ff;
--blue-11: #3ea5ff;
--blue-12: #41a6ff;
--blue-13: #2361ae;
--blue-14: #357ebd;
--red-1: #a94442;
@@ -78,6 +84,7 @@
--red-3: #a11;
--red-4: #d9534f;
--red-5: #ff2727;
--red-6: #ff00e0;
--red-7: #f00;
--green-1: #164;
@@ -86,31 +93,35 @@
--orange-1: #e86925;
--BE-only: var(--ui-warning-7);
--BE-only: var(--orange-1);
}
/* Default Theme */
--bg-card-color: var(--white-color);
/* Default Theme */
:root {
--bg-card-color: var(--grey-10);
--bg-main-color: var(--white-color);
--bg-body-color: var(--grey-9);
--bg-checkbox-border-color: var(--grey-49);
--bg-sidebar-color: var(--grey-37);
--bg-sidebar-header-color: var(--grey-37);
--bg-widget-color: var(--white-color);
--bg-widget-header-color: var(--grey-10);
--bg-widget-table-color: var(--ui-gray-3);
--bg-widget-table-color: var(--grey-13);
--bg-header-color: var(--white-color);
--bg-hover-table-color: var(--grey-14);
--bg-switch-box-color: var(--ui-gray-5);
--bg-input-group-addon-color: var(--ui-gray-3);
--bg-switch-box-color: var(--white-color);
--bg-input-group-addon-color: var(--grey-11);
--bg-btn-default-color: var(--white-color);
--bg-blocklist-hover-color: var(--ui-blue-2);
--bg-boxselector-color: var(--ui-gray-2);
--bg-blocklist-hover-color: var(--grey-12);
--bg-boxselector-color: var(--white-color);
--bg-table-color: var(--white-color);
--bg-md-checkbox-color: var(--grey-12);
--bg-form-control-disabled-color: var(--grey-11);
--bg-modal-content-color: var(--white-color);
--bg-code-color: var(--grey-15);
--bg-navtabs-color: var(--white-color);
--bg-navtabs-hover-color: var(--grey-16);
--bg-table-selected-color: var(--grey-14);
--bg-codemirror-color: var(--white-color);
--bg-codemirror-gutters-color: var(--grey-17);
--bg-dropdown-menu-color: var(--white-color);
--bg-log-viewer-color: var(--white-color);
@@ -118,20 +129,20 @@
--bg-pre-color: var(--grey-14);
--bg-blocklist-item-selected-color: var(--grey-12);
--bg-progress-color: var(--grey-14);
--bg-pagination-color: var(--ui-blue-3);
--border-pagination-color: var(--ui-white);
--bg-pagination-color: var(--white-color);
--bg-pagination-span-color: var(--white-color);
--bg-pagination-hover-color: var(--ui-blue-3);
--bg-pagination-hover-color: var(--grey-11);
--bg-ui-select-hover-color: var(--grey-14);
--bg-motd-body-color: var(--grey-20);
--bg-item-highlighted-color: var(--grey-21);
--bg-item-highlighted-null-color: var(--grey-14);
--bg-row-header-color: var(--white-color);
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
--bg-multiselect-checkbox-color: var(--white-color);
--bg-sidebar-wrapper-color: var(--blue-5);
--bg-panel-body-color: var(--white-color);
--bg-codemirror-color: var(--white-color);
--bg-codemirror-selected-color: var(--grey-22);
--bg-tooltip-color: var(--ui-gray-11);
--bg-input-sm-color: var(--white-color);
--bg-app-datatable-thead: var(--grey-23);
--bg-app-datatable-tbody: var(--grey-24);
--bg-multiselect-color: var(--white-color);
--bg-daterangepicker-color: var(--white-color);
--bg-calendar-color: var(--white-color);
@@ -140,41 +151,34 @@
--bg-daterangepicker-hover: var(--grey-16);
--bg-daterangepicker-in-range: var(--grey-58);
--bg-daterangepicker-active: var(--blue-14);
--bg-tooltip-color: var(--white-color);
--bg-input-autofill-color: var(--white-color);
--bg-btn-default-hover-color: var(--grey-59);
--bg-btn-focus: var(--grey-59);
--bg-boxselector-disabled-color: var(--white-color);
--bg-small-select-color: var(--white-color);
--bg-app-datatable-thead: var(--grey-23);
--bg-app-datatable-tbody: var(--grey-24);
--bg-stepper-item-active: var(--white-color);
--bg-stepper-item-counter: var(--grey-61);
--bg-sortbutton-color: var(--white-color);
--bg-dashboard-item: var(--ui-blue-3);
--bg-searchbar: var(--ui-gray-2);
--bg-inputbox: var(--ui-gray-2);
--bg-dropdown-hover: var(--ui-gray-3);
--bg-webeditor-color: var(--ui-gray-3);
--bg-button-group-color: var(--ui-white);
--bg-pagination-disabled-color: var(--ui-white);
--bg-nav-container-color: var(--ui-gray-2);
--bg-code-script-color: var(--ui-white);
--bg-nav-tabs-active-color: var(--ui-gray-4);
--bg-stepper-color: var(--ui-white);
--bg-stepper-active-color: var(--ui-blue-1);
--bg-code-color: var(--ui-white);
--text-main-color: var(--grey-7);
--text-body-color: var(--grey-6);
--text-widget-header-color: var(--ui-gray-11);
--text-sidebar-title-color: var(--blue-3);
--text-widget-header-color: var(--grey-7);
--text-form-control-color: var(--grey-25);
--text-muted-color: var(--grey-26);
--text-link-color: var(--blue-2);
--text-link-hover-color: var(--blue-4);
--text-input-group-addon-color: var(--grey-25);
--text-btn-default-color: var(--grey-6);
--text-blocklist-hover-color: var(--grey-37);
--text-dashboard-item-color: var(--grey-32);
--text-danger-color: var(--red-1);
--text-code-color: var(--ui-gray-9);
--text-code-color: var(--red-2);
--text-navtabs-color: var(--grey-25);
--text-form-section-title-color: var(--grey-26);
--text-cm-default-color: var(--blue-1);
--text-cm-meta-color: var(--black-color);
--text-cm-string-color: var(--red-3);
@@ -189,25 +193,23 @@
--text-blocklist-item-selected-color: var(--grey-37);
--text-progress-bar-color: var(--grey-27);
--text-pagination-color: var(--grey-26);
--text-pagination-span-color: var(--grey-3);
--text-pagination-span-hover-color: var(--grey-3);
--text-pagination-span-color: var(--blue-2);
--text-pagination-span-hover-color: var(--blue-4);
--text-ui-select-color: var(--grey-6);
--text-ui-select-hover-color: var(--grey-28);
--text-summary-color: var(--black-color);
--text-tooltip-color: var(--white-color);
--text-multiselect-button-color: var(--grey-29);
--text-multiselect-item-color: var(--grey-30);
--text-sidebar-list-color: var(--grey-56);
--text-rzslider-color: var(--grey-36);
--text-rzslider-limit-color: var(--grey-36);
--text-daterangepicker-end-date: var(--grey-57);
--text-daterangepicker-in-range: var(--black-color);
--text-daterangepicker-active: var(--white-color);
--text-tooltip-color: var(--grey-6);
--text-input-autofill-color: var(--black-color);
--text-button-hover-color: var(--grey-6);
--text-small-select-color: var(--grey-25);
--text-bootbox: var(--ui-gray-7);
--text-button-group-color: var(--ui-gray-9);
--text-button-dangerlight-color: var(--ui-error-5);
--text-stepper-active-color: var(--ui-blue-8);
--text-boxselector-header: var(--ui-black);
--border-color: var(--grey-42);
--border-widget-color: var(--grey-43);
@@ -222,33 +224,35 @@
--border-boxselector-color: var(--grey-6);
--border-md-checkbox-color: var(--grey-19);
--border-modal-header-color: var(--grey-45);
--border-navtabs-color: var(--ui-white);
--border-navtabs-color: var(--grey-19);
--border-form-section-title-color: var(--grey-26);
--border-codemirror-cursor-color: var(--black-color);
--border-codemirror-gutters-color: var(--grey-19);
--border-pre-color: var(--grey-43);
--border-blocklist-item-selected-color: var(--grey-46);
--border-pagination-span-color: var(--ui-white);
--border-pagination-hover-color: var(--ui-white);
--border-pagination-color: var(--grey-19);
--border-pagination-span-color: var(--grey-19);
--border-pagination-hover-color: var(--grey-19);
--border-multiselect-button-color: var(--grey-48);
--border-searchbar-color: var(--grey-10);
--border-panel-color: var(--white-color);
--border-input-sm-color: var(--grey-47);
--border-daterangepicker-color: var(--grey-19);
--border-calendar-table: var(--white-color);
--border-daterangepicker: var(--grey-19);
--border-pre-next-month: var(--black-color);
--border-daterangepicker-after: var(--white-color);
--border-tooltip-color: var(--grey-47);
--border-modal: 0px;
--border-sortbutton: var(--grey-8);
--border-bootbox: var(--ui-gray-5);
--border-blocklist: var(--ui-gray-5);
--border-widget: var(--ui-gray-5);
--border-nav-container-color: var(--ui-gray-5);
--border-stepper-color: var(--ui-gray-4);
--hover-sidebar-color: var(--grey-37);
--shadow-box-color: 0 3px 10px -2px var(--grey-50);
--shadow-boxselector-color: 0 3px 10px -2px var(--grey-50);
--blue-color: var(--blue-13);
--button-close-color: var(--black-color);
--button-opacity: 0.2;
--button-opacity-hover: 0.5;
--bg-boxselector-wrapper-color: var(--grey-6);
--bg-image-multiselect: linear-gradient(var(--blue-2), var(--blue-2));
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
@@ -261,108 +265,95 @@
--text-multiselect-item: var(--grey-30);
--bg-multiselect-helpercontainer: var(--white-color);
--text-input-textarea: var(--white-color);
--sort-icon-muted: var(--ui-gray-5);
--sort-icon-hover: var(--ui-gray-6);
--sort-icon: var(--ui-gray-9);
--border-checkbox: var(--ui-gray-5);
--bg-checkbox: var(--white-color);
--border-searchbar: var(--ui-gray-5);
--bg-button-group: var(--white-color);
--border-button-group: var(--ui-gray-5);
--text-button-group: var(--ui-gray-9);
--bg-service-datatable-thead: var(--grey-23);
--bg-inner-datatable-thead: var(--grey-23);
--bg-service-datatable-tbody: var(--grey-24);
}
/* Dark Theme */
[theme='dark'] {
--bg-body-color: var(--grey-2);
--bg-btn-default-color: var(--grey-3);
--bg-blocklist-hover-color: var(--ui-gray-iron-10);
--bg-boxselector-color: var(--ui-gray-iron-10);
--bg-blocklist-item-selected-color: var(--grey-3);
:root[theme='dark'] {
--bg-card-color: var(--grey-1);
--bg-checkbox-border-color: var(--grey-8);
--bg-code-color: var(--ui-gray-warm-11);
--bg-codemirror-color: var(--ui-gray-warm-11);
--bg-codemirror-gutters-color: var(--ui-gray-warm-8);
--bg-codemirror-selected-color: var(--ui-gray-warm-7);
--bg-dropdown-menu-color: var(--ui-gray-7);
--bg-main-color: var(--grey-2);
--bg-widget-color: var(--ui-gray-warm-10);
--bg-body-color: var(--grey-2);
--bg-checkbox-border-color: var(--grey-8);
--bg-sidebar-color: var(--grey-3);
--bg-widget-color: var(--grey-1);
--bg-widget-header-color: var(--grey-1);
--bg-widget-table-color: var(--ui-gray-warm-9);
--bg-widget-table-color: var(--grey-1);
--bg-header-color: var(--grey-2);
--bg-hover-table-color: var(--grey-3);
--bg-switch-box-color: var(--grey-53);
--bg-input-group-addon-color: var(--grey-3);
--bg-btn-default-color: var(--grey-3);
--bg-blocklist-hover-color: var(--grey-3);
--bg-boxselector-color: var(--grey-54);
--bg-table-color: var(--grey-1);
--bg-md-checkbox-color: var(--grey-31);
--bg-form-control-disabled-color: var(--grey-3);
--bg-modal-content-color: var(--grey-1);
--bg-navtabs-color: var(--ui-gray-warm-11);
--bg-code-color: var(--red-4);
--bg-navtabs-color: var(--grey-3);
--bg-navtabs-hover-color: var(--grey-3);
--bg-table-selected-color: var(--grey-3);
--bg-codemirror-color: var(--grey-2);
--bg-codemirror-gutters-color: var(--grey-2);
--bg-dropdown-menu-color: var(--grey-1);
--bg-log-viewer-color: var(--grey-2);
--bg-log-line-selected-color: var(--grey-3);
--bg-pre-color: var(--grey-2);
--bg-blocklist-item-selected-color: var(--grey-3);
--bg-progress-color: var(--grey-3);
--bg-pagination-color: var(--grey-3);
--bg-pagination-span-color: var(--grey-1);
--bg-pagination-hover-color: var(--grey-3);
--bg-pagination-span-color: var(--grey-3);
--bg-pagination-hover-color: var(--grey-4);
--bg-ui-select-hover-color: var(--grey-3);
--bg-motd-body-color: var(--grey-1);
--bg-item-highlighted-color: var(--grey-2);
--bg-item-highlighted-null-color: var(--grey-2);
--bg-row-header-color: var(--grey-2);
--bg-multiselect-button-color: var(--grey-3);
--bg-image-multiselect-button: none !important;
--bg-multiselect-checkbox-color: var(--grey-3);
--bg-sidebar-wrapper-color: var(--grey-1);
--bg-panel-body-color: var(--grey-1);
--bg-input-group-addon-color: var(--grey-3);
--bg-tooltip-color: var(--grey-3);
--bg-input-sm-color: var(--grey-1);
--bg-service-datatable-thead: var(--grey-1);
--bg-inner-datatable-thead: var(--grey-1);
--bg-app-datatable-thead: var(--grey-1);
--bg-service-datatable-tbody: var(--grey-1);
--bg-app-datatable-tbody: var(--grey-1);
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
--bg-codemirror-selected-color: var(--grey-3);
--bg-sidebar-header-color: var(--grey-1);
--bg-multiselect-color: var(--grey-1);
--bg-daterangepicker-color: var(--grey-3);
--bg-calendar-color: var(--grey-3);
--bg-calendar-table-color: var(--grey-3);
--bg-daterangepicker-end-date: var(--grey-4);
--bg-daterangepicker-hover: var(--grey-4);
--bg-daterangepicker-in-range: var(--ui-gray-warm-11);
--bg-daterangepicker-in-range: var(--grey-2);
--bg-daterangepicker-active: var(--blue-14);
--bg-tooltip-color: var(--grey-3);
--bg-input-autofill-color: var(--grey-2);
--bg-btn-default-hover-color: var(--grey-3);
--bg-btn-focus: var(--grey-3);
--bg-boxselector-disabled-color: var(--grey-54);
--bg-small-select-color: var(--grey-2);
--bg-app-datatable-thead: var(--grey-1);
--bg-app-datatable-tbody: var(--grey-1);
--bg-stepper-item-active: var(--grey-1);
--bg-stepper-item-counter: var(--grey-7);
--bg-sortbutton-color: var(--grey-1);
--bg-dashboard-item: var(--grey-3);
--bg-searchbar: var(--ui-grey-warm-11);
--bg-inputbox: var(--grey-2);
--bg-dropdown-hover: var(--grey-3);
--bg-webeditor-color: var(--ui-gray-warm-9);
--bg-button-group-color: var(--ui-black);
--bg-pagination-disabled-color: var(--grey-1);
--bg-nav-container-color: var(--ui-gray-iron-10);
--bg-code-script-color: var(--ui-gray-warm-11);
--bg-nav-tabs-active-color: var(--ui-gray-warm-9);
--bg-stepper-color: var(--ui-gray-iron-10);
--bg-stepper-active-color: var(--ui-blue-8);
--text-main-color: var(--white-color);
--text-body-color: var(--white-color);
--text-sidebar-title-color: var(--grey-8);
--text-widget-header-color: var(--white-color);
--text-form-control-color: var(--white-color);
--text-muted-color: var(--grey-8);
--text-link-color: var(--blue-9);
--text-link-hover-color: var(--blue-2);
--text-input-group-addon-color: var(--grey-8);
--text-btn-default-color: var(--grey-8);
--text-blocklist-hover-color: var(--white-color);
--text-dashboard-item-color: var(--blue-2);
--text-danger-color: var(--red-4);
--text-code-color: var(--white-color);
--text-navtabs-color: var(--white-color);
--text-form-section-title-color: var(--grey-8);
--text-cm-default-color: var(--blue-10);
--text-cm-meta-color: var(--white-color);
--text-cm-string-color: var(--red-5);
@@ -377,26 +368,23 @@
--text-blocklist-item-selected-color: var(--white-color);
--text-progress-bar-color: var(--white-color);
--text-pagination-color: var(--white-color);
--text-pagination-span-color: var(--ui-white);
--text-pagination-span-hover-color: var(--ui-white);
--text-pagination-span-color: var(--blue-2);
--text-pagination-span-hover-color: var(--white-color);
--text-ui-select-color: var(--white-color);
--text-ui-select-hover-color: var(--white-color);
--text-summary-color: var(--white-color);
--text-multiselect-button-color: var(--white-color);
--text-multiselect-item-color: var(--white-color);
--text-sidebar-list-color: var(--white-color);
--text-boxselector-wrapper-color: var(--white-color);
--text-tooltip-color: var(--white-color);
--text-rzslider-color: var(--white-color);
--text-rzslider-limit-color: var(--white-color);
--text-daterangepicker-end-date: var(--grey-7);
--text-daterangepicker-in-range: var(--white-color);
--text-daterangepicker-active: var(--white-color);
--text-tooltip-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-input-autofill-color: var(--grey-8);
--text-button-hover-color: var(--white-color);
--text-small-select-color: var(--grey-7);
--text-bootbox: var(--white-color);
--text-button-group-color: var(--ui-white);
--text-button-dangerlight-color: var(--ui-error-7);
--text-stepper-active-color: var(--ui-white);
--text-boxselector-header: var(--ui-white);
--border-color: var(--grey-3);
--border-widget-color: var(--grey-1);
@@ -412,27 +400,28 @@
--border-md-checkbox-color: var(--grey-41);
--border-modal-header-color: var(--grey-1);
--border-navtabs-color: var(--grey-38);
--border-form-section-title-color: var(--grey-8);
--border-codemirror-cursor-color: var(--white-color);
--border-codemirror-gutters-color: var(--grey-26);
--border-pre-color: var(--grey-3);
--border-blocklist-item-selected-color: var(--grey-38);
--border-pagination-span-color: var(--grey-1);
--border-pagination-color: var(--grey-3);
--border-pagination-span-color: var(--grey-3);
--border-pagination-hover-color: var(--grey-3);
--border-pagination-hover-color: var(--grey-3);
--border-multiselect-button-color: var(--grey-3);
--border-searchbar-color: var(--grey-1);
--border-panel-color: var(--grey-2);
--border-input-sm-color: var(--grey-3);
--border-daterangepicker-color: var(--grey-3);
--border-calendar-table: var(--grey-3);
--border-daterangepicker: var(--grey-4);
--border-pre-next-month: var(--white-color);
--border-daterangepicker-after: var(--grey-3);
--border-tooltip-color: var(--grey-3);
--border-modal: 0px;
--border-sortbutton: var(--grey-3);
--border-bootbox: var(--ui-gray-9);
--border-blocklist: var(--ui-gray-9);
--border-widget: var(--ui-gray-9);
--border-pagination-color: var(--grey-1);
--border-nav-container-color: var(--ui-gray-neutral-8);
--border-stepper-color: var(--ui-gray-warm-9);
--hover-sidebar-color: var(--grey-3);
--blue-color: var(--blue-2);
--button-close-color: var(--white-color);
--button-opacity: 0.6;
@@ -450,24 +439,17 @@
--text-multiselect-item: var(--white-color);
--bg-multiselect-helpercontainer: var(--grey-1);
--text-input-textarea: var(--grey-1);
--sort-icon-muted: var(--ui-gray-7);
--sort-icon-hover: var(--ui-gray-6);
--sort-icon: var(--ui-gray-3);
--border-checkbox: var(--ui-gray-5);
--bg-checkbox: var(--white-color);
--border-searchbar: var(--ui-gray-warm-9);
--bg-button-group: var(--white-color);
--border-button-group: var(--ui-gray-5);
--text-button-group: var(--ui-gray-9);
--bg-service-datatable-thead: var(--grey-1);
--bg-inner-datatable-thead: var(--grey-1);
--bg-service-datatable-tbody: var(--grey-1);
}
/* High Contrast Theme */
[theme='highcontrast'] {
:root[theme='highcontrast'] {
--bg-card-color: var(--black-color);
--bg-main-color: var(--black-color);
--bg-body-color: var(--black-color);
--bg-checkbox-border-color: var(--grey-8);
--bg-sidebar-color: var(--black-color);
--bg-widget-color: var(--black-color);
--bg-widget-header-color: var(--black-color);
--bg-widget-table-color: var(--black-color);
@@ -475,29 +457,31 @@
--bg-hover-table-color: var(--grey-3);
--bg-switch-box-color: var(--grey-53);
--bg-panel-body-color: var(--black-color);
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
--bg-dropdown-menu-color: var(--black-color);
--bg-codemirror-selected-color: var(--grey-3);
--bg-row-header-color: var(--black-color);
--bg-sidebar-wrapper-color: var(--black-color);
--bg-motd-body-color: var(--black-color);
--bg-blocklist-hover-color: var(--black-color);
--bg-blocklist-item-selected-color: var(--black-color);
--bg-input-group-addon-color: var(--grey-3);
--bg-input-group-addon-color: var(--grey-1);
--bg-table-color: var(--black-color);
--bg-codemirror-gutters-color: var(--ui-gray-warm-11);
--bg-codemirror-gutters-color: var(--black-color);
--bg-codemirror-color: var(--black-color);
--bg-codemirror-selected-color: var(--grey-3);
--bg-log-viewer-color: var(--black-color);
--bg-log-line-selected-color: var(--grey-3);
--bg-sidebar-header-color: var(--black-color);
--bg-modal-content-color: var(--black-color);
--bg-form-control-disabled-color: var(--grey-1);
--bg-input-sm-color: var(--black-color);
--bg-item-highlighted-color: var(--black-color);
--bg-service-datatable-thead: var(--black-color);
--bg-inner-datatable-thead: var(--black-color);
--bg-app-datatable-thead: var(--black-color);
--bg-service-datatable-tbody: var(--black-color);
--bg-app-datatable-tbody: var(--black-color);
--bg-pagination-color: var(--grey-3);
--bg-pagination-span-color: var(--ui-black);
--bg-pagination-span-color: var(--grey-3);
--bg-multiselect-color: var(--grey-1);
--bg-daterangepicker-color: var(--black-color);
--bg-calendar-color: var(--black-color);
@@ -511,9 +495,11 @@
--bg-pre-color: var(--grey-2);
--bg-navtabs-hover-color: var(--grey-3);
--bg-btn-default-color: var(--black-color);
--bg-code-color: var(--red-4);
--bg-navtabs-color: var(--black-color);
--bg-input-autofill-color: var(--black-color);
--bg-code-color: var(--ui-black);
--bg-code-color: var(--grey-2);
--bg-navtabs-color: var(--grey-2);
--bg-navtabs-hover-color: var(--grey-3);
--bg-btn-default-hover-color: var(--grey-3);
--bg-btn-default-color: var(--black-color);
@@ -521,23 +507,15 @@
--bg-boxselector-color: var(--black-color);
--bg-boxselector-disabled-color: var(--black-color);
--bg-small-select-color: var(--black-color);
--bg-app-datatable-thead: var(--black-color);
--bg-app-datatable-tbody: var(--black-color);
--bg-stepper-item-active: var(--black-color);
--bg-stepper-item-counter: var(--grey-3);
--bg-sortbutton-color: var(--grey-1);
--bg-inputbox: var(--black-color);
--bg-searchbar: var(--black-color);
--bg-dropdown-hover: var(--black-color);
--bg-webeditor-color: var(--ui-gray-warm-9);
--bg-pagination-disabled-color: var(--ui-black);
--bg-pagination-hover-color: var(--ui-black);
--bg-nav-container-color: var(--ui-black);
--bg-code-script-color: var(--ui-black);
--bg-nav-tabs-active-color: var(--ui-black);
--bg-stepper-active-color: var(--ui-blue-8);
--bg-stepper-color: var(--ui-black);
--text-main-color: var(--white-color);
--text-body-color: var(--white-color);
--text-sidebar-title-color: var(--grey-8);
--text-widget-header-color: var(--white-color);
--text-link-color: var(--blue-9);
--text-link-hover-color: var(--blue-9);
@@ -547,6 +525,7 @@
--text-blocklist-hover-color: var(--blue-11);
--text-boxselector-wrapper-color: var(--white-color);
--text-dashboard-item-color: var(--blue-12);
--text-form-section-title-color: var(--white-color);
--text-muted-color: var(--white-color);
--text-tooltip-color: var(--white-color);
--text-blocklist-item-selected-color: var(--blue-9);
@@ -558,10 +537,12 @@
--text-rzslider-color: var(--white-color);
--text-rzslider-limit-color: var(--white-color);
--text-pagination-color: var(--white-color);
--text-daterangepicker-end-date: var(--ui-white);
--text-daterangepicker-end-date: var(--grey-7);
--text-daterangepicker-in-range: var(--white-color);
--text-daterangepicker-active: var(--white-color);
--text-sidebar-list-color: var(--white-color);
--text-ui-select-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-json-tree-color: var(--white-color);
--text-json-tree-leaf-color: var(--white-color);
--text-json-tree-branch-preview-color: var(--white-color);
@@ -570,12 +551,10 @@
--text-input-autofill-color: var(--white-color);
--text-navtabs-color: var(--white-color);
--text-button-hover-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-small-select-color: var(--white-color);
--text-pagination-span-color: var(--ui-white);
--text-bootbox: var(--white-color);
--text-pagination-span-hover-color: var(--ui-white);
--text-stepper-active-color: var(--ui-white);
--text-boxselector-header: var(--ui-white);
--text-multiselect-item-color: var(--white-color);
--text-pagination-span-color: var(--blue-2);
--border-color: var(--grey-55);
--border-widget-color: var(--white-color);
@@ -586,28 +565,28 @@
--border-datatable-top-color: var(--grey-55);
--border-sidebar-high-contrast: 1px solid var(--blue-9);
--border-code-high-contrast: 1px solid var(--white-color);
--border-boxselector-wrapper: 3px solid var(--blue-2);
--border-boxselector-wrapper-hover: 3px solid var(--blue-8);
--border-panel-color: var(--white-color);
--border-input-group-addon-color: var(--grey-54);
--border-modal-header-color: var(--grey-3);
--border-input-sm-color: var(--white-color);
--border-pagination-color: var(--grey-1);
--border-pagination-span-color: var(--grey-1);
--border-pagination-color: var(--grey-3);
--border-pagination-span-color: var(--grey-3);
--border-daterangepicker-color: var(--white-color);
--border-calendar-table: var(--black-color);
--border-daterangepicker: var(--black-color);
--border-pre-next-month: var(--white-color);
--border-daterangepicker-after: var(--black-color);
--border-tooltip-color: var(--white-color);
--border-pre-color: var(--grey-3);
--border-codemirror-cursor-color: var(--white-color);
--border-modal: 1px solid var(--white-color);
--border-blocklist-color: var(--white-color);
--border-sortbutton: var(--black-color);
--border-bootbox: var(--black-color);
--border-blocklist: var(--white-color);
--border-widget: var(--white-color);
--border-nav-container-color: var(--ui-white);
--border-stepper-color: var(--ui-gray-warm-9);
--hover-sidebar-color: var(--blue-9);
--hover-sidebar-color: var(--black-color);
--shadow-box-color: none;
--shadow-boxselector-color: none;
@@ -626,14 +605,4 @@
--text-cm-meta-color: var(--white-color);
--text-cm-string-color: var(--red-7);
--text-progress-bar-color: var(--black-color);
--sort-icon-muted: var(--ui-gray-7);
--sort-icon-hover: var(--ui-gray-6);
--sort-icon: var(--ui-gray-3);
--border-checkbox: var(--ui-gray-5);
--bg-checkbox: var(--white-color);
--border-searchbar: var(--ui-gray-5);
--bg-button-group: var(--white-color);
--border-button-group: var(--ui-gray-5);
--text-button-group: var(--ui-gray-9);
}
+42 -100
View File
@@ -1,7 +1,7 @@
/* Overide Vendor CSS */
.form-control {
background-color: var(--bg-main-color) !important;
border: 1px solid var(--border-form-control-color);
background-color: var(--bg-inputbox);
color: var(--text-form-control-color);
}
@@ -10,7 +10,7 @@
}
.table > thead > tr > th {
border-bottom: 1px solid var(--border-table-color);
border-bottom: 2px solid var(--border-table-color);
}
.table-hover > tbody > tr:hover {
@@ -31,18 +31,33 @@
border-top: 1px solid var(--border-table-top-color);
}
a {
color: var(--text-link-color);
}
a:hover,
a:focus {
color: var(--text-link-hover-color);
}
.input-group-addon {
color: var(--text-input-group-addon-color);
background-color: var(--bg-input-group-addon-color);
border: 1px solid var(--border-input-group-addon-color);
}
.btn-default {
color: var(--text-btn-default-color);
background-color: var(--bg-btn-default-color);
border-color: var(--border-btn-default-color);
}
.text-danger {
color: var(--ui-error-9);
color: var(--text-danger-color);
}
.table .table {
background-color: initial;
background-color: var(--bg-table-color);
}
.table-bordered {
@@ -146,22 +161,12 @@ code {
.CodeMirror-gutters {
background: var(--bg-codemirror-gutters-color);
border-right: 0px;
}
.CodeMirror-linenumber {
text-align: left;
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 20px;
border-right: 1px solid var(--border-codemirror-gutters-color);
}
.CodeMirror {
background: var(--bg-codemirror-color);
color: var(--text-codemirror-color);
border-radius: 8px;
}
.CodeMirror-selected {
@@ -190,7 +195,6 @@ code {
.dropdown-menu {
background: var(--bg-dropdown-menu-color);
border-radius: 8px;
}
.dropdown-menu > li > a {
@@ -199,7 +203,6 @@ code {
pre {
border: 1px solid var(--border-pre-color);
border-radius: 8px;
background-color: var(--bg-pre-color);
color: var(--text-pre-color);
}
@@ -219,27 +222,6 @@ json-tree .branch-preview {
background-color: var(--bg-progress-color);
}
.ui-select-search,
.ui-select-toggle {
height: 30px;
min-width: 260px;
padding: 4px 12px;
}
.ui-select-toggle {
justify-content: flex-start !important;
}
.ui-select-match-text {
display: flex;
flex-direction: row-reverse;
align-items: center;
}
.ui-select-match-text > a {
verical-align: middle;
}
.ui-select-bootstrap .ui-select-choices-row > span {
color: var(--text-ui-select-color);
}
@@ -261,10 +243,6 @@ json-tree .branch-preview {
.panel {
border: 1px solid var(--border-panel-color);
background-color: var(--bg-panel-body-color);
border-radius: 8px;
-webkit-box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05);
-moz-box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05);
}
.theme-information .col-sm-12 {
@@ -294,15 +272,8 @@ json-tree .branch-preview {
.rzslider .rz-bubble.rz-limit {
color: var(--text-rzslider-limit-color);
}
.rz-bubble.rz-limit.rz-ceil {
position: absolute;
right: 0;
left: auto !important;
top: -26px;
}
input,
button,
select,
textarea {
background: var(--text-input-textarea);
@@ -382,60 +353,31 @@ input:-webkit-autofill {
-webkit-text-fill-color: var(--text-input-autofill-color) !important;
}
.btn:hover {
color: var(--text-button-hover-color);
}
.btn-default:hover {
background-color: var(--bg-btn-default-hover-color);
}
.btn-primary:hover {
color: var(--white-color) !important;
}
.btn-danger:hover {
color: var(--white-color);
}
/* Overide Vendor CSS */
.btn-link:hover {
color: var(--text-link-hover-color) !important;
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
pointer-events: none;
touch-action: none;
}
.multiSelect.inlineBlock button {
margin: 0;
}
.nav-tabs > li.active > a {
border: 0px;
}
.label-default {
line-height: 11px;
}
/* Code Script Style */
.code-script {
background-color: var(--bg-code-script-color);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding: 5px;
}
.nav-container {
border: 1px solid var(--border-nav-container-color);
background-color: var(--bg-nav-container-color);
border-radius: 8px;
padding: 10px;
}
.nav-tabs > li {
background-color: var(--bg-nav-tabs-active-color);
border-top-right-radius: 8px;
}
/* Code Script Style */
.code-script {
background-color: var(--bg-code-script-color);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding: 5px;
}
.nav-container {
border: 1px solid var(--border-nav-container-color);
background-color: var(--bg-nav-container-color);
border-radius: 8px;
padding: 10px;
}
.nav-tabs > li {
background-color: var(--bg-nav-tabs-active-color);
border-top-right-radius: 8px;
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.
Binary file not shown.
-11
View File
@@ -1,11 +0,0 @@
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
<g clip-path="url(#clip0_9538_418895)">
<path d="M15.0049 13.2509L8.75488 20.7509H14.3799L13.7549 25.7509L20.0049 18.2509H14.3799L15.0049 13.2509Z" stroke="#0086C9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_9538_418895">
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 719 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
<path d="M9.32758 23.1544V23.0281C9.32758 21.9669 9.32758 21.4364 9.53409 21.0311C9.71574 20.6746 10.0056 20.3847 10.3621 20.2031C10.7674 19.9966 11.298 19.9966 12.3591 19.9966H16.4011C17.4622 19.9966 17.9928 19.9966 18.3981 20.2031C18.7546 20.3847 19.0444 20.6746 19.2261 21.0311C19.4326 21.4364 19.4326 21.9669 19.4326 23.0281V23.1544M9.32758 23.1544C8.62997 23.1544 8.06445 23.7199 8.06445 24.4175C8.06445 25.1151 8.62997 25.6806 9.32758 25.6806C10.0252 25.6806 10.5907 25.1151 10.5907 24.4175C10.5907 23.7199 10.0252 23.1544 9.32758 23.1544ZM19.4326 23.1544C18.735 23.1544 18.1695 23.7199 18.1695 24.4175C18.1695 25.1151 18.735 25.6806 19.4326 25.6806C20.1302 25.6806 20.6957 25.1151 20.6957 24.4175C20.6957 23.7199 20.1302 23.1544 19.4326 23.1544ZM14.3801 23.1544C13.6825 23.1544 13.117 23.7199 13.117 24.4175C13.117 25.1151 13.6825 25.6806 14.3801 25.6806C15.0777 25.6806 15.6432 25.1151 15.6432 24.4175C15.6432 23.7199 15.0777 23.1544 14.3801 23.1544ZM14.3801 23.1544V16.8388M10.5907 16.8388H18.1695C18.758 16.8388 19.0523 16.8388 19.2844 16.7426C19.5939 16.6144 19.8398 16.3685 19.968 16.059C20.0641 15.8269 20.0641 15.5326 20.0641 14.9441C20.0641 14.3555 20.0641 14.0613 19.968 13.8291C19.8398 13.5196 19.5939 13.2737 19.2844 13.1455C19.0523 13.0494 18.758 13.0494 18.1695 13.0494H10.5907C10.0022 13.0494 9.70789 13.0494 9.47576 13.1455C9.16626 13.2737 8.92036 13.5196 8.79217 13.8291C8.69602 14.0613 8.69602 14.3555 8.69602 14.9441C8.69602 15.5326 8.69602 15.8269 8.79217 16.059C8.92036 16.3685 9.16626 16.6144 9.47576 16.7426C9.70789 16.8388 10.0022 16.8388 10.5907 16.8388Z" stroke="#0086C9" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M2.01501 12H23M23 12L16.0001 5M23 12L16.0001 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 252 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M49.9999 87.5001L49.9999 12.5004M49.9999 87.5001L67.6776 69.8224M49.9999 87.5001L32.3222 69.8224M49.9999 12.5004L32.3223 30.178M49.9999 12.5004L67.6776 30.1781" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 368 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M49.9999 9.22363V92.557M79.4627 21.4275L20.5371 80.3531M91.6666 50.8903H8.33325M79.4627 80.3531L20.5371 21.4275" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 320 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M39.5835 91.6665C56.8424 91.6665 70.8335 77.6754 70.8335 60.4165C70.8335 43.1576 56.8424 29.1665 39.5835 29.1665C22.3246 29.1665 8.3335 43.1576 8.3335 60.4165C8.3335 77.6754 22.3246 91.6665 39.5835 91.6665Z" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20.8335 62.4998C20.8335 50.9939 30.1609 41.6665 41.6668 41.6665" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M71.6499 9.34864V4.1665M85.4165 14.7403L89.0808 11.076M85.2593 42.1688L88.9237 45.8332M57.8308 14.7403L54.1665 11.076M90.651 28.3498H95.8332M63.0022 36.9975L74.9998 24.9998" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 853 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.21426" y="0.5" width="15" height="15" rx="7.5" fill="#0086C9"/>
<path d="M12.0474 5.5L7.4641 10.0833L5.38077 8" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="1.21426" y="0.5" width="15" height="15" rx="7.5" stroke="#0086C9"/>
</svg>

Before

Width:  |  Height:  |  Size: 390 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M37.1986 10.3367C20.4498 15.7383 8.33334 31.4541 8.33334 49.9999C8.33334 73.0118 26.9881 91.6666 50 91.6666C73.0119 91.6666 91.6667 73.0118 91.6667 49.9999C91.6667 31.4541 79.5502 15.7383 62.8014 10.3367" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 412 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M22.7 13.5L20.7005 11.5L18.6999 13.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C15.3019 3 18.1885 4.77814 19.7545 7.42909M12 7V12L15 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 382 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>

Before

Width:  |  Height:  |  Size: 284 B

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