Compare commits
1 Commits
2.15.1
...
fix/EE-368
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84582d6e1f |
1
.gitignore
vendored
@@ -7,7 +7,6 @@ storybook-static
|
||||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
.vscode
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/url"
|
||||
)
|
||||
|
||||
// GetAgentVersionAndPlatform returns the agent version and platform
|
||||
//
|
||||
// it sends a ping to the agent and parses the version and platform from the headers
|
||||
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
|
||||
httpCli := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
httpCli.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
parsedURL.Scheme = "https"
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
version := resp.Header.Get(portainer.PortainerAgentHeader)
|
||||
if version == "" {
|
||||
return 0, "", errors.New("Version Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return 0, "", errors.New("Agent Platform Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
if agentPlatformNumber == 0 {
|
||||
return 0, "", errors.New("Agent platform is invalid")
|
||||
}
|
||||
|
||||
return portainer.AgentPlatform(agentPlatformNumber), version, nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package build
|
||||
|
||||
// Variables to be set during the build time
|
||||
var BuildNumber string
|
||||
var ImageTag string
|
||||
var NodejsVersion string
|
||||
var YarnVersion string
|
||||
var WebpackVersion string
|
||||
var GoVersion string
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
@@ -744,15 +743,7 @@ func main() {
|
||||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"Version": portainer.APIVersion,
|
||||
"BuildNumber": build.BuildNumber,
|
||||
"ImageTag": build.ImageTag,
|
||||
"NodejsVersion": build.NodejsVersion,
|
||||
"YarnVersion": build.YarnVersion,
|
||||
"WebpackVersion": build.WebpackVersion,
|
||||
"GoVersion": build.GoVersion},
|
||||
).Print("[INFO] [cmd,main] Starting Portainer")
|
||||
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
|
||||
err := server.Start()
|
||||
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -103,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,
|
||||
@@ -919,7 +913,7 @@
|
||||
],
|
||||
"version": {
|
||||
"DB_UPDATING": "false",
|
||||
"DB_VERSION": "61",
|
||||
"DB_VERSION": "60",
|
||||
"INSTANCE_ID": "null"
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
_container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
||||
@@ -155,35 +154,11 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
healthyContainers := 0
|
||||
unhealthyContainers := 0
|
||||
stacks := make(map[string]struct{})
|
||||
gpuUseSet := make(map[string]struct{})
|
||||
gpuUseAll := false
|
||||
for _, container := range containers {
|
||||
if container.State == "exited" {
|
||||
stoppedContainers++
|
||||
} else if container.State == "running" {
|
||||
runningContainers++
|
||||
|
||||
// snapshot GPUs
|
||||
response, err := cli.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gpuOptions *_container.DeviceRequest = nil
|
||||
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
|
||||
gpuOptions = &deviceRequest
|
||||
}
|
||||
}
|
||||
|
||||
if gpuOptions != nil {
|
||||
if gpuOptions.Count == -1 {
|
||||
gpuUseAll = true
|
||||
}
|
||||
for _, id := range gpuOptions.DeviceIDs {
|
||||
gpuUseSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(container.Status, "(healthy)") {
|
||||
@@ -199,14 +174,6 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
}
|
||||
}
|
||||
|
||||
gpuUseList := make([]string, 0, len(gpuUseSet))
|
||||
for gpuUse := range gpuUseSet {
|
||||
gpuUseList = append(gpuUseList, gpuUse)
|
||||
}
|
||||
|
||||
snapshot.GpuUseAll = gpuUseAll
|
||||
snapshot.GpuUseList = gpuUseList
|
||||
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.HealthyContainerCount = healthyContainers
|
||||
|
||||
@@ -85,27 +85,6 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
|
||||
// but does not start containers based on those images.
|
||||
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proxy != nil {
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFile, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Pull(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
|
||||
return errors.Wrap(err, "failed to pull images of the stack")
|
||||
}
|
||||
|
||||
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
||||
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
|
||||
@@ -89,7 +89,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
}
|
||||
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
@@ -101,9 +101,6 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
|
||||
} else {
|
||||
args = append(args, "stack", "deploy", "--with-registry-auth")
|
||||
}
|
||||
if !pullImage {
|
||||
args = append(args, "--resolve-image=never")
|
||||
}
|
||||
|
||||
args = configureFilePaths(args, filePaths)
|
||||
args = append(args, stack.Name)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
10
api/go.sum
@@ -213,9 +213,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
@@ -355,8 +354,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 h1:GFTn2e5AyIoBuK6hXbdVNkuV2m450DQnYmgQDZRU3x8=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f h1:GMIjRVV2LADpJprPG2+8MdRH6XvrFgC7wHm7dFUdOpc=
|
||||
@@ -438,8 +437,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -629,6 +626,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type containerGpusResponse struct {
|
||||
Gpus string `json:"gpus"`
|
||||
}
|
||||
|
||||
// @id dockerContainerGpusInspect
|
||||
// @summary Fetch container gpus data
|
||||
// @description
|
||||
// @description **Access policy**:
|
||||
// @tags docker
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param environmentId path int true "Environment identifier"
|
||||
// @param containerId path int true "Container identifier"
|
||||
// @success 200 {object} containerGpusResponse "Success"
|
||||
// @failure 404 "Environment or container not found"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 500 "Internal server error"
|
||||
// @router /docker/{environmentId}/containers/{containerId}/gpus [get]
|
||||
func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
containerId, err := request.RetrieveRouteVariableValue(r, "containerId")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid container identifier route variable", err)
|
||||
}
|
||||
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to find an environment on request context", err)
|
||||
}
|
||||
|
||||
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
|
||||
|
||||
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to connect to the Docker daemon", err)
|
||||
}
|
||||
|
||||
container, err := cli.ContainerInspect(r.Context(), containerId)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to find the container", err)
|
||||
}
|
||||
|
||||
if container.HostConfig == nil {
|
||||
return httperror.NotFound("Unable to find the container host config", err)
|
||||
}
|
||||
|
||||
gpuOptionsIndex := slices.IndexFunc(container.HostConfig.DeviceRequests, func(opt containertypes.DeviceRequest) bool {
|
||||
if opt.Driver == "nvidia" {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(opt.Capabilities) == 0 || len(opt.Capabilities[0]) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return opt.Capabilities[0][0] == "gpu"
|
||||
})
|
||||
|
||||
if gpuOptionsIndex == -1 {
|
||||
return response.JSON(w, containerGpusResponse{Gpus: "none"})
|
||||
}
|
||||
|
||||
gpuOptions := container.HostConfig.DeviceRequests[gpuOptionsIndex]
|
||||
|
||||
gpu := "all"
|
||||
if gpuOptions.Count != -1 {
|
||||
gpu = "id:" + strings.Join(gpuOptions.DeviceIDs, ",")
|
||||
}
|
||||
|
||||
return response.JSON(w, containerGpusResponse{Gpus: gpu})
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||
func NewHandler(routePrefix string, bouncer *security.RequestBouncer, dockerClientFactory *docker.ClientFactory) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
|
||||
router := h.PathPrefix(routePrefix).Subrouter()
|
||||
router.Use(bouncer.AuthenticatedAccess)
|
||||
|
||||
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/containers"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
dataStore dataservices.DataStore
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
authorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
authorizationService: authorizationService,
|
||||
dataStore: dataStore,
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
|
||||
// endpoints
|
||||
endpointRouter := h.PathPrefix("/{id}").Subrouter()
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
endpointRouter.Use(dockerOnlyMiddleware)
|
||||
|
||||
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dockerClientFactory)
|
||||
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
|
||||
return h
|
||||
}
|
||||
|
||||
func dockerOnlyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
|
||||
endpoint, err := middlewares.FetchEndpoint(request)
|
||||
if err != nil {
|
||||
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !endpointutils.IsDockerEndpoint(endpoint) {
|
||||
errMessage := "environment is not a docker environment"
|
||||
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(rw, request)
|
||||
})
|
||||
}
|
||||
@@ -77,16 +77,13 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
if endpoint.EdgeID == "" {
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
endpoint.EdgeID = edgeIdentifier
|
||||
}
|
||||
|
||||
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
|
||||
if agentPlatformErr != nil {
|
||||
return httperror.BadRequest("agent platform header is not valid", err)
|
||||
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
|
||||
if agentPlatformErr != nil {
|
||||
return httperror.BadRequest("agent platform header is not valid", err)
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
|
||||
version := r.Header.Get(portainer.PortainerAgentHeader)
|
||||
endpoint.Agent.Version = version
|
||||
|
||||
endpoint.LastCheckInDate = time.Now().Unix()
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ var endpointTestCases = []endpointTestCase{
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: 2,
|
||||
},
|
||||
http.StatusForbidden,
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
@@ -194,9 +194,7 @@ func TestWithEndpoints(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -241,7 +239,6 @@ func TestLastCheckInDateIncreases(t *testing.T) {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -358,7 +355,6 @@ func TestEdgeStackStatus(t *testing.T) {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -428,7 +424,6 @@ func TestEdgeJobsResponse(t *testing.T) {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
)
|
||||
|
||||
// @id AgentVersions
|
||||
// @summary List agent versions
|
||||
// @description List all agent versions based on the current user authorizations and query parameters.
|
||||
// @description **Access policy**: restricted
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {array} string "List of available agent versions"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/agent_versions [get]
|
||||
|
||||
func (handler *Handler) agentVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
agentVersions := set.Set[string]{}
|
||||
for _, endpoint := range filteredEndpoints {
|
||||
if endpoint.Agent.Version != "" {
|
||||
agentVersions[endpoint.Agent.Version] = true
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, agentVersions.Keys())
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/agent"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
@@ -24,7 +25,6 @@ type endpointCreatePayload struct {
|
||||
URL string
|
||||
EndpointCreationType endpointCreationEnum
|
||||
PublicURL string
|
||||
Gpus []portainer.Pair
|
||||
GroupID int
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
@@ -142,13 +142,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
payload.PublicURL = publicURL
|
||||
}
|
||||
|
||||
gpus := make([]portainer.Pair, 0)
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Gpus parameter")
|
||||
}
|
||||
payload.Gpus = gpus
|
||||
|
||||
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
|
||||
payload.EdgeCheckinInterval = checkinInterval
|
||||
|
||||
@@ -244,7 +237,6 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
var err error
|
||||
switch payload.EndpointCreationType {
|
||||
case azureEnvironment:
|
||||
return handler.createAzureEndpoint(payload)
|
||||
@@ -257,22 +249,12 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
||||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
var agentVersion string
|
||||
if payload.EndpointCreationType == agentEnvironment {
|
||||
var tlsConfig *tls.Config
|
||||
if payload.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to create TLS configuration", err)
|
||||
}
|
||||
}
|
||||
|
||||
agentPlatform, version, err := agent.GetAgentVersionAndPlatform(payload.URL, tlsConfig)
|
||||
agentPlatform, err := handler.pingAndCheckPlatform(payload)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err}
|
||||
}
|
||||
|
||||
agentVersion = version
|
||||
if agentPlatform == portainer.AgentPlatformDocker {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
} else if agentPlatform == portainer.AgentPlatformKubernetes {
|
||||
@@ -282,7 +264,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType, agentVersion)
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType)
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
@@ -308,7 +290,6 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
Type: portainer.AzureEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
AzureCredentials: credentials,
|
||||
@@ -342,7 +323,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
URL: portainerHost,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
@@ -398,7 +378,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
@@ -433,7 +412,6 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
||||
Type: portainer.KubernetesLocalEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.TLS,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
@@ -454,7 +432,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
@@ -463,7 +441,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.TLS,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
@@ -477,8 +454,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = agentVersion
|
||||
|
||||
err := handler.storeTLSFiles(endpoint, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -572,3 +547,58 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) {
|
||||
httpCli := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
httpCli.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
url.Scheme = "https"
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return 0, errors.New("Agent Platform Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if agentPlatformNumber == 0 {
|
||||
return 0, errors.New("Agent platform is invalid")
|
||||
}
|
||||
|
||||
return portainer.AgentPlatform(agentPlatformNumber), nil
|
||||
}
|
||||
|
||||
@@ -4,14 +4,24 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
EdgeDeviceFilterAll = "all"
|
||||
EdgeDeviceFilterTrusted = "trusted"
|
||||
EdgeDeviceFilterUntrusted = "untrusted"
|
||||
EdgeDeviceFilterNone = "none"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,6 +29,8 @@ const (
|
||||
EdgeDeviceIntervalAdd = 20
|
||||
)
|
||||
|
||||
var endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
|
||||
// @id EndpointList
|
||||
// @summary List environments(endpoints)
|
||||
// @description List all environments(endpoints) based on the current user authorizations. Will
|
||||
@@ -30,20 +42,14 @@ const (
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param start query int false "Start searching from"
|
||||
// @param limit query int false "Limit results to this value"
|
||||
// @param sort query int false "Sort results by this value"
|
||||
// @param order query int false "Order sorted results by desc/asc" Enum("asc", "desc")
|
||||
// @param search query string false "Search query"
|
||||
// @param groupIds query []int false "List environments(endpoints) of these groups"
|
||||
// @param status query []int false "List environments(endpoints) by this status"
|
||||
// @param groupId query int false "List environments(endpoints) of this group"
|
||||
// @param limit query int false "Limit results to this value"
|
||||
// @param types query []int false "List environments(endpoints) of this type"
|
||||
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
|
||||
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these environments(endpoints)"
|
||||
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
|
||||
// @param agentVersions query []string false "will return only environments with on of these agent versions"
|
||||
// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)"
|
||||
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)"
|
||||
// @param edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
|
||||
// @param name query string false "will return only environments(endpoints) with this name"
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 "Server error"
|
||||
@@ -54,42 +60,103 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
start--
|
||||
}
|
||||
|
||||
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||
if search != "" {
|
||||
search = strings.ToLower(search)
|
||||
}
|
||||
|
||||
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
|
||||
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
|
||||
|
||||
var endpointTypes []int
|
||||
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
|
||||
|
||||
var tagIDs []portainer.TagID
|
||||
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
|
||||
|
||||
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
|
||||
|
||||
var endpointIDs []portainer.EndpointID
|
||||
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
|
||||
|
||||
var statuses []int
|
||||
request.RetrieveJSONQueryParameter(r, "status", &statuses, true)
|
||||
|
||||
var groupIDs []int
|
||||
request.RetrieveJSONQueryParameter(r, "groupIds", &groupIDs, true)
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
query, err := parseQuery(r)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameters", err)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to filter endpoints", err)
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
|
||||
}
|
||||
|
||||
if endpointIDs != nil {
|
||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
|
||||
}
|
||||
|
||||
if len(groupIDs) > 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, groupIDs)
|
||||
}
|
||||
|
||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||
if name != "" {
|
||||
filteredEndpoints = filterEndpointsByName(filteredEndpoints, name)
|
||||
}
|
||||
|
||||
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
|
||||
if edgeDeviceFilter != "" {
|
||||
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
|
||||
}
|
||||
|
||||
if len(statuses) > 0 {
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
|
||||
}
|
||||
tagsMap := make(map[portainer.TagID]string)
|
||||
for _, tag := range tags {
|
||||
tagsMap[tag.ID] = tag.Name
|
||||
}
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
|
||||
}
|
||||
|
||||
if endpointTypes != nil {
|
||||
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, endpointTypes)
|
||||
}
|
||||
|
||||
if tagIDs != nil {
|
||||
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
|
||||
}
|
||||
|
||||
// Sort endpoints by field
|
||||
sortEndpointsByField(filteredEndpoints, endpointGroups, sortField, sortOrder == "desc")
|
||||
|
||||
filteredEndpointCount := len(filteredEndpoints)
|
||||
@@ -129,6 +196,64 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
|
||||
return endpoints[start:end]
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []int) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if utils.Contains(endpointGroupIDs, int(endpoint.GroupID)) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
|
||||
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
status := endpoint.Status
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
isCheckValid := false
|
||||
edgeCheckinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
|
||||
}
|
||||
status = portainer.EndpointStatusDown // Offline
|
||||
if isCheckValid {
|
||||
status = portainer.EndpointStatusUp // Online
|
||||
}
|
||||
}
|
||||
|
||||
if utils.Contains(statuses, int(status)) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, sortField string, isSortDesc bool) {
|
||||
|
||||
switch sortField {
|
||||
@@ -140,7 +265,7 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
|
||||
}
|
||||
|
||||
case "Group":
|
||||
endpointGroupNames := make(map[portainer.EndpointGroupID]string, 0)
|
||||
endpointGroupNames = make(map[portainer.EndpointGroupID]string, 0)
|
||||
for _, group := range endpointGroups {
|
||||
endpointGroupNames[group.ID] = group.Name
|
||||
}
|
||||
@@ -169,6 +294,123 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
|
||||
}
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||
return true
|
||||
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||
return true
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
|
||||
for _, group := range endpointGroups {
|
||||
if group.ID == endpoint.GroupID {
|
||||
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointType := range endpointTypes {
|
||||
typeSet[portainer.EndpointType(endpointType)] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if typeSet[endpoint.Type] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
|
||||
// none - return all endpoints that are not edge devices
|
||||
if edgeDeviceFilter == EdgeDeviceFilterNone && !endpoint.IsEdgeDevice {
|
||||
return true
|
||||
}
|
||||
|
||||
if !endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch edgeDeviceFilter {
|
||||
case EdgeDeviceFilterAll:
|
||||
return true
|
||||
case EdgeDeviceFilterTrusted:
|
||||
return endpoint.UserTrusted
|
||||
case EdgeDeviceFilterUntrusted:
|
||||
return !endpoint.UserTrusted
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
|
||||
tags := make([]string, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
tags = append(tags, tagsMap[tagID])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
|
||||
endpointMatched := false
|
||||
if partialMatch {
|
||||
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
} else {
|
||||
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
}
|
||||
|
||||
if endpointMatched {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
|
||||
var endpointGroup portainer.EndpointGroup
|
||||
for _, group := range groups {
|
||||
@@ -179,3 +421,72 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
|
||||
}
|
||||
return endpointGroup
|
||||
}
|
||||
|
||||
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
tagSet := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
tagSet[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
missingTags := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
missingTags[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
return len(missingTags) == 0
|
||||
}
|
||||
|
||||
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
idsSet := make(map[portainer.EndpointID]bool)
|
||||
for _, id := range ids {
|
||||
idsSet[id] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if idsSet[endpoint.ID] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
|
||||
}
|
||||
|
||||
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
|
||||
if name == "" {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Name == name {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
@@ -16,147 +16,66 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointListTest struct {
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
title string
|
||||
expected []portainer.EndpointID
|
||||
filter string
|
||||
}
|
||||
|
||||
func Test_EndpointList_AgentVersion(t *testing.T) {
|
||||
|
||||
version1Endpoint := portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{
|
||||
Version: "1.0.0",
|
||||
},
|
||||
}
|
||||
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1, Type: portainer.AgentOnDockerEnvironment, Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{Version: "2.0.0"}}
|
||||
noVersionEndpoint := portainer.Endpoint{ID: 3, Type: portainer.AgentOnDockerEnvironment, GroupID: 1}
|
||||
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
|
||||
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
notAgentEnvironments,
|
||||
version1Endpoint,
|
||||
version2Endpoint,
|
||||
noVersionEndpoint,
|
||||
})
|
||||
func Test_endpointList(t *testing.T) {
|
||||
var err error
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
type endpointListAgentVersionTest struct {
|
||||
endpointListTest
|
||||
filter []string
|
||||
}
|
||||
|
||||
tests := []endpointListAgentVersionTest{
|
||||
{
|
||||
endpointListTest{
|
||||
"should show version 1 agent endpoints and non-agent endpoints",
|
||||
[]portainer.EndpointID{version1Endpoint.ID, notAgentEnvironments.ID},
|
||||
},
|
||||
[]string{version1Endpoint.Agent.Version},
|
||||
},
|
||||
{
|
||||
endpointListTest{
|
||||
"should show version 2 endpoints and non-agent endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID},
|
||||
},
|
||||
[]string{version2Endpoint.Agent.Version},
|
||||
},
|
||||
{
|
||||
endpointListTest{
|
||||
"should show version 1 and 2 endpoints and non-agent endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID, version1Endpoint.ID},
|
||||
},
|
||||
[]string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
query := ""
|
||||
for _, filter := range test.filter {
|
||||
query += fmt.Sprintf("agentVersions[]=%s&", filter)
|
||||
}
|
||||
|
||||
req := buildEndpointListRequest(query)
|
||||
|
||||
resp, err := doEndpointListRequest(req, handler, is)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
|
||||
respIds := []portainer.EndpointID{}
|
||||
|
||||
for _, endpoint := range resp {
|
||||
respIds = append(respIds, endpoint.ID)
|
||||
}
|
||||
|
||||
is.ElementsMatch(test.expected, respIds)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_endpointList_edgeDeviceFilter(t *testing.T) {
|
||||
|
||||
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment}
|
||||
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
trustedEdgeDevice,
|
||||
untrustedEdgeDevice,
|
||||
endpoints := []portainer.Endpoint{
|
||||
trustedEndpoint,
|
||||
untrustedEndpoint,
|
||||
regularUntrustedEdgeEndpoint,
|
||||
regularTrustedEdgeEndpoint,
|
||||
regularEndpoint,
|
||||
})
|
||||
|
||||
defer teardown()
|
||||
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
endpointListTest
|
||||
edgeDevice *bool
|
||||
edgeDeviceUntrusted bool
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err = store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
h := NewHandler(bouncer, nil)
|
||||
h.DataStore = store
|
||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
tests := []endpointListEdgeDeviceTest{
|
||||
{
|
||||
endpointListTest: endpointListTest{
|
||||
"should show all endpoints except of the untrusted devices",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: nil,
|
||||
"should show all edge endpoints",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterAll,
|
||||
},
|
||||
{
|
||||
endpointListTest: endpointListTest{
|
||||
"should show only trusted edge devices and regular endpoints",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(true),
|
||||
"should show only trusted edge devices",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterTrusted,
|
||||
},
|
||||
{
|
||||
endpointListTest: endpointListTest{
|
||||
"should show only untrusted edge devices and regular endpoints",
|
||||
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(true),
|
||||
edgeDeviceUntrusted: true,
|
||||
"should show only untrusted edge devices",
|
||||
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterUntrusted,
|
||||
},
|
||||
{
|
||||
endpointListTest: endpointListTest{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(false),
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterNone,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -164,13 +83,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
query := fmt.Sprintf("edgeDeviceUntrusted=%v&", test.edgeDeviceUntrusted)
|
||||
if test.edgeDevice != nil {
|
||||
query += fmt.Sprintf("edgeDevice=%v&", *test.edgeDevice)
|
||||
}
|
||||
|
||||
req := buildEndpointListRequest(query)
|
||||
resp, err := doEndpointListRequest(req, handler, is)
|
||||
req := buildEndpointListRequest(test.filter)
|
||||
resp, err := doEndpointListRequest(req, h, is)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
@@ -186,28 +100,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
handler = NewHandler(bouncer, nil)
|
||||
handler.DataStore = store
|
||||
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
return handler, teardown
|
||||
}
|
||||
|
||||
func buildEndpointListRequest(query string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?%s", query), nil)
|
||||
func buildEndpointListRequest(filter string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
|
||||
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
@@ -55,7 +55,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
latestEndpointReference.Snapshots = endpoint.Snapshots
|
||||
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
|
||||
latestEndpointReference.Agent.Version = endpoint.Agent.Version
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
|
||||
@@ -47,7 +47,6 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
|
||||
|
||||
latestEndpointReference.Snapshots = endpoint.Snapshots
|
||||
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
|
||||
latestEndpointReference.Agent.Version = endpoint.Agent.Version
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,8 +22,6 @@ type endpointUpdatePayload struct {
|
||||
// URL or IP address where exposed containers will be reachable.\
|
||||
// Defaults to URL if not specified
|
||||
PublicURL *string `example:"docker.mydomain.tld:2375"`
|
||||
// GPUs information
|
||||
Gpus []portainer.Pair
|
||||
// Group identifier
|
||||
GroupID *int `example:"1"`
|
||||
// Require TLS to connect against this environment(endpoint)
|
||||
@@ -112,10 +110,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
endpoint.PublicURL = *payload.PublicURL
|
||||
}
|
||||
|
||||
if payload.Gpus != nil {
|
||||
endpoint.Gpus = payload.Gpus
|
||||
}
|
||||
|
||||
if payload.EdgeCheckinInterval != nil {
|
||||
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
|
||||
}
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type EnvironmentsQuery struct {
|
||||
search string
|
||||
types []portainer.EndpointType
|
||||
tagIds []portainer.TagID
|
||||
endpointIds []portainer.EndpointID
|
||||
tagsPartialMatch bool
|
||||
groupIds []portainer.EndpointGroupID
|
||||
status []portainer.EndpointStatus
|
||||
edgeDevice *bool
|
||||
edgeDeviceUntrusted bool
|
||||
name string
|
||||
agentVersions []string
|
||||
}
|
||||
|
||||
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||
if search != "" {
|
||||
search = strings.ToLower(search)
|
||||
}
|
||||
|
||||
status, err := getNumberArrayQueryParameter[portainer.EndpointStatus](r, "status")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
groupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "groupIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
endpointTypes, err := getNumberArrayQueryParameter[portainer.EndpointType](r, "types")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
tagIDs, err := getNumberArrayQueryParameter[portainer.TagID](r, "tagIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
|
||||
|
||||
endpointIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "endpointIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
agentVersions := getArrayQueryParameter(r, "agentVersions")
|
||||
|
||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||
|
||||
edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true)
|
||||
|
||||
var edgeDevice *bool
|
||||
if edgeDeviceParam != "" {
|
||||
edgeDevice = BoolAddr(edgeDeviceParam == "true")
|
||||
}
|
||||
|
||||
edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true)
|
||||
|
||||
return EnvironmentsQuery{
|
||||
search: search,
|
||||
types: endpointTypes,
|
||||
tagIds: tagIDs,
|
||||
endpointIds: endpointIDs,
|
||||
tagsPartialMatch: tagsPartialMatch,
|
||||
groupIds: groupIDs,
|
||||
status: status,
|
||||
edgeDevice: edgeDevice,
|
||||
edgeDeviceUntrusted: edgeDeviceUntrusted,
|
||||
name: name,
|
||||
agentVersions: agentVersions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
if len(query.endpointIds) > 0 {
|
||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
|
||||
}
|
||||
|
||||
if len(query.groupIds) > 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
|
||||
}
|
||||
|
||||
if query.name != "" {
|
||||
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
|
||||
}
|
||||
|
||||
if query.edgeDevice != nil {
|
||||
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, *query.edgeDevice, query.edgeDeviceUntrusted)
|
||||
} else {
|
||||
// If the edgeDevice parameter is not set, we need to filter out the untrusted edge devices
|
||||
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
|
||||
return !endpoint.IsEdgeDevice || endpoint.UserTrusted
|
||||
})
|
||||
}
|
||||
|
||||
if len(query.status) > 0 {
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings)
|
||||
}
|
||||
|
||||
if query.search != "" {
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
if err != nil {
|
||||
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
|
||||
}
|
||||
|
||||
tagsMap := make(map[portainer.TagID]string)
|
||||
for _, tag := range tags {
|
||||
tagsMap[tag.ID] = tag.Name
|
||||
}
|
||||
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
|
||||
}
|
||||
|
||||
if len(query.types) > 0 {
|
||||
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, query.types)
|
||||
}
|
||||
|
||||
if len(query.tagIds) > 0 {
|
||||
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, query.tagIds, groups, query.tagsPartialMatch)
|
||||
}
|
||||
|
||||
if len(query.agentVersions) > 0 {
|
||||
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
|
||||
return !endpointutils.IsAgentEndpoint(&endpoint) || contains(query.agentVersions, endpoint.Agent.Version)
|
||||
})
|
||||
}
|
||||
|
||||
return filteredEndpoints, totalAvailableEndpoints, nil
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []portainer.EndpointGroupID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if slices.Contains(endpointGroupIDs, endpoint.GroupID) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
|
||||
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portainer.EndpointStatus, settings *portainer.Settings) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
status := endpoint.Status
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
isCheckValid := false
|
||||
edgeCheckinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
|
||||
}
|
||||
|
||||
status = portainer.EndpointStatusDown // Offline
|
||||
if isCheckValid {
|
||||
status = portainer.EndpointStatusUp // Online
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(statuses, status) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||
return true
|
||||
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||
return true
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
|
||||
for _, group := range endpointGroups {
|
||||
if group.ID == endpoint.GroupID {
|
||||
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointType := range endpointTypes {
|
||||
typeSet[portainer.EndpointType(endpointType)] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if typeSet[endpoint.Type] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool {
|
||||
if !endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
return true
|
||||
}
|
||||
|
||||
if !edgeDeviceParam {
|
||||
return !endpoint.IsEdgeDevice
|
||||
}
|
||||
|
||||
return endpoint.IsEdgeDevice && endpoint.UserTrusted == !untrustedParam
|
||||
}
|
||||
|
||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
|
||||
tags := make([]string, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
tags = append(tags, tagsMap[tagID])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
|
||||
endpointMatched := false
|
||||
if partialMatch {
|
||||
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
} else {
|
||||
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
}
|
||||
|
||||
if endpointMatched {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
tagSet := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
tagSet[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
missingTags := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
missingTags[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
return len(missingTags) == 0
|
||||
}
|
||||
|
||||
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
idsSet := make(map[portainer.EndpointID]bool)
|
||||
for _, id := range ids {
|
||||
idsSet[id] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if idsSet[endpoint.ID] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
|
||||
}
|
||||
|
||||
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
|
||||
if name == "" {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Name == name {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.Endpoint) bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if predicate(endpoint) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func getArrayQueryParameter(r *http.Request, parameter string) []string {
|
||||
list, exists := r.Form[fmt.Sprintf("%s[]", parameter)]
|
||||
if !exists {
|
||||
list = []string{}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]T, error) {
|
||||
list := getArrayQueryParameter(r, parameter)
|
||||
if list == nil {
|
||||
return []T{}, nil
|
||||
}
|
||||
|
||||
var result []T
|
||||
for _, item := range list {
|
||||
number, err := strconv.Atoi(item)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to parse parameter %s", parameter)
|
||||
|
||||
}
|
||||
|
||||
result = append(result, T(number))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func contains(strings []string, param string) bool {
|
||||
for _, str := range strings {
|
||||
if str == param {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type filterTest struct {
|
||||
title string
|
||||
expected []portainer.EndpointID
|
||||
query EnvironmentsQuery
|
||||
}
|
||||
|
||||
func Test_Filter_AgentVersion(t *testing.T) {
|
||||
|
||||
version1Endpoint := portainer.Endpoint{ID: 1, GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{Version: "1.0.0"}}
|
||||
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
Agent: struct {
|
||||
Version string "example:\"1.0.0\""
|
||||
}{Version: "2.0.0"}}
|
||||
noVersionEndpoint := portainer.Endpoint{ID: 3, GroupID: 1,
|
||||
Type: portainer.AgentOnDockerEnvironment,
|
||||
}
|
||||
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
version1Endpoint,
|
||||
version2Endpoint,
|
||||
noVersionEndpoint,
|
||||
notAgentEnvironments,
|
||||
}
|
||||
|
||||
handler, teardown := setupFilterTest(t, endpoints)
|
||||
|
||||
defer teardown()
|
||||
|
||||
tests := []filterTest{
|
||||
{
|
||||
"should show version 1 endpoints",
|
||||
[]portainer.EndpointID{version1Endpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
agentVersions: []string{version1Endpoint.Agent.Version},
|
||||
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show version 2 endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
agentVersions: []string{version2Endpoint.Agent.Version},
|
||||
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show version 1 and 2 endpoints",
|
||||
[]portainer.EndpointID{version2Endpoint.ID, version1Endpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
agentVersions: []string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
|
||||
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTests(tests, t, handler, endpoints)
|
||||
}
|
||||
|
||||
func Test_Filter_edgeDeviceFilter(t *testing.T) {
|
||||
|
||||
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
trustedEdgeDevice,
|
||||
untrustedEdgeDevice,
|
||||
regularUntrustedEdgeEndpoint,
|
||||
regularTrustedEdgeEndpoint,
|
||||
regularEndpoint,
|
||||
}
|
||||
|
||||
handler, teardown := setupFilterTest(t, endpoints)
|
||||
|
||||
defer teardown()
|
||||
|
||||
tests := []filterTest{
|
||||
{
|
||||
"should show all edge endpoints except of the untrusted devices",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment},
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show only trusted edge devices and other regular endpoints",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
edgeDevice: BoolAddr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show only untrusted edge devices and other regular endpoints",
|
||||
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
edgeDevice: BoolAddr(true),
|
||||
edgeDeviceUntrusted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
edgeDevice: BoolAddr(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTests(tests, t, handler, endpoints)
|
||||
}
|
||||
|
||||
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
runTest(t, test, handler, endpoints)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
|
||||
is := assert.New(t)
|
||||
|
||||
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
|
||||
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(filteredEndpoints))
|
||||
|
||||
respIds := []portainer.EndpointID{}
|
||||
|
||||
for _, endpoint := range filteredEndpoints {
|
||||
respIds = append(respIds, endpoint.ID)
|
||||
}
|
||||
|
||||
is.ElementsMatch(test.expected, respIds)
|
||||
|
||||
}
|
||||
|
||||
func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
handler = NewHandler(bouncer, nil)
|
||||
handler.DataStore = store
|
||||
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
return handler, teardown
|
||||
}
|
||||
@@ -67,9 +67,6 @@ func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/agent_versions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
|
||||
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
|
||||
@@ -39,8 +39,8 @@ func (e EndpointsByGroup) Less(i, j int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
groupA := e.endpointGroupNames[e.endpoints[i].GroupID]
|
||||
groupB := e.endpointGroupNames[e.endpoints[j].GroupID]
|
||||
groupA := endpointGroupNames[e.endpoints[i].GroupID]
|
||||
groupB := endpointGroupNames[e.endpoints[j].GroupID]
|
||||
|
||||
return sortorder.NaturalLess(strings.ToLower(groupA), strings.ToLower(groupB))
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
func BoolAddr(b bool) *bool {
|
||||
boolVar := b
|
||||
return &boolVar
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/backup"
|
||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/docker"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
@@ -46,7 +45,6 @@ type Handler struct {
|
||||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHandler *docker.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
@@ -82,7 +80,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.15.1
|
||||
// @version 2.15.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -181,8 +179,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
|
||||
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/docker"):
|
||||
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
|
||||
|
||||
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
|
||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):
|
||||
|
||||
@@ -2,6 +2,7 @@ package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/libhelm"
|
||||
@@ -107,7 +108,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
|
||||
|
||||
hostURL := "localhost"
|
||||
if !sslSettings.SelfSigned {
|
||||
hostURL = r.Host
|
||||
hostURL = strings.Split(r.Host, ":")[0]
|
||||
}
|
||||
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -144,7 +145,8 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
|
||||
}
|
||||
|
||||
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
|
||||
hostURL := strings.Split(r.Host, ":")[0]
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||
return clientV1.NamedCluster{
|
||||
Name: buildClusterName(endpoint.Name),
|
||||
Cluster: clientV1.Cluster{
|
||||
|
||||
@@ -95,10 +95,8 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||
}
|
||||
}
|
||||
//if LDAP authentication is on, compose the related fields from application settings
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
|
||||
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
|
||||
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
|
||||
}
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
||||
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
|
||||
}
|
||||
return publicSettings
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
}
|
||||
stack.GitConfig.ConfigHash = commitID
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -386,7 +386,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -408,15 +408,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
type composeStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
registries []portainer.Registry
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
forcePullImage bool
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
registries []portainer.Registry
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, forcePullImage bool) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
||||
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
@@ -434,12 +433,11 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
config := &composeStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
registries: filteredRegistries,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
forcePullImage: forcePullImage,
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
registries: filteredRegistries,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -479,5 +477,5 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig,
|
||||
}
|
||||
}
|
||||
|
||||
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, forceCreate)
|
||||
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, forceCreate)
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false, true)
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
}
|
||||
stack.GitConfig.ConfigHash = commitID
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false, true)
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false, true)
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -360,10 +360,9 @@ type swarmStackDeploymentConfig struct {
|
||||
prune bool
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
pullImage bool
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool, pullImage bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
||||
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
@@ -387,7 +386,6 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
prune: prune,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
pullImage: pullImage,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -415,5 +413,5 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
}
|
||||
}
|
||||
|
||||
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
||||
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
@@ -135,20 +133,6 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
// if stack management is disabled for non admins and the user isn't an admin, then return false. Otherwise return true
|
||||
func (handler *Handler) userCanManageStacks(securityContext *security.RestrictedRequestContext, endpoint *portainer.Endpoint) (bool, error) {
|
||||
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpoint.ID))
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to get user from the database: %w", err)
|
||||
}
|
||||
|
||||
return canCreate, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
if err != nil {
|
||||
|
||||
@@ -82,22 +82,6 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
|
||||
}
|
||||
|
||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||
stack.SwarmID = swarmId
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
@@ -75,18 +76,22 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack creation is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
|
||||
}
|
||||
|
||||
if !canCreate {
|
||||
errMsg := "Stack creation is disabled for non-admin users"
|
||||
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
|
||||
@@ -103,15 +103,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||
}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack deletion is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
|
||||
}
|
||||
|
||||
// stop scheduler updates of the stack before removal
|
||||
if stack.AutoUpdate != nil {
|
||||
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
||||
|
||||
@@ -3,12 +3,11 @@ package stacks
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
@@ -60,15 +59,6 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
if endpoint != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
@@ -86,7 +76,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package stacks
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
@@ -55,15 +55,6 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
if endpoint != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
@@ -81,7 +72,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
|
||||
@@ -87,15 +87,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack migration is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||
@@ -189,7 +180,7 @@ func (handler *Handler) migrateStack(r *http.Request, stack *portainer.Stack, ne
|
||||
}
|
||||
|
||||
func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.Stack, next *portainer.Endpoint) *httperror.HandlerError {
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, next, false)
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, next)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -203,7 +194,7 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
|
||||
}
|
||||
|
||||
func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stack, next *portainer.Endpoint) *httperror.HandlerError {
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, next, true, true)
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, next, true)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -134,7 +125,7 @@ func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.E
|
||||
case portainer.DockerComposeStack:
|
||||
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
|
||||
case portainer.DockerSwarmStack:
|
||||
return handler.SwarmStackManager.Deploy(stack, true, true, endpoint)
|
||||
return handler.SwarmStackManager.Deploy(stack, true, endpoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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")}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ type updateComposeStackPayload struct {
|
||||
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx"`
|
||||
// A list of environment(endpoint) variables used during stack deployment
|
||||
Env []portainer.Pair
|
||||
// Force a pulling to current image with the original tag though the image is already the latest
|
||||
PullImage bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *updateComposeStackPayload) Validate(r *http.Request) error {
|
||||
@@ -40,8 +38,6 @@ type updateSwarmStackPayload struct {
|
||||
Env []portainer.Pair
|
||||
// Prune services that are no longer referenced (only available for Swarm stacks)
|
||||
Prune bool `example:"true"`
|
||||
// Force a pulling to current image with the original tag though the image is already the latest
|
||||
PullImage bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *updateSwarmStackPayload) Validate(r *http.Request) error {
|
||||
@@ -127,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
|
||||
@@ -198,7 +185,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
|
||||
}
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, payload.PullImage)
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
@@ -235,7 +222,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
|
||||
}
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune, payload.PullImage)
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
|
||||
if configErr != nil {
|
||||
return configErr
|
||||
}
|
||||
|
||||
@@ -120,15 +120,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)
|
||||
|
||||
@@ -25,8 +25,6 @@ type stackGitRedployPayload struct {
|
||||
RepositoryPassword string
|
||||
Env []portainer.Pair
|
||||
Prune bool
|
||||
// Force a pulling to current image with the original tag though the image is already the latest
|
||||
PullImage bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
|
||||
@@ -113,15 +111,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 {
|
||||
@@ -169,7 +158,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}()
|
||||
|
||||
httpErr := handler.deployStack(r, stack, payload.PullImage, endpoint)
|
||||
httpErr := handler.deployStack(r, stack, endpoint)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
@@ -201,14 +190,14 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, pullImage bool, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
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, pullImage)
|
||||
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, prune)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
@@ -218,7 +207,7 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, pul
|
||||
}
|
||||
|
||||
case portainer.DockerComposeStack:
|
||||
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint, pullImage)
|
||||
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demo
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/status/version",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
62
api/http/handler/status/status_inspect_version.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
type inspectVersionResponse struct {
|
||||
// Whether portainer has an update available
|
||||
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
|
||||
// The latest version available
|
||||
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
|
||||
}
|
||||
|
||||
type githubData struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
// @id StatusInspectVersion
|
||||
// @summary Check for portainer updates
|
||||
// @description Check if portainer has an update available
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} inspectVersionResponse "Success"
|
||||
// @router /status/version [get]
|
||||
func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) {
|
||||
motd, err := client.Get(portainer.VersionCheckURL, 5)
|
||||
if err != nil {
|
||||
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
|
||||
return
|
||||
}
|
||||
|
||||
var data githubData
|
||||
err = json.Unmarshal(motd, &data)
|
||||
if err != nil {
|
||||
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
|
||||
return
|
||||
}
|
||||
|
||||
resp := inspectVersionResponse{
|
||||
UpdateAvailable: false,
|
||||
}
|
||||
|
||||
currentVersion := semver.New(portainer.APIVersion)
|
||||
latestVersion := semver.New(data.TagName)
|
||||
if currentVersion.LessThan(*latestVersion) {
|
||||
resp.UpdateAvailable = true
|
||||
resp.LatestVersion = data.TagName
|
||||
}
|
||||
|
||||
response.JSON(w, &resp)
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type versionResponse struct {
|
||||
// Whether portainer has an update available
|
||||
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
|
||||
// The latest version available
|
||||
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
|
||||
|
||||
ServerVersion string
|
||||
DatabaseVersion string
|
||||
Build BuildInfo
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
BuildNumber string
|
||||
ImageTag string
|
||||
NodejsVersion string
|
||||
YarnVersion string
|
||||
WebpackVersion string
|
||||
GoVersion string
|
||||
}
|
||||
|
||||
// @id Version
|
||||
// @summary Check for portainer updates
|
||||
// @description Check if portainer has an update available
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} versionResponse "Success"
|
||||
// @router /status/version [get]
|
||||
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
result := &versionResponse{
|
||||
ServerVersion: portainer.APIVersion,
|
||||
DatabaseVersion: strconv.Itoa(portainer.DBVersion),
|
||||
Build: BuildInfo{
|
||||
BuildNumber: build.BuildNumber,
|
||||
ImageTag: build.ImageTag,
|
||||
NodejsVersion: build.NodejsVersion,
|
||||
YarnVersion: build.YarnVersion,
|
||||
WebpackVersion: build.WebpackVersion,
|
||||
GoVersion: build.GoVersion,
|
||||
},
|
||||
}
|
||||
|
||||
latestVersion := getLatestVersion()
|
||||
if hasNewerVersion(portainer.APIVersion, latestVersion) {
|
||||
result.UpdateAvailable = true
|
||||
result.LatestVersion = latestVersion
|
||||
}
|
||||
|
||||
response.JSON(w, &result)
|
||||
}
|
||||
|
||||
func getLatestVersion() string {
|
||||
motd, err := client.Get(portainer.VersionCheckURL, 5)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("couldn't fetch latest Portainer release version")
|
||||
return ""
|
||||
}
|
||||
|
||||
var data struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(motd, &data)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("couldn't parse latest Portainer version")
|
||||
return ""
|
||||
}
|
||||
|
||||
return data.TagName
|
||||
}
|
||||
|
||||
func hasNewerVersion(currentVersion, latestVersion string) bool {
|
||||
currentVersionSemver, err := semver.NewVersion(currentVersion)
|
||||
if err != nil {
|
||||
log.WithField("version", currentVersion).Debug("current Portainer version isn't a semver")
|
||||
return false
|
||||
}
|
||||
|
||||
latestVersionSemver, err := semver.NewVersion(latestVersion)
|
||||
if err != nil {
|
||||
log.WithField("version", latestVersion).Debug("latest Portainer version isn't a semver")
|
||||
return false
|
||||
}
|
||||
|
||||
return currentVersionSemver.LessThan(*latestVersionSemver)
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/agent"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/url"
|
||||
)
|
||||
|
||||
// ProxyServer provide an extended proxy with a local server to forward requests
|
||||
@@ -33,7 +34,7 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
|
||||
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
endpointURL, err := url.ParseURL(urlString)
|
||||
endpointURL, err := parseURL(urlString)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
|
||||
}
|
||||
@@ -98,3 +99,15 @@ func (proxy *ProxyServer) Close() {
|
||||
proxy.server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// parseURL parses the endpointURL using url.Parse.
|
||||
//
|
||||
// to prevent an error when url has port but no protocol prefix
|
||||
// we add `//` prefix if needed
|
||||
func parseURL(endpointURL string) (*url.URL, error) {
|
||||
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
|
||||
endpointURL = fmt.Sprintf("//%s", endpointURL)
|
||||
}
|
||||
|
||||
return url.Parse(endpointURL)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||
"github.com/portainer/portainer/api/internal/url"
|
||||
)
|
||||
|
||||
func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
@@ -23,7 +23,7 @@ func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
endpointURL, err := url.ParseURL(endpoint.URL)
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
|
||||
rawURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
endpointURL, err := url.ParseURL(rawURL)
|
||||
endpointURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
|
||||
}
|
||||
|
||||
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
|
||||
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||
filteredEndpoints := endpoints
|
||||
|
||||
if !context.IsAdmin {
|
||||
if !context.IsAdmin && !context.IsTeamLeader {
|
||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
@@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
|
||||
}
|
||||
|
||||
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized environment(endpoint) groups.
|
||||
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
|
||||
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
|
||||
filteredEndpointGroups := endpointGroups
|
||||
|
||||
if !context.IsAdmin {
|
||||
if !context.IsAdmin && !context.IsTeamLeader {
|
||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
for _, group := range endpointGroups {
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/backup"
|
||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||
dockerhandler "github.com/portainer/portainer/api/http/handler/docker"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
@@ -185,8 +184,6 @@ func (server *Server) Start() error {
|
||||
|
||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
|
||||
|
||||
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled)
|
||||
|
||||
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
|
||||
@@ -278,7 +275,6 @@ func (server *Server) Start() error {
|
||||
AuthHandler: authHandler,
|
||||
BackupHandler: backupHandler,
|
||||
CustomTemplatesHandler: customTemplatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
EdgeJobsHandler: edgeJobsHandler,
|
||||
EdgeStacksHandler: edgeStacksHandler,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package set
|
||||
|
||||
type SetKey interface {
|
||||
~int | ~string
|
||||
}
|
||||
|
||||
type Set[T SetKey] map[T]bool
|
||||
|
||||
func (s Set[T]) Add(key T) {
|
||||
s[key] = true
|
||||
}
|
||||
|
||||
func (s Set[T]) Contains(key T) bool {
|
||||
_, ok := s[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s Set[T]) Remove(key T) {
|
||||
delete(s, key)
|
||||
}
|
||||
|
||||
func (s Set[T]) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s Set[T]) IsEmpty() bool {
|
||||
return len(s) == 0
|
||||
}
|
||||
|
||||
func (s Set[T]) Keys() []T {
|
||||
keys := make([]T, s.Len())
|
||||
|
||||
i := 0
|
||||
for k := range s {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
@@ -2,14 +2,11 @@ package snapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/agent"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
@@ -90,24 +87,6 @@ func SupportDirectSnapshot(endpoint *portainer.Endpoint) bool {
|
||||
// SnapshotEndpoint will create a snapshot of the environment(endpoint) based on the environment(endpoint) type.
|
||||
// If the snapshot is a success, it will be associated to the environment(endpoint).
|
||||
func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error {
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
var err error
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, version, err := agent.GetAgentVersionAndPlatform(endpoint.URL, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = version
|
||||
}
|
||||
|
||||
switch endpoint.Type {
|
||||
case portainer.AzureEnvironment:
|
||||
return nil
|
||||
@@ -196,7 +175,6 @@ func (service *Service) snapshotEndpoints() error {
|
||||
|
||||
latestEndpointReference.Snapshots = endpoint.Snapshots
|
||||
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
|
||||
latestEndpointReference.Agent.Version = endpoint.Agent.Version
|
||||
|
||||
err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,24 +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, "//") &&
|
||||
!strings.HasPrefix(endpointURL, `unix:`) &&
|
||||
!strings.HasPrefix(endpointURL, `npipe:`) {
|
||||
endpointURL = fmt.Sprintf("//%s", endpointURL)
|
||||
}
|
||||
|
||||
return url.Parse(endpointURL)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
|
||||
@@ -95,20 +94,11 @@ func (service *kubeClusterAccessService) IsSecure() bool {
|
||||
// - pass down params to binaries
|
||||
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
|
||||
baseURL := service.baseURL
|
||||
|
||||
// When the api call is internal, the baseURL should not be used.
|
||||
if hostURL == "localhost" {
|
||||
hostURL = hostURL + service.httpsBindAddr
|
||||
baseURL = "/"
|
||||
}
|
||||
|
||||
if baseURL != "/" {
|
||||
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
|
||||
}
|
||||
|
||||
logrus.Infof("[kubeconfig] [hostURL: %s, httpsBindAddr: %s, baseURL: %s]", hostURL, service.httpsBindAddr, baseURL)
|
||||
|
||||
clusterURL := hostURL + baseURL
|
||||
clusterURL := hostURL + service.httpsBindAddr + baseURL
|
||||
|
||||
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
|
||||
clusterAccessDetails := kcs.GetData("mysite.com", 1)
|
||||
|
||||
wantClusterAccessDetails := kubernetesClusterAccessData{
|
||||
ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes",
|
||||
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
|
||||
CertificateAuthorityFile: "",
|
||||
CertificateAuthorityData: "",
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -1243,7 +1236,6 @@ type (
|
||||
NormalizeStackName(name string) string
|
||||
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, forceRereate bool) error
|
||||
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
||||
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data
|
||||
@@ -1393,7 +1385,7 @@ type (
|
||||
SwarmStackManager interface {
|
||||
Login(registries []Registry, endpoint *Endpoint) error
|
||||
Logout(endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, prune bool, pullImage bool, endpoint *Endpoint) error
|
||||
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
|
||||
Remove(stack *Stack, endpoint *Endpoint) error
|
||||
NormalizeStackName(name string) string
|
||||
}
|
||||
@@ -1401,9 +1393,9 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.15.1"
|
||||
APIVersion = "2.15.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 61
|
||||
DBVersion = 60
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
|
||||
@@ -89,12 +89,12 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
err := deployer.DeployComposeStack(stack, endpoint, registries, true, false)
|
||||
err := deployer.DeployComposeStack(stack, endpoint, registries, false)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
}
|
||||
case portainer.DockerSwarmStack:
|
||||
err := deployer.DeploySwarmStack(stack, endpoint, registries, true, true)
|
||||
err := deployer.DeploySwarmStack(stack, endpoint, registries, true)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, pass
|
||||
|
||||
type noopDeployer struct{}
|
||||
|
||||
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error {
|
||||
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
type StackDeployer interface {
|
||||
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
|
||||
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error
|
||||
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error
|
||||
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error
|
||||
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
|
||||
}
|
||||
|
||||
@@ -35,36 +35,24 @@ func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStac
|
||||
}
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
|
||||
return d.swarmStackManager.Deploy(stack, prune, pullImage, endpoint)
|
||||
return d.swarmStackManager.Deploy(stack, prune, endpoint)
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error {
|
||||
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
|
||||
if err != nil {
|
||||
d.composeStackManager.Down(context.TODO(), stack, endpoint)
|
||||
}
|
||||
return err
|
||||
return d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
|
||||
|
||||
@@ -693,12 +693,6 @@ definitions:
|
||||
$ref: '#/definitions/portainer.DockerSnapshotRaw'
|
||||
DockerVersion:
|
||||
type: string
|
||||
GpuUseAll:
|
||||
type: boolean
|
||||
GpuUseList:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
HealthyContainerCount:
|
||||
type: integer
|
||||
ImageCount:
|
||||
@@ -855,11 +849,6 @@ definitions:
|
||||
EdgeKey:
|
||||
description: The key which is used to map the agent to Portainer
|
||||
type: string
|
||||
Gpus:
|
||||
description: Endpoint Gpus information
|
||||
items:
|
||||
$ref: '#/definitions/portainer.Pair'
|
||||
type: array
|
||||
GroupId:
|
||||
description: Endpoint group identifier
|
||||
example: 1
|
||||
|
||||
@@ -1,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,4 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import feather from 'feather-icons';
|
||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
/* @ngInject */
|
||||
@@ -28,6 +29,10 @@ export function onStartupAngular($rootScope, $state, $interval, LocalStorage, En
|
||||
HttpRequestHelper.resetAgentHeaders();
|
||||
});
|
||||
|
||||
$transitions.onSuccess({}, () => {
|
||||
feather.replace();
|
||||
});
|
||||
|
||||
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||
$interval(() => {
|
||||
ping(EndpointProvider, SystemService);
|
||||
|
||||
@@ -40,16 +40,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 +81,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 {
|
||||
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 +116,10 @@ input[type='checkbox'] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a[ng-click] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.space-right {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -147,6 +150,19 @@ 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;
|
||||
}
|
||||
@@ -229,6 +245,7 @@ input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-blocklist);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-box-color);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@@ -266,6 +283,7 @@ input[type='checkbox'] {
|
||||
.blocklist-item-logo {
|
||||
width: 100%;
|
||||
max-width: 60px;
|
||||
height: 100%;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
@@ -383,7 +401,7 @@ input[type='checkbox'] {
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px 25px;
|
||||
padding: 30px 25px;
|
||||
background-color: var(--white-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -477,7 +495,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 {
|
||||
@@ -850,34 +868,3 @@ json-tree .branch-preview {
|
||||
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;
|
||||
}
|
||||
|
||||
157
app/assets/css/bootstrap-override.css
vendored
@@ -1,22 +1,13 @@
|
||||
/* 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;
|
||||
color: var(--ui-gray-7);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
color: var(--ui-gray-9);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
@@ -25,12 +16,6 @@
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.blue {
|
||||
background: var(--bg-dashboard-item) !important;
|
||||
}
|
||||
@@ -71,16 +56,6 @@
|
||||
|
||||
.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 */
|
||||
@@ -95,8 +70,6 @@
|
||||
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 {
|
||||
@@ -113,8 +86,6 @@
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--ui-blue-8);
|
||||
@apply th-dark:bg-blue-9;
|
||||
@apply th-highcontrast:bg-blue-9;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
@@ -184,16 +155,8 @@ input:checked + .slider:before {
|
||||
|
||||
/* 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-icon i {
|
||||
color: var(--ui-blue-8);
|
||||
}
|
||||
|
||||
.widget .widget-body table thead {
|
||||
@@ -204,20 +167,16 @@ input:checked + .slider:before {
|
||||
|
||||
#toast-container > .toast-success {
|
||||
background-image: url(../images/icon-success.svg) !important;
|
||||
background-size: 40px 40px;
|
||||
background-position: top 12px left 12px;
|
||||
background-position: top 20px left 20px;
|
||||
}
|
||||
|
||||
#toast-container > .toast-error {
|
||||
background-image: url(../images/icon-error.svg) !important;
|
||||
background-size: 40px 40px;
|
||||
background-position: top 12px left 12px;
|
||||
background-position: top 20px left 20px;
|
||||
}
|
||||
|
||||
#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 {
|
||||
@@ -234,7 +193,7 @@ input:checked + .slider:before {
|
||||
color: var(--ui-gray-7);
|
||||
background-color: var(--white-color);
|
||||
border-radius: 8px;
|
||||
padding: 18px 20px 18px 68px;
|
||||
padding: 20px 20px 20px 80px;
|
||||
width: 300px;
|
||||
opacity: 1;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||
@@ -250,7 +209,6 @@ input:checked + .slider:before {
|
||||
.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);
|
||||
@@ -268,10 +226,8 @@ input:checked + .slider:before {
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 500;
|
||||
color: var(--black-color);
|
||||
padding-right: 10px;
|
||||
margin-bottom: 4px;
|
||||
padding: 10px 0px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
@@ -281,31 +237,19 @@ input:checked + .slider:before {
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 55px 20px 20px 20px;
|
||||
background-image: url(../images/icon-warning.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
background-position: top 10px left 10px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: 10px;
|
||||
padding: 0px;
|
||||
padding: 10px 0px 10px 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.modal-header .close {
|
||||
margin-top: 0px;
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.modal-header .modal-title {
|
||||
@@ -340,21 +284,34 @@ input:checked + .slider:before {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
/* Databatle Setting Menu */
|
||||
|
||||
.tableMenu {
|
||||
border: 1px solid var(--border-bootbox);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
[data-reach-menu-list],
|
||||
[data-reach-menu-items] {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dropdown-menu .tableMenu {
|
||||
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);
|
||||
}
|
||||
@@ -366,44 +323,6 @@ input:checked + .slider:before {
|
||||
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;
|
||||
.control-label {
|
||||
@apply inline-flex items-center;
|
||||
}
|
||||
|
||||
@@ -1,161 +1,92 @@
|
||||
.btn {
|
||||
@apply !outline-none;
|
||||
@apply border border-solid border-transparent;
|
||||
|
||||
border-radius: 8px;
|
||||
border-radius: 5px;
|
||||
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-group {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.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-primary {
|
||||
background-color: var(--ui-blue-8);
|
||||
}
|
||||
|
||||
.btn.btn-primary:active,
|
||||
.btn.btn-primary.active,
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus,
|
||||
.btn-primary:active .active {
|
||||
background-color: var(--ui-blue-9);
|
||||
}
|
||||
|
||||
.btn-primary:active,
|
||||
.btn-primary.active,
|
||||
.open > .dropdown-toggle.btn-primary {
|
||||
@apply bg-blue-9 border-blue-5;
|
||||
background-color: var(--ui-blue-9);
|
||||
}
|
||||
|
||||
.nav-pills > li.active > a,
|
||||
.nav-pills > li.active > a:hover,
|
||||
.nav-pills > li.active > a:focus {
|
||||
@apply bg-blue-8;
|
||||
background-color: var(--ui-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-danger {
|
||||
background-color: var(--ui-error-8);
|
||||
}
|
||||
|
||||
.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 {
|
||||
.btn-success {
|
||||
background-color: var(--ui-success-7);
|
||||
}
|
||||
|
||||
.btn.btn-success:hover {
|
||||
color: var(--white-color);
|
||||
.btn-dangerlight {
|
||||
background-color: var(--ui-error-2) !important;
|
||||
border: 1px solid var(--border-button-group);
|
||||
color: var(--ui-error-8);
|
||||
}
|
||||
|
||||
/* 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-iron-10 th-dark:border-gray-warm-7 th-dark:text-gray-warm-4;
|
||||
@apply th-dark:hover:bg-gray-iron-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-dangerlight:hover {
|
||||
color: var(--ui-error-9) !important;
|
||||
background-color: var(--ui-error-3) !important;
|
||||
}
|
||||
|
||||
.btn.btn-light:active,
|
||||
.btn.btn-light.active,
|
||||
.btn-light {
|
||||
background-color: var(--bg-button-group);
|
||||
border: 1px solid var(--border-button-group);
|
||||
color: var(--text-button-group);
|
||||
}
|
||||
|
||||
.btn-light:hover {
|
||||
background-color: var(--ui-gray-2) !important;
|
||||
}
|
||||
|
||||
.btn-light:active,
|
||||
.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;
|
||||
/* Button Secondary */
|
||||
.btn-secondary {
|
||||
background-color: var(--ui-blue-2);
|
||||
border: 1px solid var(--ui-blue-8);
|
||||
color: var(--ui-blue-9);
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: inline-flex;
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus,
|
||||
.btn-secondary:active .active {
|
||||
background-color: var(--ui-blue-3) !important;
|
||||
color: var(--ui-blue-9) !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
.btn-secondary:disabled {
|
||||
background-color: var(--ui-blue-1);
|
||||
border: 1px solid var(--ui-blue-1);
|
||||
color: var(--ui-blue-5);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"3": "#f5f5f4",
|
||||
"4": "#e7e5e4",
|
||||
"5": "#d7d3d0",
|
||||
"6": "#a9a29d",
|
||||
"6": "#d7d3d0",
|
||||
"7": "#79716b",
|
||||
"8": "#57534e",
|
||||
"9": "#44403c",
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
}
|
||||
|
||||
pr-icon {
|
||||
display: inline-flex;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: currentColor;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
|
||||
font-size: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
@@ -46,8 +45,7 @@ pr-icon {
|
||||
stroke: var(--white-color);
|
||||
}
|
||||
|
||||
.icon-primary,
|
||||
.icon-blue {
|
||||
.icon-primary {
|
||||
color: var(--ui-blue-8);
|
||||
}
|
||||
|
||||
@@ -65,8 +63,7 @@ pr-icon {
|
||||
stroke: var(--black-color);
|
||||
}
|
||||
|
||||
.icon-warning,
|
||||
.icon-orange {
|
||||
.icon-warning {
|
||||
color: var(--ui-warning-8);
|
||||
}
|
||||
|
||||
@@ -76,11 +73,11 @@ pr-icon {
|
||||
}
|
||||
|
||||
.icon-danger {
|
||||
color: var(--ui-error-9);
|
||||
color: var(--ui-error-8);
|
||||
}
|
||||
|
||||
.icon.icon-danger-alt {
|
||||
fill: var(--ui-error-9);
|
||||
fill: var(--ui-error-8);
|
||||
stroke: var(--white-color);
|
||||
}
|
||||
|
||||
@@ -101,10 +98,17 @@ pr-icon {
|
||||
padding: 1.5%;
|
||||
}
|
||||
|
||||
.icon-nested-gray {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: var(--ui-gray-4);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.icon-nested-blue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
padding: 5px;
|
||||
@@ -114,20 +118,7 @@ pr-icon {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -96,11 +96,13 @@ div.input-mask {
|
||||
}
|
||||
.widget .widget-header {
|
||||
color: var(--text-widget-header-color);
|
||||
padding: 20px 20px 10px 20px;
|
||||
padding: 10px 15px;
|
||||
line-height: 30px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.widget .widget-header i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.widget .widget-body {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
@@ -146,7 +148,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;
|
||||
|
||||
@@ -86,34 +86,32 @@
|
||||
|
||||
--orange-1: #e86925;
|
||||
|
||||
--BE-only: var(--ui-warning-7);
|
||||
--BE-only: var(--orange-1);
|
||||
|
||||
/* Default Theme */
|
||||
--bg-card-color: var(--white-color);
|
||||
--bg-main-color: var(--white-color);
|
||||
--bg-body-color: var(--grey-9);
|
||||
--bg-checkbox-border-color: var(--grey-49);
|
||||
--bg-sidebar-color: var(--ui-blue-10);
|
||||
--bg-sidebar-nav-color: var(--ui-blue-11);
|
||||
--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-btn-default-color: var(--ui-blue-10);
|
||||
--bg-blocklist-hover-color: var(--ui-blue-2);
|
||||
--bg-boxselector-color: var(--ui-gray-2);
|
||||
--bg-btn-default-color: var(--white-color);
|
||||
--bg-blocklist-hover-color: var(--ui-blue-3);
|
||||
--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-nav-container-color: var(--ui-gray-2);
|
||||
--bg-code-color: var(--grey-15);
|
||||
--bg-navtabs-color: var(--white-color);
|
||||
--bg-navtabs-hover-color: var(--grey-16);
|
||||
--bg-nav-tab-active-color: var(--ui-gray-4);
|
||||
--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);
|
||||
@@ -121,19 +119,25 @@
|
||||
--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-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-service-datatable-thead: var(--grey-23);
|
||||
--bg-app-datatable-thead: var(--grey-23);
|
||||
--bg-inner-datatable-thead: var(--grey-23);
|
||||
--bg-service-datatable-tbody: var(--grey-24);
|
||||
--bg-app-datatable-tbody: var(--grey-24);
|
||||
--bg-multiselect-color: var(--white-color);
|
||||
--bg-daterangepicker-color: var(--white-color);
|
||||
@@ -144,10 +148,12 @@
|
||||
--bg-daterangepicker-in-range: var(--grey-58);
|
||||
--bg-daterangepicker-active: var(--blue-14);
|
||||
--bg-input-autofill-color: var(--white-color);
|
||||
--bg-btn-default-hover-color: var(--ui-blue-9);
|
||||
--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);
|
||||
@@ -155,13 +161,6 @@
|
||||
--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-code-script-color: var(--ui-white);
|
||||
--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);
|
||||
@@ -171,13 +170,13 @@
|
||||
--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-navtabs-color: var(--grey-7);
|
||||
--text-navtabs-hover-color: var(--grey-6);
|
||||
--text-nav-tab-active-color: var(--grey-25);
|
||||
--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);
|
||||
@@ -192,8 +191,8 @@
|
||||
--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(--ui-gray-3);
|
||||
--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);
|
||||
@@ -207,14 +206,10 @@
|
||||
--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);
|
||||
--border-sidebar-color: var(--ui-blue-9);
|
||||
--border-sidebar-color: var(--white-color);
|
||||
--border-form-control-color: var(--grey-44);
|
||||
--border-table-color: var(--grey-19);
|
||||
--border-table-top-color: var(--grey-19);
|
||||
@@ -225,26 +220,29 @@
|
||||
--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);
|
||||
|
||||
--shadow-box-color: 0 3px 10px -2px var(--grey-50);
|
||||
--shadow-boxselector-color: 0 3px 10px -2px var(--grey-50);
|
||||
@@ -252,6 +250,7 @@
|
||||
--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));
|
||||
@@ -265,95 +264,86 @@
|
||||
--bg-multiselect-helpercontainer: var(--white-color);
|
||||
--text-input-textarea: var(--white-color);
|
||||
|
||||
--user-menu-icon-color: var(--ui-gray-4);
|
||||
--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(--grey-44);
|
||||
--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);
|
||||
}
|
||||
|
||||
/* 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(--grey-2);
|
||||
--bg-codemirror-color: var(--grey-2);
|
||||
--bg-codemirror-gutters-color: var(--grey-3);
|
||||
--bg-codemirror-selected-color: var(--grey-3);
|
||||
--bg-dropdown-menu-color: var(--ui-gray-7);
|
||||
--bg-main-color: var(--grey-2);
|
||||
--bg-sidebar-color: var(--grey-1);
|
||||
--bg-sidebar-nav-color: var(--grey-2);
|
||||
--bg-body-color: var(--grey-2);
|
||||
--bg-checkbox-border-color: var(--grey-8);
|
||||
--bg-widget-color: var(--grey-1);
|
||||
--bg-widget-header-color: var(--grey-3);
|
||||
--bg-widget-table-color: var(--grey-3);
|
||||
--bg-widget-header-color: var(--grey-1);
|
||||
--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-nav-container-color: var(--ui-gray-iron-10);
|
||||
--bg-code-color: var(--red-4);
|
||||
--bg-navtabs-color: var(--grey-3);
|
||||
--bg-navtabs-hover-color: var(--grey-3);
|
||||
--bg-nav-tab-active-color: var(--grey-2);
|
||||
--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-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-4);
|
||||
--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-searchbar: var(--grey-1);
|
||||
--bg-inputbox: var(--grey-2);
|
||||
--bg-dropdown-hover: var(--grey-3);
|
||||
--bg-webeditor-color: var(--ui-gray-iron-10);
|
||||
--bg-button-group-color: var(--ui-black);
|
||||
--bg-pagination-disabled-color: var(--grey-1);
|
||||
--bg-code-script-color: var(--ui-gray-warm-11);
|
||||
--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);
|
||||
@@ -363,13 +353,13 @@
|
||||
--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(--grey-8);
|
||||
--text-navtabs-hover-color: var(--grey-9);
|
||||
--text-nav-tab-active-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);
|
||||
@@ -384,30 +374,27 @@
|
||||
--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(--ui-gray-3);
|
||||
--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-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);
|
||||
--border-sidebar-color: var(--ui-gray-8);
|
||||
--border-sidebar-color: var(--blue-9);
|
||||
--border-form-control-color: var(--grey-54);
|
||||
--border-table-color: var(--grey-3);
|
||||
--border-table-top-color: var(--grey-3);
|
||||
@@ -419,26 +406,27 @@
|
||||
--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-boxselector-wrapper-hover: 3px solid var(--blue-8);
|
||||
--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(--grey-3);
|
||||
--border-pagination-color: var(--grey-1);
|
||||
--border-nav-container-color: var(--ui-gray-neutral-8);
|
||||
--border-stepper-color: var(--ui-gray-warm-9);
|
||||
--border-widget: var(--ui-gray-9);
|
||||
|
||||
--blue-color: var(--blue-2);
|
||||
--button-close-color: var(--white-color);
|
||||
@@ -458,26 +446,22 @@
|
||||
--bg-multiselect-helpercontainer: var(--grey-1);
|
||||
--text-input-textarea: var(--grey-1);
|
||||
|
||||
--user-menu-icon-color: var(--grey-3);
|
||||
--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(--grey-54);
|
||||
--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);
|
||||
}
|
||||
|
||||
/* 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-sidebar-nav-color: var(--black-color);
|
||||
--bg-widget-color: var(--black-color);
|
||||
--bg-widget-header-color: var(--black-color);
|
||||
--bg-widget-table-color: var(--black-color);
|
||||
@@ -485,29 +469,30 @@
|
||||
--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-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-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);
|
||||
@@ -519,29 +504,28 @@
|
||||
--bg-tooltip-color: var(--black-color);
|
||||
--bg-table-selected-color: var(--grey-3);
|
||||
--bg-pre-color: var(--grey-2);
|
||||
--bg-nav-container-color: var(--ui-black);
|
||||
--bg-navtabs-hover-color: var(--grey-3);
|
||||
--bg-nav-tab-active-color: var(--ui-black);
|
||||
--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-btn-default-hover-color: var(--grey-4);
|
||||
--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);
|
||||
--bg-btn-focus: var(--black-color);
|
||||
--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-code-script-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);
|
||||
@@ -554,6 +538,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);
|
||||
@@ -565,46 +550,48 @@
|
||||
--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-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);
|
||||
--text-pre-color: var(--white-color);
|
||||
--text-navtabs-color: var(--white-color);
|
||||
--text-navtabs-hover-color: var(--white-color);
|
||||
--text-nav-tab-active-color: var(--white-color);
|
||||
--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-multiselect-item-color: var(--white-color);
|
||||
--text-pagination-span-color: var(--ui-gray-3);
|
||||
--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);
|
||||
|
||||
--border-color: var(--grey-55);
|
||||
--border-widget-color: var(--white-color);
|
||||
--border-sidebar-color: var(--white-color);
|
||||
--border-sidebar-color: var(--blue-9);
|
||||
--border-form-control-color: var(--grey-54);
|
||||
--border-table-color: var(--grey-55);
|
||||
--border-table-top-color: var(--grey-55);
|
||||
--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);
|
||||
@@ -613,8 +600,6 @@
|
||||
--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);
|
||||
|
||||
--shadow-box-color: none;
|
||||
--shadow-boxselector-color: none;
|
||||
@@ -635,7 +620,6 @@
|
||||
--text-cm-string-color: var(--red-7);
|
||||
--text-progress-bar-color: var(--black-color);
|
||||
|
||||
--user-menu-icon-color: var(--white-color);
|
||||
--sort-icon-muted: var(--ui-gray-7);
|
||||
--sort-icon-hover: var(--ui-gray-6);
|
||||
--sort-icon: var(--ui-gray-3);
|
||||
|
||||
@@ -31,18 +31,35 @@
|
||||
border-top: 1px solid var(--border-table-top-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.table .table {
|
||||
background-color: initial;
|
||||
background-color: var(--bg-table-color);
|
||||
}
|
||||
|
||||
.table-bordered {
|
||||
@@ -104,8 +121,8 @@ code {
|
||||
.nav-tabs > li.active > a,
|
||||
.nav-tabs > li.active > a:hover,
|
||||
.nav-tabs > li.active > a:focus {
|
||||
color: var(--text-nav-tab-active-color);
|
||||
background-color: var(--bg-nav-tab-active-color);
|
||||
color: var(--text-navtabs-color);
|
||||
background-color: var(--bg-navtabs-color);
|
||||
border: 1px solid var(--border-navtabs-color);
|
||||
}
|
||||
|
||||
@@ -117,13 +134,8 @@ code {
|
||||
border-color: var(--border-navtabs-color);
|
||||
}
|
||||
|
||||
.nav > li > a {
|
||||
color: var(--text-navtabs-color);
|
||||
}
|
||||
|
||||
.nav > li > a:hover,
|
||||
.nav > li > a:focus {
|
||||
color: var(--text-navtabs-hover-color);
|
||||
background-color: var(--bg-navtabs-hover-color);
|
||||
}
|
||||
|
||||
@@ -151,22 +163,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 {
|
||||
@@ -195,7 +197,6 @@ code {
|
||||
|
||||
.dropdown-menu {
|
||||
background: var(--bg-dropdown-menu-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a {
|
||||
@@ -204,7 +205,6 @@ code {
|
||||
|
||||
pre {
|
||||
border: 1px solid var(--border-pre-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-pre-color);
|
||||
color: var(--text-pre-color);
|
||||
}
|
||||
@@ -224,27 +224,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);
|
||||
}
|
||||
@@ -299,15 +278,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);
|
||||
@@ -387,10 +359,33 @@ 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);
|
||||
}
|
||||
|
||||
.btn-success: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 {
|
||||
@@ -398,48 +393,8 @@ input:-webkit-autofill {
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 0px;
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
border-bottom: 3px solid red;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<g clip-path="url(#clip0_9538_418895)">
|
||||
<path d="M15.0049 13.2509L8.75488 20.7509H14.3799L13.7549 25.7509L20.0049 18.2509H14.3799L15.0049 13.2509Z" stroke="#0086C9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9538_418895">
|
||||
<rect width="15" height="15" fill="white" transform="translate(6.87988 12.0009)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 719 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path d="M9.32758 23.1544V23.0281C9.32758 21.9669 9.32758 21.4364 9.53409 21.0311C9.71574 20.6746 10.0056 20.3847 10.3621 20.2031C10.7674 19.9966 11.298 19.9966 12.3591 19.9966H16.4011C17.4622 19.9966 17.9928 19.9966 18.3981 20.2031C18.7546 20.3847 19.0444 20.6746 19.2261 21.0311C19.4326 21.4364 19.4326 21.9669 19.4326 23.0281V23.1544M9.32758 23.1544C8.62997 23.1544 8.06445 23.7199 8.06445 24.4175C8.06445 25.1151 8.62997 25.6806 9.32758 25.6806C10.0252 25.6806 10.5907 25.1151 10.5907 24.4175C10.5907 23.7199 10.0252 23.1544 9.32758 23.1544ZM19.4326 23.1544C18.735 23.1544 18.1695 23.7199 18.1695 24.4175C18.1695 25.1151 18.735 25.6806 19.4326 25.6806C20.1302 25.6806 20.6957 25.1151 20.6957 24.4175C20.6957 23.7199 20.1302 23.1544 19.4326 23.1544ZM14.3801 23.1544C13.6825 23.1544 13.117 23.7199 13.117 24.4175C13.117 25.1151 13.6825 25.6806 14.3801 25.6806C15.0777 25.6806 15.6432 25.1151 15.6432 24.4175C15.6432 23.7199 15.0777 23.1544 14.3801 23.1544ZM14.3801 23.1544V16.8388M10.5907 16.8388H18.1695C18.758 16.8388 19.0523 16.8388 19.2844 16.7426C19.5939 16.6144 19.8398 16.3685 19.968 16.059C20.0641 15.8269 20.0641 15.5326 20.0641 14.9441C20.0641 14.3555 20.0641 14.0613 19.968 13.8291C19.8398 13.5196 19.5939 13.2737 19.2844 13.1455C19.0523 13.0494 18.758 13.0494 18.1695 13.0494H10.5907C10.0022 13.0494 9.70789 13.0494 9.47576 13.1455C9.16626 13.2737 8.92036 13.5196 8.79217 13.8291C8.69602 14.0613 8.69602 14.3555 8.69602 14.9441C8.69602 15.5326 8.69602 15.8269 8.79217 16.059C8.92036 16.3685 9.16626 16.6144 9.47576 16.7426C9.70789 16.8388 10.0022 16.8388 10.5907 16.8388Z" stroke="#0086C9" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M2.01501 12H23M23 12L16.0001 5M23 12L16.0001 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M49.9999 87.5001L49.9999 12.5004M49.9999 87.5001L67.6776 69.8224M49.9999 87.5001L32.3222 69.8224M49.9999 12.5004L32.3223 30.178M49.9999 12.5004L67.6776 30.1781" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 368 B |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M49.9999 9.22363V92.557M79.4627 21.4275L20.5371 80.3531M91.6666 50.8903H8.33325M79.4627 80.3531L20.5371 21.4275" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 320 B |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M39.5835 91.6665C56.8424 91.6665 70.8335 77.6754 70.8335 60.4165C70.8335 43.1576 56.8424 29.1665 39.5835 29.1665C22.3246 29.1665 8.3335 43.1576 8.3335 60.4165C8.3335 77.6754 22.3246 91.6665 39.5835 91.6665Z" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20.8335 62.4998C20.8335 50.9939 30.1609 41.6665 41.6668 41.6665" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M71.6499 9.34864V4.1665M85.4165 14.7403L89.0808 11.076M85.2593 42.1688L88.9237 45.8332M57.8308 14.7403L54.1665 11.076M90.651 28.3498H95.8332M63.0022 36.9975L74.9998 24.9998" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 853 B |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M37.1986 10.3367C20.4498 15.7383 8.33334 31.4541 8.33334 49.9999C8.33334 73.0118 26.9881 91.6666 50 91.6666C73.0119 91.6666 91.6667 73.0118 91.6667 49.9999C91.6667 31.4541 79.5502 15.7383 62.8014 10.3367" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 412 B |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M22.7 13.5L20.7005 11.5L18.6999 13.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C15.3019 3 18.1885 4.77814 19.7545 7.42909M12 7V12L15 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 382 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 284 B |
@@ -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 +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 |
@@ -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 +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 |
@@ -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 +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 |
@@ -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-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg><svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M58.3333 9.45654V26.6671C58.3333 29.0007 58.3333 30.1675 58.7875 31.0588C59.1869 31.8428 59.8244 32.4802 60.6084 32.8797C61.4997 33.3338 62.6665 33.3338 65 33.3338H82.2106M58.3333 72.9168L68.75 62.5002L58.3333 52.0835M41.6667 52.0835L31.25 62.5002L41.6667 72.9168M83.3333 41.6178V71.6668C83.3333 78.6675 83.3333 82.1678 81.9709 84.8417C80.7725 87.1937 78.8602 89.106 76.5082 90.3044C73.8343 91.6668 70.334 91.6668 63.3333 91.6668H36.6667C29.666 91.6668 26.1657 91.6668 23.4918 90.3044C21.1398 89.106 19.2275 87.1937 18.0291 84.8417C16.6667 82.1678 16.6667 78.6675 16.6667 71.6668V28.3335C16.6667 21.3328 16.6667 17.8325 18.0291 15.1586C19.2275 12.8066 21.1398 10.8943 23.4918 9.69591C26.1657 8.3335 29.666 8.3335 36.6667 8.3335H50.0491C53.1064 8.3335 54.6351 8.3335 56.0737 8.67887C57.3492 8.98508 58.5685 9.49014 59.6869 10.1755C60.9484 10.9485 62.0293 12.0295 64.1912 14.1914L77.4755 27.4756C79.6374 29.6375 80.7183 30.7185 81.4913 31.9799C82.1767 33.0983 82.6818 34.3176 82.988 35.5931C83.3333 37.0317 83.3333 38.5604 83.3333 41.6178Z" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M58.3333 9.45654V26.6671C58.3333 29.0007 58.3333 30.1675 58.7875 31.0588C59.1869 31.8428 59.8244 32.4802 60.6084 32.8797C61.4997 33.3338 62.6664 33.3338 65 33.3338H82.2106M37.5 66.6668L45.8333 75.0002L64.5833 56.2502M58.3333 8.3335H36.6667C29.666 8.3335 26.1657 8.3335 23.4918 9.69591C21.1397 10.8943 19.2275 12.8066 18.0291 15.1586C16.6667 17.8325 16.6667 21.3328 16.6667 28.3335V71.6668C16.6667 78.6675 16.6667 82.1678 18.0291 84.8417C19.2275 87.1937 21.1397 89.106 23.4918 90.3044C26.1657 91.6668 29.666 91.6668 36.6667 91.6668H63.3333C70.334 91.6668 73.8343 91.6668 76.5082 90.3044C78.8602 89.106 80.7725 87.1937 81.9709 84.8417C83.3333 82.1678 83.3333 78.6675 83.3333 71.6668V33.3335L58.3333 8.3335Z" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 913 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 349 B |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 12H18M3 6H21M9 18H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
Before Width: | Height: | Size: 226 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M37.5 25.0002V43.7552C37.5 46.0507 37.5 47.1985 37.2138 48.2621C36.9603 49.2044 36.5432 50.095 35.9816 50.893C35.3477 51.7938 34.4659 52.5286 32.7025 53.9981L17.2975 66.8356C15.5341 68.3051 14.6523 69.0399 14.0184 69.9406C13.4568 70.7387 13.0397 71.6292 12.7862 72.5716C12.5 73.6352 12.5 74.783 12.5 77.0785V78.3335C12.5 83.0006 12.5 85.3342 13.4083 87.1168C14.2072 88.6848 15.4821 89.9596 17.0501 90.7586C18.8327 91.6668 21.1662 91.6668 25.8333 91.6668H74.1667C78.8338 91.6668 81.1673 91.6668 82.9499 90.7586C84.5179 89.9596 85.7928 88.6848 86.5917 87.1168C87.5 85.3342 87.5 83.0006 87.5 78.3335V77.0785C87.5 74.783 87.5 73.6352 87.2138 72.5716C86.9603 71.6292 86.5432 70.7387 85.9816 69.9406C85.3477 69.0399 84.4659 68.3051 82.7025 66.8356L67.2975 53.9981C65.5341 52.5286 64.6523 51.7938 64.0184 50.893C63.4568 50.095 63.0397 49.2044 62.7861 48.2621C62.5 47.1985 62.5 46.0507 62.5 43.7552V25.0002M34.5833 25.0002H65.4167C66.5834 25.0002 67.1668 25.0002 67.6125 24.7731C68.0045 24.5734 68.3232 24.2546 68.5229 23.8626C68.75 23.417 68.75 22.8336 68.75 21.6668V11.6668C68.75 10.5001 68.75 9.91667 68.5229 9.47102C68.3232 9.07901 68.0045 8.7603 67.6125 8.56057C67.1668 8.3335 66.5834 8.3335 65.4167 8.3335H34.5833C33.4166 8.3335 32.8332 8.3335 32.3875 8.56057C31.9955 8.7603 31.6768 9.07901 31.4771 9.47102C31.25 9.91667 31.25 10.5001 31.25 11.6668V21.6668C31.25 22.8336 31.25 23.417 31.4771 23.8626C31.6768 24.2546 31.9955 24.5734 32.3875 24.7731C32.8332 25.0002 33.4166 25.0002 34.5833 25.0002ZM22.9167 70.8335H77.0833C79.0194 70.8335 79.9874 70.8335 80.7924 70.9936C84.0982 71.6512 86.6823 74.2353 87.3399 77.5411C87.5 78.3461 87.5 79.3141 87.5 81.2502C87.5 83.1862 87.5 84.1543 87.3399 84.9593C86.6823 88.265 84.0982 90.8492 80.7924 91.5067C79.9874 91.6668 79.0194 91.6668 77.0833 91.6668H22.9167C20.9806 91.6668 20.0126 91.6668 19.2076 91.5067C15.9018 90.8492 13.3177 88.265 12.6601 84.9593C12.5 84.1543 12.5 83.1862 12.5 81.2502C12.5 79.3141 12.5 78.3461 12.6601 77.5411C13.3177 74.2353 15.9018 71.6512 19.2076 70.9936C20.0126 70.8335 20.9806 70.8335 22.9167 70.8335Z" stroke="currentColor" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.3629 17.9077L15.5368 5.65985C14.6877 4.78038 13.3115 4.78038 12.4624 5.65985L10.0975 8.10918L13.3098 11.4363C13.5655 11.327 13.8452 11.2663 14.1389 11.2663C15.3414 11.2663 16.3162 12.2758 16.3162 13.5213C16.3162 13.8255 16.2575 14.1151 16.152 14.38L19.227 17.5648C19.4827 17.4555 19.7624 17.3948 20.0561 17.3948C21.2586 17.3948 22.2334 18.4044 22.2334 19.6499C22.2334 20.8953 21.2586 21.9049 20.0561 21.9049C18.8536 21.9049 17.8788 20.8953 17.8788 19.6499C17.8788 19.3457 17.9374 19.056 18.0429 18.7912L14.9757 15.6145V23.639C15.7624 23.979 16.3156 24.7827 16.3156 25.7212C16.3156 26.9666 15.3408 27.9762 14.1383 27.9762C12.9358 27.9762 11.961 26.9666 11.961 25.7212C11.961 24.7833 12.5143 23.979 13.3009 23.639V15.6035C12.5143 15.2635 11.961 14.4598 11.961 13.5213C11.961 13.2172 12.0196 12.9275 12.1252 12.6627L8.91281 9.33559L0.636858 17.9077C-0.212286 18.7872 -0.212286 20.2125 0.636858 21.0919L12.4629 33.3404C13.3121 34.2198 14.6882 34.2198 15.5374 33.3404L27.3634 21.0919C28.2126 20.2125 28.2126 18.7872 27.3634 17.9077H27.3629Z" fill="#E15B39"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.542 76.0248C47.5986 74.1886 52.6393 74.1886 56.6959 76.0248M39.6892 69.9387C44.8263 73.9617 44.8263 80.4844 39.6892 84.5075C34.5521 88.5305 26.2232 88.5305 21.0861 84.5075C15.9491 80.4844 15.9491 73.9618 21.0861 69.9387C26.2232 65.9156 34.5521 65.9156 39.6892 69.9387ZM79.1522 69.9387C84.2892 73.9617 84.2892 80.4844 79.1522 84.5075C74.0151 88.5305 65.6862 88.5305 60.5491 84.5075C55.412 80.4844 55.412 73.9618 60.5491 69.9387C65.6862 65.9156 74.0151 65.9156 79.1522 69.9387Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28.7138 40.7474C15.866 42.0672 8.27072 45.0645 8.27072 47.9823C8.27072 52.2584 26.9536 55.7248 50 55.7248C73.0465 55.7248 91.7294 52.2584 91.7294 47.9823C91.7294 45.4722 85.0332 42.5759 75.0544 41.1611" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28.8541 40.2396L37.3725 14.6046L45.8429 20.592C45.8429 20.592 51.7915 18.0993 55.1599 16.6418C59.9267 14.5791 66.8807 12.4751 66.8807 12.4751L70.8711 26.3574L74.8616 40.2396" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M15.5 11.5H14.5L13 14.5L11 8.5L9.5 11.5H8.5M11.9932 5.13581C9.9938 2.7984 6.65975 2.16964 4.15469 4.31001C1.64964 6.45038 1.29697 10.029 3.2642 12.5604C4.75009 14.4724 8.97129 18.311 10.948 20.0749C11.3114 20.3991 11.4931 20.5613 11.7058 20.6251C11.8905 20.6805 12.0958 20.6805 12.2805 20.6251C12.4932 20.5613 12.6749 20.3991 13.0383 20.0749C15.015 18.311 19.2362 14.4724 20.7221 12.5604C22.6893 10.029 22.3797 6.42787 19.8316 4.31001C17.2835 2.19216 13.9925 2.7984 11.9932 5.13581Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 687 B |