Compare commits

...

12 Commits

Author SHA1 Message Date
matias.spinarolli
4502236690 chore(edgestacks): fix edge stack tests EE-4951
Some checks are pending
Test / test-client (push) Waiting to run
2023-01-31 15:50:41 -03:00
matias-portainer
2874a79279 fix(doc): update endpoint creation swagger documentation EE-4925 (#8415) 2023-01-31 11:06:27 -03:00
Ali
8574dd2371 fix(edge stacks): allow viewing existing kompose stacks [EE-4967] (#8405)
Co-authored-by: testa113 <testa113>
Co-authored-by: Matt Hook <hookenz@gmail.com>
2023-01-31 10:03:21 +13:00
Dakota Walsh
53eb5aa1ee fix(kube): 30 second delay to storage detection EE-4822 (#8360) 2023-01-31 09:58:57 +13:00
cmeng
eb8644330e fix(edge/job): init endpoints if it is null [EE-4972] (#8411) 2023-01-27 22:08:13 +13:00
cmeng
8663de580a fix(settings): EE-4959 Cannot turn on Edge Compute Features on CE (#8396) 2023-01-27 17:04:40 +13:00
Oscar Zhou
34298d96c5 fix: pass endpoint entity instead of endpoint.id (#8407) 2023-01-27 12:41:54 +13:00
cmeng
9d103ffbeb fix(UI): EE-4937 low resolution hides add container button (#8401) 2023-01-27 09:18:48 +13:00
Chaim Lev-Ari
5847c2b8ef fix(system/update): submit license form [EE-4743] (#8381) 2023-01-26 20:35:04 +05:30
matias-portainer
a09fe7e10c chore(edgejobs): AddEdgeJob disregards async mode EE-4855 (#8287) 2023-01-26 11:32:11 -03:00
Ali
5640cce4d6 chore(kompose): remove from settings [EE-4741] (#8375) 2023-01-26 16:03:44 +13:00
Chaim Lev-Ari
00bbf4ac63 refactor(auth): cache user data [EE-4935] (#8380) 2023-01-26 07:40:05 +05:30
60 changed files with 509 additions and 332 deletions

View File

@@ -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()
}

View File

@@ -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 {

View File

@@ -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,
}
}

View File

@@ -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")

View 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
}

View File

@@ -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.

View 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\"}"
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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")
}()
}

View File

@@ -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)
}

View File

@@ -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' : '';
},
},
];
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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"
>

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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();
}
}

View File

@@ -1,6 +1,5 @@
export const KubernetesDeployManifestTypes = Object.freeze({
KUBERNETES: 1,
COMPOSE: 2,
});
export const KubernetesDeployBuildMethods = Object.freeze({

View File

@@ -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"

View File

@@ -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"

View File

@@ -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;

View File

@@ -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...).

View File

@@ -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 = () => {

View File

@@ -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 () {

View File

@@ -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;
}

View File

@@ -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);
};

View 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');
}
}

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -64,7 +64,7 @@ function useHubspotForm({
formId,
portalId,
region,
onFormSubmit: onSubmitted,
onFormSubmitted: onSubmitted,
});
},
{

View File

@@ -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

View File

@@ -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}
/>
</>
);
}

View 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);
});
});
});

View 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];
}

View File

@@ -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);
}
}

View File

@@ -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..."

View File

@@ -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,
};

View File

@@ -3,7 +3,4 @@ export interface FormValues {
EdgePortainerUrl: string;
EnforceEdgeID: boolean;
EdgeAgentCheckinInterval: number;
Edge: {
TunnelServerAddress: string;
};
}

View File

@@ -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 */

View File

@@ -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

View File

@@ -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 %>';