Compare commits
12 Commits
chore/remo
...
chore/EE-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4502236690 | ||
|
|
2874a79279 | ||
|
|
8574dd2371 | ||
|
|
53eb5aa1ee | ||
|
|
eb8644330e | ||
|
|
8663de580a | ||
|
|
34298d96c5 | ||
|
|
9d103ffbeb | ||
|
|
5847c2b8ef | ||
|
|
a09fe7e10c | ||
|
|
5640cce4d6 | ||
|
|
00bbf4ac63 |
@@ -6,9 +6,13 @@ import (
|
||||
)
|
||||
|
||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
||||
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
|
||||
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
return
|
||||
}
|
||||
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
tunnel := service.getTunnelDetails(endpoint.ID)
|
||||
|
||||
existingJobIndex := -1
|
||||
for idx, existingJob := range tunnel.Jobs {
|
||||
@@ -24,7 +28,7 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
|
||||
tunnel.Jobs[existingJobIndex] = *edgeJob
|
||||
}
|
||||
|
||||
cache.Del(endpointID)
|
||||
cache.Del(endpoint.ID)
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package chisel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
@@ -66,6 +67,10 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portai
|
||||
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
|
||||
}
|
||||
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
|
||||
@@ -82,6 +82,7 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
EdgeStackService: store.EdgeStackService,
|
||||
EdgeJobService: store.EdgeJobService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ func (m *Migrator) migrateDBVersionToDB80() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateExistingEndpointsToNotDetectStorageAPIForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,6 +44,27 @@ func (m *Migrator) updateExistingEndpointsToNotDetectMetricsAPIForDB80() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateExistingEndpointsToNotDetectStorageAPIForDB80() error {
|
||||
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB80() error {
|
||||
log.Info().Msg("transfer type field to details field for edge stack status")
|
||||
|
||||
|
||||
36
api/datastore/migrator/migrate_dbversion81.go
Normal file
36
api/datastore/migrator/migrate_dbversion81.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB81() error {
|
||||
return m.updateEdgeStackStatusForDB81()
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB81() error {
|
||||
log.Info().Msg("clean up deleted endpoints from edge jobs")
|
||||
|
||||
edgeJobs, err := m.edgeJobService.EdgeJobs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeJob := range edgeJobs {
|
||||
for endpointId := range edgeJob.Endpoints {
|
||||
_, err := m.endpointService.Endpoint(endpointId)
|
||||
if err == portainerDsErrors.ErrObjectNotFound {
|
||||
delete(edgeJob.Endpoints, endpointId)
|
||||
|
||||
err = m.edgeJobService.UpdateEdgeJob(edgeJob.ID, &edgeJob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package migrator
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices/edgejob"
|
||||
"github.com/portainer/portainer/api/dataservices/edgestack"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
@@ -56,6 +57,7 @@ type (
|
||||
authorizationService *authorization.Service
|
||||
dockerhubService *dockerhub.Service
|
||||
edgeStackService *edgestack.Service
|
||||
edgeJobService *edgejob.Service
|
||||
}
|
||||
|
||||
// MigratorParameters represents the required parameters to create a new Migrator instance.
|
||||
@@ -81,6 +83,7 @@ type (
|
||||
AuthorizationService *authorization.Service
|
||||
DockerhubService *dockerhub.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
}
|
||||
)
|
||||
|
||||
@@ -108,6 +111,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
||||
authorizationService: parameters.AuthorizationService,
|
||||
dockerhubService: parameters.DockerhubService,
|
||||
edgeStackService: parameters.EdgeStackService,
|
||||
edgeJobService: parameters.EdgeJobService,
|
||||
}
|
||||
|
||||
migrator.initMigrations()
|
||||
@@ -205,7 +209,7 @@ func (m *Migrator) initMigrations() {
|
||||
m.addMigrations("2.16", m.migrateDBVersionToDB70)
|
||||
m.addMigrations("2.16.1", m.migrateDBVersionToDB71)
|
||||
m.addMigrations("2.17", m.migrateDBVersionToDB80)
|
||||
m.addMigrations("2.18")
|
||||
m.addMigrations("2.18", m.migrateDBVersionToDB81)
|
||||
|
||||
// Add new migrations below...
|
||||
// One function per migration, each versions migration funcs in the same file.
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
"UseServerMetrics": false
|
||||
},
|
||||
"Flags": {
|
||||
"IsServerMetricsDetected": false
|
||||
"IsServerMetricsDetected": false,
|
||||
"IsServerStorageDetected": false
|
||||
},
|
||||
"Snapshots": []
|
||||
},
|
||||
@@ -934,6 +935,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
continue
|
||||
}
|
||||
|
||||
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpointID, edgeJobs, operation)
|
||||
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) er
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID, endpointID portainer.EndpointID, edgeJobs []portainer.EdgeJob, operation string) error {
|
||||
func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID, endpoint *portainer.Endpoint, edgeJobs []portainer.EdgeJob, operation string) error {
|
||||
for _, edgeJob := range edgeJobs {
|
||||
if !slices.Contains(edgeJob.EdgeGroups, edgeGroupID) {
|
||||
continue
|
||||
@@ -208,9 +208,9 @@ func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID
|
||||
|
||||
switch operation {
|
||||
case "add":
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, &edgeJob)
|
||||
case "remove":
|
||||
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpointID, edgeJob.ID)
|
||||
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpoint.ID, edgeJob.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,7 +274,12 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
}
|
||||
|
||||
for endpointID := range endpointsMap {
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
}
|
||||
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
|
||||
@@ -67,7 +67,12 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request
|
||||
return httperror.InternalServerError("Unable to clear log file from disk", err)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
|
||||
@@ -75,7 +75,7 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque
|
||||
return httperror.BadRequest("Async Edge Endpoints are not supported in Portainer CE", nil)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -212,7 +212,12 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
maps.Copy(endpointsFromGroupsToAddMap, edgeJob.Endpoints)
|
||||
|
||||
for endpointID := range endpointsFromGroupsToAddMap {
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
}
|
||||
|
||||
for endpointID := range endpointsToRemove {
|
||||
|
||||
@@ -622,7 +622,7 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data.EdgeGroups, payload.EdgeGroups) {
|
||||
t.Fatalf("expected EdgeGroups to be equal")
|
||||
t.Fatal("expected EdgeGroups to be equal")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, edgeJob)
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist edge job changes to the database", err)
|
||||
|
||||
@@ -416,7 +416,7 @@ func TestEdgeJobsResponse(t *testing.T) {
|
||||
Version: 57,
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
|
||||
handler.ReverseTunnelService.AddEdgeJob(&endpoint, &edgeJob)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -55,13 +55,13 @@ const (
|
||||
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid environment 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 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)")
|
||||
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)
|
||||
|
||||
@@ -74,7 +74,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
var tagIDs []portainer.TagID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid TagIds parameter")
|
||||
return errors.New("invalid TagIds parameter")
|
||||
}
|
||||
payload.TagIDs = tagIDs
|
||||
if payload.TagIDs == nil {
|
||||
@@ -93,7 +93,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
if !payload.TLSSkipVerify {
|
||||
caCert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
|
||||
if err != nil {
|
||||
return errors.New("Invalid CA certificate file. Ensure that the file is uploaded correctly")
|
||||
return errors.New("invalid CA certificate file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSCACertFile = caCert
|
||||
}
|
||||
@@ -101,13 +101,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
if !payload.TLSSkipClientVerify {
|
||||
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
|
||||
if err != nil {
|
||||
return errors.New("Invalid certificate file. Ensure that the file is uploaded correctly")
|
||||
return errors.New("invalid certificate file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSCertFile = cert
|
||||
|
||||
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
|
||||
if err != nil {
|
||||
return errors.New("Invalid key file. Ensure that the file is uploaded correctly")
|
||||
return errors.New("invalid key file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSKeyFile = key
|
||||
}
|
||||
@@ -117,19 +117,19 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
case azureEnvironment:
|
||||
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Azure application ID")
|
||||
return errors.New("invalid Azure application ID")
|
||||
}
|
||||
payload.AzureApplicationID = azureApplicationID
|
||||
|
||||
azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Azure tenant ID")
|
||||
return errors.New("invalid Azure tenant ID")
|
||||
}
|
||||
payload.AzureTenantID = azureTenantID
|
||||
|
||||
azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Azure authentication key")
|
||||
return errors.New("invalid Azure authentication key")
|
||||
}
|
||||
payload.AzureAuthenticationKey = azureAuthenticationKey
|
||||
|
||||
@@ -146,7 +146,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
default:
|
||||
endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid environment URL")
|
||||
return errors.New("invalid environment URL")
|
||||
}
|
||||
payload.URL = endpointURL
|
||||
|
||||
@@ -157,7 +157,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
gpus := make([]portainer.Pair, 0)
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Gpus parameter")
|
||||
return errors.New("invalid Gpus parameter")
|
||||
}
|
||||
payload.Gpus = gpus
|
||||
|
||||
@@ -195,6 +195,9 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
// @param AzureAuthenticationKey formData string false "Azure authentication key. Required if environment(endpoint) type is set to 3"
|
||||
// @param TagIDs formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
|
||||
// @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)"
|
||||
// @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel"
|
||||
// @param IsEdgeDevice formData bool false "Is Edge Device"
|
||||
// @param Gpus formData array false "List of GPUs"
|
||||
// @success 200 {object} portainer.Endpoint "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
|
||||
@@ -61,6 +61,15 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
)
|
||||
}
|
||||
|
||||
isServerStorageDetected := endpoint.Kubernetes.Flags.IsServerStorageDetected
|
||||
if !isServerStorageDetected && handler.K8sClientFactory != nil {
|
||||
endpointutils.InitialStorageDetection(
|
||||
endpoint,
|
||||
handler.DataStore.Endpoint(),
|
||||
handler.K8sClientFactory,
|
||||
)
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@ func LoadEdgeJobs(dataStore dataservices.DataStore, reverseTunnelService portain
|
||||
|
||||
for _, edgeJob := range edgeJobs {
|
||||
for endpointID := range edgeJob.Endpoints {
|
||||
reverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
|
||||
endpoint, err := dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reverseTunnelService.AddEdgeJob(endpoint, &edgeJob)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package endpointutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
@@ -127,17 +129,21 @@ func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||
}
|
||||
}
|
||||
|
||||
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) error {
|
||||
cli, err := factory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to create Kubernetes client for initial storage detection")
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
storage, err := cli.GetStorage()
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to fetch storage classes: leaving storage classes disabled")
|
||||
return
|
||||
return err
|
||||
}
|
||||
if len(storage) == 0 {
|
||||
log.Info().Err(err).Msg("zero storage classes found: they may be still building, retrying in 30 seconds")
|
||||
return fmt.Errorf("zero storage classes found: they may be still building, retrying in 30 seconds")
|
||||
}
|
||||
endpoint.Kubernetes.Configuration.StorageClasses = storage
|
||||
err = endpointService.UpdateEndpoint(
|
||||
@@ -146,6 +152,23 @@ func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||
)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to enable storage class inside the database")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
log.Info().Msg("attempting to detect storage classes in the cluster")
|
||||
err := storageDetect(endpoint, endpointService, factory)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Err(err).Msg("error while detecting storage classes")
|
||||
go func() {
|
||||
// Retry after 30 seconds if the initial detection failed.
|
||||
log.Info().Msg("retrying storage detection in 30 seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
err := storageDetect(endpoint, endpointService, factory)
|
||||
log.Err(err).Msg("final error while detecting storage classes")
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -567,6 +567,7 @@ type (
|
||||
|
||||
KubernetesFlags struct {
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
}
|
||||
|
||||
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
|
||||
@@ -1454,7 +1455,7 @@ type (
|
||||
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
|
||||
GetTunnelDetails(endpointID EndpointID) TunnelDetails
|
||||
GetActiveTunnel(endpoint *Endpoint) (TunnelDetails, error)
|
||||
AddEdgeJob(endpointID EndpointID, edgeJob *EdgeJob)
|
||||
AddEdgeJob(endpoint *Endpoint, edgeJob *EdgeJob)
|
||||
RemoveEdgeJob(edgeJobID EdgeJobID)
|
||||
RemoveEdgeJobFromEndpoint(endpointID EndpointID, edgeJobID EdgeJobID)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { compose, kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
||||
|
||||
export default class EdgeStackDeploymentTypeSelectorController {
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
this.deploymentOptions = [
|
||||
{
|
||||
...compose,
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
...kubernetes,
|
||||
value: 1,
|
||||
disabled: () => {
|
||||
return this.hasDockerEndpoint();
|
||||
},
|
||||
tooltip: () => {
|
||||
return this.hasDockerEndpoint() ? 'Cannot use this option with Edge Docker endpoints' : '';
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<div class="col-sm-12 form-section-title"> Deployment type </div>
|
||||
<box-selector radio-name="'deploymentType'" value="$ctrl.value" options="$ctrl.deploymentOptions" on-change="($ctrl.onChange)"></box-selector>
|
||||
@@ -1,15 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import controller from './edge-stack-deployment-type-selector.controller.js';
|
||||
|
||||
export const edgeStackDeploymentTypeSelector = {
|
||||
templateUrl: './edge-stack-deployment-type-selector.html',
|
||||
controller,
|
||||
|
||||
bindings: {
|
||||
value: '<',
|
||||
onChange: '<',
|
||||
hasDockerEndpoint: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.edge').component('edgeStackDeploymentTypeSelector', edgeStackDeploymentTypeSelector);
|
||||
@@ -4,30 +4,42 @@
|
||||
<div class="col-sm-12">
|
||||
<edge-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!$ctrl.validateEndpointsForDeployment()">
|
||||
<div class="col-sm-12">
|
||||
<div class="small text-muted space-right text-warning">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
One or more of the selected Edge group contains Edge Docker endpoints that cannot be used with a Kubernetes Edge stack.
|
||||
</div>
|
||||
</div>
|
||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.model.DeploymentType === undefined">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> There are no available deployment types when there is more than one type of environment in your edge group
|
||||
selection (e.g. Kubernetes and Docker environments). Please select edge groups that have environments of the same type.
|
||||
</p>
|
||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.model.DeploymentType === $ctrl.EditorType.Compose && $ctrl.hasKubeEndpoint()">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Edge groups with kubernetes environments no longer support compose deployment types in Portainer. Please select
|
||||
edge groups that only have docker environments when using compose deployment types.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<edge-stack-deployment-type-selector
|
||||
allow-kube-to-select-compose="$ctrl.allowKubeToSelectCompose"
|
||||
value="$ctrl.model.DeploymentType"
|
||||
has-docker-endpoint="$ctrl.hasDockerEndpoint"
|
||||
has-docker-endpoint="$ctrl.hasDockerEndpoint()"
|
||||
has-kube-endpoint="$ctrl.hasKubeEndpoint()"
|
||||
on-change="($ctrl.onChangeDeploymentType)"
|
||||
read-only="$ctrl.state.readOnlyCompose"
|
||||
></edge-stack-deployment-type-selector>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.model.DeploymentType === 0 && $ctrl.hasKubeEndpoint()">
|
||||
<div class="col-sm-12">
|
||||
<div class="small text-muted space-right">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not all the
|
||||
Compose format options are supported by Kompose at the moment.
|
||||
</div>
|
||||
<div class="flex gap-1 text-muted small" ng-show="!$ctrl.model.DeploymentType && $ctrl.hasKubeEndpoint()">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'" class-name="'!mt-1'"></pr-icon>
|
||||
<div>
|
||||
<p>
|
||||
Portainer no longer supports <a href="https://docs.docker.com/compose/compose-file/" target="_blank">docker-compose</a> format manifests for Kubernetes deployments, and we
|
||||
have removed the <a href="https://kompose.io/" target="_blank">Kompose</a> conversion tool which enables this. The reason for this is because Kompose now poses a security
|
||||
risk, since it has a number of Common Vulnerabilities and Exposures (CVEs).
|
||||
</p>
|
||||
<p
|
||||
>Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and new pull requests
|
||||
to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.</p
|
||||
>
|
||||
<p>
|
||||
We advise installing your own instance of Kompose in a sandbox environment, performing conversions of your Docker Compose files to Kubernetes manifests and using those
|
||||
manifests to set up applications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +50,7 @@
|
||||
identifier="compose-editor"
|
||||
placeholder="# Define or paste the content of your docker compose file here"
|
||||
on-change="($ctrl.onChangeComposeConfig)"
|
||||
read-only="$ctrl.hasKubeEndpoint()"
|
||||
>
|
||||
<editor-description>
|
||||
<div>
|
||||
@@ -82,8 +95,8 @@
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.actionInProgress || !$ctrl.isFormValid()"
|
||||
class="btn btn-primary btn-sm !ml-0"
|
||||
ng-disabled="$ctrl.actionInProgress || !$ctrl.isFormValid() || (!$ctrl.model.DeploymentType && $ctrl.hasKubeEndpoint())"
|
||||
ng-click="$ctrl.submitAction()"
|
||||
button-spinner="$ctrl.actionInProgress"
|
||||
>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||
import { EditorType } from '@/react/edge/edge-stacks/types';
|
||||
|
||||
import { getValidEditorTypes } from '@/react/edge/edge-stacks/utils';
|
||||
export class EditEdgeStackFormController {
|
||||
/* @ngInject */
|
||||
constructor($scope) {
|
||||
this.$scope = $scope;
|
||||
this.state = {
|
||||
endpointTypes: [],
|
||||
readOnlyCompose: false,
|
||||
};
|
||||
|
||||
this.fileContents = {
|
||||
@@ -26,6 +27,7 @@ export class EditEdgeStackFormController {
|
||||
this.removeLineBreaks = this.removeLineBreaks.bind(this);
|
||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||
this.onChangeUseManifestNamespaces = this.onChangeUseManifestNamespaces.bind(this);
|
||||
this.selectValidDeploymentType = this.selectValidDeploymentType.bind(this);
|
||||
}
|
||||
|
||||
onChangeUseManifestNamespaces(value) {
|
||||
@@ -45,8 +47,9 @@ export class EditEdgeStackFormController {
|
||||
onChangeGroups(groups) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.model.EdgeGroups = groups;
|
||||
|
||||
this.checkEndpointTypes(groups);
|
||||
this.setEnvironmentTypesInSelection(groups);
|
||||
this.selectValidDeploymentType();
|
||||
this.state.readOnlyCompose = this.hasKubeEndpoint();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,11 +57,19 @@ export class EditEdgeStackFormController {
|
||||
return this.model.EdgeGroups.length && this.model.StackFileContent && this.validateEndpointsForDeployment();
|
||||
}
|
||||
|
||||
checkEndpointTypes(groups) {
|
||||
setEnvironmentTypesInSelection(groups) {
|
||||
const edgeGroups = groups.map((id) => this.edgeGroups.find((e) => e.Id === id));
|
||||
this.state.endpointTypes = edgeGroups.flatMap((group) => group.EndpointTypes);
|
||||
}
|
||||
|
||||
selectValidDeploymentType() {
|
||||
const validTypes = getValidEditorTypes(this.state.endpointTypes, this.allowKubeToSelectCompose);
|
||||
|
||||
if (!validTypes.includes(this.model.DeploymentType)) {
|
||||
this.onChangeDeploymentType(validTypes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
removeLineBreaks(value) {
|
||||
return value.replace(/(\r\n|\n|\r)/gm, '');
|
||||
}
|
||||
@@ -81,9 +92,10 @@ export class EditEdgeStackFormController {
|
||||
}
|
||||
|
||||
onChangeDeploymentType(deploymentType) {
|
||||
this.model.DeploymentType = deploymentType;
|
||||
|
||||
this.model.StackFileContent = this.fileContents[deploymentType];
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.model.DeploymentType = deploymentType;
|
||||
this.model.StackFileContent = this.fileContents[deploymentType];
|
||||
});
|
||||
}
|
||||
|
||||
validateEndpointsForDeployment() {
|
||||
@@ -91,6 +103,14 @@ export class EditEdgeStackFormController {
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.checkEndpointTypes(this.model.EdgeGroups);
|
||||
this.setEnvironmentTypesInSelection(this.model.EdgeGroups);
|
||||
this.fileContents[this.model.DeploymentType] = this.model.StackFileContent;
|
||||
|
||||
// allow kube to view compose if it's an existing kube compose stack
|
||||
const initiallyContainsKubeEnv = this.hasKubeEndpoint();
|
||||
const isComposeStack = this.model.DeploymentType === 0;
|
||||
this.allowKubeToSelectCompose = initiallyContainsKubeEnv && isComposeStack;
|
||||
this.state.readOnlyCompose = this.allowKubeToSelectCompose;
|
||||
this.selectValidDeploymentType();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
|
||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.edge.react.components', [])
|
||||
@@ -43,4 +44,14 @@ export const componentsModule = angular
|
||||
'readonly',
|
||||
'fieldSettings',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'edgeStackDeploymentTypeSelector',
|
||||
r2a(withReactQuery(EdgeStackDeploymentTypeSelector), [
|
||||
'value',
|
||||
'onChange',
|
||||
'hasDockerEndpoint',
|
||||
'hasKubeEndpoint',
|
||||
'allowKubeToSelectCompose',
|
||||
])
|
||||
).name;
|
||||
|
||||
@@ -154,6 +154,7 @@ export class EdgeJobController {
|
||||
this.tags = tags;
|
||||
|
||||
this.edgeJob.EdgeGroups = this.edgeJob.EdgeGroups ? this.edgeJob.EdgeGroups : [];
|
||||
this.edgeJob.Endpoints = this.edgeJob.Endpoints ? this.edgeJob.Endpoints : [];
|
||||
|
||||
if (results.length > 0) {
|
||||
const endpointIds = _.map(results, (result) => result.EndpointId);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EditorType } from '@/react/edge/edge-stacks/types';
|
||||
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||
import { getValidEditorTypes } from '@/react/edge/edge-stacks/utils';
|
||||
|
||||
export default class CreateEdgeStackViewController {
|
||||
/* @ngInject */
|
||||
@@ -43,6 +45,7 @@ export default class CreateEdgeStackViewController {
|
||||
this.createStackFromGitRepository = this.createStackFromGitRepository.bind(this);
|
||||
this.onChangeGroups = this.onChangeGroups.bind(this);
|
||||
this.hasDockerEndpoint = this.hasDockerEndpoint.bind(this);
|
||||
this.hasKubeEndpoint = this.hasKubeEndpoint.bind(this);
|
||||
this.onChangeDeploymentType = this.onChangeDeploymentType.bind(this);
|
||||
}
|
||||
|
||||
@@ -134,18 +137,23 @@ export default class CreateEdgeStackViewController {
|
||||
checkIfEndpointTypes(groups) {
|
||||
const edgeGroups = groups.map((id) => this.edgeGroups.find((e) => e.Id === id));
|
||||
this.state.endpointTypes = edgeGroups.flatMap((group) => group.EndpointTypes);
|
||||
this.selectValidDeploymentType();
|
||||
}
|
||||
|
||||
if (this.hasDockerEndpoint() && this.formValues.DeploymentType == 1) {
|
||||
this.onChangeDeploymentType(0);
|
||||
selectValidDeploymentType() {
|
||||
const validTypes = getValidEditorTypes(this.state.endpointTypes);
|
||||
|
||||
if (!validTypes.includes(this.formValues.DeploymentType)) {
|
||||
this.onChangeDeploymentType(validTypes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
hasKubeEndpoint() {
|
||||
return this.state.endpointTypes.includes(7);
|
||||
return this.state.endpointTypes.includes(PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment);
|
||||
}
|
||||
|
||||
hasDockerEndpoint() {
|
||||
return this.state.endpointTypes.includes(4);
|
||||
return this.state.endpointTypes.includes(PortainerEndpointTypes.EdgeAgentOnDockerEnvironment);
|
||||
}
|
||||
|
||||
validateForm(method) {
|
||||
@@ -217,9 +225,11 @@ export default class CreateEdgeStackViewController {
|
||||
}
|
||||
|
||||
onChangeDeploymentType(deploymentType) {
|
||||
this.formValues.DeploymentType = deploymentType;
|
||||
this.state.Method = 'editor';
|
||||
this.formValues.StackFileContent = '';
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.DeploymentType = deploymentType;
|
||||
this.state.Method = 'editor';
|
||||
this.formValues.StackFileContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
formIsInvalid() {
|
||||
|
||||
@@ -39,24 +39,19 @@
|
||||
<div ng-if="$ctrl.noGroups" class="col-sm-12 small text-muted">
|
||||
No Edge groups are available. Head over to the <a ui-sref="edge.groups">Edge groups view</a> to create one.
|
||||
</div>
|
||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.formValues.DeploymentType === undefined">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> There are no available deployment types when there is more than one type of environment in your edge
|
||||
group selection (e.g. Kubernetes and Docker environments). Please select edge groups that have environments of the same type.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<edge-stack-deployment-type-selector
|
||||
value="$ctrl.formValues.DeploymentType"
|
||||
has-docker-endpoint="$ctrl.hasDockerEndpoint"
|
||||
has-docker-endpoint="$ctrl.hasDockerEndpoint()"
|
||||
has-kube-endpoint="$ctrl.hasKubeEndpoint()"
|
||||
on-change="($ctrl.onChangeDeploymentType)"
|
||||
></edge-stack-deployment-type-selector>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<div class="small text-muted space-right" ng-if="$ctrl.formValues.DeploymentType === 0 && $ctrl.hasKubeEndpoint()">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not all
|
||||
the Compose format options are supported by Kompose at the moment.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edge-stacks-docker-compose-form
|
||||
ng-if="$ctrl.formValues.DeploymentType == $ctrl.EditorType.Compose"
|
||||
form-values="$ctrl.formValues"
|
||||
|
||||
@@ -59,7 +59,11 @@ export class EditEdgeStackViewController {
|
||||
}
|
||||
|
||||
async uiCanExit() {
|
||||
if (this.formValues.StackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== this.oldFileContent.replace(/(\r\n|\n|\r)/gm, '') && this.state.isEditorDirty) {
|
||||
if (
|
||||
this.formValues.StackFileContent &&
|
||||
this.formValues.StackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== this.oldFileContent.replace(/(\r\n|\n|\r)/gm, '') &&
|
||||
this.state.isEditorDirty
|
||||
) {
|
||||
return this.ModalService.confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const KubernetesDeployManifestTypes = Object.freeze({
|
||||
KUBERNETES: 1,
|
||||
COMPOSE: 2,
|
||||
});
|
||||
|
||||
export const KubernetesDeployBuildMethods = Object.freeze({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<page-header
|
||||
ng-if="!ctrl.state.isEdit"
|
||||
ng-if="!ctrl.state.isEdit && !ctrl.stack.IsComposeFormat && ctrl.state.viewReady"
|
||||
title="'Create application'"
|
||||
breadcrumbs="[
|
||||
{ label:'Applications', link:'kubernetes.applications' },
|
||||
@@ -10,7 +10,7 @@
|
||||
</page-header>
|
||||
|
||||
<page-header
|
||||
ng-if="ctrl.state.isEdit"
|
||||
ng-if="ctrl.state.isEdit && !ctrl.stack.IsComposeFormat && ctrl.state.viewReady"
|
||||
title="'Edit application'"
|
||||
breadcrumbs="[
|
||||
{ label:'Namespaces', link:'kubernetes.resourcePools' },
|
||||
@@ -31,6 +31,28 @@
|
||||
>
|
||||
</page-header>
|
||||
|
||||
<page-header
|
||||
ng-if="ctrl.stack.IsComposeFormat"
|
||||
title="'View application'"
|
||||
breadcrumbs="[
|
||||
{ label:'Namespaces', link:'kubernetes.resourcePools' },
|
||||
{
|
||||
label:ctrl.application.ResourcePool,
|
||||
link: 'kubernetes.resourcePools.resourcePool',
|
||||
linkParams:{ id: ctrl.application.ResourcePool }
|
||||
},
|
||||
{ label:'Applications', link:'kubernetes.applications' },
|
||||
{
|
||||
label:ctrl.application.Name,
|
||||
link: 'kubernetes.applications.application',
|
||||
linkParams:{ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool }
|
||||
},
|
||||
'View',
|
||||
]"
|
||||
reload="true"
|
||||
>
|
||||
</page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row kubernetes-create">
|
||||
@@ -88,6 +110,7 @@
|
||||
|
||||
<!-- #region web editor -->
|
||||
<web-editor-form
|
||||
read-only="ctrl.stack.IsComposeFormat"
|
||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT"
|
||||
value="ctrl.stackFileContent"
|
||||
yml="true"
|
||||
@@ -96,27 +119,24 @@
|
||||
on-change="(ctrl.onChangeFileContent)"
|
||||
>
|
||||
<editor-description>
|
||||
<span class="text-muted small" ng-show="ctrl.stack.IsComposeFormat">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||
<span>
|
||||
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that
|
||||
not all the Compose format options are supported by Kompose at the moment.
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
You can get more information about Compose file format in the
|
||||
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
<p
|
||||
>In a forthcoming Portainer release, we plan to remove support for docker-compose format manifests for Kubernetes deployments, and the Kompose conversion tool
|
||||
which enables this. The reason for this is because Kompose now poses a security risk, since it has a number of Common Vulnerabilities and Exposures (CVEs).</p
|
||||
>
|
||||
<p
|
||||
>Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and new
|
||||
pull requests to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.</p
|
||||
>
|
||||
</span>
|
||||
<div class="flex gap-1 text-muted small" ng-show="ctrl.stack.IsComposeFormat">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'" class-name="'!mt-1'"></pr-icon>
|
||||
<div>
|
||||
<p>
|
||||
Portainer no longer supports <a href="https://docs.docker.com/compose/compose-file/" target="_blank">docker-compose</a> format manifests for Kubernetes
|
||||
deployments, and we have removed the <a href="https://kompose.io/" target="_blank">Kompose</a> conversion tool which enables this. The reason for this is
|
||||
because Kompose now poses a security risk, since it has a number of Common Vulnerabilities and Exposures (CVEs).
|
||||
</p>
|
||||
<p
|
||||
>Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and
|
||||
new pull requests to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.</p
|
||||
>
|
||||
<p>
|
||||
We advise installing your own instance of Kompose in a sandbox environment, performing conversions of your Docker Compose files to Kubernetes manifests and
|
||||
using those manifests to set up applications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-muted small" ng-show="!ctrl.stack.IsComposeFormat">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
@@ -1345,9 +1365,9 @@
|
||||
<!-- kubernetes summary for external application -->
|
||||
<kubernetes-summary-view ng-if="ctrl.isExternalApplication()" form-values="ctrl.formValues" old-form-values="ctrl.savedFormValues"></kubernetes-summary-view>
|
||||
<!-- kubernetes summary for external application -->
|
||||
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT"> Actions </div>
|
||||
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
|
||||
<!-- #region ACTIONS -->
|
||||
<div class="form-group">
|
||||
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
style="margin-left: 0"
|
||||
data-cy="k8sAppDetail-editAppButton"
|
||||
>
|
||||
<pr-icon icon="'pencil'" class="mr-1"></pr-icon>Edit this application
|
||||
<pr-icon icon="'pencil'" class="mr-1"></pr-icon>{{ ctrl.stack.IsComposeFormat ? 'View this application' : 'Edit this application' }}
|
||||
</button>
|
||||
<button
|
||||
authorization="K8sApplicationDetailsW"
|
||||
|
||||
@@ -323,6 +323,9 @@ class KubernetesApplicationController {
|
||||
this.KubernetesNodeService.get(),
|
||||
]);
|
||||
this.application = application;
|
||||
if (this.application.StackId) {
|
||||
this.stack = await this.StackService.stack(application.StackId);
|
||||
}
|
||||
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
|
||||
this.formValues.Note = this.application.Note;
|
||||
this.formValues.Services = this.application.Services;
|
||||
|
||||
@@ -116,20 +116,7 @@
|
||||
placeholder="# Define or paste the content of your manifest file here"
|
||||
>
|
||||
<editor-description>
|
||||
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||
<span>
|
||||
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary
|
||||
that not all the Compose format options are supported by Kompose at the moment.
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
You can get more information about Compose file format in the
|
||||
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
</span>
|
||||
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
|
||||
|
||||
@@ -7,9 +7,8 @@ import PortainerError from '@/portainer/error';
|
||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { compose, kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
||||
import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
||||
import { editor, git, template, url } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { getPublicSettings } from '@/react/portainer/settings/settings.service';
|
||||
|
||||
class KubernetesDeployController {
|
||||
/* @ngInject */
|
||||
@@ -339,16 +338,6 @@ class KubernetesDeployController {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const publicSettings = await getPublicSettings();
|
||||
this.showKomposeBuildOption = publicSettings.ShowKomposeBuildOption;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to get public settings');
|
||||
}
|
||||
if (this.showKomposeBuildOption) {
|
||||
this.deployOptions = [...this.deployOptions, { ...compose, value: KubernetesDeployManifestTypes.COMPOSE }];
|
||||
}
|
||||
|
||||
this.state.viewReady = true;
|
||||
|
||||
this.$window.onbeforeunload = () => {
|
||||
|
||||
@@ -5,6 +5,10 @@ angular.module('portainer.app').controller('CodeEditorController', function Code
|
||||
if (value && value.currentValue && ctrl.editor && ctrl.editor.getValue() !== value.currentValue) {
|
||||
ctrl.editor.setValue(value.currentValue);
|
||||
}
|
||||
|
||||
if (ctrl.editor) {
|
||||
ctrl.editor.setOption('readOnly', ctrl.readOnly);
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
|
||||
@@ -20,7 +20,6 @@ export function SettingsViewModel(data) {
|
||||
this.EnforceEdgeID = data.EnforceEdgeID;
|
||||
this.AgentSecret = data.AgentSecret;
|
||||
this.EdgePortainerUrl = data.EdgePortainerUrl;
|
||||
this.ShowKomposeBuildOption = data.ShowKomposeBuildOption;
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
@@ -37,7 +36,6 @@ export function PublicSettingsViewModel(settings) {
|
||||
this.Features = settings.Features;
|
||||
this.Edge = new EdgeSettingsViewModel(settings.Edge);
|
||||
this.DefaultRegistry = settings.DefaultRegistry;
|
||||
this.ShowKomposeBuildOption = settings.ShowKomposeBuildOption;
|
||||
this.IsAMTEnabled = settings.IsAMTEnabled;
|
||||
this.IsFDOEnabled = settings.IsFDOEnabled;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { UserTokenModel, UserViewModel } from '@/portainer/models/user';
|
||||
import { getUser, getUsers } from '@/portainer/users/user.service';
|
||||
import { getUsers } from '@/portainer/users/user.service';
|
||||
import { getUser } from '@/portainer/users/queries/useUser';
|
||||
|
||||
import { TeamMembershipModel } from '../../models/teamMembership';
|
||||
|
||||
@@ -15,8 +17,8 @@ export function UserService($q, Users, TeamService, TeamMembershipService) {
|
||||
return users.map((u) => new UserViewModel(u));
|
||||
};
|
||||
|
||||
service.user = async function (includeAdministrators) {
|
||||
const user = await getUser(includeAdministrators);
|
||||
service.user = async function (userId) {
|
||||
const user = await getUser(userId);
|
||||
|
||||
return new UserViewModel(user);
|
||||
};
|
||||
|
||||
27
app/portainer/users/queries/useUser.ts
Normal file
27
app/portainer/users/queries/useUser.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { buildUrl } from '../user.service';
|
||||
import { User, UserId } from '../types';
|
||||
|
||||
export function useUser(
|
||||
id: UserId,
|
||||
{ staleTime }: { staleTime?: number } = {}
|
||||
) {
|
||||
return useQuery(['users', id], () => getUser(id), {
|
||||
...withError('Unable to retrieve user details'),
|
||||
staleTime,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUser(id: UserId) {
|
||||
try {
|
||||
const { data: user } = await axios.get<User>(buildUrl(id));
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve user details');
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,6 @@ export async function getUsers(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUser(id: UserId) {
|
||||
try {
|
||||
const { data: user } = await axios.get<User>(buildUrl(id));
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve user details');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserMemberships(id: UserId) {
|
||||
try {
|
||||
const { data } = await axios.get<TeamMembership[]>(
|
||||
@@ -40,7 +30,7 @@ export async function getUserMemberships(id: UserId) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(id?: UserId, entity?: string) {
|
||||
export function buildUrl(id?: UserId, entity?: string) {
|
||||
let url = '/users';
|
||||
|
||||
if (id) {
|
||||
|
||||
@@ -184,16 +184,6 @@
|
||||
tooltip="'Hides the \'Add with form\' buttons and prevents adding/editing of resources via forms'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<por-switch-field
|
||||
label="'Allow docker-compose format Kubernetes manifests'"
|
||||
checked="formValues.ShowKomposeBuildOption"
|
||||
name="'toggle_showKomposeBuildOption'"
|
||||
on-change="(onToggleShowKompose)"
|
||||
field-class="'col-sm-12'"
|
||||
label-class="'col-sm-3 col-lg-2'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
<!-- !deployment options -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
// import trackEvent directly because the event only fires once with $analytics.trackEvent
|
||||
import { trackEvent } from '@/angulartics.matomo/analytics-services';
|
||||
import { options } from './options';
|
||||
|
||||
angular.module('portainer.app').controller('SettingsController', [
|
||||
'$scope',
|
||||
'$analytics',
|
||||
'$state',
|
||||
'Notifications',
|
||||
'SettingsService',
|
||||
'ModalService',
|
||||
@@ -16,7 +12,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
'BackupService',
|
||||
'FileSaver',
|
||||
'Blob',
|
||||
function ($scope, $analytics, $state, Notifications, SettingsService, ModalService, StateManager, BackupService, FileSaver) {
|
||||
function ($scope, Notifications, SettingsService, ModalService, StateManager, BackupService, FileSaver) {
|
||||
$scope.customBannerFeatureId = FeatureId.CUSTOM_LOGIN_BANNER;
|
||||
$scope.s3BackupFeatureId = FeatureId.S3_BACKUP_SETTING;
|
||||
$scope.enforceDeploymentOptions = FeatureId.ENFORCE_DEPLOYMENT_OPTIONS;
|
||||
@@ -57,7 +53,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
|
||||
$scope.formValues = {
|
||||
customLogo: false,
|
||||
ShowKomposeBuildOption: false,
|
||||
KubeconfigExpiry: undefined,
|
||||
HelmRepositoryURL: undefined,
|
||||
BlackListedLabels: [],
|
||||
@@ -83,33 +78,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onToggleShowKompose = async function onToggleShowKompose(checked) {
|
||||
if (checked) {
|
||||
ModalService.confirmWarn({
|
||||
title: 'Are you sure?',
|
||||
message: `<p>In a forthcoming Portainer release, we plan to remove support for docker-compose format manifests for Kubernetes deployments, and the Kompose conversion tool which enables this. The reason for this is because Kompose now poses a security risk, since it has a number of Common Vulnerabilities and Exposures (CVEs).</p>
|
||||
<p>Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and new pull requests to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.</p>`,
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Ok',
|
||||
className: 'btn-warning',
|
||||
},
|
||||
},
|
||||
callback: function (confirmed) {
|
||||
$scope.setShowCompose(confirmed);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
$scope.setShowCompose(checked);
|
||||
};
|
||||
|
||||
$scope.setShowCompose = function setShowCompose(checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.ShowKomposeBuildOption = checked;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onToggleAutoBackups = function onToggleAutoBackups(checked) {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.formValues.scheduleAutomaticBackups = checked;
|
||||
@@ -187,13 +155,8 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
KubeconfigExpiry: $scope.formValues.KubeconfigExpiry,
|
||||
HelmRepositoryURL: $scope.formValues.HelmRepositoryURL,
|
||||
GlobalDeploymentOptions: $scope.formValues.GlobalDeploymentOptions,
|
||||
ShowKomposeBuildOption: $scope.formValues.ShowKomposeBuildOption,
|
||||
};
|
||||
|
||||
if (kubeSettingsPayload.ShowKomposeBuildOption !== $scope.initialFormValues.ShowKomposeBuildOption && $scope.initialFormValues.enableTelemetry) {
|
||||
trackEvent('kubernetes-allow-compose', { category: 'kubernetes', metadata: { 'kubernetes-allow-compose': kubeSettingsPayload.ShowKomposeBuildOption } });
|
||||
}
|
||||
|
||||
$scope.state.kubeSettingsActionInProgress = true;
|
||||
updateSettings(kubeSettingsPayload, 'Kubernetes settings updated');
|
||||
};
|
||||
@@ -205,7 +168,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
StateManager.updateLogo(settings.LogoURL);
|
||||
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
|
||||
$scope.initialFormValues.ShowKomposeBuildOption = response.ShowKomposeBuildOption;
|
||||
$scope.initialFormValues.enableTelemetry = response.EnableTelemetry;
|
||||
$scope.formValues.BlackListedLabels = response.BlackListedLabels;
|
||||
})
|
||||
@@ -235,11 +197,6 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
$scope.formValues.KubeconfigExpiry = settings.KubeconfigExpiry;
|
||||
$scope.formValues.HelmRepositoryURL = settings.HelmRepositoryURL;
|
||||
$scope.formValues.BlackListedLabels = settings.BlackListedLabels;
|
||||
if (settings.ShowKomposeBuildOption) {
|
||||
$scope.formValues.ShowKomposeBuildOption = settings.ShowKomposeBuildOption;
|
||||
}
|
||||
|
||||
$scope.initialFormValues.ShowKomposeBuildOption = settings.ShowKomposeBuildOption;
|
||||
$scope.initialFormValues.enableTelemetry = settings.EnableTelemetry;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ComponentType } from 'react';
|
||||
|
||||
import { UserProvider } from '@/react/hooks/useUser';
|
||||
|
||||
import { withReactQuery } from './withReactQuery';
|
||||
|
||||
export function withCurrentUser<T>(
|
||||
WrappedComponent: ComponentType<T>
|
||||
): ComponentType<T> {
|
||||
@@ -12,13 +14,14 @@ export function withCurrentUser<T>(
|
||||
function WrapperComponent(props: T) {
|
||||
return (
|
||||
<UserProvider>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} />
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = displayName;
|
||||
WrapperComponent.displayName = `withCurrentUser(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
// User provider makes a call to the API to get the current user.
|
||||
// We need to wrap it with React Query to make that call.
|
||||
return withReactQuery(WrapperComponent);
|
||||
}
|
||||
|
||||
@@ -10,13 +10,12 @@ export function withI18nSuspense<T>(
|
||||
function WrapperComponent(props: T) {
|
||||
return (
|
||||
<Suspense fallback="Loading translations...">
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = displayName;
|
||||
WrapperComponent.displayName = `withI18nSuspense(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
}
|
||||
|
||||
@@ -14,13 +14,12 @@ export function withReactQuery<T>(
|
||||
function WrapperComponent(props: T) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = displayName;
|
||||
WrapperComponent.displayName = `withReactQuery(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@ export function withUIRouter<T>(
|
||||
function WrapperComponent(props: T) {
|
||||
return (
|
||||
<UIRouterContextComponent>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} />
|
||||
</UIRouterContextComponent>
|
||||
);
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = displayName;
|
||||
WrapperComponent.displayName = `withUIRouter(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||
|
||||
import './BoxSelectorItem.css';
|
||||
|
||||
@@ -29,7 +29,7 @@ export function BoxOption<T extends number | string>({
|
||||
type = 'radio',
|
||||
children,
|
||||
}: PropsWithChildren<Props<T>>) {
|
||||
return (
|
||||
const BoxOption = (
|
||||
<div className={clsx('box-selector-item', className)}>
|
||||
<input
|
||||
type={type}
|
||||
@@ -44,13 +44,13 @@ export function BoxOption<T extends number | string>({
|
||||
<label htmlFor={option.id} data-cy={`${radioName}_${option.value}`}>
|
||||
{children}
|
||||
</label>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
className="portainer-tooltip"
|
||||
message={tooltip}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipWithChildren message={tooltip}>{BoxOption}</TooltipWithChildren>
|
||||
);
|
||||
}
|
||||
return BoxOption;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function useHubspotForm({
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onFormSubmit: onSubmitted,
|
||||
onFormSubmitted: onSubmitted,
|
||||
});
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ export function SearchBar({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('searchBar items-center flex min-w-[350px]', className)}
|
||||
className={clsx('searchBar items-center flex min-w-[90px]', className)}
|
||||
>
|
||||
<Search className="searchIcon lucide shrink-0" />
|
||||
<input
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { EditorType } from '@/react/edge/edge-stacks/types';
|
||||
|
||||
import { BoxSelector } from '@@/BoxSelector';
|
||||
import { BoxSelectorOption } from '@@/BoxSelector/types';
|
||||
import {
|
||||
compose,
|
||||
kubernetes,
|
||||
} from '@@/BoxSelector/common-options/deployment-methods';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
onChange(value: number): void;
|
||||
hasDockerEndpoint: boolean;
|
||||
hasKubeEndpoint: boolean;
|
||||
allowKubeToSelectCompose?: boolean;
|
||||
}
|
||||
|
||||
export function EdgeStackDeploymentTypeSelector({
|
||||
value,
|
||||
onChange,
|
||||
hasDockerEndpoint,
|
||||
hasKubeEndpoint,
|
||||
allowKubeToSelectCompose,
|
||||
}: Props) {
|
||||
const deploymentOptions: BoxSelectorOption<number>[] = [
|
||||
{
|
||||
...compose,
|
||||
value: EditorType.Compose,
|
||||
disabled: () => (allowKubeToSelectCompose ? false : hasKubeEndpoint),
|
||||
tooltip: () =>
|
||||
hasKubeEndpoint
|
||||
? 'Cannot use this option with Edge Kubernetes environments'
|
||||
: '',
|
||||
},
|
||||
{
|
||||
...kubernetes,
|
||||
value: EditorType.Kubernetes,
|
||||
disabled: () => hasDockerEndpoint,
|
||||
tooltip: () =>
|
||||
hasDockerEndpoint
|
||||
? 'Cannot use this option with Edge Docker environments'
|
||||
: '',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-sm-12 form-section-title"> Deployment type</div>
|
||||
<BoxSelector
|
||||
radioName="deploymentType"
|
||||
value={value}
|
||||
options={deploymentOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
app/react/edge/edge-stacks/utils.test.ts
Normal file
40
app/react/edge/edge-stacks/utils.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||
|
||||
import { EditorType } from './types';
|
||||
import { getValidEditorTypes } from './utils';
|
||||
|
||||
interface GetValidEditorTypesTest {
|
||||
endpointTypes: EnvironmentType[];
|
||||
expected: EditorType[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
describe('getValidEditorTypes', () => {
|
||||
const tests: GetValidEditorTypesTest[] = [
|
||||
{
|
||||
endpointTypes: [EnvironmentType.EdgeAgentOnDocker],
|
||||
expected: [EditorType.Compose],
|
||||
title: 'should return compose for docker envs',
|
||||
},
|
||||
{
|
||||
endpointTypes: [EnvironmentType.EdgeAgentOnKubernetes],
|
||||
expected: [EditorType.Kubernetes],
|
||||
title: 'should return kubernetes for kubernetes envs',
|
||||
},
|
||||
{
|
||||
endpointTypes: [
|
||||
EnvironmentType.EdgeAgentOnDocker,
|
||||
EnvironmentType.EdgeAgentOnKubernetes,
|
||||
],
|
||||
expected: [],
|
||||
title: 'should return empty for docker and kubernetes envs',
|
||||
},
|
||||
];
|
||||
|
||||
tests.forEach((test) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
it(test.title, () => {
|
||||
expect(getValidEditorTypes(test.endpointTypes)).toEqual(test.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
21
app/react/edge/edge-stacks/utils.ts
Normal file
21
app/react/edge/edge-stacks/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||
|
||||
import { EditorType } from './types';
|
||||
|
||||
export function getValidEditorTypes(
|
||||
endpointTypes: EnvironmentType[],
|
||||
allowKubeToSelectCompose?: boolean
|
||||
) {
|
||||
const right: Partial<Record<EnvironmentType, EditorType[]>> = {
|
||||
[EnvironmentType.EdgeAgentOnDocker]: [EditorType.Compose],
|
||||
[EnvironmentType.EdgeAgentOnKubernetes]: allowKubeToSelectCompose
|
||||
? [EditorType.Kubernetes, EditorType.Compose]
|
||||
: [EditorType.Kubernetes],
|
||||
};
|
||||
|
||||
return endpointTypes.length
|
||||
? _.intersection(...endpointTypes.map((type) => right[type]))
|
||||
: [EditorType.Compose, EditorType.Kubernetes];
|
||||
}
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
|
||||
import { isAdmin } from '@/portainer/users/user.helpers';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { getUser } from '@/portainer/users/user.service';
|
||||
import { User, UserId } from '@/portainer/users/types';
|
||||
import { User } from '@/portainer/users/types';
|
||||
import { useUser as useLoadUser } from '@/portainer/users/queries/useUser';
|
||||
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
@@ -24,7 +22,12 @@ interface State {
|
||||
export const UserContext = createContext<State | null>(null);
|
||||
UserContext.displayName = 'UserContext';
|
||||
|
||||
export function useUser() {
|
||||
/**
|
||||
* @deprecated use `useCurrentUser` instead
|
||||
*/
|
||||
export const useUser = useCurrentUser;
|
||||
|
||||
export function useCurrentUser() {
|
||||
const context = useContext(UserContext);
|
||||
|
||||
if (context === null) {
|
||||
@@ -147,23 +150,19 @@ interface UserProviderProps {
|
||||
|
||||
export function UserProvider({ children }: UserProviderProps) {
|
||||
const [jwt] = useLocalStorage('JWT', '');
|
||||
const [user, setUser] = useState<User>();
|
||||
|
||||
useEffect(() => {
|
||||
if (jwt !== '') {
|
||||
const tokenPayload = jwtDecode(jwt) as { id: number };
|
||||
const tokenPayload = useMemo(() => jwtDecode(jwt) as { id: number }, [jwt]);
|
||||
|
||||
loadUser(tokenPayload.id);
|
||||
}
|
||||
}, [jwt]);
|
||||
const userQuery = useLoadUser(tokenPayload.id, {
|
||||
staleTime: Infinity, // should reload te user details only on page load
|
||||
});
|
||||
|
||||
const providerState = useMemo(() => ({ user }), [user]);
|
||||
const providerState = useMemo(
|
||||
() => ({ user: userQuery.data }),
|
||||
[userQuery.data]
|
||||
);
|
||||
|
||||
if (jwt === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!providerState.user) {
|
||||
if (jwt === '' || !providerState.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -172,9 +171,4 @@ export function UserProvider({ children }: UserProviderProps) {
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
async function loadUser(id: UserId) {
|
||||
const user = await getUser(id);
|
||||
setUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<SearchBar
|
||||
className="!bg-transparent !m-0"
|
||||
className="!bg-transparent !m-0 !min-w-[350px]"
|
||||
value={searchBarValue}
|
||||
onChange={setSearchBarValue}
|
||||
placeholder="Search by name, group, tag, status, URL..."
|
||||
|
||||
@@ -28,9 +28,6 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
||||
const initialValues: FormValues = {
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EdgePortainerUrl: settings.EdgePortainerUrl,
|
||||
Edge: {
|
||||
TunnelServerAddress: settings.Edge.TunnelServerAddress,
|
||||
},
|
||||
EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||
EnforceEdgeID: settings.EnforceEdgeID,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,4 @@ export interface FormValues {
|
||||
EdgePortainerUrl: string;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgeAgentCheckinInterval: number;
|
||||
Edge: {
|
||||
TunnelServerAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,8 +160,6 @@ export interface PublicSettingsResponse {
|
||||
RequiredPasswordLength: number;
|
||||
/** Deployment options for encouraging deployment as code (only on BE) */
|
||||
GlobalDeploymentOptions: GlobalDeploymentOptions;
|
||||
/** Show the Kompose build option (discontinued in 2.18) */
|
||||
ShowKomposeBuildOption: boolean;
|
||||
/** Whether edge compute features are enabled */
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
/** Supported feature flags */
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "Illegal number of parameters" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLATFORM=$1
|
||||
ARCH=$2
|
||||
KOMPOSE_VERSION=$3
|
||||
|
||||
|
||||
if [[ ${PLATFORM} == "windows" ]]; then
|
||||
wget -O "dist/kompose.exe" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-windows-amd64.exe"
|
||||
chmod +x "dist/kompose.exe"
|
||||
elif [[ ${PLATFORM} == "darwin" ]]; then
|
||||
# kompose 1.22 doesn't have arm support yet, we could merge darwin and linux scripts after upgrading kompose to >= 1.26.0
|
||||
wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-amd64"
|
||||
chmod +x "dist/kompose"
|
||||
else
|
||||
wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}"
|
||||
chmod +x "dist/kompose"
|
||||
fi
|
||||
15
gruntfile.js
15
gruntfile.js
@@ -29,7 +29,6 @@ module.exports = function (grunt) {
|
||||
dockerVersion: 'v20.10.21',
|
||||
dockerComposePluginVersion: 'v2.13.0',
|
||||
helmVersion: 'v3.9.3',
|
||||
komposeVersion: 'v1.22.0',
|
||||
kubectlVersion: 'v1.24.1',
|
||||
},
|
||||
env: gruntConfig.env,
|
||||
@@ -78,7 +77,6 @@ module.exports = function (grunt) {
|
||||
`shell:download_docker_binary:${platform}:${a}`,
|
||||
`shell:download_docker_compose_binary:${platform}:${a}`,
|
||||
`shell:download_helm_binary:${platform}:${a}`,
|
||||
`shell:download_kompose_binary:${platform}:${a}`,
|
||||
`shell:download_kubectl_binary:${platform}:${a}`,
|
||||
]);
|
||||
});
|
||||
@@ -117,7 +115,6 @@ gruntConfig.shell = {
|
||||
build_binary_azuredevops: { command: shell_build_binary_azuredevops },
|
||||
download_docker_binary: { command: shell_download_docker_binary },
|
||||
download_helm_binary: { command: shell_download_helm_binary },
|
||||
download_kompose_binary: { command: shell_download_kompose_binary },
|
||||
download_kubectl_binary: { command: shell_download_kubectl_binary },
|
||||
download_docker_compose_binary: { command: shell_download_docker_compose_binary },
|
||||
run_container: { command: shell_run_container },
|
||||
@@ -228,18 +225,6 @@ function shell_download_helm_binary(platform, arch) {
|
||||
`;
|
||||
}
|
||||
|
||||
function shell_download_kompose_binary(platform, arch) {
|
||||
const binaryVersion = '<%= binaries.komposeVersion %>';
|
||||
|
||||
return `
|
||||
if [ -f dist/kompose ] || [ -f dist/kompose.exe ]; then
|
||||
echo "kompose binary exists";
|
||||
else
|
||||
build/download_kompose_binary.sh ${platform} ${arch} ${binaryVersion};
|
||||
fi
|
||||
`;
|
||||
}
|
||||
|
||||
function shell_download_kubectl_binary(platform, arch) {
|
||||
var binaryVersion = '<%= binaries.kubectlVersion %>';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user