Compare commits
19 Commits
feat/EE-16
...
fix/EE-166
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a2a2e2bf | ||
|
|
05efac44f6 | ||
|
|
555c9f238f | ||
|
|
e3f7561ced | ||
|
|
c7760b7d48 | ||
|
|
1633eceed5 | ||
|
|
e437a3b570 | ||
|
|
396a921b12 | ||
|
|
1374e53dfa | ||
|
|
756ef060db | ||
|
|
d8b88d1004 | ||
|
|
2a60b8fcdf | ||
|
|
e86a586651 | ||
|
|
d166a09511 | ||
|
|
63f64a6a06 | ||
|
|
5c8450c4c0 | ||
|
|
79ca51c92e | ||
|
|
9f179fe3ec | ||
|
|
1543ad4c42 |
16
.github/ISSUE_TEMPLATE.md
vendored
16
.github/ISSUE_TEMPLATE.md
vendored
@@ -28,17 +28,15 @@ Briefly describe the problem you are having in a few paragraphs.
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
1. 2. 3.
|
||||
|
||||
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
|
||||
|
||||
**Technical details:**
|
||||
|
||||
* Portainer version:
|
||||
* Target Docker version (the host/cluster you manage):
|
||||
* Platform (windows/linux):
|
||||
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
||||
* Target Swarm version (if applicable):
|
||||
* Browser:
|
||||
- Portainer version:
|
||||
- Target Docker version (the host/cluster you manage):
|
||||
- Platform (windows/linux):
|
||||
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
|
||||
- Target Swarm version (if applicable):
|
||||
- Browser:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -4,7 +4,6 @@ about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -46,7 +45,7 @@ You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#ho
|
||||
- Docker version (managed by Portainer):
|
||||
- Kubernetes version (managed by Portainer):
|
||||
- Platform (windows/linux):
|
||||
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
||||
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
|
||||
- Browser:
|
||||
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
|
||||
- Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
@@ -91,7 +91,7 @@ Then build and run the project:
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
Portainer can now be accessed at <http://localhost:9000>.
|
||||
Portainer can now be accessed at <https://localhost:9443>.
|
||||
|
||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API.
|
||||
Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8
|
||||
Examples are available at https://documentation.portainer.io/api/api-examples/
|
||||
You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/).
|
||||
|
||||
# Authentication
|
||||
@@ -50,4 +50,4 @@ Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you
|
||||
|
||||
To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API).
|
||||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://documentation.portainer.io/api/api-examples/).
|
||||
|
||||
@@ -16,7 +16,18 @@ import (
|
||||
|
||||
const rwxr__r__ os.FileMode = 0744
|
||||
|
||||
var filesToBackup = []string{"compose", "config.json", "custom_templates", "edge_jobs", "edge_stacks", "extensions", "portainer.key", "portainer.pub", "tls"}
|
||||
var filesToBackup = []string{
|
||||
"certs",
|
||||
"compose",
|
||||
"config.json",
|
||||
"custom_templates",
|
||||
"edge_jobs",
|
||||
"edge_stacks",
|
||||
"extensions",
|
||||
"portainer.key",
|
||||
"portainer.pub",
|
||||
"tls",
|
||||
}
|
||||
|
||||
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
|
||||
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) (string, error) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/helmuserrepository"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/customtemplate"
|
||||
@@ -44,33 +46,34 @@ const (
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
path string
|
||||
connection *internal.DbConnection
|
||||
isNew bool
|
||||
fileService portainer.FileService
|
||||
CustomTemplateService *customtemplate.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
SSLSettingsService *ssl.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
path string
|
||||
connection *internal.DbConnection
|
||||
isNew bool
|
||||
fileService portainer.FileService
|
||||
CustomTemplateService *customtemplate.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
HelmUserRepositoryService *helmuserrepository.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
SSLSettingsService *ssl.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
|
||||
73
api/bolt/helmuserrepository/helmuserrepository.go
Normal file
73
api/bolt/helmuserrepository/helmuserrepository.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package helmuserrepository
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "helm_user_repository"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present.
|
||||
func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) {
|
||||
var result = make([]portainer.HelmUserRepository, 0)
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var record portainer.HelmUserRepository
|
||||
err := internal.UnmarshalObject(v, &record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record.UserID == userID {
|
||||
result = append(result, record)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CreateHelmUserRepository creates a new HelmUserRepository object.
|
||||
func (service *Service) CreateHelmUserRepository(record *portainer.HelmUserRepository) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
record.ID = portainer.HelmUserRepositoryID(id)
|
||||
|
||||
data, err := internal.MarshalObject(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(record.ID)), data)
|
||||
})
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func (store *Store) Init() error {
|
||||
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
}
|
||||
@@ -80,7 +81,7 @@ func (store *Store) Init() error {
|
||||
if len(groups) == 0 {
|
||||
unassignedGroup := &portainer.EndpointGroup{
|
||||
Name: "Unassigned",
|
||||
Description: "Unassigned endpoints",
|
||||
Description: "Unassigned environments",
|
||||
Labels: []portainer.Pair{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
|
||||
@@ -28,6 +28,10 @@ func (m *Migrator) migrateDBVersionToDB32() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.helmRepositoryURLToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -138,7 +142,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
|
||||
func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching endpoints: %w", err)
|
||||
return fmt.Errorf("failed fetching environments: %w", err)
|
||||
}
|
||||
|
||||
resourceControls, err := m.resourceControlService.ResourceControls()
|
||||
@@ -170,7 +174,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
|
||||
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching endpoint docker id: %w", err)
|
||||
return fmt.Errorf("failed fetching environment docker id: %w", err)
|
||||
}
|
||||
|
||||
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
|
||||
@@ -223,4 +227,13 @@ func (m *Migrator) kubeconfigExpiryToDB32() error {
|
||||
}
|
||||
settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrator) helmRepositoryURLToDB32() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.HelmRepositoryURL = portainer.DefaultHelmRepositoryURL
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/helmuserrepository"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
@@ -88,6 +89,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.ExtensionService = extensionService
|
||||
|
||||
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.HelmUserRepositoryService = helmUserRepositoryService
|
||||
|
||||
registryService, err := registry.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -204,6 +211,11 @@ func (store *Store) EndpointRelation() portainer.EndpointRelationService {
|
||||
return store.EndpointRelationService
|
||||
}
|
||||
|
||||
// HelmUserRepository access the helm user repository settings
|
||||
func (store *Store) HelmUserRepository() portainer.HelmUserRepositoryService {
|
||||
return store.HelmUserRepositoryService
|
||||
}
|
||||
|
||||
// Registry gives access to the Registry data management layer
|
||||
func (store *Store) Registry() portainer.RegistryService {
|
||||
return store.RegistryService
|
||||
|
||||
@@ -141,7 +141,7 @@ func (service *Service) checkTunnels() {
|
||||
}
|
||||
|
||||
elapsed := time.Since(tunnel.LastActivity)
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: endpoint tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||
continue
|
||||
@@ -156,19 +156,19 @@ func (service *Service) checkTunnels() {
|
||||
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
}
|
||||
|
||||
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge endpoint (id: %s): %s", item.Key, err)
|
||||
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tunnel.Jobs) > 0 {
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -56,32 +56,6 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
err := service.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
|
||||
}
|
||||
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
settings, err := service.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
|
||||
}
|
||||
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||
// It sets the status to ACTIVE.
|
||||
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
type Service struct{}
|
||||
|
||||
var (
|
||||
errInvalidEndpointProtocol = errors.New("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
|
||||
errInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
|
||||
errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
|
||||
errInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
|
||||
errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
|
||||
@@ -35,7 +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(),
|
||||
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
|
||||
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
||||
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
||||
@@ -47,7 +47,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
@@ -98,8 +99,12 @@ func initSwarmStackManager(assetsPath string, configPath string, signatureServic
|
||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
|
||||
}
|
||||
|
||||
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
|
||||
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
|
||||
}
|
||||
|
||||
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
||||
@@ -316,7 +321,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
}
|
||||
|
||||
return dataStore.Endpoint().CreateEndpoint(endpoint)
|
||||
@@ -362,7 +367,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
}
|
||||
|
||||
return dataStore.Endpoint().CreateEndpoint(endpoint)
|
||||
@@ -379,7 +384,7 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
|
||||
log.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -420,6 +425,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sslSettings, err := sslService.GetSSLSettings()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get ssl settings: %s", err)
|
||||
}
|
||||
|
||||
err = initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing key pai: %v", err)
|
||||
@@ -445,6 +455,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
authorizationService.K8sClientFactory = kubernetesClientFactory
|
||||
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
@@ -456,7 +469,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatalf("failed initializing swarm stack manager: %s", err)
|
||||
}
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
||||
|
||||
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing helm package manager: %s", err)
|
||||
}
|
||||
|
||||
if dataStore.IsNew() {
|
||||
err = updateSettingsFromFlags(dataStore, flags)
|
||||
@@ -474,7 +492,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing endpoint: %v", err)
|
||||
log.Fatalf("failed initializing environment: %v", err)
|
||||
}
|
||||
|
||||
adminPasswordHash := ""
|
||||
@@ -518,13 +536,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatalf("failed starting tunnel server: %s", err)
|
||||
}
|
||||
|
||||
sslSettings, err := dataStore.SSLSettings().Settings()
|
||||
sslDBSettings, err := dataStore.SSLSettings().Settings()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to fetch ssl settings from DB")
|
||||
}
|
||||
|
||||
scheduler := scheduler.NewScheduler(shutdownCtx)
|
||||
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
|
||||
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager)
|
||||
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
||||
|
||||
return &http.Server{
|
||||
@@ -533,12 +551,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
BindAddressHTTPS: *flags.AddrHTTPS,
|
||||
HTTPEnabled: sslSettings.HTTPEnabled,
|
||||
HTTPEnabled: sslDBSettings.HTTPEnabled,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
HelmPackageManager: helmPackageManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
@@ -547,6 +566,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
GitService: gitService,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
KubeConfigService: kubeConfigService,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
SSLService: sslService,
|
||||
|
||||
@@ -4,5 +4,5 @@ import "errors"
|
||||
|
||||
// Docker errors
|
||||
var (
|
||||
ErrUnableToPingEndpoint = errors.New("Unable to communicate with the endpoint")
|
||||
ErrUnableToPingEndpoint = errors.New("Unable to communicate with the environment")
|
||||
)
|
||||
|
||||
@@ -47,44 +47,44 @@ func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Dock
|
||||
|
||||
err = snapshotInfo(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine information] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine information] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
if snapshot.Swarm {
|
||||
err = snapshotSwarmServices(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm services] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm services] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotNodes(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm nodes] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm nodes] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = snapshotContainers(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot containers] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot containers] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotImages(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot images] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot images] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotVolumes(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot volumes] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot volumes] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotNetworks(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot networks] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot networks] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotVersion(snapshot, cli)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine version] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine version] [environment: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
snapshot.Time = time.Now().Unix()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -46,7 +47,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
||||
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch endpoint proxy")
|
||||
return errors.Wrap(err, "failed to featch environment proxy")
|
||||
}
|
||||
|
||||
if proxy != nil {
|
||||
@@ -58,8 +59,9 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
|
||||
return manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
|
||||
filePaths := getStackFiles(stack)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
|
||||
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
|
||||
@@ -72,13 +74,13 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
filePaths := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
|
||||
|
||||
return manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
filePaths := getStackFiles(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
||||
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
func (w *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
@@ -88,7 +90,7 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
proxy, err := manager.proxyManager.CreateAgentProxyServer(endpoint)
|
||||
proxy, err := manager.proxyManager.CreateComposeProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -115,3 +117,27 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
|
||||
return "stack.env", nil
|
||||
}
|
||||
|
||||
// getStackFiles returns list of stack's confile file paths.
|
||||
// items in the list would be sanitized according to following criterias:
|
||||
// 1. no empty paths
|
||||
// 2. no "../xxx" paths that are trying to escape stack folder
|
||||
// 3. no dir paths
|
||||
// 4. root paths would be made relative
|
||||
func getStackFiles(stack *portainer.Stack) []string {
|
||||
paths := make([]string, 0, len(stack.AdditionalFiles)+1)
|
||||
|
||||
for _, p := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
p = `.` + p
|
||||
}
|
||||
|
||||
if p == `` || p == `.` || strings.HasPrefix(p, `..`) || strings.HasSuffix(p, string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -64,3 +64,21 @@ func Test_createEnvFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getStackFiles(t *testing.T) {
|
||||
stack := &portainer.Stack{
|
||||
EntryPoint: "./file", // picks entry point
|
||||
AdditionalFiles: []string{
|
||||
``, // ignores empty string
|
||||
`.`, // ignores .
|
||||
`..`, // ignores ..
|
||||
`./dir/`, // ignrores paths that end with trailing /
|
||||
`/with-root-prefix`, // replaces "root" based paths with relative
|
||||
`./relative`, // keeps relative paths
|
||||
`../escape`, // prevents dir escape
|
||||
},
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
assert.ElementsMatch(t, filePaths, []string{`./file`, `./with-root-prefix`, `./relative`})
|
||||
}
|
||||
|
||||
@@ -2,20 +2,24 @@ package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
)
|
||||
|
||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
||||
@@ -26,11 +30,10 @@ type KubernetesDeployer struct {
|
||||
signatureService portainer.DigitalSignatureService
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
||||
return &KubernetesDeployer{
|
||||
binaryPath: binaryPath,
|
||||
dataStore: datastore,
|
||||
@@ -38,33 +41,32 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
|
||||
signatureService: signatureService,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
proxyManager: proxyManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||
|
||||
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
user, err := deployer.dataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to fetch the user")
|
||||
}
|
||||
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return tokenManager.GetAdminServiceAccountToken(), nil
|
||||
}
|
||||
|
||||
token, err := tokenManager.GetUserServiceAccountToken(int(user.ID), endpoint.ID)
|
||||
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -75,58 +77,156 @@ func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Deploy upserts Kubernetes resources defined in manifest(s)
|
||||
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return deployer.command("apply", userID, endpoint, manifestFiles, namespace)
|
||||
}
|
||||
|
||||
// Remove deletes Kubernetes resources defined in manifest(s)
|
||||
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return deployer.command("delete", userID, endpoint, manifestFiles, namespace)
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed generating a user token")
|
||||
}
|
||||
|
||||
command := path.Join(deployer.binaryPath, "kubectl")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--token", token,
|
||||
"--namespace", namespace,
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
url, proxy, err := deployer.getAgentURL(endpoint)
|
||||
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
||||
// Otherwise it will use kubectl to deploy the manifest.
|
||||
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
||||
token, err := deployer.getToken(request, endpoint, true)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer proxy.Close()
|
||||
args = append(args, "--server", url)
|
||||
command := path.Join(deployer.binaryPath, "kubectl")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "--server", endpoint.URL)
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
args = append(args, "--token", token)
|
||||
args = append(args, "--namespace", namespace)
|
||||
args = append(args, "apply", "-f", "-")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = strings.NewReader(stackConfig)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
args = append(args, operation)
|
||||
for _, path := range manifestFiles {
|
||||
args = append(args, "-f", strings.TrimSpace(path))
|
||||
// agent
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
|
||||
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
settings, err := deployer.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
|
||||
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
transport := &http.Transport{}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
httpCli := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpointURL, "http") {
|
||||
endpointURL = fmt.Sprintf("https://%s", endpointURL)
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
reqPayload, err := json.Marshal(
|
||||
struct {
|
||||
StackConfig string
|
||||
Namespace string
|
||||
}{
|
||||
StackConfig: stackConfig,
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := deployer.getToken(request, endpoint, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
||||
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errorResponseData struct {
|
||||
Message string
|
||||
Details string
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
|
||||
if err != nil {
|
||||
output, parseStringErr := ioutil.ReadAll(resp.Body)
|
||||
if parseStringErr != nil {
|
||||
return "", parseStringErr
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
|
||||
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
|
||||
}
|
||||
|
||||
var responseData struct{ Output string }
|
||||
err = json.NewDecoder(resp.Body).Decode(&responseData)
|
||||
if err != nil {
|
||||
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
|
||||
if parseStringErr != nil {
|
||||
return "", parseStringErr
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
|
||||
}
|
||||
|
||||
return responseData.Output, nil
|
||||
|
||||
}
|
||||
|
||||
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
|
||||
@@ -151,12 +251,3 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", proxy.Port), proxy, nil
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WriteToFile(dst string, content []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)
|
||||
}
|
||||
|
||||
file, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open a file %q", dst)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(content)
|
||||
return errors.Wrapf(err, "failed to write a file %q", dst)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFilePath := path.Join(tmpDir, "dummy")
|
||||
|
||||
content := []byte("content")
|
||||
err := WriteToFile(tmpFilePath, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||
assert.Equal(t, content, fileContent)
|
||||
}
|
||||
|
||||
func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFilePath := path.Join(tmpDir, "dummy")
|
||||
|
||||
err := WriteToFile(tmpFilePath, []byte("content"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
content := []byte("new content")
|
||||
err = WriteToFile(tmpFilePath, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||
assert.Equal(t, content, fileContent)
|
||||
}
|
||||
|
||||
func Test_WriteFile_CanWriteANestedPath(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFilePath := path.Join(tmpDir, "dir", "sub-dir", "dummy")
|
||||
|
||||
content := []byte("content")
|
||||
err := WriteToFile(tmpFilePath, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||
assert.Equal(t, content, fileContent)
|
||||
}
|
||||
@@ -36,8 +36,9 @@ require (
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
|
||||
@@ -206,10 +206,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548 h1:5I9j0e6f9KG/RV6YBKWyks8LSHheE+ltJgpMyyWYUoo=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210906052132-ef24824f7548/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909011155-9ff375eac059 h1:98v0k3x3ZXa09NaHP/HmSA83rcN8cuE/zTKo6xvNmoM=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909011155-9ff375eac059/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 h1:0ZGSu3Atz7RHMDsoITHV676igRfsb51mlgELGo37ELU=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97 h1:ZcRVgWHTac8V7WU9TUBr73H3e5ajVFYTPjPl9TWULDA=
|
||||
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
|
||||
@@ -4,7 +4,7 @@ import "errors"
|
||||
|
||||
var (
|
||||
// ErrEndpointAccessDenied Access denied to endpoint error
|
||||
ErrEndpointAccessDenied = errors.New("Access denied to endpoint")
|
||||
ErrEndpointAccessDenied = errors.New("Access denied to environment")
|
||||
// ErrUnauthorized Unauthorized error
|
||||
ErrUnauthorized = errors.New("Unauthorized")
|
||||
// ErrResourceAccessDenied Access denied to resource error
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @accept json, multipart/form-data
|
||||
// @accept json,multipart/form-data
|
||||
// @produce json
|
||||
// @param method query string true "method for creating template" Enums(string, file, repository)
|
||||
// @param body_string body customTemplateFromFileContentPayload false "Required when using method=string"
|
||||
|
||||
@@ -27,7 +27,7 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||
return errors.New("TagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
|
||||
return errors.New("Endpoints is mandatory for a static Edge group")
|
||||
return errors.New("Environment is mandatory for a static Edge group")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||
// @produce json
|
||||
// @param body body edgeGroupCreatePayload true "EdgeGroup data"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @router /edge_groups [post]
|
||||
func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -77,7 +77,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// @produce json
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @success 204
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @router /edge_groups/{id} [delete]
|
||||
func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// @produce json
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @router /edge_groups/{id} [get]
|
||||
func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -38,7 +38,7 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
|
||||
if edgeGroup.Dynamic {
|
||||
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments and environment groups for Edge group", err}
|
||||
}
|
||||
|
||||
edgeGroup.Endpoints = endpoints
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -10,7 +11,8 @@ import (
|
||||
|
||||
type decoratedEdgeGroup struct {
|
||||
portainer.EdgeGroup
|
||||
HasEdgeStack bool `json:"HasEdgeStack"`
|
||||
HasEdgeStack bool `json:"HasEdgeStack"`
|
||||
EndpointTypes []portainer.EndpointType
|
||||
}
|
||||
|
||||
// @id EdgeGroupList
|
||||
@@ -22,7 +24,7 @@ type decoratedEdgeGroup struct {
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeGroup{HasEdgeStack=bool} "EdgeGroups"
|
||||
// @failure 500
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_groups [get]
|
||||
func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
@@ -46,17 +48,25 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h
|
||||
decoratedEdgeGroups := []decoratedEdgeGroup{}
|
||||
for _, orgEdgeGroup := range edgeGroups {
|
||||
edgeGroup := decoratedEdgeGroup{
|
||||
EdgeGroup: orgEdgeGroup,
|
||||
EdgeGroup: orgEdgeGroup,
|
||||
EndpointTypes: []portainer.EndpointType{},
|
||||
}
|
||||
if edgeGroup.Dynamic {
|
||||
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
|
||||
endpointIDs, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments and environment groups for Edge group", err}
|
||||
}
|
||||
|
||||
edgeGroup.Endpoints = endpoints
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
endpointTypes, err := getEndpointTypes(handler.DataStore.Endpoint(), edgeGroup.Endpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint types for Edge group", err}
|
||||
}
|
||||
|
||||
edgeGroup.EndpointTypes = endpointTypes
|
||||
|
||||
edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID]
|
||||
|
||||
decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup)
|
||||
@@ -64,3 +74,22 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
return response.JSON(w, decoratedEdgeGroups)
|
||||
}
|
||||
|
||||
func getEndpointTypes(endpointService portainer.EndpointService, endpointIds []portainer.EndpointID) ([]portainer.EndpointType, error) {
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointID := range endpointIds {
|
||||
endpoint, err := endpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching endpoint: %w", err)
|
||||
}
|
||||
|
||||
typeSet[endpoint.Type] = true
|
||||
}
|
||||
|
||||
endpointTypes := make([]portainer.EndpointType, 0, len(typeSet))
|
||||
for endpointType := range typeSet {
|
||||
endpointTypes = append(endpointTypes, endpointType)
|
||||
}
|
||||
|
||||
return endpointTypes, nil
|
||||
}
|
||||
|
||||
53
api/http/handler/edgegroups/edgegroup_list_test.go
Normal file
53
api/http/handler/edgegroups/edgegroup_list_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getEndpointTypes(t *testing.T) {
|
||||
endpoints := []portainer.Endpoint{
|
||||
{ID: 1, Type: portainer.DockerEnvironment},
|
||||
{ID: 2, Type: portainer.AgentOnDockerEnvironment},
|
||||
{ID: 3, Type: portainer.AzureEnvironment},
|
||||
{ID: 4, Type: portainer.EdgeAgentOnDockerEnvironment},
|
||||
{ID: 5, Type: portainer.KubernetesLocalEnvironment},
|
||||
{ID: 6, Type: portainer.AgentOnKubernetesEnvironment},
|
||||
{ID: 7, Type: portainer.EdgeAgentOnKubernetesEnvironment},
|
||||
}
|
||||
|
||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints(endpoints))
|
||||
|
||||
tests := []struct {
|
||||
endpointIds []portainer.EndpointID
|
||||
expected []portainer.EndpointType
|
||||
}{
|
||||
{endpointIds: []portainer.EndpointID{1}, expected: []portainer.EndpointType{portainer.DockerEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{2}, expected: []portainer.EndpointType{portainer.AgentOnDockerEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{3}, expected: []portainer.EndpointType{portainer.AzureEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{4}, expected: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{5}, expected: []portainer.EndpointType{portainer.KubernetesLocalEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{6}, expected: []portainer.EndpointType{portainer.AgentOnKubernetesEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{7}, expected: []portainer.EndpointType{portainer.EdgeAgentOnKubernetesEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{7, 2}, expected: []portainer.EndpointType{portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnDockerEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{6, 4, 1}, expected: []portainer.EndpointType{portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnDockerEnvironment, portainer.DockerEnvironment}},
|
||||
{endpointIds: []portainer.EndpointID{1, 2, 3}, expected: []portainer.EndpointType{portainer.DockerEnvironment, portainer.AgentOnDockerEnvironment, portainer.AzureEnvironment}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ans, err := getEndpointTypes(datastore.Endpoint(), test.endpointIds)
|
||||
assert.NoError(t, err, "getEndpointTypes shouldn't fail")
|
||||
|
||||
assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) {
|
||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
|
||||
|
||||
_, err := getEndpointTypes(datastore.Endpoint(), []portainer.EndpointID{1})
|
||||
assert.Error(t, err, "getEndpointTypes should fail")
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
return errors.New("TagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
|
||||
return errors.New("Endpoints is mandatory for a static Edge group")
|
||||
return errors.New("Environments is mandatory for a static Edge group")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @param body body edgeGroupUpdatePayload true "EdgeGroup data"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @router /edge_groups/{id} [put]
|
||||
func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -81,12 +81,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from database", err}
|
||||
}
|
||||
|
||||
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
@@ -99,7 +99,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
@@ -124,7 +124,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
for _, endpointID := range endpointsToUpdate {
|
||||
err = handler.updateEndpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Endpoint relation changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Environment relation changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file, string)
|
||||
// @param body body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"
|
||||
// @param body body edgeJobCreateFromFilePayload true "EdgeGroup data when method is file"
|
||||
// @param body_string body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"
|
||||
// @param body_file body edgeJobCreateFromFilePayload true "EdgeGroup data when method is file"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @router /edge_jobs [post]
|
||||
func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -66,7 +66,7 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er
|
||||
}
|
||||
|
||||
if payload.Endpoints == nil || len(payload.Endpoints) == 0 {
|
||||
return errors.New("Invalid endpoints payload")
|
||||
return errors.New("Invalid environment payload")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
@@ -119,9 +119,9 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Environments", &endpoints, false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid endpoints")
|
||||
return errors.New("Invalid environments")
|
||||
}
|
||||
payload.Endpoints = endpoints
|
||||
|
||||
@@ -206,7 +206,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
}
|
||||
|
||||
if len(edgeJob.Endpoints) == 0 {
|
||||
return errors.New("Endpoints are mandatory for an Edge job")
|
||||
return errors.New("Environments are mandatory for an Edge job")
|
||||
}
|
||||
|
||||
scriptPath, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), file)
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id} [delete]
|
||||
func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -25,7 +25,7 @@ type edgeJobFileResponse struct {
|
||||
// @success 200 {object} edgeJobFileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id}/file [get]
|
||||
func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -26,7 +26,7 @@ type edgeJobInspectResponse struct {
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id} [get]
|
||||
func (handler *Handler) edgeJobInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// @success 200 {array} portainer.EdgeJob
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs [get]
|
||||
// GET request on /api/edge_jobs
|
||||
func (handler *Handler) edgeJobList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id}/tasks/{taskID}/logs [delete]
|
||||
func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id}/tasks/{taskID}/logs [post]
|
||||
func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -25,7 +25,7 @@ type fileResponse struct {
|
||||
// @success 200 {object} fileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id}/tasks/{taskID}/logs [get]
|
||||
func (handler *Handler) edgeJobTaskLogsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -28,7 +28,7 @@ type taskContainer struct {
|
||||
// @success 200 {array} taskContainer
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id}/tasks [get]
|
||||
func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -40,7 +40,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_jobs/{id} [post]
|
||||
func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -2,6 +2,7 @@ package edgestacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -29,7 +30,7 @@ import (
|
||||
// @param body_repository body swarmStackFromGitRepositoryPayload true "Required when using method=repository"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks [post]
|
||||
func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
@@ -42,37 +43,6 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
|
||||
}
|
||||
|
||||
relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
|
||||
for _, endpointID := range relatedEndpoints {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
}
|
||||
|
||||
relation.EdgeStacks[edgeStack.ID] = true
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStack)
|
||||
}
|
||||
|
||||
@@ -95,6 +65,11 @@ type swarmStackFromFileContentPayload struct {
|
||||
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"`
|
||||
// List of identifiers of EdgeGroups
|
||||
EdgeGroups []portainer.EdgeGroupID `example:"1"`
|
||||
// Deployment type to deploy this stack
|
||||
// Valid values are: 0 - 'compose', 1 - 'kubernetes'
|
||||
// for compose stacks will use kompose to convert to kubernetes manifest for kubernetes endpoints
|
||||
// kubernetes deploytype is enabled only for kubernetes endpoints
|
||||
DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
|
||||
@@ -124,21 +99,64 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta
|
||||
|
||||
stackID := handler.DataStore.EdgeStack().GetNextIdentifier()
|
||||
stack := &portainer.EdgeStack{
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
DeploymentType: payload.DeploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find environment relations in database: %w", err)
|
||||
}
|
||||
|
||||
relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to persist environment relation in database: %w", err)
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if stack.DeploymentType == portainer.EdgeStackDeploymentCompose {
|
||||
stack.EntryPoint = filesystem.ComposeFileDefaultName
|
||||
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating and storing kube manifest: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
hasDockerEndpoint, err := hasDockerEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check for existence of docker endpoint: %w", err)
|
||||
}
|
||||
|
||||
if hasDockerEndpoint {
|
||||
return nil, fmt.Errorf("edge stack with docker endpoint cannot be deployed with kubernetes config")
|
||||
}
|
||||
|
||||
stack.ManifestPath = filesystem.ManifestFileDefaultName
|
||||
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.ManifestPath, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stack.ProjectPath = projectPath
|
||||
}
|
||||
|
||||
err = updateEndpointRelations(handler.DataStore.EndpointRelation(), stack.ID, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to update endpoint relations: %w", err)
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.DataStore.EdgeStack().CreateEdgeStack(stack)
|
||||
if err != nil {
|
||||
@@ -162,9 +180,14 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||
// Password used in basic authentication. Required when RepositoryAuthentication is true.
|
||||
RepositoryPassword string `example:"myGitPassword"`
|
||||
// Path to the Stack file inside the Git repository
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
// List of identifiers of EdgeGroups
|
||||
EdgeGroups []portainer.EdgeGroupID `example:"1"`
|
||||
// Deployment type to deploy this stack
|
||||
// Valid values are: 0 - 'compose', 1 - 'kubernetes'
|
||||
// for compose stacks will use kompose to convert to kubernetes manifest for kubernetes endpoints
|
||||
// kubernetes deploytype is enabled only for kubernetes endpoints
|
||||
DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -177,8 +200,8 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
if govalidator.IsNull(payload.FilePathInRepository) {
|
||||
payload.FilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
|
||||
return errors.New("Edge Groups are mandatory for an Edge stack")
|
||||
@@ -200,13 +223,13 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
|
||||
|
||||
stackID := handler.DataStore.EdgeStack().GetNextIdentifier()
|
||||
stack := &portainer.EdgeStack{
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
DeploymentType: payload.DeploymentType,
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -219,11 +242,37 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
|
||||
repositoryPassword = ""
|
||||
}
|
||||
|
||||
relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching relations config: %w", err)
|
||||
}
|
||||
|
||||
relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve related endpoints: %w", err)
|
||||
}
|
||||
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stack.DeploymentType == portainer.EdgeStackDeploymentCompose {
|
||||
stack.EntryPoint = payload.FilePathInRepository
|
||||
|
||||
err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating and storing kube manifest: %w", err)
|
||||
}
|
||||
} else {
|
||||
stack.ManifestPath = payload.FilePathInRepository
|
||||
}
|
||||
|
||||
err = updateEndpointRelations(handler.DataStore.EndpointRelation(), stack.ID, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to update endpoint relations: %w", err)
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeStack().CreateEdgeStack(stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -236,6 +285,7 @@ type swarmStackFromFileUploadPayload struct {
|
||||
Name string
|
||||
StackFileContent []byte
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
DeploymentType portainer.EdgeStackDeploymentType
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
|
||||
@@ -257,6 +307,13 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
|
||||
return errors.New("Edge Groups are mandatory for an Edge stack")
|
||||
}
|
||||
payload.EdgeGroups = edgeGroups
|
||||
|
||||
deploymentType, err := request.RetrieveNumericMultiPartFormValue(r, "DeploymentType", true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid deployment type")
|
||||
}
|
||||
payload.DeploymentType = portainer.EdgeStackDeploymentType(deploymentType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -274,21 +331,54 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
|
||||
|
||||
stackID := handler.DataStore.EdgeStack().GetNextIdentifier()
|
||||
stack := &portainer.EdgeStack{
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
DeploymentType: payload.DeploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching relations config: %w", err)
|
||||
}
|
||||
|
||||
relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve related endpoints: %w", err)
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if stack.DeploymentType == portainer.EdgeStackDeploymentCompose {
|
||||
stack.EntryPoint = filesystem.ComposeFileDefaultName
|
||||
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating and storing kube manifest: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
stack.ManifestPath = filesystem.ManifestFileDefaultName
|
||||
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.ManifestPath, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
}
|
||||
|
||||
err = updateEndpointRelations(handler.DataStore.EndpointRelation(), stack.ID, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to update endpoint relations: %w", err)
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.DataStore.EdgeStack().CreateEdgeStack(stack)
|
||||
if err != nil {
|
||||
@@ -311,3 +401,22 @@ func (handler *Handler) validateUniqueName(name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateEndpointRelations adds a relation between the Edge Stack to the related endpoints
|
||||
func updateEndpointRelations(endpointRelationService portainer.EndpointRelationService, edgeStackID portainer.EdgeStackID, relatedEndpointIds []portainer.EndpointID) error {
|
||||
for _, endpointID := range relatedEndpointIds {
|
||||
relation, err := endpointRelationService.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find endpoint relation in database: %w", err)
|
||||
}
|
||||
|
||||
relation.EdgeStacks[edgeStackID] = true
|
||||
|
||||
err = endpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to persist endpoint relation in database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
38
api/http/handler/edgestacks/edgestack_create_test.go
Normal file
38
api/http/handler/edgestacks/edgestack_create_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
|
||||
edgeStackID := portainer.EdgeStackID(5)
|
||||
endpointRelations := []portainer.EndpointRelation{
|
||||
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
}
|
||||
|
||||
relatedIds := []portainer.EndpointID{2, 3}
|
||||
|
||||
dataStore := testhelpers.NewDatastore(testhelpers.WithEndpointRelations(endpointRelations))
|
||||
|
||||
err := updateEndpointRelations(dataStore.EndpointRelation(), edgeStackID, relatedIds)
|
||||
|
||||
assert.NoError(t, err, "updateEndpointRelations should not fail")
|
||||
|
||||
relatedSet := map[portainer.EndpointID]bool{}
|
||||
for _, relationID := range relatedIds {
|
||||
relatedSet[relationID] = true
|
||||
}
|
||||
|
||||
for _, relation := range endpointRelations {
|
||||
shouldBeRelated := relatedSet[relation.EndpointID]
|
||||
assert.Equal(t, shouldBeRelated, relation.EdgeStacks[edgeStackID])
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks/{id} [delete]
|
||||
func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
@@ -42,34 +42,27 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments relations config from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
|
||||
}
|
||||
|
||||
relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
|
||||
for _, endpointID := range relatedEndpoints {
|
||||
for _, endpointID := range relatedEndpointIds {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err}
|
||||
}
|
||||
|
||||
delete(relation.EdgeStacks, edgeStack.ID)
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ type stackFileResponse struct {
|
||||
// @success 200 {object} stackFileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks/{id}/file [get]
|
||||
func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
@@ -41,7 +41,12 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
fileName := stack.EntryPoint
|
||||
if stack.DeploymentType == portainer.EdgeStackDeploymentKubernetes {
|
||||
fileName = stack.ManifestPath
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, fileName))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks/{id} [get]
|
||||
func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// @success 200 {array} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks [get]
|
||||
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
|
||||
@@ -23,7 +23,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
||||
return errors.New("Invalid status")
|
||||
}
|
||||
if payload.EndpointID == nil {
|
||||
return errors.New("Invalid EndpointID")
|
||||
return errors.New("Invalid EnvironmentID")
|
||||
}
|
||||
if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) {
|
||||
return errors.New("Error message is mandatory when status is error")
|
||||
@@ -65,14 +65,14 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(*payload.EndpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
|
||||
|
||||
@@ -5,24 +5,24 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
)
|
||||
|
||||
type updateEdgeStackPayload struct {
|
||||
StackFileContent string
|
||||
Version *int
|
||||
Prune *bool
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
DeploymentType portainer.EdgeStackDeploymentType
|
||||
}
|
||||
|
||||
func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
if payload.StackFileContent == "" {
|
||||
return errors.New("Invalid stack file content")
|
||||
}
|
||||
if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 {
|
||||
@@ -43,7 +43,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 Edge compute features are disabled
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks/{id} [put]
|
||||
func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
@@ -64,33 +64,23 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
relationConfig, err := fetchEndpointRelationsConfig(handler.DataStore)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments relations config from database", err}
|
||||
}
|
||||
|
||||
relatedEndpointIds, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err}
|
||||
}
|
||||
|
||||
if payload.EdgeGroups != nil {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, relationConfig.endpoints, relationConfig.endpointGroups, relationConfig.edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
|
||||
}
|
||||
|
||||
oldRelated, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
|
||||
}
|
||||
|
||||
newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
|
||||
}
|
||||
|
||||
oldRelatedSet := EndpointSet(oldRelated)
|
||||
oldRelatedSet := EndpointSet(relatedEndpointIds)
|
||||
newRelatedSet := EndpointSet(newRelated)
|
||||
|
||||
endpointsToRemove := map[portainer.EndpointID]bool{}
|
||||
@@ -103,14 +93,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
for endpointID := range endpointsToRemove {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err}
|
||||
}
|
||||
|
||||
delete(relation.EdgeStacks, stack.ID)
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,29 +114,67 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
for endpointID := range endpointsToAdd {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err}
|
||||
}
|
||||
|
||||
relation.EdgeStacks[stack.ID] = true
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
stack.EdgeGroups = payload.EdgeGroups
|
||||
|
||||
relatedEndpointIds = newRelated
|
||||
}
|
||||
|
||||
if payload.Prune != nil {
|
||||
stack.Prune = *payload.Prune
|
||||
if stack.DeploymentType != payload.DeploymentType {
|
||||
// deployment type was changed - need to delete the old file
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clear old files", err}
|
||||
}
|
||||
|
||||
stack.EntryPoint = ""
|
||||
stack.ManifestPath = ""
|
||||
stack.DeploymentType = payload.DeploymentType
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
if payload.DeploymentType == portainer.EdgeStackDeploymentCompose {
|
||||
if stack.EntryPoint == "" {
|
||||
stack.EntryPoint = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
}
|
||||
|
||||
err = handler.convertAndStoreKubeManifestIfNeeded(stack, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to convert and persist updated Kubernetes manifest file on disk", err}
|
||||
}
|
||||
|
||||
} else {
|
||||
if stack.ManifestPath == "" {
|
||||
stack.ManifestPath = filesystem.ManifestFileDefaultName
|
||||
}
|
||||
|
||||
hasDockerEndpoint, err := hasDockerEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for existence of docker endpoint", err}
|
||||
}
|
||||
|
||||
if hasDockerEndpoint {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Edge stack with docker endpoint cannot be deployed with kubernetes config", err}
|
||||
}
|
||||
|
||||
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.ManifestPath, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Version != nil && *payload.Version != stack.Version {
|
||||
|
||||
60
api/http/handler/edgestacks/endpoints.go
Normal file
60
api/http/handler/edgestacks/endpoints.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
func hasKubeEndpoint(endpointService portainer.EndpointService, endpointIDs []portainer.EndpointID) (bool, error) {
|
||||
return hasEndpointPredicate(endpointService, endpointIDs, endpointutils.IsKubernetesEndpoint)
|
||||
}
|
||||
|
||||
func hasDockerEndpoint(endpointService portainer.EndpointService, endpointIDs []portainer.EndpointID) (bool, error) {
|
||||
return hasEndpointPredicate(endpointService, endpointIDs, endpointutils.IsDockerEndpoint)
|
||||
}
|
||||
|
||||
func hasEndpointPredicate(endpointService portainer.EndpointService, endpointIDs []portainer.EndpointID, predicate func(*portainer.Endpoint) bool) (bool, error) {
|
||||
for _, endpointID := range endpointIDs {
|
||||
endpoint, err := endpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to retrieve endpoint from database: %w", err)
|
||||
}
|
||||
|
||||
if predicate(endpoint) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type endpointRelationsConfig struct {
|
||||
endpoints []portainer.Endpoint
|
||||
endpointGroups []portainer.EndpointGroup
|
||||
edgeGroups []portainer.EdgeGroup
|
||||
}
|
||||
|
||||
func fetchEndpointRelationsConfig(dataStore portainer.DataStore) (*endpointRelationsConfig, error) {
|
||||
endpoints, err := dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve environments from database: %w", err)
|
||||
}
|
||||
|
||||
endpointGroups, err := dataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve environment groups from database: %w", err)
|
||||
}
|
||||
|
||||
edgeGroups, err := dataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve edge groups from database: %w", err)
|
||||
}
|
||||
|
||||
return &endpointRelationsConfig{
|
||||
endpoints: endpoints,
|
||||
endpointGroups: endpointGroups,
|
||||
edgeGroups: edgeGroups,
|
||||
}, nil
|
||||
}
|
||||
99
api/http/handler/edgestacks/endpoints_test.go
Normal file
99
api/http/handler/edgestacks/endpoints_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_hasKubeEndpoint(t *testing.T) {
|
||||
endpoints := []portainer.Endpoint{
|
||||
{ID: 1, Type: portainer.DockerEnvironment},
|
||||
{ID: 2, Type: portainer.AgentOnDockerEnvironment},
|
||||
{ID: 3, Type: portainer.AzureEnvironment},
|
||||
{ID: 4, Type: portainer.EdgeAgentOnDockerEnvironment},
|
||||
{ID: 5, Type: portainer.KubernetesLocalEnvironment},
|
||||
{ID: 6, Type: portainer.AgentOnKubernetesEnvironment},
|
||||
{ID: 7, Type: portainer.EdgeAgentOnKubernetesEnvironment},
|
||||
}
|
||||
|
||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints(endpoints))
|
||||
|
||||
tests := []struct {
|
||||
endpointIds []portainer.EndpointID
|
||||
expected bool
|
||||
}{
|
||||
{endpointIds: []portainer.EndpointID{1}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{2}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{3}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{4}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{5}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{6}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{7}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{7, 2}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{6, 4, 1}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{1, 2, 3}, expected: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
ans, err := hasKubeEndpoint(datastore.Endpoint(), test.endpointIds)
|
||||
assert.NoError(t, err, "hasKubeEndpoint shouldn't fail")
|
||||
|
||||
assert.Equal(t, test.expected, ans, "hasKubeEndpoint expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hasKubeEndpoint_failWhenEndpointDontExist(t *testing.T) {
|
||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
|
||||
|
||||
_, err := hasKubeEndpoint(datastore.Endpoint(), []portainer.EndpointID{1})
|
||||
assert.Error(t, err, "hasKubeEndpoint should fail")
|
||||
}
|
||||
|
||||
func Test_hasDockerEndpoint(t *testing.T) {
|
||||
endpoints := []portainer.Endpoint{
|
||||
{ID: 1, Type: portainer.DockerEnvironment},
|
||||
{ID: 2, Type: portainer.AgentOnDockerEnvironment},
|
||||
{ID: 3, Type: portainer.AzureEnvironment},
|
||||
{ID: 4, Type: portainer.EdgeAgentOnDockerEnvironment},
|
||||
{ID: 5, Type: portainer.KubernetesLocalEnvironment},
|
||||
{ID: 6, Type: portainer.AgentOnKubernetesEnvironment},
|
||||
{ID: 7, Type: portainer.EdgeAgentOnKubernetesEnvironment},
|
||||
}
|
||||
|
||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints(endpoints))
|
||||
|
||||
tests := []struct {
|
||||
endpointIds []portainer.EndpointID
|
||||
expected bool
|
||||
}{
|
||||
{endpointIds: []portainer.EndpointID{1}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{2}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{3}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{4}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{5}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{6}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{7}, expected: false},
|
||||
{endpointIds: []portainer.EndpointID{7, 2}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{6, 4, 1}, expected: true},
|
||||
{endpointIds: []portainer.EndpointID{1, 2, 3}, expected: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
ans, err := hasDockerEndpoint(datastore.Endpoint(), test.endpointIds)
|
||||
assert.NoError(t, err, "hasDockerEndpoint shouldn't fail")
|
||||
|
||||
assert.Equal(t, test.expected, ans, "hasDockerEndpoint expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hasDockerEndpoint_failWhenEndpointDontExist(t *testing.T) {
|
||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
|
||||
|
||||
_, err := hasDockerEndpoint(datastore.Endpoint(), []portainer.EndpointID{1})
|
||||
assert.Error(t, err, "hasDockerEndpoint should fail")
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"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/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
KubernetesDeployer portainer.KubernetesDeployer
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
@@ -40,3 +45,34 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) convertAndStoreKubeManifestIfNeeded(edgeStack *portainer.EdgeStack, relatedEndpointIds []portainer.EndpointID) error {
|
||||
hasKubeEndpoint, err := hasKubeEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to check if edge stack has kube endpoints: %w", err)
|
||||
}
|
||||
|
||||
if !hasKubeEndpoint {
|
||||
return nil
|
||||
}
|
||||
|
||||
composeConfig, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve Compose file from disk: %w", err)
|
||||
}
|
||||
|
||||
kompose, err := handler.KubernetesDeployer.ConvertCompose(composeConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed converting compose file to kubernetes manifest: %w", err)
|
||||
}
|
||||
|
||||
komposeFileName := filesystem.ManifestFileDefaultName
|
||||
_, err = handler.FileService.StoreEdgeStackFileFromBytes(strconv.Itoa(int(edgeStack.ID)), komposeFileName, kompose)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store kube manifest file: %w", err)
|
||||
}
|
||||
|
||||
edgeStack.ManifestPath = komposeFileName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,19 +34,19 @@ func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "jobID")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
@@ -8,11 +9,11 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
type configResponse struct {
|
||||
Prune bool
|
||||
StackFileContent string
|
||||
Name string
|
||||
}
|
||||
@@ -23,7 +24,7 @@ type configResponse struct {
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "Endpoint Id"
|
||||
// @param stackID path string true "EdgeStack Id"
|
||||
// @param stackId path string true "EdgeStack Id"
|
||||
// @success 200 {object} configResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
@@ -32,19 +33,19 @@ type configResponse struct {
|
||||
func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId")
|
||||
@@ -53,19 +54,33 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
|
||||
fileName := edgeStack.EntryPoint
|
||||
if endpointutils.IsDockerEndpoint(endpoint) {
|
||||
if fileName == "" {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Docker is not supported by this stack", errors.New("Docker is not supported by this stack")}
|
||||
}
|
||||
}
|
||||
|
||||
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
fileName = edgeStack.ManifestPath
|
||||
|
||||
if fileName == "" {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack")}
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, fileName))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, configResponse{
|
||||
Prune: edgeStack.Prune,
|
||||
StackFileContent: string(stackFileContent),
|
||||
Name: edgeStack.Name,
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ type endpointGroupCreatePayload struct {
|
||||
|
||||
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("Invalid endpoint group name")
|
||||
return errors.New("Invalid environment group name")
|
||||
}
|
||||
if payload.TagIDs == nil {
|
||||
payload.TagIDs = []portainer.TagID{}
|
||||
@@ -61,12 +61,12 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
err = handler.DataStore.EndpointGroup().CreateEndpointGroup(endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the endpoint group inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the environment group inside the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
}
|
||||
|
||||
for _, id := range payload.AssociatedEndpoints {
|
||||
@@ -76,12 +76,12 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment", err}
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
@@ -28,28 +28,28 @@ import (
|
||||
func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err}
|
||||
}
|
||||
|
||||
if endpointGroupID == 1 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", errors.New("Cannot remove the default endpoint group")}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", errors.New("Cannot remove the default environment group")}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointGroup().DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the endpoint group from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the environment group from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
@@ -57,12 +57,12 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update environment", err}
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(&endpoint, nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,38 +26,38 @@ import (
|
||||
func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(endpoint, endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
@@ -25,38 +25,38 @@ import (
|
||||
func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
_, err = handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(endpoint, nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
@@ -22,18 +22,18 @@ import (
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/:id [get]
|
||||
// @router /endpoint_groups/{id} [get]
|
||||
func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroup)
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
|
||||
@@ -41,11 +41,11 @@ func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/:id [put]
|
||||
// @router /endpoint_groups/{id} [put]
|
||||
func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment group identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointGroupUpdatePayload
|
||||
@@ -56,9 +56,9 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
@@ -123,7 +123,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
if updateAuthorizations {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
@@ -140,13 +140,13 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment group changes inside the database", err}
|
||||
}
|
||||
|
||||
if tagsChanged {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
if endpoint.GroupID == endpointGroup.ID {
|
||||
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relations changes inside the database", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,19 @@ import (
|
||||
func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
|
||||
@@ -17,24 +17,24 @@ import (
|
||||
func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
if endpoint.EdgeID == "" {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
@@ -17,24 +17,24 @@ import (
|
||||
func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
if endpoint.EdgeID == "" {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
@@ -17,19 +17,19 @@ import (
|
||||
func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
var storidgeExtension *portainer.EndpointExtension
|
||||
@@ -40,7 +40,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
if storidgeExtension == nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", errors.New("This extension is not supported")}
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this environment", errors.New("This extension is not supported")}
|
||||
}
|
||||
|
||||
proxyExtensionKey := strconv.Itoa(endpointID) + "_" + strconv.Itoa(int(portainer.StoridgeEndpointExtension)) + "_" + storidgeExtension.URL
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @id EndpointAssociationDelete
|
||||
@@ -26,22 +27,22 @@ import (
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /api/endpoints/:id/association [put]
|
||||
// @router /api/endpoints/{id}/association [put]
|
||||
func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint type", errors.New("Invalid endpoint type")}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment type", errors.New("Invalid environment type")}
|
||||
}
|
||||
|
||||
endpoint.EdgeID = ""
|
||||
@@ -55,7 +56,7 @@ func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting environment in database", err}
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.SetTunnelStatusToIdle(endpoint.ID)
|
||||
@@ -84,5 +85,5 @@ func (handler *Handler) updateEdgeKey(edgeKey string) (string, error) {
|
||||
|
||||
func getPort(url string) string {
|
||||
items := strings.Split(url, ":")
|
||||
return items[len(items) - 1]
|
||||
return items[len(items)-1]
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ const (
|
||||
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid endpoint name")
|
||||
return errors.New("Invalid environment name")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
endpointCreationType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointCreationType", false)
|
||||
if err != nil || endpointCreationType == 0 {
|
||||
return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)")
|
||||
return errors.New("Invalid environment type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)")
|
||||
}
|
||||
payload.EndpointCreationType = endpointCreationEnum(endpointCreationType)
|
||||
|
||||
@@ -133,7 +133,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
default:
|
||||
endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid endpoint URL")
|
||||
return errors.New("Invalid environment URL")
|
||||
}
|
||||
payload.URL = endpointURL
|
||||
|
||||
@@ -189,7 +189,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment group inside the database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
@@ -238,7 +238,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
|
||||
if payload.EndpointCreationType == agentEnvironment {
|
||||
agentPlatform, err := handler.pingAndCheckPlatform(payload)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get endpoint type", err}
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err}
|
||||
}
|
||||
|
||||
if agentPlatform == portainer.AgentPlatformDocker {
|
||||
@@ -288,7 +288,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
@@ -299,7 +299,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
|
||||
portainerURL, err := url.Parse(payload.URL)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", err}
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", err}
|
||||
}
|
||||
|
||||
portainerHost, _, err := net.SplitHostPort(portainerURL.Host)
|
||||
@@ -308,7 +308,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
}
|
||||
|
||||
if portainerHost == "localhost" {
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", errors.New("cannot use localhost as endpoint URL")}
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", errors.New("cannot use localhost as environment URL")}
|
||||
}
|
||||
|
||||
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
@@ -335,7 +335,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
@@ -455,12 +455,12 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
|
||||
if strings.Contains(err.Error(), "Invalid request signature") {
|
||||
err = errors.New("agent already paired with another Portainer instance")
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with environment", err}
|
||||
}
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -26,14 +26,14 @@ import (
|
||||
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
@@ -46,14 +46,14 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
err = handler.DataStore.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment relation from the database", err}
|
||||
}
|
||||
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
|
||||
@@ -26,14 +26,14 @@ type dockerhubStatusResponse struct {
|
||||
func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if !endpointutils.IsLocalEndpoint(endpoint) {
|
||||
|
||||
@@ -32,14 +32,14 @@ func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
|
||||
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload endpointExtensionAddPayload
|
||||
@@ -69,7 +69,7 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, extension)
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
extensionType, err := request.RetrieveNumericRouteVariableValue(r, "extensionType")
|
||||
@@ -38,7 +38,7 @@ func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.R
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
@@ -26,19 +26,19 @@ import (
|
||||
func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
hideFields(endpoint)
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
// @param tagsPartialMatch query bool false "If true, will return endpoint which has one of tagIds, if false (or missing) will return only endpoints that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these endpoints"
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 Server error
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints [get]
|
||||
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
start, _ := request.RetrieveNumericQueryParameter(r, "start", true)
|
||||
@@ -60,12 +60,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
|
||||
|
||||
@@ -27,14 +27,14 @@ func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Re
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
isAdmin := securityContext.IsAdmin
|
||||
@@ -92,7 +92,7 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name
|
||||
|
||||
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to retrieve endpoint's namespaces policies")
|
||||
return false, errors.Wrap(err, "unable to retrieve environment's namespaces policies")
|
||||
}
|
||||
|
||||
namespacePolicy, ok := accessPolicies[namespace]
|
||||
|
||||
@@ -25,7 +25,7 @@ func (payload *registryAccessPayload) Validate(r *http.Request) error {
|
||||
func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
|
||||
@@ -35,9 +35,9 @@ func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Re
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
@@ -47,7 +47,7 @@ func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Re
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
@@ -56,9 +56,9 @@ func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Re
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
var payload registryAccessPayload
|
||||
|
||||
@@ -49,11 +49,11 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /api/endpoints/:id/settings [put]
|
||||
// @router /api/endpoints/{id}/settings [put]
|
||||
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointSettingsUpdatePayload
|
||||
@@ -64,9 +64,9 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securitySettings := endpoint.SecuritySettings
|
||||
@@ -111,7 +111,7 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting environment in database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
|
||||
@@ -26,25 +26,25 @@ import (
|
||||
func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if !snapshot.SupportDirectSnapshot(endpoint) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this environment", err}
|
||||
}
|
||||
|
||||
snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint)
|
||||
|
||||
latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
@@ -57,7 +57,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
@@ -34,13 +34,13 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
|
||||
|
||||
latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
log.Printf("background schedule error (environment snapshot). Environment not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
|
||||
log.Printf("background schedule error (environment snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,19 +65,19 @@ type endpointStatusInspectResponse struct {
|
||||
func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
if endpoint.EdgeID == "" {
|
||||
@@ -107,7 +107,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
||||
@@ -71,7 +71,7 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
||||
func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointUpdatePayload
|
||||
@@ -82,9 +82,9 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.Name != nil {
|
||||
@@ -257,7 +257,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the environment", err}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,18 +272,18 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation inside the database", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint group inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment group inside the database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
@@ -307,7 +307,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation changes inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment relation changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
@@ -47,7 +48,9 @@ type Handler struct {
|
||||
EndpointEdgeHandler *endpointedge.Handler
|
||||
EndpointGroupHandler *endpointgroups.Handler
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointHelmHandler *helm.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
@@ -69,14 +72,14 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.6.3
|
||||
// @version 2.9.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
// @contact.email info@portainer.io
|
||||
|
||||
// @license.name
|
||||
// @license.url
|
||||
// @license.name zlib
|
||||
// @license.url https://github.com/portainer/portainer/blob/develop/LICENSE
|
||||
|
||||
// @host
|
||||
// @BasePath /api
|
||||
@@ -118,8 +121,6 @@ type Handler struct {
|
||||
// @tag.description Manage Portainer settings
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name stacks
|
||||
// @tag.description Manage Docker stacks
|
||||
// @tag.name users
|
||||
// @tag.description Manage users
|
||||
// @tag.name tags
|
||||
@@ -166,6 +167,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
|
||||
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
|
||||
|
||||
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
|
||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointHelmHandler).ServeHTTP(w, r)
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||
switch {
|
||||
case strings.Contains(r.URL.Path, "/docker/"):
|
||||
@@ -199,6 +205,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/tags"):
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates/helm"):
|
||||
http.StripPrefix("/api", h.HelmTemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||
|
||||
103
api/http/handler/helm/handler.go
Normal file
103
api/http/handler/helm/handler.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
handlerActivityContext = "Kubernetes"
|
||||
)
|
||||
|
||||
type requestBouncer interface {
|
||||
AuthenticatedAccess(h http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer requestBouncer
|
||||
dataStore portainer.DataStore
|
||||
kubeConfigService kubernetes.KubeConfigService
|
||||
helmPackageManager libhelm.HelmPackageManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
dataStore: dataStore,
|
||||
helmPackageManager: helmPackageManager,
|
||||
kubeConfigService: kubeConfigService,
|
||||
}
|
||||
|
||||
h.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
|
||||
// `helm list -o json`
|
||||
h.Handle("/{id}/kubernetes/helm",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmList))).Methods(http.MethodGet)
|
||||
|
||||
// `helm delete RELEASE_NAME`
|
||||
h.Handle("/{id}/kubernetes/helm/{release}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmDelete))).Methods(http.MethodDelete)
|
||||
|
||||
// `helm install [NAME] [CHART] flags`
|
||||
h.Handle("/{id}/kubernetes/helm",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmInstall))).Methods(http.MethodPost)
|
||||
|
||||
h.Handle("/{id}/kubernetes/helm/repositories",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userGetHelmRepos))).Methods(http.MethodGet)
|
||||
h.Handle("/{id}/kubernetes/helm/repositories",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userCreateHelmRepo))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// NewTemplateHandler creates a template handler to manage endpoint group operations.
|
||||
func NewTemplateHandler(bouncer requestBouncer, helmPackageManager libhelm.HelmPackageManager) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
helmPackageManager: helmPackageManager,
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/templates/helm",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmRepoSearch))).Methods(http.MethodGet)
|
||||
|
||||
// helm show [COMMAND] [CHART] [REPO] flags
|
||||
h.Handle("/templates/helm/{command:chart|values|readme}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.helmShow))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// getHelmClusterAccess obtains the core k8s cluster access details from request.
|
||||
// The cluster access includes the cluster server url, the user's bearer token and the tls certificate.
|
||||
// The cluster access is passed in as kube config CLI params to helm binary.
|
||||
func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.KubernetesClusterAccess, *httperror.HandlerError) {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err}
|
||||
}
|
||||
|
||||
bearerToken, err := security.ExtractBearerToken(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
|
||||
}
|
||||
|
||||
kubeConfigInternal := handler.kubeConfigService.GetKubeConfigInternal(endpoint.ID, bearerToken)
|
||||
return &options.KubernetesClusterAccess{
|
||||
ClusterServerURL: kubeConfigInternal.ClusterServerURL,
|
||||
CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile,
|
||||
AuthToken: kubeConfigInternal.AuthToken,
|
||||
}, nil
|
||||
}
|
||||
56
api/http/handler/helm/helm_delete.go
Normal file
56
api/http/handler/helm/helm_delete.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id HelmDelete
|
||||
// @summary Delete Helm Release
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @param release path string true "The name of the release/application to uninstall"
|
||||
// @param namespace query string true "An optional namespace"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid endpoint id or bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error or helm error"
|
||||
// @router /endpoints/{id}/kubernetes/helm/{release} [delete]
|
||||
func (handler *Handler) helmDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
release, err := request.RetrieveRouteVariableValue(r, "release")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "No release specified", err}
|
||||
}
|
||||
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return httperr
|
||||
}
|
||||
|
||||
uninstallOpts := options.UninstallOptions{
|
||||
Name: release,
|
||||
KubernetesClusterAccess: clusterAccess,
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
if namespace := q.Get("namespace"); namespace != "" {
|
||||
uninstallOpts.Namespace = namespace
|
||||
}
|
||||
|
||||
err = handler.helmPackageManager.Uninstall(uninstallOpts)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Helm returned an error", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
53
api/http/handler/helm/helm_delete_test.go
Normal file
53
api/http/handler/helm/helm_delete_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func Test_helmDelete(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
|
||||
is.NoError(err, "Error creating endpoint")
|
||||
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "Error creating a user")
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
// Install a single chart directly, to be deleted by the handler
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
|
||||
h.helmPackageManager.Install(options)
|
||||
|
||||
t.Run("helmDelete succeeds with admin user", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/1/kubernetes/helm/%s", options.Name), nil)
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusNoContent, rr.Code, "Status should be 204")
|
||||
})
|
||||
}
|
||||
135
api/http/handler/helm/helm_install.go
Normal file
135
api/http/handler/helm/helm_install.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/kubernetes/validation"
|
||||
)
|
||||
|
||||
type installChartPayload struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Chart string `json:"chart"`
|
||||
Repo string `json:"repo"`
|
||||
Values string `json:"values"`
|
||||
}
|
||||
|
||||
var errChartNameInvalid = errors.New("invalid chart name. " +
|
||||
"Chart name must consist of lower case alphanumeric characters, '-' or '.'," +
|
||||
" and must start and end with an alphanumeric character",
|
||||
)
|
||||
|
||||
// @id HelmInstall
|
||||
// @summary Install Helm Chart
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @param payload body installChartPayload true "Chart details"
|
||||
// @success 201 {object} release.Release "Created"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/kubernetes/helm [post]
|
||||
func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload installChartPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid Helm install payload",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
release, err := handler.installChart(r, payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to install a chart",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return response.JSON(w, release)
|
||||
}
|
||||
|
||||
func (p *installChartPayload) Validate(_ *http.Request) error {
|
||||
var required []string
|
||||
if p.Repo == "" {
|
||||
required = append(required, "repo")
|
||||
}
|
||||
if p.Name == "" {
|
||||
required = append(required, "name")
|
||||
}
|
||||
if p.Namespace == "" {
|
||||
required = append(required, "namespace")
|
||||
}
|
||||
if p.Chart == "" {
|
||||
required = append(required, "chart")
|
||||
}
|
||||
if len(required) > 0 {
|
||||
return fmt.Errorf("required field(s) missing: %s", strings.Join(required, ", "))
|
||||
}
|
||||
|
||||
if errs := validation.IsDNS1123Subdomain(p.Name); len(errs) > 0 {
|
||||
return errChartNameInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*release.Release, error) {
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return nil, httperr.Err
|
||||
}
|
||||
installOpts := options.InstallOptions{
|
||||
Name: p.Name,
|
||||
Chart: p.Chart,
|
||||
Namespace: p.Namespace,
|
||||
Repo: p.Repo,
|
||||
KubernetesClusterAccess: &options.KubernetesClusterAccess{
|
||||
ClusterServerURL: clusterAccess.ClusterServerURL,
|
||||
CertificateAuthorityFile: clusterAccess.CertificateAuthorityFile,
|
||||
AuthToken: clusterAccess.AuthToken,
|
||||
},
|
||||
}
|
||||
|
||||
if p.Values != "" {
|
||||
file, err := os.CreateTemp("", "helm-values")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
_, err = file.WriteString(p.Values)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
installOpts.ValuesFile = file.Name()
|
||||
}
|
||||
|
||||
release, err := handler.helmPackageManager.Install(installOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return release, nil
|
||||
}
|
||||
65
api/http/handler/helm/helm_install_test.go
Normal file
65
api/http/handler/helm/helm_install_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_helmInstall(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
|
||||
is.NoError(err, "error creating endpoint")
|
||||
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
// Install a single chart. We expect to get these values back
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default", Repo: "https://charts.bitnami.com/bitnami"}
|
||||
optdata, err := json.Marshal(options)
|
||||
is.NoError(err)
|
||||
|
||||
t.Run("helmInstall succeeds with admin user", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/1/kubernetes/helm", bytes.NewBuffer(optdata))
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusCreated, rr.Code, "Status should be 201")
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
|
||||
resp := release.Release{}
|
||||
err = json.Unmarshal(body, &resp)
|
||||
is.NoError(err, "response should be json")
|
||||
is.EqualValues(options.Name, resp.Name, "Name doesn't match")
|
||||
is.EqualValues(options.Namespace, resp.Namespace, "Namespace doesn't match")
|
||||
})
|
||||
}
|
||||
64
api/http/handler/helm/helm_list.go
Normal file
64
api/http/handler/helm/helm_list.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id HelmList
|
||||
// @summary List Helm Releases
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @param namespace query string true "specify an optional namespace"
|
||||
// @param filter query string true "specify an optional filter"
|
||||
// @param selector query string true "specify an optional selector"
|
||||
// @success 200 {array} release.ReleaseElement "Success"
|
||||
// @failure 400 "Invalid endpoint identifier"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/kubernetes/helm [get]
|
||||
func (handler *Handler) helmList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
clusterAccess, httperr := handler.getHelmClusterAccess(r)
|
||||
if httperr != nil {
|
||||
return httperr
|
||||
}
|
||||
|
||||
listOpts := options.ListOptions{
|
||||
KubernetesClusterAccess: clusterAccess,
|
||||
}
|
||||
|
||||
params := r.URL.Query()
|
||||
|
||||
// optional namespace. The library defaults to "default"
|
||||
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
if namespace != "" {
|
||||
listOpts.Namespace = namespace
|
||||
}
|
||||
|
||||
// optional filter
|
||||
if filter := params.Get("filter"); filter != "" {
|
||||
listOpts.Filter = filter
|
||||
}
|
||||
|
||||
// optional selector
|
||||
if selector := params.Get("selector"); selector != "" {
|
||||
listOpts.Selector = selector
|
||||
}
|
||||
|
||||
releases, err := handler.helmPackageManager.List(listOpts)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Helm returned an error", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, releases)
|
||||
}
|
||||
60
api/http/handler/helm/helm_list_test.go
Normal file
60
api/http/handler/helm/helm_list_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func Test_helmList(t *testing.T) {
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().CreateEndpoint(&portainer.Endpoint{ID: 1})
|
||||
assert.NoError(t, err, "error creating endpoint")
|
||||
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
assert.NoError(t, err, "error creating a user")
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
// Install a single chart. We expect to get these values back
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
|
||||
h.helmPackageManager.Install(options)
|
||||
|
||||
t.Run("helmList", func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm", nil)
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code, "Status should be 200 OK")
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
|
||||
data := []release.ReleaseElement{}
|
||||
json.Unmarshal(body, &data)
|
||||
if is.Equal(1, len(data), "Expected one chart entry") {
|
||||
is.EqualValues(options.Name, data[0].Name, "Name doesn't match")
|
||||
is.EqualValues(options.Chart, data[0].Chart, "Chart doesn't match")
|
||||
}
|
||||
})
|
||||
}
|
||||
56
api/http/handler/helm/helm_repo_search.go
Normal file
56
api/http/handler/helm/helm_repo_search.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
)
|
||||
|
||||
// @id HelmRepoSearch
|
||||
// @summary Search Helm Charts
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} string "Success"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /templates/helm [get]
|
||||
func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
repo := r.URL.Query().Get("repo")
|
||||
if repo == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `repo` query parameter")}
|
||||
}
|
||||
|
||||
_, err := url.ParseRequestURI(repo)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))}
|
||||
}
|
||||
|
||||
searchOpts := options.SearchRepoOptions{
|
||||
Repo: repo,
|
||||
}
|
||||
|
||||
result, err := libhelm.SearchRepo(searchOpts)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Search failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
51
api/http/handler/helm/helm_repo_search_test.go
Normal file
51
api/http/handler/helm/helm_repo_search_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func Test_helmRepoSearch(t *testing.T) {
|
||||
helper.IntegrationTest(t)
|
||||
is := assert.New(t)
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
|
||||
|
||||
assert.NotNil(t, h, "Handler should not fail")
|
||||
|
||||
repos := []string{"https://charts.bitnami.com/bitnami", "https://portainer.github.io/k8s"}
|
||||
|
||||
for _, repo := range repos {
|
||||
t.Run(repo, func(t *testing.T) {
|
||||
repoUrlEncoded := url.QueryEscape(repo)
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code, "Status should be 200 OK")
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
is.NotEmpty(body, "Body should not be empty")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("fails on invalid URL", func(t *testing.T) {
|
||||
repo := "abc.com"
|
||||
repoUrlEncoded := url.QueryEscape(repo)
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusBadRequest, rr.Code, "Status should be 400 Bad request")
|
||||
})
|
||||
}
|
||||
70
api/http/handler/helm/helm_show.go
Normal file
70
api/http/handler/helm/helm_show.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
)
|
||||
|
||||
// @id HelmShow
|
||||
// @summary Show Helm Chart Information
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm_chart
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @param chart query string true "Chart name"
|
||||
// @param command path string true "chart/values/readme"
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce text/plain
|
||||
// @success 200 {object} string "Success"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Endpoint or ServiceAccount not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /templates/helm/{command} [get]
|
||||
func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
repo := r.URL.Query().Get("repo")
|
||||
if repo == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `repo` query parameter")}
|
||||
}
|
||||
_, err := url.ParseRequestURI(repo)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo))}
|
||||
}
|
||||
|
||||
chart := r.URL.Query().Get("chart")
|
||||
if chart == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Bad request", Err: errors.New("missing `chart` query parameter")}
|
||||
}
|
||||
|
||||
cmd, err := request.RetrieveRouteVariableValue(r, "command")
|
||||
if err != nil {
|
||||
cmd = "all"
|
||||
log.Printf("[DEBUG] [internal,helm] [message: command not provided, defaulting to %s]", cmd)
|
||||
}
|
||||
|
||||
showOptions := options.ShowOptions{
|
||||
OutputFormat: options.ShowOutputFormat(cmd),
|
||||
Chart: chart,
|
||||
Repo: repo,
|
||||
}
|
||||
result, err := handler.helmPackageManager.Show(showOptions)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Unable to show chart",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
47
api/http/handler/helm/helm_show_test.go
Normal file
47
api/http/handler/helm/helm_show_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_helmShow(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
commands := map[string]string{
|
||||
"values": test.MockDataValues,
|
||||
"chart": test.MockDataChart,
|
||||
"readme": test.MockDataReadme,
|
||||
}
|
||||
|
||||
for cmd, expect := range commands {
|
||||
t.Run(cmd, func(t *testing.T) {
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
repoUrlEncoded := url.QueryEscape("https://charts.bitnami.com/bitnami")
|
||||
chart := "nginx"
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/templates/helm/%s?repo=%s&chart=%s", cmd, repoUrlEncoded, chart), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(rr.Code, http.StatusOK, "Status should be 200 OK")
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
is.EqualValues(string(body), expect, "Unexpected search response")
|
||||
})
|
||||
}
|
||||
}
|
||||
127
api/http/handler/helm/user_helm_repos.go
Normal file
127
api/http/handler/helm/user_helm_repos.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type helmUserRepositoryResponse struct {
|
||||
GlobalRepository string `json:"GlobalRepository"`
|
||||
UserRepositories []portainer.HelmUserRepository `json:"UserRepositories"`
|
||||
}
|
||||
|
||||
type addHelmRepoUrlPayload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
|
||||
return libhelm.ValidateHelmRepositoryURL(p.URL)
|
||||
}
|
||||
|
||||
// @id HelmUserRepositoryCreate
|
||||
// @summary Create a user helm repository
|
||||
// @description Create a user helm repository.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @param payload body addHelmRepoUrlPayload true "Helm Repository"
|
||||
// @success 200 {object} portainer.HelmUserRepository "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/kubernetes/helm/repositories [post]
|
||||
func (handler *Handler) userCreateHelmRepo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
userID := portainer.UserID(tokenData.ID)
|
||||
|
||||
p := new(addHelmRepoUrlPayload)
|
||||
err = request.DecodeAndValidateJSONPayload(r, p)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "Invalid Helm repository URL",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// lowercase, remove trailing slash
|
||||
p.URL = strings.TrimSuffix(strings.ToLower(p.URL), "/")
|
||||
|
||||
records, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to access the DataStore", err}
|
||||
}
|
||||
|
||||
// check if repo already exists - by doing case insensitive comparison
|
||||
for _, record := range records {
|
||||
if strings.EqualFold(record.URL, p.URL) {
|
||||
errMsg := "Helm repo already registered for user"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
}
|
||||
|
||||
record := portainer.HelmUserRepository{
|
||||
UserID: userID,
|
||||
URL: p.URL,
|
||||
}
|
||||
|
||||
err = handler.dataStore.HelmUserRepository().CreateHelmUserRepository(&record)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save a user Helm repository URL", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, record)
|
||||
}
|
||||
|
||||
// @id HelmUserRepositoriesList
|
||||
// @summary List a users helm repositories
|
||||
// @description Inspect a user helm repositories.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "User identifier"
|
||||
// @success 200 {object} helmUserRepositoryResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/kubernetes/helm/repositories [get]
|
||||
func (handler *Handler) userGetHelmRepos(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
}
|
||||
userID := portainer.UserID(tokenData.ID)
|
||||
|
||||
settings, err := handler.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
userRepos, err := handler.dataStore.HelmUserRepository().HelmUserRepositoryByUserID(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user Helm repositories", err}
|
||||
}
|
||||
|
||||
resp := helmUserRepositoryResponse{
|
||||
GlobalRepository: settings.HelmRepositoryURL,
|
||||
UserRepositories: userRepos,
|
||||
}
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
@@ -56,12 +56,12 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
|
||||
endpoint, err := middlewares.FetchEndpoint(request)
|
||||
if err != nil {
|
||||
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an endpoint on request context", err)
|
||||
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
errMessage := "Endpoint is not a kubernetes endpoint"
|
||||
errMessage := "Environment is not a kubernetes environment"
|
||||
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package kubernetes
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -10,8 +12,6 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
kcli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// @id GetKubernetesConfig
|
||||
@@ -33,19 +33,19 @@ import (
|
||||
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// @id getKubernetesNodesLimits
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @success 200 {object} K8sNodesLimits "Success"
|
||||
// @success 200 {object} portainer.K8sNodesLimits "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 403 "Permission denied"
|
||||
@@ -28,14 +29,14 @@ import (
|
||||
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
|
||||
@@ -36,7 +36,7 @@ func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
|
||||
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err}
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment on request context", err}
|
||||
}
|
||||
|
||||
namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||
|
||||
@@ -22,6 +22,8 @@ type publicSettingsResponse struct {
|
||||
OAuthLogoutURI string `json:"OAuthLogoutURI" example:"https://gitlab.com/oauth/logout"`
|
||||
// Whether telemetry is enabled
|
||||
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
|
||||
// The expiry of a Kubeconfig
|
||||
KubeconfigExpiry string `example:"24h" default:"0"`
|
||||
}
|
||||
|
||||
// @id SettingsPublic
|
||||
@@ -49,6 +51,7 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||
AuthenticationMethod: appSettings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: appSettings.EnableTelemetry,
|
||||
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
||||
}
|
||||
//if OAuth authentication is on, compose the related fields from application settings
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -36,6 +38,8 @@ type settingsUpdatePayload struct {
|
||||
KubeconfigExpiry *string `example:"24h" default:"0"`
|
||||
// Whether telemetry is enabled
|
||||
EnableTelemetry *bool `example:"false"`
|
||||
// Helm repository URL
|
||||
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -48,6 +52,12 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
|
||||
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid Helm repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
}
|
||||
if payload.UserSessionTimeout != nil {
|
||||
_, err := time.ParseDuration(*payload.UserSessionTimeout)
|
||||
if err != nil {
|
||||
@@ -101,6 +111,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.TemplatesURL = *payload.TemplatesURL
|
||||
}
|
||||
|
||||
if payload.HelmRepositoryURL != nil {
|
||||
settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
settings.BlackListedLabels = payload.BlackListedLabels
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user