Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b998d270 | |||
| 605ff8c1da | |||
| 13f93f4262 | |||
| 16be5ed329 | |||
| c6612898f3 | |||
| 564f34b0ba | |||
| 392fbdb4a7 | |||
| a826c78786 | |||
| a35f0607f1 | |||
| 081d32af0d | |||
| 4cc0b1f567 | |||
| d4da7e1760 | |||
| aced418880 | |||
| 614f42fe5a | |||
| 58736fe93b | |||
| b78330b10d | |||
| eed4a92ca8 | |||
| 0e7468a1e8 | |||
| b807481f1c | |||
| da27de2154 | |||
| 6743e4fbb2 | |||
| b489ffaa63 | |||
| 6e12499d61 | |||
| f7acbe16ba |
@@ -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:
|
||||
|
||||
@@ -7,7 +7,6 @@ storybook-static
|
||||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
.vscode
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
func BoolAddr(b bool) *bool {
|
||||
boolVar := b
|
||||
return &boolVar
|
||||
}
|
||||
@@ -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,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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 +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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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: "",
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 86 KiB |
@@ -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 |
@@ -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 +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 +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 +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 +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 |
@@ -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 +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 +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 +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 |