Compare commits

..

39 Commits

Author SHA1 Message Date
congs 8c1977e0aa fix(permission): EE-3772 Team leaders are able to see all environments (#7330)
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
2022-07-26 11:02:33 +12:00
Matt Hook c47fd9f9ed bump version to 2.14.2 (#7304) 2022-07-22 10:55:17 +12:00
LP B 767d1d1970 fix(edge): pagination on create/edit edge jobs/groups [EE-3219] (#7297) 2022-07-21 18:47:20 +02:00
itsconquest ef81e5c0e0 fix(auth): correctly calculate LDAP teamsync [EE-3704] (#7292) 2022-07-21 21:29:27 +12:00
Prabhat Khera 234b7a3d5e fix(kubeconfig): fix kubeconfig url EE-3455 (#7283) 2022-07-21 16:59:48 +12:00
itsconquest af49305e64 fix(TLS): remove file type validation [EE-3672] (#7279) 2022-07-21 16:25:19 +12:00
LP B d181d1251c fix(app/mustache): reuse mustache variables in templates [EE-3689] (#7287) 2022-07-19 15:38:10 +02:00
LP B 5f7db66e95 fix(app/templates): handle special characters in mustache templates [EE-3708] (#7289) 2022-07-19 14:05:31 +02:00
Dmitry Salakhov 17378bdef6 fix(users): admin can change password with any auth method (#7269) [EE-3671] 2022-07-19 11:26:43 +12:00
Oscar Zhou 010542ac1e fix(setting): update the por switch field component property (#7258) 2022-07-15 08:27:53 +12:00
Matt Hook 1bb253479a bump version to 2.14.1 (#7237)
Test / test-client (push) Has been cancelled
2022-07-12 11:03:49 +12:00
Dakota Walsh f0a13a2ad1 fix(migration): close the database before running backups EE-3627 (#7217)
* fix(migration): close the database before running backups

On certain filesystems, particuarly NTFS when a network mounted windows
file server is used to store portainer's database, you are unable to
copy the database while it is open. To fix this we simply close the
database and then re-open it after a backup.

* handle close and open errors

* dont return error on nil
2022-07-08 21:04:55 +12:00
Matt Hook f9b28aa0a1 fix(compose): use docker-compose plugin directly [EE-3631] (#7201)
* use simplifed method of calling compose directly with new compose wrapper

* download compose binary to docker-compose

* update to newer wrapper that fixes -H issue

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

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

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

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

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

After

Width:  |  Height:  |  Size: 86 KiB

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

Before

Width:  |  Height:  |  Size: 719 B

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

Before

Width:  |  Height:  |  Size: 2.0 KiB

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

Before

Width:  |  Height:  |  Size: 252 B

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

Before

Width:  |  Height:  |  Size: 368 B

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

Before

Width:  |  Height:  |  Size: 320 B

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

Before

Width:  |  Height:  |  Size: 853 B

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

Before

Width:  |  Height:  |  Size: 390 B

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

Before

Width:  |  Height:  |  Size: 412 B

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

Before

Width:  |  Height:  |  Size: 382 B

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

Before

Width:  |  Height:  |  Size: 284 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M87.7688 12.2312L66.9354 33.0645M66.9354 33.0645H87.7688M66.9354 33.0645V12.2312M12.2964 12.2964L22.7131 22.7131L33.1298 33.1298M33.1298 33.1298V12.2964M33.1298 33.1298H12.2964M87.4042 87.4042L66.5709 66.5709M66.5709 66.5709L66.5709 87.4042M66.5709 66.5709L87.4042 66.5709M12.6353 87.3647L33.4686 66.5314M33.4686 66.5314H12.6353M33.4686 66.5314V87.3647" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 561 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M11.6992 13.533L6.96646 16.1623M6.96646 16.1623L2.2337 13.533M6.96646 16.1623L6.96647 21.4519M11.9776 18.4221V13.9026M11.9776 18.4221C11.9776 18.6129 11.9776 18.7083 11.9495 18.7934C11.9246 18.8686 11.884 18.9377 11.8303 18.996C11.7696 19.0619 11.6862 19.1082 11.5194 19.2009L7.39912 21.4899C7.24121 21.5777 7.16226 21.6215 7.07864 21.6387C7.00464 21.6539 6.92831 21.6539 6.85431 21.6387C6.77069 21.6215 6.69174 21.5777 6.53383 21.4899L2.41355 19.2009C2.24678 19.1082 2.16339 19.0619 2.10267 18.996C2.04895 18.9377 2.0083 18.8686 1.98343 18.7934C1.95532 18.7083 1.95532 18.6129 1.95532 18.4221V13.9026C1.95532 13.7118 1.95532 13.6164 1.98343 13.5313C2.0083 13.4561 2.04895 13.387 2.10267 13.3287C2.16339 13.2628 2.24677 13.2165 2.41355 13.1238L6.53383 10.8348C6.69174 10.747 6.77069 10.7032 6.85431 10.686C6.92831 10.6708 7.00464 10.6708 7.07864 10.686C7.16226 10.7032 7.24121 10.747 7.39912 10.8348L11.5194 13.1238C11.6862 13.2165 11.7696 13.2628 11.8303 13.3287C11.884 13.387 11.9246 13.4561 11.9495 13.5313C11.9776 13.6164 11.9776 13.7118 11.9776 13.9026M11.9776 18.4221C11.9776 18.6129 11.9777 18.7083 12.0058 18.7934C12.0306 18.8686 12.0713 18.9377 12.125 18.996C12.1857 19.0619 12.2691 19.1082 12.4359 19.2009L16.5562 21.4899C16.7141 21.5777 16.793 21.6215 16.8766 21.6387C16.9506 21.6539 17.027 21.6539 17.101 21.6387C17.1846 21.6215 17.2635 21.5777 17.4215 21.4899L21.5417 19.2009C21.7085 19.1082 21.7919 19.0619 21.8526 18.996C21.9063 18.9377 21.947 18.8686 21.9719 18.7934C22 18.7083 22 18.6129 22 18.4221V13.9026C22 13.7118 22 13.6164 21.9719 13.5313C21.947 13.4561 21.9063 13.387 21.8526 13.3287C21.7919 13.2628 21.7085 13.2165 21.5417 13.1238L17.4215 10.8348C17.2635 10.747 17.1846 10.7032 17.101 10.686C17.027 10.6708 16.9506 10.6708 16.8766 10.686C16.793 10.7032 16.7141 10.747 16.5562 10.8348L12.4359 13.1238C12.2691 13.2165 12.1857 13.2628 12.125 13.3287C12.0713 13.387 12.0306 13.4561 12.0058 13.5313C11.9777 13.6164 11.9776 13.7118 11.9776 13.9026M16.7328 5.20832L12 7.83763M12 7.83763L7.26727 5.20832M12 7.83763L12 13.1272M21.7215 13.533L16.9888 16.1623M16.9888 16.1623L12.256 13.533M16.9888 16.1623L16.9888 21.4519M17.0112 10.0974V5.57786C17.0112 5.38708 17.0112 5.29169 16.9831 5.20661C16.9582 5.13135 16.9176 5.06226 16.8638 5.00397C16.8031 4.93808 16.7197 4.89175 16.553 4.7991L12.4327 2.51006C12.2748 2.42233 12.1958 2.37847 12.1122 2.36127C12.0382 2.34605 11.9619 2.34605 11.8879 2.36127C11.8043 2.37847 11.7253 2.42233 11.5674 2.51006L7.44712 4.7991C7.28034 4.89175 7.19696 4.93808 7.13624 5.00397C7.08252 5.06226 7.04187 5.13135 7.017 5.20661C6.98889 5.29169 6.98889 5.38708 6.98889 5.57786V10.0974C6.98889 10.2882 6.98889 10.3836 7.017 10.4687C7.04187 10.5439 7.08252 10.613 7.13624 10.6713C7.19696 10.7372 7.28034 10.7835 7.44712 10.8762L11.5674 13.1652C11.7253 13.253 11.8043 13.2968 11.8879 13.314C11.9619 13.3292 12.0382 13.3292 12.1122 13.314C12.1958 13.2968 12.2748 13.253 12.4327 13.1652L16.553 10.8762C16.7197 10.7835 16.8031 10.7372 16.8638 10.6713C16.9176 10.613 16.9582 10.5439 16.9831 10.4687C17.0112 10.3836 17.0112 10.2882 17.0112 10.0974Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3817 5.28009C22.1592 5.28009 28.4649 11.5864 28.4649 19.3646C28.4649 27.1428 22.1592 33.4491 14.3817 33.4491C6.60065 33.4526 0.294922 27.1428 0.294922 19.3646C0.294922 11.5864 6.60065 5.28009 14.3817 5.28009Z" fill="#E0F2FE"/>
<path d="M18.059 25.043H10.414C9.9746 25.043 9.56654 24.8721 9.25613 24.5617C8.94573 24.2513 8.77832 23.8397 8.77832 23.4037V15.7615C8.77832 15.3255 8.94922 14.9139 9.25962 14.6035C9.57002 14.293 9.98157 14.1221 10.4175 14.1221H14.24C14.54 14.1221 14.7876 14.3663 14.7876 14.6697C14.7876 14.9732 14.5435 15.2174 14.24 15.2174H10.4175C10.271 15.2174 10.135 15.2732 10.0304 15.3778C9.92577 15.4824 9.86996 15.6185 9.86996 15.765V23.4072C9.86996 23.5537 9.92577 23.6897 10.0304 23.7943C10.135 23.899 10.271 23.9548 10.4175 23.9548H18.059C18.2055 23.9548 18.3415 23.899 18.4462 23.7943C18.5508 23.6897 18.6066 23.5537 18.6066 23.4072V19.5843C18.6066 19.2844 18.8507 19.0367 19.1542 19.0367C19.4576 19.0367 19.7017 19.2809 19.7017 19.5843V23.4072C19.7017 23.8432 19.5308 24.2547 19.2204 24.5652C18.91 24.8756 18.4985 25.0465 18.0625 25.0465L18.059 25.043ZM12.6008 21.7678C12.4578 21.7678 12.3183 21.712 12.2137 21.6074C12.0777 21.4713 12.0254 21.276 12.0707 21.0876L12.6148 18.9042C12.6392 18.8065 12.688 18.7193 12.7578 18.6495L17.9439 13.4629C18.5892 12.8176 19.7087 12.8176 20.3539 13.4629C20.6748 13.7838 20.8527 14.2128 20.8527 14.6663C20.8527 15.1197 20.6748 15.5487 20.3539 15.8696L15.1678 21.0563C15.098 21.126 15.0108 21.1748 14.9132 21.1993L12.7299 21.7469C12.6845 21.7573 12.6427 21.7643 12.5973 21.7643L12.6008 21.7678ZM13.6401 19.3157L13.3507 20.4703L14.5051 20.1808L19.5832 15.1023C19.6983 14.9872 19.761 14.8337 19.761 14.6697C19.761 14.5058 19.6983 14.3523 19.5832 14.2372C19.353 14.007 18.9484 14.007 18.7182 14.2372L13.6401 19.3157Z" fill="#0086C9"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M17 20H16.8C15.1198 20 14.2798 20 13.638 19.673C13.0735 19.3854 12.6146 18.9265 12.327 18.362C12 17.7202 12 16.8802 12 15.2V8.8C12 7.11984 12 6.27976 12.327 5.63803C12.6146 5.07354 13.0735 4.6146 13.638 4.32698C14.2798 4 15.1198 4 16.8 4H17M17 20C17 21.1046 17.8954 22 19 22C20.1046 22 21 21.1046 21 20C21 18.8954 20.1046 18 19 18C17.8954 18 17 18.8954 17 20ZM17 4C17 5.10457 17.8954 6 19 6C20.1046 6 21 5.10457 21 4C21 2.89543 20.1046 2 19 2C17.8954 2 17 2.89543 17 4ZM7 12L17 12M7 12C7 13.1046 6.10457 14 5 14C3.89543 14 3 13.1046 3 12C3 10.8954 3.89543 10 5 10C6.10457 10 7 10.8954 7 12ZM17 12C17 13.1046 17.8954 14 19 14C20.1046 14 21 13.1046 21 12C21 10.8954 20.1046 10 19 10C17.8954 10 17 10.8954 17 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 914 B

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M4 18V17.8C4 16.1198 4 15.2798 4.32698 14.638C4.6146 14.0735 5.07354 13.6146 5.63803 13.327C6.27976 13 7.11984 13 8.8 13H15.2C16.8802 13 17.7202 13 18.362 13.327C18.9265 13.6146 19.3854 14.0735 19.673 14.638C20 15.2798 20 16.1198 20 17.8V18M4 18C2.89543 18 2 18.8954 2 20C2 21.1046 2.89543 22 4 22C5.10457 22 6 21.1046 6 20C6 18.8954 5.10457 18 4 18ZM20 18C18.8954 18 18 18.8954 18 20C18 21.1046 18.8954 22 20 22C21.1046 22 22 21.1046 22 20C22 18.8954 21.1046 18 20 18ZM12 18C10.8954 18 10 18.8954 10 20C10 21.1046 10.8954 22 12 22C13.1046 22 14 21.1046 14 20C14 18.8954 13.1046 18 12 18ZM12 18V8M6 8H18C18.9319 8 19.3978 8 19.7654 7.84776C20.2554 7.64477 20.6448 7.25542 20.8478 6.76537C21 6.39783 21 5.93188 21 5C21 4.06812 21 3.60218 20.8478 3.23463C20.6448 2.74458 20.2554 2.35523 19.7654 2.15224C19.3978 2 18.9319 2 18 2H6C5.06812 2 4.60218 2 4.23463 2.15224C3.74458 2.35523 3.35523 2.74458 3.15224 3.23463C3 3.60218 3 4.06812 3 5C3 5.93188 3 6.39783 3.15224 6.76537C3.35523 7.25542 3.74458 7.64477 4.23463 7.84776C4.60218 8 5.06812 8 6 8Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
<g clip-path="url(#clip0_9538_418898)">
<path d="M18.1297 18.2509H17.3422C17.1084 17.3452 16.6252 16.5233 15.9476 15.8786C15.27 15.2339 14.4252 14.7921 13.509 14.6035C12.5929 14.415 11.6423 14.4871 10.7651 14.8118C9.88797 15.1366 9.11948 15.7008 8.54699 16.4405C7.9745 17.1801 7.62095 18.0655 7.52652 18.9961C7.4321 19.9266 7.60058 20.865 8.01282 21.7046C8.42506 22.5442 9.06453 23.2513 9.85857 23.7456C10.6526 24.2399 11.5694 24.5016 12.5047 24.5009H18.1297C18.9585 24.5009 19.7534 24.1716 20.3394 23.5856C20.9255 22.9995 21.2547 22.2047 21.2547 21.3759C21.2547 20.5471 20.9255 19.7522 20.3394 19.1661C19.7534 18.5801 18.9585 18.2509 18.1297 18.2509Z" stroke="#0086C9" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_9538_418898">
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M66.6667 33.3333L87.5 12.5M87.5 12.5H66.6667M87.5 12.5V33.3333M33.3333 33.3333L12.5 12.5M12.5 12.5L12.5 33.3333M12.5 12.5L33.3333 12.5M33.3333 66.6667L12.5 87.5M12.5 87.5H33.3333M12.5 87.5L12.5 66.6667M66.6667 66.6667L87.5 87.5M87.5 87.5V66.6667M87.5 87.5H66.6667" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>

Before

Width:  |  Height:  |  Size: 472 B

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