Compare commits
21 Commits
debug-api-
...
edge-wss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a386c5a533 | ||
|
|
a4e99ce3c5 | ||
|
|
75d854e6ad | ||
|
|
0b2217a916 | ||
|
|
ca30efeca7 | ||
|
|
dc98850489 | ||
|
|
01dc9066b7 | ||
|
|
3aacaa7caf | ||
|
|
b031a30f62 | ||
|
|
12cddbd896 | ||
|
|
3791b7a16f | ||
|
|
d754532ab1 | ||
|
|
9a48ceaec1 | ||
|
|
1132c9ce87 | ||
|
|
668d526604 | ||
|
|
0e257c200f | ||
|
|
df05914fac | ||
|
|
0ffb84aaa6 | ||
|
|
b01180bb29 | ||
|
|
16f8b737f1 | ||
|
|
d9d1d6bfaa |
@@ -3,22 +3,29 @@ package chisel
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance.
|
||||
// The key represents the following data in this particular format:
|
||||
// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID
|
||||
// http(s)://serverURL|http(s)://host:tunnelServerPort|tunnel_server_fingerprint|endpoint_ID
|
||||
// Protocol used to connect to the tunnel server will match the one used by the Portainer server (http or https).
|
||||
// The key returned by this function is a base64 encoded version of the data.
|
||||
func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
|
||||
func (service *Service) GenerateEdgeKey(serverURL, host string, endpointIdentifier int) (string, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
keyInformation := []string{
|
||||
url,
|
||||
fmt.Sprintf("%s:%s", host, service.serverPort),
|
||||
serverURL,
|
||||
fmt.Sprintf("%s://%s:%s", u.Scheme, host, service.serverPort),
|
||||
service.serverFingerprint,
|
||||
strconv.Itoa(endpointIdentifier),
|
||||
}
|
||||
|
||||
key := strings.Join(keyInformation, "|")
|
||||
return base64.RawStdEncoding.EncodeToString([]byte(key))
|
||||
return base64.RawStdEncoding.EncodeToString([]byte(key)), nil
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
// be found inside the database, it will generate a new one randomly and persist it.
|
||||
// It starts the tunnel status verification process in the background.
|
||||
// The snapshotter is used in the tunnel status verification process.
|
||||
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
|
||||
func (service *Service) StartTunnelServer(addr, port, certPath, keyPath string, snapshotService portainer.SnapshotService) error {
|
||||
keySeed, err := service.retrievePrivateKeySeed()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -104,6 +104,10 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
|
||||
config := &chserver.Config{
|
||||
Reverse: true,
|
||||
KeySeed: keySeed,
|
||||
TLS: chserver.TLSConfig{
|
||||
Cert: certPath,
|
||||
Key: keyPath,
|
||||
},
|
||||
}
|
||||
|
||||
chiselServer, err := chserver.NewServer(config)
|
||||
|
||||
@@ -35,6 +35,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
|
||||
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
@@ -572,6 +573,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
openAMTService := openamt.NewService()
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
@@ -607,7 +609,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
@@ -634,6 +636,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
applicationStatus := initStatus(instanceID)
|
||||
|
||||
demoService := demo.NewService()
|
||||
if *flags.DemoEnvironment {
|
||||
err := demoService.Init(dataStore, cryptoService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing demo environment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed initializing environment: %v", err)
|
||||
@@ -675,7 +685,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
}
|
||||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
||||
certPath, keyPath := fileService.GetDefaultSSLCertsPath()
|
||||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, certPath, keyPath, snapshotService)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed starting tunnel server: %v", err)
|
||||
}
|
||||
@@ -722,6 +734,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
DemoService: demoService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,8 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
|
||||
case portainer.EdgeAgentOnKubernetesEnvironment:
|
||||
cs := chisel.NewService(store, nil)
|
||||
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
|
||||
edgeKey := cs.GenerateEdgeKey(URL, "", int(id))
|
||||
edgeKey, err := cs.GenerateEdgeKey(URL, "", int(id))
|
||||
is.NoError(err, "GenerateEdgeKey() should not return an error")
|
||||
expectedEndpoint.EdgeKey = edgeKey
|
||||
store.testTunnelServer(t)
|
||||
|
||||
|
||||
118
api/demo/demo.go
Normal file
118
api/demo/demo.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type EnvironmentDetails struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Users []portainer.UserID `json:"users"`
|
||||
Environments []portainer.EndpointID `json:"environments"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
details EnvironmentDetails
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (service *Service) Details() EnvironmentDetails {
|
||||
return service.details
|
||||
}
|
||||
|
||||
func (service *Service) Init(store dataservices.DataStore, cryptoService portainer.CryptoService) error {
|
||||
log.Print("[INFO] [main] Starting demo environment")
|
||||
|
||||
isClean, err := isCleanStore(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed checking if store is clean")
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return errors.New(" Demo environment can only be initialized on a clean database")
|
||||
}
|
||||
|
||||
id, err := initDemoUser(store, cryptoService)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed creating demo user")
|
||||
}
|
||||
|
||||
endpointIds, err := initDemoEndpoints(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed creating demo endpoint")
|
||||
}
|
||||
|
||||
err = initDemoSettings(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed updating demo settings")
|
||||
}
|
||||
|
||||
service.details = EnvironmentDetails{
|
||||
Enabled: true,
|
||||
Users: []portainer.UserID{id},
|
||||
// endpoints 2,3 are created after deployment of portainer
|
||||
Environments: endpointIds,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCleanStore(store dataservices.DataStore) (bool, error) {
|
||||
endpoints, err := store.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
users, err := store.User().Users()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (service *Service) IsDemo() bool {
|
||||
return service.details.Enabled
|
||||
}
|
||||
|
||||
func (service *Service) IsDemoEnvironment(environmentID portainer.EndpointID) bool {
|
||||
if !service.IsDemo() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, demoEndpointID := range service.details.Environments {
|
||||
if environmentID == demoEndpointID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *Service) IsDemoUser(userID portainer.UserID) bool {
|
||||
if !service.IsDemo() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, demoUserID := range service.details.Users {
|
||||
if userID == demoUserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
79
api/demo/init.go
Normal file
79
api/demo/init.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
func initDemoUser(
|
||||
store dataservices.DataStore,
|
||||
cryptoService portainer.CryptoService,
|
||||
) (portainer.UserID, error) {
|
||||
|
||||
password, err := cryptoService.Hash("tryportainer")
|
||||
if err != nil {
|
||||
return 0, errors.WithMessage(err, "failed creating password hash")
|
||||
}
|
||||
|
||||
admin := &portainer.User{
|
||||
Username: "admin",
|
||||
Password: password,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
err = store.User().Create(admin)
|
||||
return admin.ID, errors.WithMessage(err, "failed creating user")
|
||||
}
|
||||
|
||||
func initDemoEndpoints(store dataservices.DataStore) ([]portainer.EndpointID, error) {
|
||||
localEndpointId, err := initDemoLocalEndpoint(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// second and third endpoints are going to be created with docker-compose as a part of the demo environment set up.
|
||||
// ref: https://github.com/portainer/portainer-demo/blob/master/docker-compose.yml
|
||||
return []portainer.EndpointID{localEndpointId, localEndpointId + 1, localEndpointId + 2}, nil
|
||||
}
|
||||
|
||||
func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, error) {
|
||||
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
|
||||
localEndpoint := &portainer.Endpoint{
|
||||
ID: id,
|
||||
Name: "local",
|
||||
URL: "unix:///var/run/docker.sock",
|
||||
PublicURL: "demo.portainer.io",
|
||||
Type: portainer.DockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
}
|
||||
|
||||
err := store.Endpoint().Create(localEndpoint)
|
||||
return id, errors.WithMessage(err, "failed creating local endpoint")
|
||||
}
|
||||
|
||||
func initDemoSettings(
|
||||
store dataservices.DataStore,
|
||||
) error {
|
||||
settings, err := store.Settings().Settings()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed fetching settings")
|
||||
}
|
||||
|
||||
settings.EnableTelemetry = false
|
||||
settings.LogoURL = ""
|
||||
|
||||
err = store.Settings().UpdateSettings(settings)
|
||||
return errors.WithMessage(err, "failed updating settings")
|
||||
}
|
||||
@@ -108,12 +108,12 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
|
||||
return "", errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
refsUrl, err := a.buildRefsUrl(config, options.referenceName)
|
||||
rootItemUrl, err := a.buildRootItemUrl(config, options.referenceName)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to build azure refs url")
|
||||
return "", errors.WithMessage(err, "failed to build azure root item url")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", refsUrl, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", rootItemUrl, nil)
|
||||
if options.username != "" || options.password != "" {
|
||||
req.SetBasicAuth(options.username, options.password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
@@ -131,26 +131,24 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to get repository refs with a status \"%v\"", resp.Status)
|
||||
return "", fmt.Errorf("failed to get repository root item with a status \"%v\"", resp.Status)
|
||||
}
|
||||
|
||||
var refs struct {
|
||||
var items struct {
|
||||
Value []struct {
|
||||
Name string `json:"name"`
|
||||
ObjectId string `json:"objectId"`
|
||||
}
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&refs); err != nil {
|
||||
return "", errors.Wrap(err, "could not parse Azure Refs response")
|
||||
}
|
||||
|
||||
for _, ref := range refs.Value {
|
||||
if strings.EqualFold(ref.Name, options.referenceName) {
|
||||
return ref.ObjectId, nil
|
||||
CommitId string `json:"commitId"`
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.Errorf("could not find ref %q in the repository", options.referenceName)
|
||||
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
|
||||
return "", errors.Wrap(err, "could not parse Azure items response")
|
||||
}
|
||||
|
||||
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
|
||||
return "", errors.Errorf("failed to get latest commitID in the repository")
|
||||
}
|
||||
|
||||
return items.Value[0].CommitId, nil
|
||||
}
|
||||
|
||||
func parseUrl(rawUrl string) (*azureOptions, error) {
|
||||
@@ -236,8 +234,10 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
|
||||
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
|
||||
q.Set("scopePath", "/")
|
||||
q.Set("download", "true")
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
if referenceName != "" {
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
}
|
||||
q.Set("$format", "zip")
|
||||
q.Set("recursionLevel", "full")
|
||||
q.Set("api-version", "6.0")
|
||||
@@ -246,8 +246,8 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName string) (string, error) {
|
||||
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/refs",
|
||||
func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName string) (string, error) {
|
||||
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
|
||||
a.baseUrl,
|
||||
url.PathEscape(config.organisation),
|
||||
url.PathEscape(config.project),
|
||||
@@ -255,12 +255,15 @@ func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName strin
|
||||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse refs url path %s", rawUrl)
|
||||
return "", errors.Wrapf(err, "failed to parse root item url path %s", rawUrl)
|
||||
}
|
||||
|
||||
// filterContains=main&api-version=6.0
|
||||
q := u.Query()
|
||||
q.Set("filterContains", formatReferenceName(referenceName))
|
||||
q.Set("scopePath", "/")
|
||||
if referenceName != "" {
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
}
|
||||
q.Set("api-version", "6.0")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
|
||||
@@ -28,15 +28,15 @@ func Test_buildDownloadUrl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_buildRefsUrl(t *testing.T) {
|
||||
func Test_buildRootItemUrl(t *testing.T) {
|
||||
a := NewAzureDownloader(nil)
|
||||
u, err := a.buildRefsUrl(&azureOptions{
|
||||
u, err := a.buildRootItemUrl(&azureOptions{
|
||||
organisation: "organisation",
|
||||
project: "project",
|
||||
repository: "repository",
|
||||
}, "refs/heads/main")
|
||||
|
||||
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/refs?filterContains=main&api-version=6.0")
|
||||
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&api-version=6.0&versionDescriptor.version=main&versionDescriptor.versionType=branch")
|
||||
actualUrl, _ := url.Parse(u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
|
||||
@@ -270,63 +270,17 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := `{
|
||||
"value": [
|
||||
{
|
||||
"name": "refs/heads/feature/calcApp",
|
||||
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
"creator": {
|
||||
"displayName": "Normal Paulk",
|
||||
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
}
|
||||
},
|
||||
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"uniqueName": "dev@mailserver.com",
|
||||
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
},
|
||||
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2FcalcApp"
|
||||
},
|
||||
{
|
||||
"name": "refs/heads/feature/replacer",
|
||||
"objectId": "917131a709996c5cfe188c3b57e9a6ad90e8b85c",
|
||||
"creator": {
|
||||
"displayName": "Normal Paulk",
|
||||
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
}
|
||||
},
|
||||
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"uniqueName": "dev@mailserver.com",
|
||||
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
},
|
||||
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2Freplacer"
|
||||
},
|
||||
{
|
||||
"name": "refs/heads/master",
|
||||
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
"creator": {
|
||||
"displayName": "Normal Paulk",
|
||||
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
}
|
||||
},
|
||||
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"uniqueName": "dev@mailserver.com",
|
||||
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
},
|
||||
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Fmaster"
|
||||
}
|
||||
],
|
||||
"count": 3
|
||||
"count": 1,
|
||||
"value": [
|
||||
{
|
||||
"objectId": "1a5630f017127db7de24d8771da0f536ff98fc9b",
|
||||
"gitObjectType": "tree",
|
||||
"commitId": "27104ad7549d9e66685e115a497533f18024be9c",
|
||||
"path": "/",
|
||||
"isFolder": true,
|
||||
"url": "https://dev.azure.com/simonmeng0474/4b546a97-c481-4506-bdd5-976e9592f91a/_apis/git/repositories/a22247ad-053f-43bc-88a7-62ff4846bb97/items?path=%2F&versionType=Branch&versionOptions=None"
|
||||
}
|
||||
]
|
||||
}`
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(response))
|
||||
@@ -347,19 +301,11 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
{
|
||||
name: "should be able to parse response",
|
||||
args: fetchOptions{
|
||||
referenceName: "refs/heads/master",
|
||||
referenceName: "",
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
|
||||
want: "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
want: "27104ad7549d9e66685e115a497533f18024be9c",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should be able to parse response",
|
||||
args: fetchOptions{
|
||||
referenceName: "refs/heads/unknown",
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -82,8 +82,17 @@ func (c gitClient) latestCommitID(ctx context.Context, opt fetchOptions) (string
|
||||
return "", errors.Wrap(err, "failed to list repository refs")
|
||||
}
|
||||
|
||||
referenceName := opt.referenceName
|
||||
if referenceName == "" {
|
||||
for _, ref := range refs {
|
||||
if strings.EqualFold(ref.Name().String(), "HEAD") {
|
||||
referenceName = ref.Target().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
if strings.EqualFold(ref.Name().String(), opt.referenceName) {
|
||||
if strings.EqualFold(ref.Name().String(), referenceName) {
|
||||
return ref.Hash().String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
14
api/go.mod
14
api/go.mod
@@ -27,7 +27,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/jpillora/chisel v1.7.7
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
@@ -42,9 +42,9 @@ require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
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/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
k8s.io/api v0.22.5
|
||||
@@ -69,7 +69,7 @@ require (
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.1.0 // indirect
|
||||
@@ -106,9 +106,9 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
||||
33
api/go.sum
33
api/go.sum
@@ -373,8 +373,8 @@ github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
|
||||
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
@@ -621,17 +621,13 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
|
||||
github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s=
|
||||
github.com/jpillora/ansi v1.0.2/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389 h1:K3JsoRqX6C4gmTvY4jqtFGCfK8uToj9DMahciJaoWwE=
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389/go.mod h1:wHQUFFnFySoqdAOzjHkTvb4DsVM1h/73PS9l2vnioRM=
|
||||
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY=
|
||||
github.com/jpillora/chisel v1.7.7 h1:eLbzoX+ekDhVmF5CpSJD01NtH/w7QMYeaFCIFbzn9ns=
|
||||
github.com/jpillora/chisel v1.7.7/go.mod h1:X3ZzJDlOSlkMLVY3DMsdrd03rMtugLYk2IOUhvX0SXo=
|
||||
github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA=
|
||||
github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
|
||||
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
|
||||
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
|
||||
github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -1021,7 +1017,6 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -1035,9 +1030,11 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1077,7 +1074,6 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1128,11 +1124,13 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1157,13 +1155,13 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1257,13 +1255,16 @@ golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -9,4 +9,6 @@ var (
|
||||
ErrUnauthorized = errors.New("Unauthorized")
|
||||
// ErrResourceAccessDenied Access denied to resource error
|
||||
ErrResourceAccessDenied = errors.New("Access denied to resource")
|
||||
// ErrNotAvailableInDemo feature is not allowed in demo
|
||||
ErrNotAvailableInDemo = errors.New("This feature is not available in the demo version of Portainer")
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -49,7 +50,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
@@ -86,7 +87,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -25,7 +27,17 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates an new instance of backup handler
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
|
||||
func NewHandler(
|
||||
bouncer *security.RequestBouncer,
|
||||
dataStore dataservices.DataStore,
|
||||
gate *offlinegate.OfflineGate,
|
||||
filestorePath string,
|
||||
shutdownTrigger context.CancelFunc,
|
||||
adminMonitor *adminmonitor.Monitor,
|
||||
demoService *demo.Service,
|
||||
|
||||
) *Handler {
|
||||
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
@@ -36,8 +48,11 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||
adminMonitor: adminMonitor,
|
||||
}
|
||||
|
||||
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
||||
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
||||
demoRestrictedRouter := h.NewRoute().Subrouter()
|
||||
demoRestrictedRouter.Use(middlewares.RestrictDemoEnv(demoService.IsDemo))
|
||||
|
||||
demoRestrictedRouter.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
||||
demoRestrictedRouter.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -50,7 +65,7 @@ func adminAccess(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perform the action", nil)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -51,7 +52,7 @@ func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
|
||||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
||||
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, test.backupPassword)
|
||||
@@ -74,7 +75,7 @@ func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
|
||||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
||||
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, "password")
|
||||
|
||||
924
api/http/handler/edgestacks/edgestack_test.go
Normal file
924
api/http/handler/edgestacks/edgestack_test.go
Normal file
@@ -0,0 +1,924 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func setupHandler(t *testing.T) (*Handler, string, func()) {
|
||||
t.Helper()
|
||||
|
||||
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
jwtService, err := jwt.NewService("1h", store)
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := &portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole}
|
||||
err = store.User().Create(user)
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
rawAPIKey, _, err := apiKeyService.GenerateApiKey(*user, "test")
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(
|
||||
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||||
store,
|
||||
)
|
||||
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fs, err := filesystem.NewService(tmpDir, "")
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler.FileService = fs
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.EnableEdgeComputeFeatures = true
|
||||
|
||||
err = handler.DataStore.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"}
|
||||
|
||||
return handler, rawAPIKey, storeTeardown
|
||||
}
|
||||
|
||||
func createEndpoint(t *testing.T, store dataservices.DataStore) portainer.Endpoint {
|
||||
t.Helper()
|
||||
|
||||
endpointID := portainer.EndpointID(5)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID portainer.EndpointID) portainer.EdgeStack {
|
||||
t.Helper()
|
||||
|
||||
edgeGroup := portainer.EdgeGroup{
|
||||
ID: 1,
|
||||
Name: "EdgeGroup 1",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{endpointID},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err := store.EdgeGroup().Create(&edgeGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
edgeStackID := portainer.EdgeStackID(14)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpointID},
|
||||
},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
|
||||
ProjectPath: "/project/path",
|
||||
EntryPoint: "entrypoint",
|
||||
Version: 237,
|
||||
ManifestPath: "/manifest/path",
|
||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpointID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||||
edgeStack.ID: true,
|
||||
},
|
||||
}
|
||||
|
||||
err = store.EdgeStack().Create(edgeStack.ID, &edgeStack)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.EndpointRelation().Create(&endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return edgeStack
|
||||
}
|
||||
|
||||
// Inspect
|
||||
func TestInspectInvalidEdgeID(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
EdgeStackID string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{"Invalid EdgeStackID", "x", 400},
|
||||
{"Non-existing EdgeStackID", "5", 404},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "/edge_stacks/"+tc.EdgeStackID, nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create
|
||||
func TestCreateAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
// Create Endpoint, EdgeGroup and EndpointRelation
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeGroup := portainer.EdgeGroup{
|
||||
ID: 1,
|
||||
Name: "EdgeGroup 1",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{endpoint.ID},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err := handler.DataStore.EdgeGroup().Create(&edgeGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
payload := swarmStackFromFileContentPayload{
|
||||
Name: "Test Stack",
|
||||
StackFileContent: "stack content",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
|
||||
// Create EdgeStack
|
||||
req, err := http.NewRequest(http.MethodPost, "/edge_stacks?method=string", r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
// Inspect
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", data.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data = portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if payload.Name != data.Name {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStack Name %s, found %s", payload.Name, data.Name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWithInvalidPayload(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload interface{}
|
||||
QueryString string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
Name: "Invalid query string parameter",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "invalid=query-string",
|
||||
ExpectedStatusCode: 400,
|
||||
},
|
||||
{
|
||||
Name: "Invalid creation method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=invalid-creation-method",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty swarmStackFromFileContentPayload with string method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty swarmStackFromFileContentPayload with repository method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=repository",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty swarmStackFromFileContentPayload with file method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=file",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Duplicated EdgeStack Name",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: edgeStack.Name,
|
||||
StackFileContent: "content",
|
||||
EdgeGroups: edgeStack.EdgeGroups,
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty EdgeStack Groups",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: edgeStack.Name,
|
||||
StackFileContent: "content",
|
||||
EdgeGroups: []portainer.EdgeGroupID{},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: "Stack name",
|
||||
StackFileContent: "content",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty Stack File Content",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: "Stack name",
|
||||
StackFileContent: "",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Clone Git respository error",
|
||||
Payload: swarmStackFromGitRepositoryPayload{
|
||||
Name: "Stack name",
|
||||
RepositoryURL: "github.com/portainer/portainer",
|
||||
RepositoryReferenceName: "ref name",
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: "",
|
||||
RepositoryPassword: "",
|
||||
FilePathInRepository: "/file/path",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
},
|
||||
QueryString: "method=repository",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
|
||||
// Create EdgeStack
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks?%s", tc.QueryString), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Delete
|
||||
func TestDeleteAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
// Create
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Inspect
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.ID != edgeStack.ID {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", int(edgeStack.ID), data.ID))
|
||||
}
|
||||
|
||||
// Delete
|
||||
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNoContent, rec.Code))
|
||||
}
|
||||
|
||||
// Inspect
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNotFound, rec.Code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteInvalidEdgeStack(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
URL string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{Name: "Non-existing EdgeStackID", URL: "/edge_stacks/-1", ExpectedStatusCode: http.StatusNotFound},
|
||||
{Name: "Invalid EdgeStackID", URL: "/edge_stacks/aaaaaaa", ExpectedStatusCode: http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodDelete, tc.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update
|
||||
func TestUpdateAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Update edge stack: create new Endpoint, EndpointRelation and EdgeGroup
|
||||
endpointID := portainer.EndpointID(6)
|
||||
newEndpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().Create(&newEndpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpointID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||||
edgeStack.ID: true,
|
||||
},
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newEdgeGroup := portainer.EdgeGroup{
|
||||
ID: 2,
|
||||
Name: "EdgeGroup 2",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{newEndpoint.ID},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newVersion := 238
|
||||
payload := updateEdgeStackPayload{
|
||||
StackFileContent: "update-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: append(edgeStack.EdgeGroups, newEdgeGroup.ID),
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
// Get updated edge stack
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.Version != *payload.Version {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", edgeStack.Version, data.Version))
|
||||
}
|
||||
|
||||
if data.DeploymentType != payload.DeploymentType {
|
||||
t.Fatalf(fmt.Sprintf("expected DeploymentType %d, found %d", edgeStack.DeploymentType, data.DeploymentType))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data.EdgeGroups, payload.EdgeGroups) {
|
||||
t.Fatalf("expected EdgeGroups to be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWithInvalidEdgeGroups(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
//newEndpoint := createEndpoint(t, handler.DataStore)
|
||||
newEdgeGroup := portainer.EdgeGroup{
|
||||
ID: 2,
|
||||
Name: "EdgeGroup 2",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{8889},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
|
||||
|
||||
newVersion := 238
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateEdgeStackPayload
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"Update with non-existing EdgeGroupID",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{9999},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Update with invalid EdgeGroup (non-existing Endpoint)",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{2},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Update DeploymentType from Docker to Kubernetes",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWithInvalidPayload(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
newVersion := 238
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateEdgeStackPayload
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"Update with empty StackFileContent",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: edgeStack.EdgeGroups,
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
"Update with empty EdgeGroups",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update Status
|
||||
func TestUpdateStatusAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Update edge stack status
|
||||
newStatus := portainer.StatusError
|
||||
payload := updateStatusPayload{
|
||||
Error: "test-error",
|
||||
Status: &newStatus,
|
||||
EndpointID: &endpoint.ID,
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
// Get updated edge stack
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].Type != *payload.Status {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusType %d, found %d", payload.Status, data.Status[endpoint.ID].Type))
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].Error != payload.Error {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusError %s, found %s", payload.Error, data.Status[endpoint.ID].Error))
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].EndpointID != *payload.EndpointID {
|
||||
t.Fatalf(fmt.Sprintf("expected EndpointID %d, found %d", payload.EndpointID, data.Status[endpoint.ID].EndpointID))
|
||||
}
|
||||
}
|
||||
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
|
||||
handler, _, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Update edge stack status
|
||||
statusError := portainer.StatusError
|
||||
statusOk := portainer.StatusOk
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateStatusPayload
|
||||
ExpectedErrorMessage string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"Update with nil Status",
|
||||
updateStatusPayload{
|
||||
Error: "test-error",
|
||||
Status: nil,
|
||||
EndpointID: &endpoint.ID,
|
||||
},
|
||||
"Invalid status",
|
||||
400,
|
||||
},
|
||||
{
|
||||
"Update with error status and empty error message",
|
||||
updateStatusPayload{
|
||||
Error: "",
|
||||
Status: &statusError,
|
||||
EndpointID: &endpoint.ID,
|
||||
},
|
||||
"Error message is mandatory when status is error",
|
||||
400,
|
||||
},
|
||||
{
|
||||
"Update with nil EndpointID",
|
||||
updateStatusPayload{
|
||||
Error: "",
|
||||
Status: &statusOk,
|
||||
EndpointID: nil,
|
||||
},
|
||||
"Invalid EnvironmentID",
|
||||
400,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Status
|
||||
func TestDeleteStatus(t *testing.T) {
|
||||
handler, _, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d/status/%d", edgeStack.ID, endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,15 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
isUnique, err := handler.isNameUnique(payload.Name, 0)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to check if name is unique", err)
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return httperror.NewError(http.StatusConflict, "Name is not unique", nil)
|
||||
}
|
||||
|
||||
endpoint, endpointCreationError := handler.createEndpoint(payload)
|
||||
if endpointCreationError != nil {
|
||||
return endpointCreationError
|
||||
@@ -306,7 +315,10 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
return nil, httperror.BadRequest("Unable to parse host", err)
|
||||
}
|
||||
|
||||
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
edgeKey, err := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
if err != nil {
|
||||
return nil, httperror.BadRequest("Unable to generate Edge key", err)
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
func TestEmptyGlobalKey(t *testing.T) {
|
||||
handler := NewHandler(
|
||||
helper.NewTestRequestBouncer(),
|
||||
nil,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://portainer.io:9443/endpoints/global-key", nil)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
||||
// @id EndpointDelete
|
||||
@@ -29,6 +30,10 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoEnvironment(portainer.EndpointID(endpointID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
|
||||
@@ -50,6 +50,7 @@ var endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
// @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 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"
|
||||
// @router /endpoints [get]
|
||||
@@ -127,6 +128,11 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
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)
|
||||
@@ -465,3 +471,18 @@ func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.Endp
|
||||
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
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func Test_endpointList(t *testing.T) {
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
h := NewHandler(bouncer)
|
||||
h := NewHandler(bouncer, nil)
|
||||
h.DataStore = store
|
||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
|
||||
@@ -88,7 +88,18 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.Name != nil {
|
||||
endpoint.Name = *payload.Name
|
||||
name := *payload.Name
|
||||
isUnique, err := handler.isNameUnique(name, endpoint.ID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to check if name is unique", err)
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return httperror.NewError(http.StatusConflict, "Name is not unique", nil)
|
||||
}
|
||||
|
||||
endpoint.Name = name
|
||||
|
||||
}
|
||||
|
||||
if payload.URL != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
@@ -35,6 +36,7 @@ type requestBouncer interface {
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer requestBouncer
|
||||
demoService *demo.Service
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
@@ -48,10 +50,11 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer requestBouncer) *Handler {
|
||||
func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
demoService: demoService,
|
||||
}
|
||||
|
||||
h.Handle("/endpoints",
|
||||
|
||||
18
api/http/handler/endpoints/unique_name.go
Normal file
18
api/http/handler/endpoints/unique_name.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package endpoints
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (handler *Handler) isNameUnique(name string, endpointID portainer.EndpointID) (bool, error) {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Name == name && (endpointID == 0 || endpoint.ID != endpointID) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -24,12 +25,14 @@ type Handler struct {
|
||||
JWTService dataservices.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
demoService *demo.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||
|
||||
@@ -113,6 +113,11 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemo() {
|
||||
payload.EnableTelemetry = nil
|
||||
payload.LogoURL = nil
|
||||
}
|
||||
|
||||
if payload.AuthenticationMethod != nil {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
|
||||
}
|
||||
|
||||
@@ -177,9 +177,6 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
||||
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||
}
|
||||
|
||||
@@ -70,9 +70,6 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.ManifestFile) {
|
||||
return errors.New("Invalid manifest file in repository")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -144,9 +144,6 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
||||
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import (
|
||||
"github.com/portainer/portainer/api/stacks"
|
||||
)
|
||||
|
||||
const defaultGitReferenceName = "refs/heads/master"
|
||||
|
||||
var (
|
||||
errStackAlreadyExists = errors.New("A stack already exists with this name")
|
||||
errWebhookIDAlreadyExists = errors.New("A webhook ID already exists")
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -26,10 +25,6 @@ type stackGitUpdatePayload struct {
|
||||
}
|
||||
|
||||
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
|
||||
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -28,9 +27,6 @@ type stackGitRedployPayload struct {
|
||||
}
|
||||
|
||||
func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,6 @@ func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error
|
||||
}
|
||||
|
||||
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,21 +5,24 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle status operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
Status *portainer.Status
|
||||
Status *portainer.Status
|
||||
demoService *demo.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage status operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
|
||||
@@ -5,16 +5,26 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
)
|
||||
|
||||
type status struct {
|
||||
*portainer.Status
|
||||
DemoEnvironment demo.EnvironmentDetails
|
||||
}
|
||||
|
||||
// @id StatusInspect
|
||||
// @summary Check Portainer status
|
||||
// @description Retrieve Portainer status
|
||||
// @description **Access policy**: public
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} portainer.Status "Success"
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /status [get]
|
||||
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, handler.Status)
|
||||
return response.JSON(w, &status{
|
||||
Status: handler.Status,
|
||||
DemoEnvironment: handler.demoService.Details(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"net/http"
|
||||
@@ -32,16 +33,18 @@ type Handler struct {
|
||||
*mux.Router
|
||||
bouncer *security.RequestBouncer
|
||||
apiKeyService apikey.APIKeyService
|
||||
demoService *demo.Service
|
||||
DataStore dataservices.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, apiKeyService apikey.APIKeyService) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, apiKeyService apikey.APIKeyService, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
apiKeyService: apiKeyService,
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
|
||||
@@ -40,7 +40,7 @@ func Test_userCreateAccessToken(t *testing.T) {
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
||||
@@ -32,7 +32,7 @@ func Test_deleteUserRemovesAccessTokens(t *testing.T) {
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
||||
|
||||
@@ -39,7 +39,7 @@ func Test_userGetAccessTokens(t *testing.T) {
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
||||
@@ -37,7 +37,7 @@ func Test_userRemoveAccessToken(t *testing.T) {
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
||||
@@ -57,6 +57,10 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoUser(portainer.UserID(userID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
|
||||
@@ -55,6 +55,10 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoUser(portainer.UserID(userID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
|
||||
@@ -32,7 +32,7 @@ func Test_updateUserRemovesAccessTokens(t *testing.T) {
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
||||
|
||||
23
api/http/middlewares/demo.go
Normal file
23
api/http/middlewares/demo.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
||||
// restrict functionality on demo environments
|
||||
func RestrictDemoEnv(isDemo func() bool) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !isDemo() {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
httperror.WriteError(w, http.StatusBadRequest, errors.ErrNotAvailableInDemo.Error(), errors.ErrNotAvailableInDemo)
|
||||
})
|
||||
}
|
||||
}
|
||||
41
api/http/middlewares/demo_test.go
Normal file
41
api/http/middlewares/demo_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_demoEnvironment_shouldFail(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
RestrictDemoEnv(func() bool { return true }).Middleware(h).ServeHTTP(w, r)
|
||||
|
||||
response := w.Result()
|
||||
defer response.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
|
||||
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
assert.Contains(t, string(body), "This feature is not available in the demo version of Portainer")
|
||||
}
|
||||
|
||||
func Test_notDemoEnvironment_shouldSucceed(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
RestrictDemoEnv(func() bool { return false }).Middleware(h).ServeHTTP(w, r)
|
||||
|
||||
response := w.Result()
|
||||
assert.Equal(t, http.StatusOK, response.StatusCode)
|
||||
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
|
||||
DockerClientFactory: factory.dockerClientFactory,
|
||||
}
|
||||
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport)
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport, factory.gitService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func findSystemNetworkResourceControl(networkObject map[string]interface{}) *por
|
||||
networkID := networkObject[networkObjectIdentifier].(string)
|
||||
networkName := networkObject[networkObjectName].(string)
|
||||
|
||||
if networkName == "bridge" || networkName == "host" || networkName == "none" {
|
||||
if networkName == "bridge" || networkName == "host" || networkName == "ingress" || networkName == "nat" || networkName == "none" {
|
||||
return authorization.NewSystemResourceControl(networkID, portainer.NetworkResourceControl)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ type (
|
||||
signatureService portainer.DigitalSignatureService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
gitService portainer.GitService
|
||||
}
|
||||
|
||||
// TransportParameters is used to create a new Transport
|
||||
@@ -62,7 +63,7 @@ type (
|
||||
)
|
||||
|
||||
// NewTransport returns a pointer to a new Transport instance.
|
||||
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport) (*Transport, error) {
|
||||
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport, gitService portainer.GitService) (*Transport, error) {
|
||||
transport := &Transport{
|
||||
endpoint: parameters.Endpoint,
|
||||
dataStore: parameters.DataStore,
|
||||
@@ -70,6 +71,7 @@ func NewTransport(parameters *TransportParameters, httpTransport *http.Transport
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
dockerClientFactory: parameters.DockerClientFactory,
|
||||
HTTPTransport: httpTransport,
|
||||
gitService: gitService,
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
@@ -381,9 +383,31 @@ func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Respo
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
|
||||
err := transport.updateDefaultGitBranch(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.interceptAndRewriteRequest(request, buildOperation)
|
||||
}
|
||||
|
||||
func (transport *Transport) updateDefaultGitBranch(request *http.Request) error {
|
||||
remote := request.URL.Query().Get("remote")
|
||||
if strings.HasSuffix(remote, ".git") {
|
||||
repositoryURL := remote[:len(remote)-4]
|
||||
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRemote := fmt.Sprintf("%s#%s", remote, latestCommitID)
|
||||
|
||||
q := request.URL.Query()
|
||||
q.Set("remote", newRemote)
|
||||
request.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/images/create":
|
||||
|
||||
73
api/http/proxy/factory/docker/transport_test.go
Normal file
73
api/http/proxy/factory/docker/transport_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type noopGitService struct{}
|
||||
|
||||
func (s *noopGitService) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopGitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return "my-latest-commit-id", nil
|
||||
}
|
||||
|
||||
func TestTransport_updateDefaultGitBranch(t *testing.T) {
|
||||
type fields struct {
|
||||
gitService portainer.GitService
|
||||
}
|
||||
|
||||
type args struct {
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
defaultFields := fields{
|
||||
gitService: &noopGitService{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
expectedQuery string
|
||||
}{
|
||||
{
|
||||
name: "append commit ID",
|
||||
fields: defaultFields,
|
||||
args: args{
|
||||
request: httptest.NewRequest(http.MethodPost, "http://unixsocket/build?dockerfile=Dockerfile&remote=https://my-host.com/my-user/my-repo.git&t=my-image", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
expectedQuery: "dockerfile=Dockerfile&remote=https%3A%2F%2Fmy-host.com%2Fmy-user%2Fmy-repo.git%23my-latest-commit-id&t=my-image",
|
||||
},
|
||||
{
|
||||
name: "not append commit ID",
|
||||
fields: defaultFields,
|
||||
args: args{
|
||||
request: httptest.NewRequest(http.MethodPost, "http://unixsocket/build?dockerfile=Dockerfile&remote=https://my-host.com/my-user/my-repo/my-file&t=my-image", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
expectedQuery: "dockerfile=Dockerfile&remote=https://my-host.com/my-user/my-repo/my-file&t=my-image",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
transport := &Transport{
|
||||
gitService: tt.fields.gitService,
|
||||
}
|
||||
err := transport.updateDefaultGitBranch(tt.args.request)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("updateDefaultGitBranch() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedQuery, tt.args.request.URL.RawQuery)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
|
||||
|
||||
proxy := &dockerLocalProxy{}
|
||||
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path))
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path), factory.gitService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
|
||||
|
||||
proxy := &dockerLocalProxy{}
|
||||
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, newNamedPipeTransport(path))
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, newNamedPipeTransport(path), factory.gitService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ type (
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
gitService portainer.GitService
|
||||
}
|
||||
)
|
||||
|
||||
// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
|
||||
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *ProxyFactory {
|
||||
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService) *ProxyFactory {
|
||||
return &ProxyFactory{
|
||||
dataStore: dataStore,
|
||||
signatureService: signatureService,
|
||||
@@ -35,6 +36,7 @@ func NewProxyFactory(dataStore dataservices.DataStore, signatureService portaine
|
||||
dockerClientFactory: clientFactory,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
gitService: gitService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ type (
|
||||
)
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager {
|
||||
func NewManager(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService) *Manager {
|
||||
return &Manager{
|
||||
endpointProxies: cmap.New(),
|
||||
k8sClientFactory: kubernetesClientFactory,
|
||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/handler"
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
@@ -98,6 +99,7 @@ type Server struct {
|
||||
ShutdownCtx context.Context
|
||||
ShutdownTrigger context.CancelFunc
|
||||
StackDeployer stackdeployer.StackDeployer
|
||||
DemoService *demo.Service
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
@@ -121,7 +123,15 @@ func (server *Server) Start() error {
|
||||
adminMonitor := adminmonitor.New(5*time.Minute, server.DataStore, server.ShutdownCtx)
|
||||
adminMonitor.Start()
|
||||
|
||||
var backupHandler = backup.NewHandler(requestBouncer, server.DataStore, offlineGate, server.FileService.GetDatastorePath(), server.ShutdownTrigger, adminMonitor)
|
||||
var backupHandler = backup.NewHandler(
|
||||
requestBouncer,
|
||||
server.DataStore,
|
||||
offlineGate,
|
||||
server.FileService.GetDatastorePath(),
|
||||
server.ShutdownTrigger,
|
||||
adminMonitor,
|
||||
server.DemoService,
|
||||
)
|
||||
|
||||
var roleHandler = roles.NewHandler(requestBouncer)
|
||||
roleHandler.DataStore = server.DataStore
|
||||
@@ -147,7 +157,7 @@ func (server *Server) Start() error {
|
||||
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
|
||||
edgeTemplatesHandler.DataStore = server.DataStore
|
||||
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer, server.DemoService)
|
||||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = server.ProxyManager
|
||||
@@ -194,7 +204,7 @@ func (server *Server) Start() error {
|
||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
var settingsHandler = settings.NewHandler(requestBouncer, server.DemoService)
|
||||
settingsHandler.DataStore = server.DataStore
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JWTService = server.JWTService
|
||||
@@ -234,7 +244,7 @@ func (server *Server) Start() error {
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.DataStore = server.DataStore
|
||||
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status, server.DemoService)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.DataStore = server.DataStore
|
||||
@@ -244,7 +254,7 @@ func (server *Server) Start() error {
|
||||
var uploadHandler = upload.NewHandler(requestBouncer)
|
||||
uploadHandler.FileService = server.FileService
|
||||
|
||||
var userHandler = users.NewHandler(requestBouncer, rateLimiter, server.APIKeyService)
|
||||
var userHandler = users.NewHandler(requestBouncer, rateLimiter, server.APIKeyService, server.DemoService)
|
||||
userHandler.DataStore = server.DataStore
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
|
||||
|
||||
@@ -1,33 +1,11 @@
|
||||
package passwordutils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const MinPasswordLen = 12
|
||||
|
||||
func lengthCheck(password string) bool {
|
||||
return len(password) >= MinPasswordLen
|
||||
}
|
||||
|
||||
func comboCheck(password string) bool {
|
||||
count := 0
|
||||
regexps := [4]*regexp.Regexp{
|
||||
regexp.MustCompile(`[a-z]`),
|
||||
regexp.MustCompile(`[A-Z]`),
|
||||
regexp.MustCompile(`[0-9]`),
|
||||
regexp.MustCompile(`[\W_]`),
|
||||
}
|
||||
|
||||
for _, re := range regexps {
|
||||
if re.FindString(password) != "" {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
return count >= 3
|
||||
}
|
||||
|
||||
func StrengthCheck(password string) bool {
|
||||
return lengthCheck(password) && comboCheck(password)
|
||||
return lengthCheck(password)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ func TestStrengthCheck(t *testing.T) {
|
||||
}{
|
||||
{"Empty password", args{""}, false},
|
||||
{"Short password", args{"portainer"}, false},
|
||||
{"Short password", args{"portaienr!@#"}, false},
|
||||
{"Short password", args{"portaienr!@#"}, true},
|
||||
{"Week password", args{"12345678!@#"}, false},
|
||||
{"Week password", args{"portaienr123"}, false},
|
||||
{"Week password", args{"portaienr123"}, true},
|
||||
{"Good password", args{"Portainer123"}, true},
|
||||
{"Good password", args{"Portainer___"}, true},
|
||||
{"Good password", args{"^portainer12"}, true},
|
||||
|
||||
@@ -102,6 +102,7 @@ type (
|
||||
Assets *string
|
||||
Data *string
|
||||
FeatureFlags *[]Pair
|
||||
DemoEnvironment *bool
|
||||
EnableEdgeComputeFeatures *bool
|
||||
EndpointURL *string
|
||||
Labels *[]Pair
|
||||
@@ -1307,9 +1308,9 @@ type (
|
||||
|
||||
// ReverseTunnelService represents a service used to manage reverse tunnel connections.
|
||||
ReverseTunnelService interface {
|
||||
StartTunnelServer(addr, port string, snapshotService SnapshotService) error
|
||||
StartTunnelServer(addr, port, certPath, keyPath string, snapshotService SnapshotService) error
|
||||
StopTunnelServer() error
|
||||
GenerateEdgeKey(url, host string, endpointIdentifier int) string
|
||||
GenerateEdgeKey(url, host string, endpointIdentifier int) (string, error)
|
||||
SetTunnelStatusToActive(endpointID EndpointID)
|
||||
SetTunnelStatusToRequired(endpointID EndpointID) error
|
||||
SetTunnelStatusToIdle(endpointID EndpointID)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import _ from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useSettings } from '@/portainer/settings/queries';
|
||||
|
||||
const categories = [
|
||||
'docker',
|
||||
@@ -61,6 +63,18 @@ export function push(
|
||||
}
|
||||
}
|
||||
|
||||
export function useAnalytics() {
|
||||
const telemetryQuery = useSettings((settings) => settings.EnableTelemetry);
|
||||
|
||||
return { trackEvent: handleTrackEvent };
|
||||
|
||||
function handleTrackEvent(...args: Parameters<typeof trackEvent>) {
|
||||
if (telemetryQuery.data) {
|
||||
trackEvent(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEvent(action: string, properties: TrackEventProps) {
|
||||
/**
|
||||
* @description Logs an event with an event category (Videos, Music, Games...), an event
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#page-wrapper,
|
||||
@@ -217,8 +221,8 @@ a[ng-click] {
|
||||
}
|
||||
|
||||
.blocklist-item {
|
||||
padding: 0.7rem;
|
||||
margin-bottom: 0.7rem;
|
||||
padding: 7px;
|
||||
margin-bottom: 7px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-blocklist-color);
|
||||
border-radius: 2px;
|
||||
|
||||
@@ -3,9 +3,10 @@ import angular from 'angular';
|
||||
import { AzureSidebarAngular } from './AzureSidebar/AzureSidebar';
|
||||
import { DashboardViewAngular } from './Dashboard/DashboardView';
|
||||
import { containerInstancesModule } from './ContainerInstances';
|
||||
import { reactModule } from './react';
|
||||
|
||||
angular
|
||||
.module('portainer.azure', ['portainer.app', containerInstancesModule])
|
||||
.module('portainer.azure', ['portainer.app', containerInstancesModule, reactModule])
|
||||
.config([
|
||||
'$stateRegistryProvider',
|
||||
function ($stateRegistryProvider) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ProviderResponse } from '../types';
|
||||
|
||||
|
||||
6
app/azure/react/components/index.ts
Normal file
6
app/azure/react/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import angular from 'angular';
|
||||
|
||||
export const componentsModule = angular.module(
|
||||
'portainer.azure.react.components',
|
||||
[]
|
||||
).name;
|
||||
9
app/azure/react/index.ts
Normal file
9
app/azure/react/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { componentsModule } from './components';
|
||||
import { viewsModule } from './views';
|
||||
|
||||
export const reactModule = angular.module('portainer.azure.react', [
|
||||
viewsModule,
|
||||
componentsModule,
|
||||
]).name;
|
||||
6
app/azure/react/views/index.ts
Normal file
6
app/azure/react/views/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import angular from 'angular';
|
||||
|
||||
export const viewsModule = angular.module(
|
||||
'portainer.azure.react.views',
|
||||
[]
|
||||
).name;
|
||||
@@ -25,7 +25,7 @@ export const DEFAULT_TEMPLATES_URL = 'https://raw.githubusercontent.com/portaine
|
||||
export const PAGINATION_MAX_ITEMS = 10;
|
||||
export const APPLICATION_CACHE_VALIDITY = 3600;
|
||||
export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.';
|
||||
export const PREDEFINED_NETWORKS = ['host', 'bridge', 'none'];
|
||||
export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||
export const KUBERNETES_DEFAULT_NAMESPACE = 'default';
|
||||
export const KUBERNETES_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer'];
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { EnvironmentStatus } from '@/portainer/environments/types';
|
||||
|
||||
import { reactModule } from './react';
|
||||
import containersModule from './containers';
|
||||
import { componentsModule } from './components';
|
||||
import { networksModule } from './networks';
|
||||
|
||||
angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule, networksModule]).config([
|
||||
angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule, networksModule, reactModule]).config([
|
||||
'$stateRegistryProvider',
|
||||
function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CellProps, Column, TableInstance } from 'react-table';
|
||||
import _ from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
import { useSref } from '@uirouter/react';
|
||||
|
||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Column } from 'react-table';
|
||||
import _ from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
||||
import type { DockerContainer, Port } from '@/docker/containers/types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Column } from 'react-table';
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { DefaultFilter } from '@/portainer/components/datatables/components/Filter';
|
||||
import type {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const systemNetworks = ['host', 'bridge', 'none'];
|
||||
const systemNetworks = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||
|
||||
export function isSystemNetwork(networkName: string) {
|
||||
return systemNetworks.includes(networkName);
|
||||
|
||||
6
app/docker/react/components/index.ts
Normal file
6
app/docker/react/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import angular from 'angular';
|
||||
|
||||
export const componentsModule = angular.module(
|
||||
'portainer.docker.react.components',
|
||||
[]
|
||||
).name;
|
||||
9
app/docker/react/index.ts
Normal file
9
app/docker/react/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { componentsModule } from './components';
|
||||
import { viewsModule } from './views';
|
||||
|
||||
export const reactModule = angular.module('portainer.docker.react', [
|
||||
viewsModule,
|
||||
componentsModule,
|
||||
]).name;
|
||||
6
app/docker/react/views/index.ts
Normal file
6
app/docker/react/views/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import angular from 'angular';
|
||||
|
||||
export const viewsModule = angular.module(
|
||||
'portainer.docker.react.views',
|
||||
[]
|
||||
).name;
|
||||
@@ -6,15 +6,3 @@
|
||||
.widget .edit-resources button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mt-7 {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@
|
||||
<div class="col-sm-12 form-section-title"> Resources </div>
|
||||
<!-- memory-reservation-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left mt-20"> Memory reservation </label>
|
||||
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory reservation </label>
|
||||
<div class="col-sm-3">
|
||||
<slider
|
||||
on-change="(handleResourceChange)"
|
||||
@@ -722,13 +722,13 @@
|
||||
<input type="number" min="0" class="form-control" ng-model="formValues.MemoryReservation" id="memory-reservation" />
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted mt-7"> Memory soft limit (<b>MB</b>) </p>
|
||||
<p class="small text-muted mt-2"> Memory soft limit (<b>MB</b>) </p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-reservation-input -->
|
||||
<!-- memory-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left mt-20"> Memory limit </label>
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory limit </label>
|
||||
<div class="col-sm-3">
|
||||
<slider
|
||||
on-change="(handleResourceChange)"
|
||||
@@ -749,7 +749,7 @@
|
||||
<!-- !memory-limit-input -->
|
||||
<!-- cpu-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left mt-20"> CPU limit </label>
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> CPU limit </label>
|
||||
<div class="col-sm-5">
|
||||
<slider
|
||||
on-change="(handleResourceChange)"
|
||||
@@ -761,7 +761,7 @@
|
||||
ng-if="state.sliderMaxCpu"
|
||||
></slider>
|
||||
</div>
|
||||
<div class="col-sm-4 mt-20">
|
||||
<div class="col-sm-4 mt-8">
|
||||
<p class="small text-muted"> Maximum CPU usage </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="showEnvUrl">
|
||||
<td>URL</td>
|
||||
<td>{{ endpoint.URL | stripprotocol }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,6 +2,7 @@ import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
angular.module('portainer.docker').controller('DashboardController', [
|
||||
'$scope',
|
||||
@@ -46,7 +47,7 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
$scope.showStacks = await shouldShowStacks();
|
||||
|
||||
$scope.showEnvUrl = endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment && endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment;
|
||||
$q.all({
|
||||
containers: ContainerService.containers(1),
|
||||
images: ImageService.images(false),
|
||||
|
||||
@@ -166,7 +166,13 @@
|
||||
<div class="form-group">
|
||||
<label for="image_url" class="col-sm-2 control-label text-left">URL</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="formValues.URL" id="image_url" placeholder="https://myhost.mydomain/myimage.tar.gz" />
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="formValues.URL"
|
||||
id="image_url"
|
||||
placeholder="https://myhost.mydomain/myimage.tar.gz or https://github.com/myname/myrepo.git#mybranch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from '@/portainer/components/datatables/components';
|
||||
import { InnerDatatable } from '@/portainer/components/datatables/components/InnerDatatable';
|
||||
import { Device } from '@/portainer/hostmanagement/open-amt/model';
|
||||
import { useAMTDevices } from '@/edge/devices/components/AMTDevicesDatatable/useAMTDevices';
|
||||
import { RowProvider } from '@/edge/devices/components/AMTDevicesDatatable/columns/RowContext';
|
||||
import { useAMTDevices } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices';
|
||||
import { RowProvider } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import PortainerError from '@/portainer/error';
|
||||
|
||||
@@ -9,8 +9,14 @@ import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { executeDeviceAction } from '@/portainer/hostmanagement/open-amt/open-amt.service';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { ActionsMenuTitle } from '@/portainer/components/datatables/components/ActionsMenuTitle';
|
||||
import { useRowContext } from '@/edge/devices/components/AMTDevicesDatatable/columns/RowContext';
|
||||
import { DeviceAction } from '@/edge/devices/types';
|
||||
|
||||
import { useRowContext } from './RowContext';
|
||||
|
||||
enum DeviceAction {
|
||||
PowerOn = 'power on',
|
||||
PowerOff = 'power off',
|
||||
Restart = 'restart',
|
||||
}
|
||||
|
||||
export const actions: Column<Device> = {
|
||||
Header: 'Actions',
|
||||
@@ -2,8 +2,27 @@ import { CellProps, Column } from 'react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Device } from '@/portainer/hostmanagement/open-amt/model';
|
||||
import { useRowContext } from '@/edge/devices/components/AMTDevicesDatatable/columns/RowContext';
|
||||
import { PowerState, PowerStateCode } from '@/edge/devices/types';
|
||||
|
||||
import { useRowContext } from './RowContext';
|
||||
|
||||
enum PowerState {
|
||||
Running = 'Running',
|
||||
Sleep = 'Sleep',
|
||||
Off = 'Off',
|
||||
Hibernate = 'Hibernate',
|
||||
PowerCycle = 'Power Cycle',
|
||||
}
|
||||
|
||||
enum PowerStateCode {
|
||||
On = 2,
|
||||
SleepLight = 3,
|
||||
SleepDeep = 4,
|
||||
OffHard = 6,
|
||||
Hibernate = 7,
|
||||
OffSoft = 8,
|
||||
PowerCycle = 9,
|
||||
OffHardGraceful = 13,
|
||||
}
|
||||
|
||||
export const powerState: Column<Device> = {
|
||||
Header: 'Power State',
|
||||
@@ -0,0 +1,269 @@
|
||||
import { useTable, useExpanded, useSortBy, useFilters } from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { PaginationControls } from '@/portainer/components/pagination-controls';
|
||||
import {
|
||||
Table,
|
||||
TableActions,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableSettingsMenu,
|
||||
TableTitle,
|
||||
TableTitleActions,
|
||||
} from '@/portainer/components/datatables/components';
|
||||
import { multiple } from '@/portainer/components/datatables/components/filter-types';
|
||||
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
|
||||
import { ColumnVisibilityMenu } from '@/portainer/components/datatables/components/ColumnVisibilityMenu';
|
||||
import { SearchBar } from '@/portainer/components/datatables/components/SearchBar';
|
||||
import { useRowSelect } from '@/portainer/components/datatables/components/useRowSelect';
|
||||
import { TableFooter } from '@/portainer/components/datatables/components/TableFooter';
|
||||
import { SelectedRowsCount } from '@/portainer/components/datatables/components/SelectedRowsCount';
|
||||
import { AMTDevicesDatatable } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
import { EnvironmentGroup } from '@/portainer/environment-groups/types';
|
||||
|
||||
import { EdgeDevicesDatatableActions } from './EdgeDevicesDatatableActions';
|
||||
import { EdgeDevicesDatatableSettings } from './EdgeDevicesDatatableSettings';
|
||||
import { RowProvider } from './columns/RowContext';
|
||||
import { useColumns } from './columns';
|
||||
import styles from './EdgeDevicesDatatable.module.css';
|
||||
import { EdgeDeviceTableSettings, Pagination } from './types';
|
||||
|
||||
export interface EdgeDevicesTableProps {
|
||||
storageKey: string;
|
||||
isFdoEnabled: boolean;
|
||||
isOpenAmtEnabled: boolean;
|
||||
showWaitingRoomLink: boolean;
|
||||
mpsServer: string;
|
||||
dataset: Environment[];
|
||||
groups: EnvironmentGroup[];
|
||||
setLoadingMessage(message: string): void;
|
||||
pagination: Pagination;
|
||||
onChangePagination(pagination: Partial<Pagination>): void;
|
||||
totalCount: number;
|
||||
search: string;
|
||||
onChangeSearch(search: string): void;
|
||||
}
|
||||
|
||||
export function EdgeDevicesDatatable({
|
||||
isFdoEnabled,
|
||||
isOpenAmtEnabled,
|
||||
showWaitingRoomLink,
|
||||
mpsServer,
|
||||
dataset,
|
||||
onChangeSearch,
|
||||
search,
|
||||
groups,
|
||||
setLoadingMessage,
|
||||
pagination,
|
||||
onChangePagination,
|
||||
totalCount,
|
||||
}: EdgeDevicesTableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<EdgeDeviceTableSettings>();
|
||||
|
||||
const columns = useColumns();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
allColumns,
|
||||
setHiddenColumns,
|
||||
} = useTable<Environment>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
hiddenColumns: settings.hiddenColumns,
|
||||
sortBy: [settings.sortBy],
|
||||
},
|
||||
isRowSelectable() {
|
||||
return true;
|
||||
},
|
||||
autoResetExpanded: false,
|
||||
autoResetSelectedRows: false,
|
||||
getRowId(originalRow: Environment) {
|
||||
return originalRow.Id.toString();
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const columnsToHide = allColumns.filter((colInstance) => {
|
||||
const columnDef = columns.find((c) => c.id === colInstance.id);
|
||||
return columnDef?.canHide;
|
||||
});
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
const someDeviceHasAMTActivated = dataset.some(
|
||||
(environment) =>
|
||||
environment.AMTDeviceGUID && environment.AMTDeviceGUID !== ''
|
||||
);
|
||||
|
||||
const groupsById = _.groupBy(groups, 'Id');
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle icon="fa-plug" label="Edge Devices">
|
||||
<TableTitleActions>
|
||||
<ColumnVisibilityMenu<Environment>
|
||||
columns={columnsToHide}
|
||||
onChange={handleChangeColumnsVisibility}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
<TableSettingsMenu>
|
||||
<EdgeDevicesDatatableSettings />
|
||||
</TableSettingsMenu>
|
||||
</TableTitleActions>
|
||||
</TableTitle>
|
||||
<TableActions>
|
||||
<EdgeDevicesDatatableActions
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
isFDOEnabled={isFdoEnabled}
|
||||
isOpenAMTEnabled={isOpenAmtEnabled}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
showWaitingRoomLink={showWaitingRoomLink}
|
||||
/>
|
||||
</TableActions>
|
||||
{isOpenAmtEnabled && someDeviceHasAMTActivated && (
|
||||
<div className={styles.kvmTip}>
|
||||
<TextTip color="blue">
|
||||
For the KVM function to work you need to have the MPS server
|
||||
added to your trusted site list, browse to this{' '}
|
||||
<a
|
||||
href={`https://${mpsServer}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="space-right"
|
||||
>
|
||||
site
|
||||
</a>
|
||||
and add to your trusted site list
|
||||
</TextTip>
|
||||
</div>
|
||||
)}
|
||||
<SearchBar value={search} onChange={handleSearchBarChange} />
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Environment>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content
|
||||
prepareRow={prepareRow}
|
||||
rows={rows}
|
||||
renderRow={(row, { key, className, role, style }) => {
|
||||
const group = groupsById[row.original.GroupId];
|
||||
|
||||
return (
|
||||
<RowProvider
|
||||
key={key}
|
||||
isOpenAmtEnabled={isOpenAmtEnabled}
|
||||
groupName={group[0]?.Name}
|
||||
>
|
||||
<TableRow<Environment>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
{row.isExpanded && (
|
||||
<tr>
|
||||
<td />
|
||||
<td colSpan={row.cells.length - 1}>
|
||||
<AMTDevicesDatatable
|
||||
environmentId={row.original.Id}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</RowProvider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
isPageInputVisible
|
||||
pageLimit={pagination.pageLimit}
|
||||
page={pagination.page}
|
||||
onPageChange={(p) => gotoPage(p)}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function gotoPage(pageIndex: number) {
|
||||
onChangePagination({ page: pageIndex });
|
||||
}
|
||||
|
||||
function setPageSize(pageSize: number) {
|
||||
onChangePagination({ pageLimit: pageSize });
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
|
||||
function handleChangeColumnsVisibility(hiddenColumns: string[]) {
|
||||
setHiddenColumns(hiddenColumns);
|
||||
setTableSettings((settings) => ({ ...settings, hiddenColumns }));
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
onChangeSearch(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings((settings) => ({
|
||||
...settings,
|
||||
sortBy: { id, desc },
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
TableSettingsProvider,
|
||||
useTableSettings,
|
||||
} from '@/portainer/components/datatables/components/useTableSettings';
|
||||
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { useSearchBarState } from '@/portainer/components/datatables/components/SearchBar';
|
||||
import { useDebounce } from '@/portainer/hooks/useDebounce';
|
||||
|
||||
import {
|
||||
EdgeDevicesDatatable,
|
||||
EdgeDevicesTableProps,
|
||||
} from './EdgeDevicesDatatable';
|
||||
import { EdgeDeviceTableSettings, Pagination } from './types';
|
||||
|
||||
export function EdgeDevicesDatatableContainer({
|
||||
...props
|
||||
}: Omit<
|
||||
EdgeDevicesTableProps,
|
||||
| 'dataset'
|
||||
| 'pagination'
|
||||
| 'onChangePagination'
|
||||
| 'totalCount'
|
||||
| 'search'
|
||||
| 'onChangeSearch'
|
||||
>) {
|
||||
const defaultSettings = {
|
||||
autoRefreshRate: 0,
|
||||
hiddenQuickActions: [],
|
||||
hiddenColumns: [],
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'state', desc: false },
|
||||
};
|
||||
|
||||
const storageKey = 'edgeDevices';
|
||||
|
||||
return (
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey={storageKey}>
|
||||
<Loader storageKey={storageKey}>
|
||||
{({
|
||||
environments,
|
||||
pagination,
|
||||
totalCount,
|
||||
setPagination,
|
||||
search,
|
||||
setSearch,
|
||||
}) => (
|
||||
<EdgeDevicesDatatable
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
storageKey={storageKey}
|
||||
dataset={environments}
|
||||
pagination={pagination}
|
||||
onChangePagination={setPagination}
|
||||
totalCount={totalCount}
|
||||
search={search}
|
||||
onChangeSearch={setSearch}
|
||||
/>
|
||||
)}
|
||||
</Loader>
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface LoaderProps {
|
||||
storageKey: string;
|
||||
children: (options: {
|
||||
environments: Environment[];
|
||||
totalCount: number;
|
||||
pagination: Pagination;
|
||||
setPagination(value: Partial<Pagination>): void;
|
||||
search: string;
|
||||
setSearch: (value: string) => void;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
||||
function Loader({ children, storageKey }: LoaderProps) {
|
||||
const { settings } = useTableSettings<EdgeDeviceTableSettings>();
|
||||
const [pagination, setPagination] = useState({
|
||||
pageLimit: settings.pageSize,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const debouncedSearchValue = useDebounce(search);
|
||||
|
||||
const { environments, isLoading, totalCount } = useEnvironmentList(
|
||||
{
|
||||
edgeDeviceFilter: 'trusted',
|
||||
search: debouncedSearchValue,
|
||||
...pagination,
|
||||
},
|
||||
settings.autoRefreshRate * 1000
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children({
|
||||
environments,
|
||||
totalCount,
|
||||
pagination,
|
||||
setPagination: handleSetPagination,
|
||||
search,
|
||||
setSearch,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
function handleSetPagination(value: Partial<Pagination>) {
|
||||
setPagination((prev) => ({ ...prev, ...value }));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TableSettingsMenuAutoRefresh } from '@/portainer/components/datatables/components/TableSettingsMenuAutoRefresh';
|
||||
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
|
||||
import { EdgeDeviceTableSettings } from '@/edge/devices/types';
|
||||
|
||||
import { EdgeDeviceTableSettings } from './types';
|
||||
|
||||
export function EdgeDevicesDatatableSettings() {
|
||||
const { settings, setTableSettings } =
|
||||
@@ -3,7 +3,8 @@ import { CellProps, Column } from 'react-table';
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
import { ExpandingCell } from '@/portainer/components/datatables/components/ExpandingCell';
|
||||
import { useRowContext } from '@/edge/devices/components/EdgeDevicesDatatable/columns/RowContext';
|
||||
|
||||
import { useRowContext } from './RowContext';
|
||||
|
||||
export const name: Column<Environment> = {
|
||||
Header: 'Name',
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/portainer/components/datatables/types';
|
||||
|
||||
export interface Pagination {
|
||||
pageLimit: number;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface EdgeDeviceTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
58
app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesView.tsx
Normal file
58
app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesView.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PageHeader } from '@/portainer/components/PageHeader';
|
||||
import { useSettings } from '@/portainer/settings/queries';
|
||||
import { useGroups } from '@/portainer/environment-groups/queries';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { ViewLoading } from '@/portainer/components/ViewLoading';
|
||||
|
||||
import { EdgeDevicesDatatableContainer } from './EdgeDevicesDatatable/EdgeDevicesDatatableContainer';
|
||||
|
||||
export function EdgeDevicesView() {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
const settingsQuery = useSettings();
|
||||
const groupsQuery = useGroups();
|
||||
|
||||
if (!settingsQuery.data || !groupsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = settingsQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Edge Devices"
|
||||
reload
|
||||
breadcrumbs={[{ label: 'EdgeDevices' }]}
|
||||
/>
|
||||
|
||||
{loadingMessage ? (
|
||||
<ViewLoading message={loadingMessage} />
|
||||
) : (
|
||||
<EdgeDevicesDatatableContainer
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
isFdoEnabled={
|
||||
settings.EnableEdgeComputeFeatures &&
|
||||
settings.fdoConfiguration.enabled
|
||||
}
|
||||
showWaitingRoomLink={
|
||||
process.env.PORTAINER_EDITION === 'BE' &&
|
||||
settings.EnableEdgeComputeFeatures &&
|
||||
!settings.TrustOnFirstConnect
|
||||
}
|
||||
isOpenAmtEnabled={
|
||||
settings.EnableEdgeComputeFeatures &&
|
||||
settings.openAMTConfiguration.enabled
|
||||
}
|
||||
mpsServer={settings.openAMTConfiguration.mpsServer}
|
||||
groups={groupsQuery.data}
|
||||
storageKey="edgeDevices"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const EdgeDevicesViewAngular = r2a(EdgeDevicesView, []);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user