mirror of https://github.com/portainer/portainer
feat(ingress): autodetect ingress controllers EE-673 (#7712)
parent
c96551e410
commit
89eda13eb3
|
@ -12,6 +12,7 @@ type (
|
||||||
Type string `json:"Type"`
|
Type string `json:"Type"`
|
||||||
Availability bool `json:"Availability"`
|
Availability bool `json:"Availability"`
|
||||||
New bool `json:"New"`
|
New bool `json:"New"`
|
||||||
|
Used bool `json:"Used"`
|
||||||
}
|
}
|
||||||
|
|
||||||
K8sIngressControllers []K8sIngressController
|
K8sIngressControllers []K8sIngressController
|
||||||
|
|
|
@ -2,11 +2,11 @@ package models
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
type K8sNamespaceInfo struct {
|
type K8sNamespaceDetails struct {
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Annotations map[string]string `json:"Annotations"`
|
Annotations map[string]string `json:"Annotations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *K8sNamespaceInfo) Validate(request *http.Request) error {
|
func (r *K8sNamespaceDetails) Validate(request *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Migrator) migrateDBVersionToDB70() error {
|
func (m *Migrator) migrateDBVersionToDB70() error {
|
||||||
// foreach endpoint
|
log.Info().Msg("- add IngressAvailabilityPerNamespace field")
|
||||||
|
if err := m.addIngressAvailabilityPerNamespaceFieldDB70(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
endpoints, err := m.endpointService.Endpoints()
|
endpoints, err := m.endpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -45,3 +49,20 @@ func (m *Migrator) migrateDBVersionToDB70() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error {
|
||||||
|
endpoints, err := m.endpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = true
|
||||||
|
|
||||||
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
"Kubernetes": {
|
"Kubernetes": {
|
||||||
"Configuration": {
|
"Configuration": {
|
||||||
"EnableResourceOverCommit": false,
|
"EnableResourceOverCommit": false,
|
||||||
|
"IngressAvailabilityPerNamespace": true,
|
||||||
"IngressClasses": null,
|
"IngressClasses": null,
|
||||||
"ResourceOverCommitPercentage": 0,
|
"ResourceOverCommitPercentage": 0,
|
||||||
"RestrictDefaultNamespace": false,
|
"RestrictDefaultNamespace": false,
|
||||||
|
|
|
@ -13,20 +13,18 @@ func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.R
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
|
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, configmaps)
|
return response.JSON(w, configmaps)
|
||||||
|
|
|
@ -24,10 +24,10 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
authorizationService *authorization.Service
|
authorizationService *authorization.Service
|
||||||
dataStore dataservices.DataStore
|
dataStore dataservices.DataStore
|
||||||
jwtService dataservices.JWTService
|
|
||||||
kubernetesClientFactory *cli.ClientFactory
|
|
||||||
kubeClusterAccessService kubernetes.KubeClusterAccessService
|
|
||||||
KubernetesClient portainer.KubeClient
|
KubernetesClient portainer.KubeClient
|
||||||
|
kubernetesClientFactory *cli.ClientFactory
|
||||||
|
jwtService dataservices.JWTService
|
||||||
|
kubeClusterAccessService kubernetes.KubeClusterAccessService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -15,35 +14,39 @@ import (
|
||||||
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid environment identifier route variable",
|
||||||
Message: "Invalid environment identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == portainerDsErrors.ErrObjectNotFound {
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{
|
return httperror.NotFound(
|
||||||
StatusCode: http.StatusNotFound,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowedOnly, err := request.RetrieveBooleanQueryParameter(r, "allowedOnly", true)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest(
|
||||||
|
"Invalid allowedOnly boolean query parameter",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to create Kubernetes client",
|
||||||
Message: "Unable to create Kubernetes client",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controllers := cli.GetIngressControllers()
|
controllers := cli.GetIngressControllers()
|
||||||
|
@ -66,11 +69,44 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
|
||||||
}
|
}
|
||||||
|
|
||||||
if controllers[i].ClassName == a.Name {
|
if controllers[i].ClassName == a.Name {
|
||||||
controllers[i].Availability = !a.Blocked
|
controllers[i].Availability = !a.GloballyBlocked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Update existingClasses to take care of New and remove no longer
|
}
|
||||||
// existing classes.
|
|
||||||
|
// Update the database to match the list of found + modified controllers.
|
||||||
|
// This includes pruning out controllers which no longer exist.
|
||||||
|
var newClasses []portainer.KubernetesIngressClassConfig
|
||||||
|
for _, controller := range controllers {
|
||||||
|
var class portainer.KubernetesIngressClassConfig
|
||||||
|
class.Name = controller.ClassName
|
||||||
|
class.Type = controller.Type
|
||||||
|
class.GloballyBlocked = !controller.Availability
|
||||||
|
class.BlockedNamespaces = []string{}
|
||||||
|
newClasses = append(newClasses, class)
|
||||||
|
}
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = newClasses
|
||||||
|
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpointID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError(
|
||||||
|
"Unable to store found IngressClasses inside the database",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the allowedOnly query parameter was set. We need to prune out
|
||||||
|
// disallowed controllers from the response.
|
||||||
|
if allowedOnly {
|
||||||
|
var allowedControllers models.K8sIngressControllers
|
||||||
|
for _, controller := range controllers {
|
||||||
|
if controller.Availability {
|
||||||
|
allowedControllers = append(allowedControllers, controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controllers = allowedControllers
|
||||||
}
|
}
|
||||||
return response.JSON(w, controllers)
|
return response.JSON(w, controllers)
|
||||||
}
|
}
|
||||||
|
@ -78,80 +114,84 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
|
||||||
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid environment identifier route variable",
|
||||||
Message: "Invalid environment identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == portainerDsErrors.ErrObjectNotFound {
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{
|
return httperror.NotFound(
|
||||||
StatusCode: http.StatusNotFound,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
cli := handler.KubernetesClient
|
||||||
if err != nil {
|
currentControllers := cli.GetIngressControllers()
|
||||||
return &httperror.HandlerError{
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
Message: "Unable to create Kubernetes client",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
controllers := cli.GetIngressControllers()
|
|
||||||
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
for i := range controllers {
|
var updatedClasses []portainer.KubernetesIngressClassConfig
|
||||||
controllers[i].Availability = true
|
var controllers models.K8sIngressControllers
|
||||||
controllers[i].New = true
|
for i := range currentControllers {
|
||||||
|
var globallyblocked bool
|
||||||
|
currentControllers[i].Availability = true
|
||||||
|
currentControllers[i].New = true
|
||||||
|
|
||||||
|
var updatedClass portainer.KubernetesIngressClassConfig
|
||||||
|
updatedClass.Name = currentControllers[i].ClassName
|
||||||
|
updatedClass.Type = currentControllers[i].Type
|
||||||
|
|
||||||
// Check if the controller is blocked globally or in the current
|
// Check if the controller is blocked globally or in the current
|
||||||
// namespace.
|
// namespace.
|
||||||
for _, a := range existingClasses {
|
for _, existingClass := range existingClasses {
|
||||||
if controllers[i].ClassName != a.Name {
|
if currentControllers[i].ClassName != existingClass.Name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
controllers[i].New = false
|
currentControllers[i].New = false
|
||||||
|
updatedClass.GloballyBlocked = existingClass.GloballyBlocked
|
||||||
|
updatedClass.BlockedNamespaces = existingClass.BlockedNamespaces
|
||||||
|
|
||||||
// If it's not blocked we're all done!
|
globallyblocked = existingClass.GloballyBlocked
|
||||||
if !a.Blocked {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global blocks.
|
// Check if the current namespace is blocked.
|
||||||
if len(a.BlockedNamespaces) == 0 {
|
for _, ns := range existingClass.BlockedNamespaces {
|
||||||
controllers[i].Availability = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check the current namespace.
|
|
||||||
for _, ns := range a.BlockedNamespaces {
|
|
||||||
if namespace == ns {
|
if namespace == ns {
|
||||||
controllers[i].Availability = false
|
currentControllers[i].Availability = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Update existingClasses to take care of New and remove no longer
|
if !globallyblocked {
|
||||||
// existing classes.
|
controllers = append(controllers, currentControllers[i])
|
||||||
|
}
|
||||||
|
updatedClasses = append(updatedClasses, updatedClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the database to match the list of found controllers.
|
||||||
|
// This includes pruning out controllers which no longer exist.
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
|
||||||
|
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpointID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError(
|
||||||
|
"Unable to store found IngressClasses inside the database",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return response.JSON(w, controllers)
|
return response.JSON(w, controllers)
|
||||||
}
|
}
|
||||||
|
@ -159,167 +199,205 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
|
||||||
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid environment identifier route variable",
|
||||||
Message: "Invalid environment identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == portainerDsErrors.ErrObjectNotFound {
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{
|
return httperror.NotFound(
|
||||||
StatusCode: http.StatusNotFound,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressControllers
|
var payload models.K8sIngressControllers
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
classes := endpoint.Kubernetes.Configuration.IngressClasses
|
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
for _, p := range payload {
|
if err != nil {
|
||||||
for i := range classes {
|
return httperror.InternalServerError(
|
||||||
if p.ClassName == classes[i].Name {
|
"Unable to create Kubernetes client",
|
||||||
classes[i].Blocked = !p.Availability
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
controllers := cli.GetIngressControllers()
|
||||||
|
for i := range controllers {
|
||||||
|
// Set existing class data. So that we don't accidentally overwrite it
|
||||||
|
// with blank data that isn't in the payload.
|
||||||
|
for ii := range existingClasses {
|
||||||
|
if controllers[i].ClassName == existingClasses[ii].Name {
|
||||||
|
controllers[i].Availability = !existingClasses[ii].GloballyBlocked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
endpoint.Kubernetes.Configuration.IngressClasses = classes
|
|
||||||
fmt.Printf("%#v\n", endpoint.Kubernetes.Configuration.IngressClasses)
|
for _, p := range payload {
|
||||||
|
for i := range controllers {
|
||||||
|
// Now set new payload data
|
||||||
|
if p.ClassName == controllers[i].ClassName {
|
||||||
|
controllers[i].Availability = p.Availability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the database to match the list of found + modified controllers.
|
||||||
|
// This includes pruning out controllers which no longer exist.
|
||||||
|
var newClasses []portainer.KubernetesIngressClassConfig
|
||||||
|
for _, controller := range controllers {
|
||||||
|
var class portainer.KubernetesIngressClassConfig
|
||||||
|
class.Name = controller.ClassName
|
||||||
|
class.Type = controller.Type
|
||||||
|
class.GloballyBlocked = !controller.Availability
|
||||||
|
class.BlockedNamespaces = []string{}
|
||||||
|
newClasses = append(newClasses, class)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = newClasses
|
||||||
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
||||||
portainer.EndpointID(endpointID),
|
portainer.EndpointID(endpointID),
|
||||||
endpoint,
|
endpoint,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to update the BlockedIngressClasses inside the database",
|
||||||
Message: "Unable to update the BlockedIngressClasses inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid environment identifier route variable",
|
||||||
Message: "Invalid environment identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == portainerDsErrors.ErrObjectNotFound {
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{
|
return httperror.NotFound(
|
||||||
StatusCode: http.StatusNotFound,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
Message: "Unable to find an environment with the specified identifier inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressControllers
|
var payload models.K8sIngressControllers
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
classes := endpoint.Kubernetes.Configuration.IngressClasses
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
var updatedClasses []portainer.KubernetesIngressClassConfig
|
||||||
PayloadLoop:
|
PayloadLoop:
|
||||||
for _, p := range payload {
|
for _, p := range payload {
|
||||||
for i := range classes {
|
for _, existingClass := range existingClasses {
|
||||||
if p.ClassName == classes[i].Name {
|
if p.ClassName != existingClass.Name {
|
||||||
if p.Availability == true {
|
updatedClasses = append(updatedClasses, existingClass)
|
||||||
classes[i].Blocked = false
|
continue
|
||||||
classes[i].BlockedNamespaces = []string{}
|
}
|
||||||
continue PayloadLoop
|
var updatedClass portainer.KubernetesIngressClassConfig
|
||||||
}
|
updatedClass.Name = existingClass.Name
|
||||||
|
updatedClass.Type = existingClass.Type
|
||||||
|
updatedClass.GloballyBlocked = existingClass.GloballyBlocked
|
||||||
|
|
||||||
// If it's meant to be blocked we need to add the current
|
// Handle "allow"
|
||||||
// namespace. First, check if it's already in the
|
if p.Availability == true {
|
||||||
// BlockedNamespaces and if not we append it.
|
// remove the namespace from the list of blocked namespaces
|
||||||
classes[i].Blocked = true
|
// in the existingClass.
|
||||||
for _, ns := range classes[i].BlockedNamespaces {
|
for _, blockedNS := range existingClass.BlockedNamespaces {
|
||||||
if namespace == ns {
|
if blockedNS != namespace {
|
||||||
continue PayloadLoop
|
updatedClass.BlockedNamespaces = append(updatedClass.BlockedNamespaces, blockedNS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
classes[i].BlockedNamespaces = append(
|
|
||||||
classes[i].BlockedNamespaces,
|
updatedClasses = append(updatedClasses, existingClass)
|
||||||
namespace,
|
continue PayloadLoop
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle "disallow"
|
||||||
|
// If it's meant to be blocked we need to add the current
|
||||||
|
// namespace. First, check if it's already in the
|
||||||
|
// BlockedNamespaces and if not we append it.
|
||||||
|
updatedClass.BlockedNamespaces = existingClass.BlockedNamespaces
|
||||||
|
for _, ns := range updatedClass.BlockedNamespaces {
|
||||||
|
if namespace == ns {
|
||||||
|
updatedClasses = append(updatedClasses, existingClass)
|
||||||
|
continue PayloadLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedClass.BlockedNamespaces = append(
|
||||||
|
updatedClass.BlockedNamespaces,
|
||||||
|
namespace,
|
||||||
|
)
|
||||||
|
updatedClasses = append(updatedClasses, updatedClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
endpoint.Kubernetes.Configuration.IngressClasses = classes
|
|
||||||
fmt.Printf("%#v\n", endpoint.Kubernetes.Configuration.IngressClasses)
|
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
|
||||||
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
||||||
portainer.EndpointID(endpointID),
|
portainer.EndpointID(endpointID),
|
||||||
endpoint,
|
endpoint,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to update the BlockedIngressClasses inside the database",
|
||||||
Message: "Unable to update the BlockedIngressClasses inside the database",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
ingresses, err := cli.GetIngresses(namespace)
|
ingresses, err := cli.GetIngresses(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, ingresses)
|
return response.JSON(w, ingresses)
|
||||||
|
@ -328,33 +406,30 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re
|
||||||
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressInfo
|
var payload models.K8sIngressInfo
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
err = cli.CreateIngress(namespace, payload)
|
err = cli.CreateIngress(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
@ -368,43 +443,39 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http
|
||||||
|
|
||||||
err = cli.DeleteIngresses(payload)
|
err = cli.DeleteIngresses(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sIngressInfo
|
var payload models.K8sIngressInfo
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
err = cli.UpdateIngress(namespace, payload)
|
err = cli.UpdateIngress(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,10 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R
|
||||||
|
|
||||||
namespaces, err := cli.GetNamespaces()
|
namespaces, err := cli.GetNamespaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, namespaces)
|
return response.JSON(w, namespaces)
|
||||||
|
@ -27,23 +26,21 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R
|
||||||
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
var payload models.K8sNamespaceInfo
|
var payload models.K8sNamespaceDetails
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.CreateNamespace(payload)
|
err = cli.CreateNamespace(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -53,20 +50,18 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt
|
||||||
|
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.DeleteNamespace(namespace)
|
err = cli.DeleteNamespace(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -75,7 +70,7 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt
|
||||||
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
var payload models.K8sNamespaceInfo
|
var payload models.K8sNamespaceDetails
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return &httperror.HandlerError{
|
||||||
|
|
|
@ -12,21 +12,19 @@ import (
|
||||||
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
services, err := cli.GetServices(namespace)
|
services, err := cli.GetServices(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve services",
|
||||||
Message: "Unable to retrieve services",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, services)
|
return response.JSON(w, services)
|
||||||
|
@ -35,31 +33,28 @@ func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Req
|
||||||
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sServiceInfo
|
var payload models.K8sServiceInfo
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
err = cli.CreateService(namespace, payload)
|
err = cli.CreateService(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -70,20 +65,18 @@ func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.
|
||||||
var payload models.K8sServiceDeleteRequests
|
var payload models.K8sServiceDeleteRequests
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cli.DeleteServices(payload)
|
err = cli.DeleteServices(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -91,31 +84,28 @@ func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.
|
||||||
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid namespace identifier route variable",
|
||||||
Message: "Invalid namespace identifier route variable",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload models.K8sServiceInfo
|
var payload models.K8sServiceInfo
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.BadRequest(
|
||||||
StatusCode: http.StatusBadRequest,
|
"Invalid request payload",
|
||||||
Message: "Invalid request payload",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := handler.KubernetesClient
|
cli := handler.KubernetesClient
|
||||||
err = cli.UpdateService(namespace, payload)
|
err = cli.UpdateService(namespace, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{
|
return httperror.InternalServerError(
|
||||||
StatusCode: http.StatusInternalServerError,
|
"Unable to retrieve nodes limits",
|
||||||
Message: "Unable to retrieve nodes limits",
|
err,
|
||||||
Err: err,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,34 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want to know which of these controllers is in use.
|
||||||
|
var ingresses []models.K8sIngressInfo
|
||||||
|
namespaces, err := kcl.GetNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for namespace := range namespaces {
|
||||||
|
t, err := kcl.GetIngresses(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ingresses = append(ingresses, t...)
|
||||||
|
}
|
||||||
|
usedClasses := make(map[string]struct{})
|
||||||
|
for _, ingress := range ingresses {
|
||||||
|
usedClasses[ingress.ClassName] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
for _, class := range classList.Items {
|
for _, class := range classList.Items {
|
||||||
var controller models.K8sIngressController
|
var controller models.K8sIngressController
|
||||||
controller.Name = class.Spec.Controller
|
controller.Name = class.Spec.Controller
|
||||||
controller.ClassName = class.Name
|
controller.ClassName = class.Name
|
||||||
|
|
||||||
|
// If the class is used mark it as such.
|
||||||
|
if _, ok := usedClasses[class.Name]; ok {
|
||||||
|
controller.Used = true
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(controller.Name, "nginx"):
|
case strings.Contains(controller.Name, "nginx"):
|
||||||
controller.Type = "nginx"
|
controller.Type = "nginx"
|
||||||
|
|
|
@ -45,7 +45,7 @@ func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
|
// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceInfo) error {
|
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
|
||||||
client := kcl.cli.CoreV1().Namespaces()
|
client := kcl.cli.CoreV1().Namespaces()
|
||||||
|
|
||||||
var ns v1.Namespace
|
var ns v1.Namespace
|
||||||
|
@ -108,7 +108,7 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
|
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceInfo) error {
|
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) error {
|
||||||
client := kcl.cli.CoreV1().Namespaces()
|
client := kcl.cli.CoreV1().Namespaces()
|
||||||
|
|
||||||
var ns v1.Namespace
|
var ns v1.Namespace
|
||||||
|
|
|
@ -546,13 +546,14 @@ type (
|
||||||
|
|
||||||
// KubernetesConfiguration represents the configuration of a Kubernetes environment(endpoint)
|
// KubernetesConfiguration represents the configuration of a Kubernetes environment(endpoint)
|
||||||
KubernetesConfiguration struct {
|
KubernetesConfiguration struct {
|
||||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||||
EnableResourceOverCommit bool `json:"EnableResourceOverCommit"`
|
EnableResourceOverCommit bool `json:"EnableResourceOverCommit"`
|
||||||
ResourceOverCommitPercentage int `json:"ResourceOverCommitPercentage"`
|
ResourceOverCommitPercentage int `json:"ResourceOverCommitPercentage"`
|
||||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||||
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
|
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
|
||||||
|
IngressAvailabilityPerNamespace bool `json:"IngressAvailabilityPerNamespace"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||||
|
@ -567,7 +568,7 @@ type (
|
||||||
KubernetesIngressClassConfig struct {
|
KubernetesIngressClassConfig struct {
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Type string `json:"Type"`
|
Type string `json:"Type"`
|
||||||
Blocked bool `json:"Blocked"`
|
GloballyBlocked bool `json:"Blocked"`
|
||||||
BlockedNamespaces []string `json:"BlockedNamespaces"`
|
BlockedNamespaces []string `json:"BlockedNamespaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1349,11 +1350,14 @@ type (
|
||||||
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
||||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
||||||
|
|
||||||
CreateNamespace(info models.K8sNamespaceInfo) error
|
HasStackName(namespace string, stackName string) (bool, error)
|
||||||
UpdateNamespace(info models.K8sNamespaceInfo) error
|
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||||
|
CreateNamespace(info models.K8sNamespaceDetails) error
|
||||||
|
UpdateNamespace(info models.K8sNamespaceDetails) error
|
||||||
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
||||||
DeleteNamespace(namespace string) error
|
DeleteNamespace(namespace string) error
|
||||||
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
|
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
|
||||||
|
GetIngressControllers() models.K8sIngressControllers
|
||||||
CreateIngress(namespace string, info models.K8sIngressInfo) error
|
CreateIngress(namespace string, info models.K8sIngressInfo) error
|
||||||
UpdateIngress(namespace string, info models.K8sIngressInfo) error
|
UpdateIngress(namespace string, info models.K8sIngressInfo) error
|
||||||
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)
|
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)
|
||||||
|
@ -1362,10 +1366,6 @@ type (
|
||||||
UpdateService(namespace string, service models.K8sServiceInfo) error
|
UpdateService(namespace string, service models.K8sServiceInfo) error
|
||||||
GetServices(namespace string) ([]models.K8sServiceInfo, error)
|
GetServices(namespace string) ([]models.K8sServiceInfo, error)
|
||||||
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
||||||
GetIngressControllers() models.K8sIngressControllers
|
|
||||||
|
|
||||||
HasStackName(namespace string, stackName string) (bool, error)
|
|
||||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
|
||||||
GetNodesLimits() (K8sNodesLimits, error)
|
GetNodesLimits() (K8sNodesLimits, error)
|
||||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||||
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
import { IngressClassDatatable } from '@/react/kubernetes/cluster/ingressClass/IngressClassDatatable';
|
||||||
import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessView/NamespacesSelector';
|
import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessView/NamespacesSelector';
|
||||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/StorageAccessModeSelector';
|
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/StorageAccessModeSelector';
|
||||||
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||||
|
@ -8,6 +9,16 @@ import { CreateNamespaceRegistriesSelector } from '@/react/kubernetes/namespaces
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
.module('portainer.kubernetes.react.components', [])
|
.module('portainer.kubernetes.react.components', [])
|
||||||
|
.component(
|
||||||
|
'ingressClassDatatable',
|
||||||
|
r2a(IngressClassDatatable, [
|
||||||
|
'onChangeAvailability',
|
||||||
|
'description',
|
||||||
|
'ingressControllers',
|
||||||
|
'noIngressControllerLabel',
|
||||||
|
'view',
|
||||||
|
])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'namespacesSelector',
|
'namespacesSelector',
|
||||||
r2a(NamespacesSelector, [
|
r2a(NamespacesSelector, [
|
||||||
|
|
|
@ -158,10 +158,12 @@ export function CreateIngressView() {
|
||||||
);
|
);
|
||||||
const ingressClassOptions: Option<string>[] = [
|
const ingressClassOptions: Option<string>[] = [
|
||||||
{ label: 'Select an ingress class', value: '' },
|
{ label: 'Select an ingress class', value: '' },
|
||||||
...(ingressControllersResults.data?.map((cls) => ({
|
...(ingressControllersResults.data
|
||||||
label: cls.ClassName,
|
?.filter((cls) => cls.Availability)
|
||||||
value: cls.ClassName,
|
.map((cls) => ({
|
||||||
})) || []),
|
label: cls.ClassName,
|
||||||
|
value: cls.ClassName,
|
||||||
|
})) || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!existingIngressClass && ingressRule.IngressClassName) {
|
if (!existingIngressClass && ingressRule.IngressClassName) {
|
||||||
|
|
|
@ -65,14 +65,12 @@ export function useIngresses(
|
||||||
'ingress',
|
'ingress',
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
const ingresses: Ingress[] = [];
|
const ingresses = await Promise.all(
|
||||||
for (let i = 0; i < namespaces.length; i += 1) {
|
namespaces.map((namespace) => getIngresses(environmentId, namespace))
|
||||||
const ings = await getIngresses(environmentId, namespaces[i]);
|
);
|
||||||
if (ings) {
|
// flatten the array and remove empty ingresses
|
||||||
ingresses.push(...ings);
|
const filteredIngresses = ingresses.flat().filter((ing) => ing);
|
||||||
}
|
return filteredIngresses;
|
||||||
}
|
|
||||||
return ingresses;
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: namespaces.length > 0,
|
enabled: namespaces.length > 0,
|
||||||
|
|
|
@ -35,124 +35,35 @@
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left col-sm-5 col-lg-4 px-0"> Allow users to use external load balancer </label>
|
<label class="control-label text-left col-sm-5 col-lg-4 px-0"> Allow users to use external load balancer </label>
|
||||||
<label class="switch mb-0 col-sm-8">
|
<label class="switch col-sm-8 mb-0">
|
||||||
<input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><span class="slider round" data-cy="kubeSetup-loadBalancerToggle"></span>
|
<input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><span class="slider round" data-cy="kubeSetup-loadBalancerToggle"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<ingress-class-datatable
|
||||||
<div class="col-sm-12 text-muted small mt-4">
|
on-change-availability="(ctrl.onChangeAvailability)"
|
||||||
<p> Configuring ingress controllers will allow users to expose application they deploy over a HTTP route. </p>
|
ingress-controllers="ctrl.originalIngressControllers"
|
||||||
<p class="mt-1 vertical-center">
|
description="'Enabling ingress controllers in your cluster allows them to be available in the Portainer UI for users to publish applications over HTTP/HTTPS. A controller must have a class name for it to be included here.'"
|
||||||
<pr-icon icon="'alert-circle'" mode="'warning'" feather="true"></pr-icon>
|
no-ingress-controller-label="'No supported ingress controllers found.'"
|
||||||
Ingress classes must be manually specified for each controller you want to use in the cluster. Make sure that each controller is running inside your cluster.
|
view="'cluster'"
|
||||||
</p>
|
></ingress-class-datatable>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left">Ingress controller</label>
|
<por-switch-field
|
||||||
<span class="label label-default interactive vertical-center" style="margin-left: 10px" ng-click="ctrl.addIngressClass()" data-cy="kubeSetup-congifIngressButton">
|
checked="ctrl.formValues.IngressAvailabilityPerNamespace"
|
||||||
<pr-icon icon="'plus'" feather="true" size="'sm'" class="vertical-center"></pr-icon> configure ingress controller
|
name="'ingressAvailabilityPerNamespace'"
|
||||||
</span>
|
label="'Configure ingress controller availability per namespace'"
|
||||||
</div>
|
tooltip="'This allows an administrator to configure, in each namespace, which ingress controllers will be available for users to select when setting up ingresses for applications.'"
|
||||||
|
on-change="(ctrl.onToggleIngressAvailabilityPerNamespace)"
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||||
<div ng-repeat-start="ingressClass in ctrl.formValues.IngressClasses" style="margin-top: 2px">
|
switch-class="'col-sm-8'"
|
||||||
<div class="col-sm-7 input-group input-group-sm" ng-class="{ striked: ingressClass.NeedsDeletion }">
|
|
||||||
<span class="input-group-addon">Ingress class</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="ingress_class_name_{{ $index }}"
|
|
||||||
ng-model="ingressClass.Name"
|
|
||||||
placeholder="nginx"
|
|
||||||
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
|
|
||||||
ng-change="ctrl.onChangeIngressClassName($index)"
|
|
||||||
required
|
|
||||||
data-cy="kubeSetup-ingressClassName"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: ingressClass.NeedsDeletion }">
|
|
||||||
<span class="input-group-addon">Type</span>
|
|
||||||
<select
|
|
||||||
class="form-control"
|
|
||||||
name="ingress_class_type_{{ $index }}"
|
|
||||||
ng-model="ingressClass.Type"
|
|
||||||
ng-options="value as value for (key, value) in ctrl.IngressClassTypes"
|
|
||||||
required
|
|
||||||
data-cy="kubeSetup-ingressType"
|
|
||||||
>
|
|
||||||
<option selected disabled hidden value="">Select a type</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1 input-group input-group-sm">
|
|
||||||
<button
|
|
||||||
ng-if="!ingressClass.NeedsDeletion"
|
|
||||||
class="btn btn-dangerlight btn-only-icon"
|
|
||||||
type="button"
|
|
||||||
ng-click="ctrl.removeIngressClass($index)"
|
|
||||||
data-cy="kubeSetup-deleteIngress"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>
|
|
||||||
</button>
|
|
||||||
<button ng-if="ingressClass.NeedsDeletion" class="btn btn-only-icon btn-light" type="button" ng-click="ctrl.restoreIngressClass($index)">
|
|
||||||
<pr-icon icon="'rotate-ccw'" feather="true"></pr-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ng-repeat-end
|
|
||||||
ng-show="
|
|
||||||
kubernetesClusterSetupForm['ingress_class_name_' + $index].$invalid ||
|
|
||||||
kubernetesClusterSetupForm['ingress_class_type_' + $index].$invalid ||
|
|
||||||
ctrl.state.duplicates.ingressClasses.refs[$index] !== undefined
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div class="col-sm-7 input-group">
|
</por-switch-field>
|
||||||
<div
|
|
||||||
class="small"
|
|
||||||
style="margin-top: 5px"
|
|
||||||
ng-if="kubernetesClusterSetupForm['ingress_class_name_' + $index].$invalid || ctrl.state.duplicates.ingressClasses.refs[$index] !== undefined"
|
|
||||||
>
|
|
||||||
<div ng-messages="kubernetesClusterSetupForm['ingress_class_name_'+$index].$error">
|
|
||||||
<p ng-message="required" class="vertical-center text-warning"
|
|
||||||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Ingress class name is required.</p
|
|
||||||
>
|
|
||||||
<p ng-message="pattern" class="vertical-center text-warning"
|
|
||||||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field must consist of lower case alphanumeric characters or '-', start
|
|
||||||
with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').</p
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<p ng-if="ctrl.state.duplicates.ingressClasses.refs[$index] !== undefined" class="vertical-center text-warning">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This ingress class is already defined.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-3 input-group">
|
|
||||||
<div class="small" style="margin-top: 5px" ng-if="kubernetesClusterSetupForm['ingress_class_type_' + $index].$invalid">
|
|
||||||
<div ng-messages="kubernetesClusterSetupForm['ingress_class_type_'+$index].$error">
|
|
||||||
<p ng-message="required" class="vertical-center text-warning"
|
|
||||||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Ingress class type is required.</p
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.hasTraefikIngress()">
|
|
||||||
<span class="col-sm-12 text-muted small">
|
|
||||||
<p>
|
|
||||||
<i class="fa fa-flask blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
|
||||||
Traefik support is experimental.
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- auto update window -->
|
<!-- auto update window -->
|
||||||
<div class="col-sm-12 form-section-title"> Change Window Settings </div>
|
<div class="col-sm-12 form-section-title"> Change Window Settings </div>
|
||||||
|
|
||||||
|
@ -163,7 +74,7 @@
|
||||||
name="'disableSysctlSettingForRegularUsers'"
|
name="'disableSysctlSettingForRegularUsers'"
|
||||||
label="'Enable Change Window'"
|
label="'Enable Change Window'"
|
||||||
feature-id="ctrl.limitedFeatureAutoWindow"
|
feature-id="ctrl.limitedFeatureAutoWindow"
|
||||||
tooltip="'Specify a timeframe during which automatic updates can occur in this environment.'"
|
tooltip="'Automatic updates to stacks or applications outside the defined change window will not occur.'"
|
||||||
on-change="(ctrl.onToggleAutoUpdate)"
|
on-change="(ctrl.onToggleAutoUpdate)"
|
||||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||||
switch-class="'col-sm-8 text-muted'"
|
switch-class="'col-sm-8 text-muted'"
|
||||||
|
|
|
@ -2,12 +2,12 @@ import _ from 'lodash-es';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
|
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
|
||||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||||
|
|
||||||
class KubernetesConfigureController {
|
class KubernetesConfigureController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
|
||||||
|
@ -41,10 +41,15 @@ class KubernetesConfigureController {
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.configureAsync = this.configureAsync.bind(this);
|
this.configureAsync = this.configureAsync.bind(this);
|
||||||
|
this.areControllersChanged = this.areControllersChanged.bind(this);
|
||||||
|
this.areFormValuesChanged = this.areFormValuesChanged.bind(this);
|
||||||
|
this.onBeforeOnload = this.onBeforeOnload.bind(this);
|
||||||
this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT;
|
this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT;
|
||||||
this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||||
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||||
|
this.onChangeAvailability = this.onChangeAvailability.bind(this);
|
||||||
this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this);
|
this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this);
|
||||||
|
this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this);
|
||||||
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
@ -66,48 +71,23 @@ class KubernetesConfigureController {
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region INGRESS CLASSES UI MANAGEMENT */
|
/* #region INGRESS CLASSES UI MANAGEMENT */
|
||||||
addIngressClass() {
|
onChangeAvailability(controllerClassMap) {
|
||||||
this.formValues.IngressClasses.push(new KubernetesIngressClass());
|
this.ingressControllers = controllerClassMap;
|
||||||
this.onChangeIngressClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreIngressClass(index) {
|
|
||||||
this.formValues.IngressClasses[index].NeedsDeletion = false;
|
|
||||||
this.onChangeIngressClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeIngressClass(index) {
|
|
||||||
if (!this.formValues.IngressClasses[index].IsNew) {
|
|
||||||
this.formValues.IngressClasses[index].NeedsDeletion = true;
|
|
||||||
} else {
|
|
||||||
this.formValues.IngressClasses.splice(index, 1);
|
|
||||||
}
|
|
||||||
this.onChangeIngressClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeIngressClass() {
|
|
||||||
const state = this.state.duplicates.ingressClasses;
|
|
||||||
const source = _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic.Name));
|
|
||||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
|
||||||
state.refs = duplicates;
|
|
||||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeIngressClassName(index) {
|
|
||||||
const fv = this.formValues.IngressClasses[index];
|
|
||||||
if (_.includes(fv.Name, KubernetesIngressClassTypes.NGINX)) {
|
|
||||||
fv.Type = KubernetesIngressClassTypes.NGINX;
|
|
||||||
} else if (_.includes(fv.Name, KubernetesIngressClassTypes.TRAEFIK)) {
|
|
||||||
fv.Type = KubernetesIngressClassTypes.TRAEFIK;
|
|
||||||
}
|
|
||||||
this.onChangeIngressClass();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasTraefikIngress() {
|
hasTraefikIngress() {
|
||||||
return _.find(this.formValues.IngressClasses, { Type: this.IngressClassTypes.TRAEFIK });
|
return _.find(this.formValues.IngressClasses, { Type: this.IngressClassTypes.TRAEFIK });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleIngressAvailabilityPerNamespace() {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.IngressAvailabilityPerNamespace = !this.formValues.IngressAvailabilityPerNamespace;
|
||||||
|
});
|
||||||
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region RESOURCES AND METRICS */
|
||||||
|
|
||||||
onChangeEnableResourceOverCommit(enabled) {
|
onChangeEnableResourceOverCommit(enabled) {
|
||||||
this.$scope.$evalAsync(() => {
|
this.$scope.$evalAsync(() => {
|
||||||
this.formValues.EnableResourceOverCommit = enabled;
|
this.formValues.EnableResourceOverCommit = enabled;
|
||||||
|
@ -117,13 +97,19 @@ class KubernetesConfigureController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/* #region CONFIGURE */
|
/* #region CONFIGURE */
|
||||||
assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) {
|
assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) {
|
||||||
endpoint.Kubernetes.Configuration.StorageClasses = storageClasses;
|
endpoint.Kubernetes.Configuration.StorageClasses = storageClasses;
|
||||||
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||||
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||||
|
endpoint.Kubernetes.Configuration.EnableResourceOverCommit = this.formValues.EnableResourceOverCommit;
|
||||||
|
endpoint.Kubernetes.Configuration.ResourceOverCommitPercentage = this.formValues.ResourceOverCommitPercentage;
|
||||||
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
|
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
|
||||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
|
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
|
||||||
|
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = this.formValues.IngressAvailabilityPerNamespace;
|
||||||
|
endpoint.ChangeWindow = this.state.autoUpdateSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
transformFormValues() {
|
transformFormValues() {
|
||||||
|
@ -150,11 +136,9 @@ class KubernetesConfigureController {
|
||||||
|
|
||||||
async removeIngressesAcrossNamespaces() {
|
async removeIngressesAcrossNamespaces() {
|
||||||
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
||||||
|
|
||||||
if (!ingressesToDel.length) {
|
if (!ingressesToDel.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
const oldEndpointID = this.EndpointProvider.endpointID();
|
const oldEndpointID = this.EndpointProvider.endpointID();
|
||||||
this.EndpointProvider.setEndpointID(this.endpoint.Id);
|
this.EndpointProvider.setEndpointID(this.endpoint.Id);
|
||||||
|
@ -213,7 +197,9 @@ class KubernetesConfigureController {
|
||||||
|
|
||||||
this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses);
|
this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses);
|
||||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||||
|
// updateIngressControllerClassMap must be done after updateEndpoint, as a hacky workaround. A better solution: saving ingresscontrollers somewhere else, is being discussed
|
||||||
|
await updateIngressControllerClassMap(this.state.endpointId, this.ingressControllers);
|
||||||
|
this.state.isSaving = true;
|
||||||
const storagePromises = _.map(storageClasses, (storageClass) => {
|
const storagePromises = _.map(storageClasses, (storageClass) => {
|
||||||
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
||||||
if (oldStorageClass) {
|
if (oldStorageClass) {
|
||||||
|
@ -291,19 +277,31 @@ class KubernetesConfigureController {
|
||||||
isServerRunning: false,
|
isServerRunning: false,
|
||||||
userClick: false,
|
userClick: false,
|
||||||
},
|
},
|
||||||
|
timeZone: '',
|
||||||
|
isSaving: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
UseLoadBalancer: false,
|
UseLoadBalancer: false,
|
||||||
UseServerMetrics: false,
|
UseServerMetrics: false,
|
||||||
|
EnableResourceOverCommit: true,
|
||||||
|
ResourceOverCommitPercentage: 20,
|
||||||
IngressClasses: [],
|
IngressClasses: [],
|
||||||
RestrictDefaultNamespace: false,
|
RestrictDefaultNamespace: false,
|
||||||
|
enableAutoUpdateTimeWindow: false,
|
||||||
|
IngressAvailabilityPerNamespace: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||||
|
|
||||||
[this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(this.state.endpointId), this.EndpointService.endpoint(this.state.endpointId)]);
|
[this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(this.state.endpointId), this.EndpointService.endpoint(this.state.endpointId)]);
|
||||||
|
|
||||||
|
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.state.endpointId });
|
||||||
|
this.originalIngressControllers = structuredClone(this.ingressControllers);
|
||||||
|
|
||||||
|
this.state.autoUpdateSettings = this.endpoint.ChangeWindow;
|
||||||
|
|
||||||
_.forEach(this.StorageClasses, (item) => {
|
_.forEach(this.StorageClasses, (item) => {
|
||||||
const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name);
|
const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name);
|
||||||
if (storage) {
|
if (storage) {
|
||||||
|
@ -316,12 +314,15 @@ class KubernetesConfigureController {
|
||||||
|
|
||||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||||
|
this.formValues.EnableResourceOverCommit = this.endpoint.Kubernetes.Configuration.EnableResourceOverCommit;
|
||||||
|
this.formValues.ResourceOverCommitPercentage = this.endpoint.Kubernetes.Configuration.ResourceOverCommitPercentage;
|
||||||
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
|
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
|
||||||
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
||||||
ic.IsNew = false;
|
ic.IsNew = false;
|
||||||
ic.NeedsDeletion = false;
|
ic.NeedsDeletion = false;
|
||||||
return ic;
|
return ic;
|
||||||
});
|
});
|
||||||
|
this.formValues.IngressAvailabilityPerNamespace = this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace;
|
||||||
|
|
||||||
this.oldFormValues = Object.assign({}, this.formValues);
|
this.oldFormValues = Object.assign({}, this.formValues);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -329,12 +330,48 @@ class KubernetesConfigureController {
|
||||||
} finally {
|
} finally {
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', this.onBeforeOnload);
|
||||||
}
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
return this.$async(this.onInit);
|
return this.$async(this.onInit);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
$onDestroy() {
|
||||||
|
window.removeEventListener('beforeunload', this.onBeforeOnload);
|
||||||
|
}
|
||||||
|
|
||||||
|
areControllersChanged() {
|
||||||
|
return !_.isEqual(this.ingressControllers, this.originalIngressControllers);
|
||||||
|
}
|
||||||
|
|
||||||
|
areFormValuesChanged() {
|
||||||
|
return !_.isEqual(this.formValues, this.oldFormValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeOnload(event) {
|
||||||
|
if (!this.state.isSaving && (this.areControllersChanged() || this.areFormValuesChanged())) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uiCanExit() {
|
||||||
|
if (!this.state.isSaving && (this.areControllersChanged() || this.areFormValuesChanged())) {
|
||||||
|
return this.ModalService.confirmAsync({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: 'You currently have unsaved changes in the cluster setup view. Are you sure you want to leave?',
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
label: 'Yes',
|
||||||
|
className: 'btn-danger',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubernetesConfigureController;
|
export default KubernetesConfigureController;
|
||||||
|
|
|
@ -180,214 +180,17 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region STORAGES -->
|
<div ng-if="$ctrl.state.ingressAvailabilityPerNamespace">
|
||||||
<div class="col-sm-12 form-section-title"> Storage </div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<span class="col-sm-12 text-muted small vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
|
||||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
|
|
||||||
prevent the usage of a specific storage option inside this namespace.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 form-section-title vertical-center">
|
|
||||||
<pr-icon icon="'svg-route'"></pr-icon>
|
|
||||||
standard
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<storage-class-switch value="sc.Selected" name="sc.Name" on-change="(ctrl.onToggleStorageQuota)" authorization="K8sResourcePoolDetailsW"></storage-class-switch>
|
|
||||||
|
|
||||||
<!-- #endregion -->
|
|
||||||
|
|
||||||
<div ng-if="$ctrl.state.canUseIngress">
|
|
||||||
<div class="col-sm-12 form-section-title"> Ingresses </div>
|
|
||||||
<!-- #region INGRESSES -->
|
<!-- #region INGRESSES -->
|
||||||
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length === 0">
|
<div class="col-sm-12 form-section-title"> Networking </div>
|
||||||
<div class="col-sm-12 small text-muted">
|
<ingress-class-datatable
|
||||||
The ingress feature must be enabled in the
|
ng-if="$ctrl.state.ingressAvailabilityPerNamespace"
|
||||||
<a ui-sref="kubernetes.cluster.setup">environment configuration view</a> to be able to register ingresses inside this namespace.
|
on-change-availability="($ctrl.onChangeIngressControllerAvailability)"
|
||||||
</div>
|
ingress-controllers="$ctrl.ingressControllers"
|
||||||
</div>
|
description="'Enable the ingress controllers that users can select when publishing applications in this namespace.'"
|
||||||
|
no-ingress-controller-label="'No ingress controllers found in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster.'"
|
||||||
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length > 0">
|
view="'namespace'"
|
||||||
<div class="col-sm-12 small text-muted">
|
></ingress-class-datatable>
|
||||||
<p class="vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
|
||||||
Enable and configure ingresses available to users when deploying applications.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-repeat-start="ic in $ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<span class="text-muted vertical-center"><pr-icon icon="'svg-route'"></pr-icon> {{ ic.IngressClass.Name }}</span>
|
|
||||||
<hr class="mt-2 mb-0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-3 col-lg-2">
|
|
||||||
<label class="control-label text-left"> Allow users to use this ingress </label>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-8 pt-2">
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" ng-model="ic.Selected" /><span class="slider round" data-cy="namespaceCreate-ingressToggle{{ ic.IngressClass.Name }}"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="ic.Selected">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<label class="control-label text-left">
|
|
||||||
Hostnames
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'Hostnames associated to the ingress inside this namespace. Users will be able to expose and access their applications over the ingress via one of these hostname.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<span class="label label-default interactive" ng-click="$ctrl.addHostname(ic)" data-cy="namespaceCreate-addHostButton{{ ic.IngressClass.Name }}">
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add hostname
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 pt-4">
|
|
||||||
<div ng-repeat="item in ic.Hosts track by $index">
|
|
||||||
<div class="form-inline">
|
|
||||||
<div class="col-sm-8 input-group input-group-sm pt-2">
|
|
||||||
<span class="input-group-addon required">Hostname</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
|
||||||
ng-model="item.Host"
|
|
||||||
ng-change="$ctrl.onChangeIngressHostname()"
|
|
||||||
placeholder="foo"
|
|
||||||
pattern="[\*a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
|
|
||||||
required
|
|
||||||
data-cy="namespaceCreate-hostnameInput{{ ic.IngressClass.Name }}_{{ $index }}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1 input-group input-group-sm !pt-2" ng-if="$index > 0">
|
|
||||||
<button class="btn btn-md btn-dangerlight btn-only-icon" type="button" ng-click="$ctrl.removeHostname(ic, $index)">
|
|
||||||
<pr-icon icon="'trash-2'" size="'md'" feather="true"></pr-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="small text-warning pt-1"
|
|
||||||
ng-show="
|
|
||||||
resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid ||
|
|
||||||
$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ng-messages for="resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
|
|
||||||
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> Hostname is required.</p>
|
|
||||||
<p class="vertical-center" ng-message="pattern">
|
|
||||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>
|
|
||||||
This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com').
|
|
||||||
</p>
|
|
||||||
</ng-messages>
|
|
||||||
<p class="vertical-center" ng-if="$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
|
|
||||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This hostname is already used.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-repeat-end class="form-group" ng-if="ic.Selected">
|
|
||||||
<div class="col-sm-12 small text-muted">
|
|
||||||
<p class="vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
|
||||||
You can specify a list of annotations that will be associated to the ingress.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<label class="control-label text-left">Annotations </label>
|
|
||||||
<label class="control-label text-left">
|
|
||||||
<span class="label label-default interactive" ng-click="$ctrl.addAnnotation(ic)" data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}">
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add annotation
|
|
||||||
</span>
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<label class="control-label text-left">
|
|
||||||
<span
|
|
||||||
class="label label-default interactive"
|
|
||||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
|
||||||
ng-click="$ctrl.addRewriteAnnotation(ic)"
|
|
||||||
data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add rewrite annotation
|
|
||||||
</span>
|
|
||||||
<portainer-tooltip
|
|
||||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
|
||||||
message="'When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<label class="control-label text-left">
|
|
||||||
<span
|
|
||||||
class="label label-default interactive"
|
|
||||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
|
||||||
ng-click="$ctrl.addUseregexAnnotation(ic)"
|
|
||||||
data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add regular expression annotation
|
|
||||||
</span>
|
|
||||||
<portainer-tooltip
|
|
||||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
|
||||||
message="'Enable use of regular expressions in ingress paths (set in the ingress details of an application). Use this along with rewrite-target to specify the regex capturing group to be replaced, e.g. path regex of ^/foo/(,*) and rewrite-target of /bar/$1 rewrites example.com/foo/account to example.com/bar/account.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 form-inline pt-4">
|
|
||||||
<div class="pt-2" ng-repeat="annotation in ic.Annotations track by $index">
|
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
|
||||||
<span class="input-group-addon">Key</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="annotation.Key"
|
|
||||||
placeholder="{{
|
|
||||||
ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX
|
|
||||||
? 'e.g. nginx.ingress.kubernetes.io/enable-rewrite-log'
|
|
||||||
: 'e.g. traefik.ingress.kubernetes.io/router.priority'
|
|
||||||
}}"
|
|
||||||
required
|
|
||||||
data-cy="namespaceCreate-annotationKey{{ ic.IngressClass.Name }}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="input-group input-group-sm col-sm-5">
|
|
||||||
<span class="input-group-addon">Value</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="annotation.Value"
|
|
||||||
placeholder="{{ ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX ? 'e.g. true or false' : 'e.g. 42' }}"
|
|
||||||
required
|
|
||||||
data-cy="namespaceCreate-annotationValue{{ ic.IngressClass.Name }}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="input-group input-group-sm col-sm-1">
|
|
||||||
<button
|
|
||||||
class="btn btn-md btn-dangerlight btn-only-icon"
|
|
||||||
type="button"
|
|
||||||
ng-click="$ctrl.removeAnnotation(ic, $index)"
|
|
||||||
data-cy="namespaceCreate-deleteAnnotationButton{{ ic.IngressClass.Name }}"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'trash-2'" size="'md'" feather="true"></pr-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -422,6 +225,25 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- #region STORAGES -->
|
||||||
|
<div class="col-sm-12 form-section-title"> Storage </div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small vertical-center">
|
||||||
|
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||||
|
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
|
||||||
|
prevent the usage of a specific storage option inside this namespace.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 form-section-title vertical-center">
|
||||||
|
<pr-icon icon="'svg-route'"></pr-icon>
|
||||||
|
standard
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<storage-class-switch value="sc.Selected" name="sc.Name" on-change="(ctrl.onToggleStorageQuota)" authorization="K8sResourcePoolDetailsW"></storage-class-switch>
|
||||||
|
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- summary -->
|
<!-- summary -->
|
||||||
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !$ctrl.isCreateButtonDisabled()" form-values="$ctrl.formValues"></kubernetes-summary-view>
|
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !$ctrl.isCreateButtonDisabled()" form-values="$ctrl.formValues"></kubernetes-summary-view>
|
||||||
<!-- !summary -->
|
<!-- !summary -->
|
||||||
|
|
|
@ -2,20 +2,12 @@ import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import {
|
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassHostFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||||
KubernetesResourcePoolFormValues,
|
|
||||||
KubernetesResourcePoolIngressClassAnnotationFormValue,
|
|
||||||
KubernetesResourcePoolIngressClassHostFormValue,
|
|
||||||
KubernetesResourcePoolNginxRewriteAnnotationFormValue,
|
|
||||||
KubernetesResourcePoolNginxUseregexAnnotationFormValue,
|
|
||||||
KubernetesResourcePoolTraefikRewriteAnnotationFormValue,
|
|
||||||
} from 'Kubernetes/models/resource-pool/formValues';
|
|
||||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
|
||||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||||
|
|
||||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||||
|
|
||||||
class KubernetesCreateResourcePoolController {
|
class KubernetesCreateResourcePoolController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -39,10 +31,17 @@ class KubernetesCreateResourcePoolController {
|
||||||
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||||
this.onToggleLoadBalancerQuota = this.onToggleLoadBalancerQuota.bind(this);
|
this.onToggleLoadBalancerQuota = this.onToggleLoadBalancerQuota.bind(this);
|
||||||
this.onToggleResourceQuota = this.onToggleResourceQuota.bind(this);
|
this.onToggleResourceQuota = this.onToggleResourceQuota.bind(this);
|
||||||
|
this.onChangeIngressControllerAvailability = this.onChangeIngressControllerAvailability.bind(this);
|
||||||
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
onRegistriesChange(registries) {
|
||||||
|
return this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.Registries = registries;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onToggleStorageQuota(storageClassName, enabled) {
|
onToggleStorageQuota(storageClassName, enabled) {
|
||||||
this.$scope.$evalAsync(() => {
|
this.$scope.$evalAsync(() => {
|
||||||
this.formValues.StorageClasses = this.formValues.StorageClasses.map((sClass) => (sClass.Name !== storageClassName ? sClass : { ...sClass, Selected: enabled }));
|
this.formValues.StorageClasses = this.formValues.StorageClasses.map((sClass) => (sClass.Name !== storageClassName ? sClass : { ...sClass, Selected: enabled }));
|
||||||
|
@ -61,74 +60,9 @@ class KubernetesCreateResourcePoolController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeIngressHostname() {
|
/* #region INGRESS MANAGEMENT */
|
||||||
const state = this.state.duplicates.ingressHosts;
|
onChangeIngressControllerAvailability(controllerClassMap) {
|
||||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
this.ingressControllers = controllerClassMap;
|
||||||
const hostnames = _.compact(hosts.map((h) => h.Host));
|
|
||||||
const hostnamesWithoutRemoved = _.filter(hostnames, (h) => !h.NeedsDeletion);
|
|
||||||
const allHosts = _.flatMap(this.allIngresses, 'Hosts');
|
|
||||||
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnamesWithoutRemoved);
|
|
||||||
_.forEach(hostnames, (host, idx) => {
|
|
||||||
if (host !== undefined && _.includes(allHosts, host)) {
|
|
||||||
formDuplicates[idx] = host;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const duplicates = {};
|
|
||||||
let count = 0;
|
|
||||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
|
||||||
duplicates[ic.IngressClass.Name] = {};
|
|
||||||
_.forEach(ic.Hosts, (hostFV, hostIdx) => {
|
|
||||||
if (hostFV.Host === formDuplicates[count]) {
|
|
||||||
duplicates[ic.IngressClass.Name][hostIdx] = hostFV.Host;
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
state.refs = duplicates;
|
|
||||||
state.hasRefs = false;
|
|
||||||
_.forIn(duplicates, (value) => {
|
|
||||||
if (Object.keys(value).length > 0) {
|
|
||||||
state.hasRefs = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRegistriesChange(registries) {
|
|
||||||
return this.$scope.$evalAsync(() => {
|
|
||||||
this.formValues.Registries = registries;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addHostname(ingressClass) {
|
|
||||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHostname(ingressClass, index) {
|
|
||||||
ingressClass.Hosts.splice(index, 1);
|
|
||||||
this.onChangeIngressHostname();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* #region ANNOTATIONS MANAGEMENT */
|
|
||||||
addAnnotation(ingressClass) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
addRewriteAnnotation(ingressClass) {
|
|
||||||
if (ingressClass.IngressClass.Type === this.IngressClassTypes.NGINX) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolNginxRewriteAnnotationFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ingressClass.IngressClass.Type === this.IngressClassTypes.TRAEFIK) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolTraefikRewriteAnnotationFormValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addUseregexAnnotation(ingressClass) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolNginxUseregexAnnotationFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAnnotation(ingressClass, index) {
|
|
||||||
ingressClass.Annotations.splice(index, 1);
|
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -175,6 +109,7 @@ class KubernetesCreateResourcePoolController {
|
||||||
this.checkDefaults();
|
this.checkDefaults();
|
||||||
this.formValues.Owner = this.Authentication.getUserDetails().username;
|
this.formValues.Owner = this.Authentication.getUserDetails().username;
|
||||||
await this.KubernetesResourcePoolService.create(this.formValues);
|
await this.KubernetesResourcePoolService.create(this.formValues);
|
||||||
|
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers, this.formValues.Name);
|
||||||
this.Notifications.success('Namespace successfully created', this.formValues.Name);
|
this.Notifications.success('Namespace successfully created', this.formValues.Name);
|
||||||
this.$state.go('kubernetes.resourcePools');
|
this.$state.go('kubernetes.resourcePools');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -239,15 +174,21 @@ class KubernetesCreateResourcePoolController {
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
isAlreadyExist: false,
|
isAlreadyExist: false,
|
||||||
hasPrefixKube: false,
|
hasPrefixKube: false,
|
||||||
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
|
canUseIngress: false,
|
||||||
duplicates: {
|
duplicates: {
|
||||||
ingressHosts: new KubernetesFormValidationReferences(),
|
ingressHosts: new KubernetesFormValidationReferences(),
|
||||||
},
|
},
|
||||||
isAdmin: this.Authentication.isAdmin(),
|
isAdmin: this.Authentication.isAdmin(),
|
||||||
|
ingressAvailabilityPerNamespace: endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodes = await this.KubernetesNodeService.get();
|
const nodes = await this.KubernetesNodeService.get();
|
||||||
|
|
||||||
|
this.ingressControllers = [];
|
||||||
|
if (this.state.ingressAvailabilityPerNamespace) {
|
||||||
|
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.endpoint.Id, allowedOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
_.forEach(nodes, (item) => {
|
_.forEach(nodes, (item) => {
|
||||||
this.state.sliderMaxMemory += filesizeParser(item.Memory);
|
this.state.sliderMaxMemory += filesizeParser(item.Memory);
|
||||||
this.state.sliderMaxCpu += item.CPU;
|
this.state.sliderMaxCpu += item.CPU;
|
||||||
|
|
|
@ -156,193 +156,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable && ctrl.state.canUseIngress">
|
<div ng-if="ctrl.isAdmin && ctrl.isEditable && ctrl.state.ingressAvailabilityPerNamespace">
|
||||||
<div class="col-sm-12 form-section-title"> Ingresses </div>
|
|
||||||
<!-- #region INGRESSES -->
|
<!-- #region INGRESSES -->
|
||||||
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length === 0">
|
<div class="col-sm-12 form-section-title"> Networking </div>
|
||||||
<div class="col-sm-12 small text-muted">
|
<ingress-class-datatable
|
||||||
The ingress feature must be enabled in the
|
ng-if="ctrl.state.ingressAvailabilityPerNamespace"
|
||||||
<a ui-sref="kubernetes.cluster.setup">environment configuration view</a> to be able to register ingresses inside this namespace.
|
on-change-availability="(ctrl.onChangeIngressControllerAvailability)"
|
||||||
</div>
|
ingress-controllers="ctrl.ingressControllers"
|
||||||
</div>
|
description="'Enable the ingress controllers that users can select when publishing applications in this namespace.'"
|
||||||
|
no-ingress-controller-label="'No ingress controllers found in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster.'"
|
||||||
|
view="'namespace'"
|
||||||
|
></ingress-class-datatable>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length > 0">
|
|
||||||
<div class="col-sm-12 small text-muted">
|
|
||||||
<p class="vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
|
||||||
Enable and configure ingresses available to users when deploying applications.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
|
|
||||||
<div class="text-muted col-sm-12" style="width: 100%">
|
|
||||||
<pr-icon icon="'svg-route'" mode="'primary'"></pr-icon>
|
|
||||||
{{ ic.IngressClass.Name }}
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-3 col-lg-2">
|
|
||||||
<label class="control-label text-left"> Allow users to use this ingress </label>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-9 pt-2">
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" ng-model="ic.Selected" />
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="ic.Selected">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<label class="control-label text-left">
|
|
||||||
Hostnames
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'Hostnames associated to the ingress inside this namespace. Users will be able to expose and access their applications over the ingress via one of these hostname.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<span class="vertical-center label label-default interactive" style="margin-left: 10px" ng-click="ctrl.addHostname(ic)">
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add hostname
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12" style="margin-top: 10px">
|
|
||||||
<div ng-repeat="item in ic.Hosts track by $index" style="margin-top: 2px">
|
|
||||||
<div class="form-inline">
|
|
||||||
<div class="col-sm-10 input-group input-group-sm" ng-class="{ striked: item.NeedsDeletion }">
|
|
||||||
<span class="input-group-addon required">Hostname</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
|
||||||
ng-model="item.Host"
|
|
||||||
ng-change="ctrl.onChangeIngressHostname()"
|
|
||||||
placeholder="foo"
|
|
||||||
pattern="[\*a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
|
|
||||||
<button ng-if="!item.NeedsDeletion" class="btn btn-sm btn-dangerlight" type="button" ng-click="ctrl.removeHostname(ic, $index)">
|
|
||||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>
|
|
||||||
</button>
|
|
||||||
<button ng-if="item.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreHostname(item)">
|
|
||||||
<pr-icon icon="'svg-restore'"></pr-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="small text-warning"
|
|
||||||
style="margin-top: 5px"
|
|
||||||
ng-show="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid || item.Duplicate"
|
|
||||||
>
|
|
||||||
<ng-messages for="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
|
|
||||||
<p ng-message="required" class="vertical-center">
|
|
||||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>
|
|
||||||
Hostname is required.
|
|
||||||
</p>
|
|
||||||
<p ng-message="pattern" class="vertical-center">
|
|
||||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>
|
|
||||||
This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g.
|
|
||||||
'example.com').
|
|
||||||
</p>
|
|
||||||
</ng-messages>
|
|
||||||
<p class="vertical-center" ng-if="item.Duplicate">
|
|
||||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>
|
|
||||||
This hostname is already used.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-repeat-end class="form-group" ng-if="ic.Selected" style="margin-bottom: 20px">
|
|
||||||
<div class="col-sm-12 small text-muted" style="margin-top: 5px">
|
|
||||||
<p class="vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
|
||||||
You can specify a list of annotations that will be associated to the ingress.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<label class="control-label text-left">Annotations</label>
|
|
||||||
<span class="vertical-center label label-default interactive" style="margin-left: 10px" ng-click="ctrl.addAnnotation(ic)">
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add annotation
|
|
||||||
</span>
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
<span
|
|
||||||
class="vertical-center label label-default interactive"
|
|
||||||
ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX"
|
|
||||||
style="margin-left: 10px"
|
|
||||||
ng-click="ctrl.addRewriteAnnotation(ic)"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add rewrite annotation
|
|
||||||
</span>
|
|
||||||
<portainer-tooltip
|
|
||||||
ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX"
|
|
||||||
message="'When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
<span
|
|
||||||
class="vertical-center label label-default interactive"
|
|
||||||
ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX"
|
|
||||||
style="margin-left: 10px"
|
|
||||||
ng-click="ctrl.addUseregexAnnotation(ic)"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add regular expression annotation
|
|
||||||
</span>
|
|
||||||
<portainer-tooltip
|
|
||||||
ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX"
|
|
||||||
message="'Enable use of regular expressions in ingress paths (set in the ingress details of an application). Use this along with rewrite-target to specify the regex capturing group to be replaced, e.g. path regex of ^/foo/(,*) and rewrite-target of /bar/$1 rewrites example.com/foo/account to example.com/bar/account.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
|
||||||
<div ng-repeat="annotation in ic.Annotations track by $index" style="margin-top: 2px">
|
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
|
||||||
<span class="input-group-addon">Key</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="annotation.Key"
|
|
||||||
placeholder="{{
|
|
||||||
ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX
|
|
||||||
? 'e.g. nginx.ingress.kubernetes.io/enable-rewrite-log'
|
|
||||||
: 'e.g. traefik.ingress.kubernetes.io/router.priority'
|
|
||||||
}}"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
|
||||||
<span class="input-group-addon">Value</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="annotation.Value"
|
|
||||||
placeholder="{{ ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX ? 'e.g. true or false' : 'e.g. 42' }}"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1 input-group input-group-sm">
|
|
||||||
<button class="btn btn-md btn-dangerlight btn-only-icon" type="button" ng-click="ctrl.removeAnnotation(ic, $index)">
|
|
||||||
<pr-icon icon="'trash-2'" feather="true" size="'md'"></pr-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- #region REGISTRIES -->
|
<!-- #region REGISTRIES -->
|
||||||
<div>
|
<div>
|
||||||
<div class="col-sm-12 form-section-title"> Registries </div>
|
<div class="col-sm-12 form-section-title"> Registries </div>
|
||||||
|
|
|
@ -5,21 +5,15 @@ import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quot
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
import {
|
|
||||||
KubernetesResourcePoolFormValues,
|
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassHostFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||||
KubernetesResourcePoolIngressClassAnnotationFormValue,
|
|
||||||
KubernetesResourcePoolIngressClassHostFormValue,
|
|
||||||
KubernetesResourcePoolNginxRewriteAnnotationFormValue,
|
|
||||||
KubernetesResourcePoolNginxUseregexAnnotationFormValue,
|
|
||||||
KubernetesResourcePoolTraefikRewriteAnnotationFormValue,
|
|
||||||
} from 'Kubernetes/models/resource-pool/formValues';
|
|
||||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
|
||||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||||
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
||||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { updateIngressControllerClassMap, getIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||||
|
|
||||||
class KubernetesResourcePoolController {
|
class KubernetesResourcePoolController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -73,35 +67,11 @@ class KubernetesResourcePoolController {
|
||||||
this.getEvents = this.getEvents.bind(this);
|
this.getEvents = this.getEvents.bind(this);
|
||||||
this.onToggleLoadBalancersQuota = this.onToggleLoadBalancersQuota.bind(this);
|
this.onToggleLoadBalancersQuota = this.onToggleLoadBalancersQuota.bind(this);
|
||||||
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||||
|
this.onChangeIngressControllerAvailability = this.onChangeIngressControllerAvailability.bind(this);
|
||||||
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region ANNOTATIONS MANAGEMENT */
|
|
||||||
addAnnotation(ingressClass) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
addRewriteAnnotation(ingressClass) {
|
|
||||||
if (ingressClass.IngressClass.Type === this.IngressClassTypes.NGINX) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolNginxRewriteAnnotationFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ingressClass.IngressClass.Type === this.IngressClassTypes.TRAEFIK) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolTraefikRewriteAnnotationFormValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addUseregexAnnotation(ingressClass) {
|
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolNginxUseregexAnnotationFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAnnotation(ingressClass, index) {
|
|
||||||
ingressClass.Annotations.splice(index, 1);
|
|
||||||
this.onChangeIngressHostname();
|
|
||||||
}
|
|
||||||
/* #endregion */
|
|
||||||
|
|
||||||
onRegistriesChange(registries) {
|
onRegistriesChange(registries) {
|
||||||
return this.$scope.$evalAsync(() => {
|
return this.$scope.$evalAsync(() => {
|
||||||
this.formValues.Registries = registries;
|
this.formValues.Registries = registries;
|
||||||
|
@ -120,55 +90,10 @@ class KubernetesResourcePoolController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #region INGRESS MANAGEMENT */
|
onChangeIngressControllerAvailability(controllerClassMap) {
|
||||||
onChangeIngressHostname() {
|
this.ingressControllers = controllerClassMap;
|
||||||
const state = this.state.duplicates.ingressHosts;
|
|
||||||
const otherIngresses = _.without(this.allIngresses, ...this.ingresses);
|
|
||||||
const allHosts = _.flatMap(otherIngresses, 'Hosts');
|
|
||||||
|
|
||||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
|
||||||
const hostsWithoutRemoved = _.filter(hosts, { NeedsDeletion: false });
|
|
||||||
const hostnames = _.map(hostsWithoutRemoved, 'Host');
|
|
||||||
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnames);
|
|
||||||
_.forEach(hostnames, (host, idx) => {
|
|
||||||
if (host !== undefined && _.includes(allHosts, host)) {
|
|
||||||
formDuplicates[idx] = host;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const duplicatedHostnames = Object.values(formDuplicates);
|
|
||||||
state.hasRefs = false;
|
|
||||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
|
||||||
_.forEach(ic.Hosts, (hostFV) => {
|
|
||||||
if (_.includes(duplicatedHostnames, hostFV.Host) && hostFV.NeedsDeletion === false) {
|
|
||||||
hostFV.Duplicate = true;
|
|
||||||
state.hasRefs = true;
|
|
||||||
} else {
|
|
||||||
hostFV.Duplicate = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addHostname(ingressClass) {
|
|
||||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHostname(ingressClass, index) {
|
|
||||||
if (!ingressClass.Hosts[index].IsNew) {
|
|
||||||
ingressClass.Hosts[index].NeedsDeletion = true;
|
|
||||||
} else {
|
|
||||||
ingressClass.Hosts.splice(index, 1);
|
|
||||||
}
|
|
||||||
this.onChangeIngressHostname();
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreHostname(host) {
|
|
||||||
if (!host.IsNew) {
|
|
||||||
host.NeedsDeletion = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* #endregion*/
|
|
||||||
|
|
||||||
selectTab(index) {
|
selectTab(index) {
|
||||||
this.LocalStorage.storeActiveTab('resourcePool', index);
|
this.LocalStorage.storeActiveTab('resourcePool', index);
|
||||||
}
|
}
|
||||||
|
@ -219,6 +144,7 @@ class KubernetesResourcePoolController {
|
||||||
try {
|
try {
|
||||||
this.checkDefaults();
|
this.checkDefaults();
|
||||||
await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues);
|
await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues);
|
||||||
|
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers, this.formValues.Name);
|
||||||
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
|
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
|
||||||
this.$state.reload(this.$state.current);
|
this.$state.reload(this.$state.current);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -426,11 +352,12 @@ class KubernetesResourcePoolController {
|
||||||
ingressesLoading: true,
|
ingressesLoading: true,
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
eventWarningCount: 0,
|
eventWarningCount: 0,
|
||||||
canUseIngress: this.endpoint.Kubernetes.Configuration.IngressClasses.length,
|
canUseIngress: false,
|
||||||
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
||||||
duplicates: {
|
duplicates: {
|
||||||
ingressHosts: new KubernetesFormValidationReferences(),
|
ingressHosts: new KubernetesFormValidationReferences(),
|
||||||
},
|
},
|
||||||
|
ingressAvailabilityPerNamespace: this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
||||||
|
@ -439,6 +366,11 @@ class KubernetesResourcePoolController {
|
||||||
|
|
||||||
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
|
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
|
||||||
|
|
||||||
|
this.ingressControllers = [];
|
||||||
|
if (this.state.ingressAvailabilityPerNamespace) {
|
||||||
|
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.endpoint.Id, namespace: name });
|
||||||
|
}
|
||||||
|
|
||||||
this.pool = _.find(pools, { Namespace: { Name: name } });
|
this.pool = _.find(pools, { Namespace: { Name: name } });
|
||||||
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
|
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
|
||||||
this.formValues.Name = this.pool.Namespace.Name;
|
this.formValues.Name = this.pool.Namespace.Name;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { Loading } from '@@/Widget/Loading';
|
||||||
import { PasswordCheckHint } from '@@/PasswordCheckHint';
|
import { PasswordCheckHint } from '@@/PasswordCheckHint';
|
||||||
import { ViewLoading } from '@@/ViewLoading';
|
import { ViewLoading } from '@@/ViewLoading';
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
import { Badge } from '@@/Badge';
|
||||||
import { TableColumnHeaderAngular } from '@@/datatables/TableHeaderCell';
|
import { TableColumnHeaderAngular } from '@@/datatables/TableHeaderCell';
|
||||||
import { DashboardItem } from '@@/DashboardItem';
|
import { DashboardItem } from '@@/DashboardItem';
|
||||||
import { SearchBar } from '@@/datatables/SearchBar';
|
import { SearchBar } from '@@/datatables/SearchBar';
|
||||||
|
@ -47,6 +48,7 @@ export const componentsModule = angular
|
||||||
'portainerTooltip',
|
'portainerTooltip',
|
||||||
r2a(Tooltip, ['message', 'position', 'className'])
|
r2a(Tooltip, ['message', 'position', 'className'])
|
||||||
)
|
)
|
||||||
|
.component('badge', r2a(Badge, ['type', 'className']))
|
||||||
.component('fileUploadField', fileUploadField)
|
.component('fileUploadField', fileUploadField)
|
||||||
.component('porSwitchField', switchField)
|
.component('porSwitchField', switchField)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
|
|
||||||
<div class="form-group overflow-auto" ng-if="ctrl.state.showStandardLogin">
|
<div class="form-group overflow-auto" ng-if="ctrl.state.showStandardLogin">
|
||||||
<!-- login button -->
|
<!-- login button -->
|
||||||
<div class="col-sm-12 d-flex">
|
<div class="col-sm-12 d-flex py-1">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary btn-lg btn-block"
|
class="btn btn-primary btn-lg btn-block"
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Badge, Props } from './Badge';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: Badge,
|
||||||
|
title: 'Components/Badge',
|
||||||
|
argTypes: {
|
||||||
|
type: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['success', 'danger', 'warn', 'info'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta<Props>;
|
||||||
|
|
||||||
|
// : JSX.IntrinsicAttributes & PropsWithChildren<Props>
|
||||||
|
function Template({ type = 'success' }: Props) {
|
||||||
|
const message = {
|
||||||
|
success: 'success badge',
|
||||||
|
danger: 'danger badge',
|
||||||
|
warn: 'warn badge',
|
||||||
|
info: 'info badge',
|
||||||
|
};
|
||||||
|
return <Badge type={type}>{message[type]}</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example = Template.bind({});
|
|
@ -0,0 +1,59 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
type BadgeType = 'success' | 'danger' | 'warn' | 'info';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type?: BadgeType;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this component is used in tables and lists in portainer. It looks like this:
|
||||||
|
// https://www.figma.com/file/g5TUMngrblkXM7NHSyQsD1/New-UI?node-id=76%3A2
|
||||||
|
export function Badge({ type, className, children }: PropsWithChildren<Props>) {
|
||||||
|
const baseClasses =
|
||||||
|
'flex w-fit items-center !text-xs font-medium rounded-full px-2 py-0.5';
|
||||||
|
const typeClasses = getClasses(type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={clsx(baseClasses, typeClasses, className)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// classes in full to prevent a dev server bug, where tailwind doesn't render the interpolated classes
|
||||||
|
function getClasses(type: BadgeType | undefined) {
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
return clsx(
|
||||||
|
`text-success-9 bg-success-2`,
|
||||||
|
`th-dark:text-success-3 th-dark:bg-success-10`,
|
||||||
|
`th-highcontrast:text-success-3 th-highcontrast:bg-success-10`
|
||||||
|
);
|
||||||
|
case 'warn':
|
||||||
|
return clsx(
|
||||||
|
`text-warning-9 bg-warning-2`,
|
||||||
|
`th-dark:text-warning-3 th-dark:bg-warning-10`,
|
||||||
|
`th-highcontrast:text-warning-3 th-highcontrast:bg-warning-10`
|
||||||
|
);
|
||||||
|
case 'danger':
|
||||||
|
return clsx(
|
||||||
|
`text-error-9 bg-error-2`,
|
||||||
|
`th-dark:text-error-3 th-dark:bg-error-10`,
|
||||||
|
`th-highcontrast:text-error-3 th-highcontrast:bg-error-10`
|
||||||
|
);
|
||||||
|
case 'info':
|
||||||
|
return clsx(
|
||||||
|
`text-blue-9 bg-blue-2`,
|
||||||
|
`th-dark:text-blue-3 th-dark:bg-blue-10`,
|
||||||
|
`th-highcontrast:text-blue-3 th-highcontrast:bg-blue-10`
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return clsx(
|
||||||
|
`text-blue-9 bg-blue-2`,
|
||||||
|
`th-dark:text-blue-3 th-dark:bg-blue-10`,
|
||||||
|
`th-highcontrast:text-blue-3 th-highcontrast:bg-blue-10`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { Badge } from './Badge';
|
|
@ -54,6 +54,7 @@ interface Props<
|
||||||
initialTableState?: Partial<TableState<D>>;
|
initialTableState?: Partial<TableState<D>>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
|
description?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Datatable<
|
export function Datatable<
|
||||||
|
@ -74,6 +75,7 @@ export function Datatable<
|
||||||
initialTableState = {},
|
initialTableState = {},
|
||||||
isLoading,
|
isLoading,
|
||||||
totalCount = dataset.length,
|
totalCount = dataset.length,
|
||||||
|
description,
|
||||||
}: Props<D, TSettings>) {
|
}: Props<D, TSettings>) {
|
||||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||||
|
|
||||||
|
@ -145,6 +147,7 @@ export function Datatable<
|
||||||
label={titleOptions.title}
|
label={titleOptions.title}
|
||||||
icon={titleOptions.icon}
|
icon={titleOptions.icon}
|
||||||
featherIcon={titleOptions.featherIcon}
|
featherIcon={titleOptions.featherIcon}
|
||||||
|
description={description}
|
||||||
>
|
>
|
||||||
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
|
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
|
||||||
{renderTableActions && (
|
{renderTableActions && (
|
||||||
|
|
|
@ -20,8 +20,8 @@ export function SearchBar({
|
||||||
const [searchValue, setSearchValue] = useDebounce(value, onChange);
|
const [searchValue, setSearchValue] = useDebounce(value, onChange);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="searchBar items-center flex">
|
<div className="searchBar items-center flex min-w-[90px]">
|
||||||
<Search className="searchIcon feather" />
|
<Search className="searchIcon feather shrink-0" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="searchInput"
|
className="searchInput"
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function TableHeaderCell({
|
||||||
render,
|
render,
|
||||||
onSortClick,
|
onSortClick,
|
||||||
isSorted,
|
isSorted,
|
||||||
isSortedDesc,
|
isSortedDesc = true,
|
||||||
canFilter,
|
canFilter,
|
||||||
renderFilter,
|
renderFilter,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
@ -55,7 +55,7 @@ function SortWrapper({
|
||||||
children,
|
children,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
isSorted,
|
isSorted,
|
||||||
isSortedDesc,
|
isSortedDesc = true,
|
||||||
}: PropsWithChildren<SortWrapperProps>) {
|
}: PropsWithChildren<SortWrapperProps>) {
|
||||||
if (!canSort) {
|
if (!canSort) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
@ -93,7 +93,7 @@ export function TableColumnHeaderAngular({
|
||||||
canSort,
|
canSort,
|
||||||
isSorted,
|
isSorted,
|
||||||
colTitle,
|
colTitle,
|
||||||
isSortedDesc,
|
isSortedDesc = true,
|
||||||
}: TableColumnHeaderAngularProps) {
|
}: TableColumnHeaderAngularProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row flex-nowrap h-full">
|
<div className="flex flex-row flex-nowrap h-full">
|
||||||
|
|
|
@ -6,6 +6,7 @@ interface Props {
|
||||||
icon?: ReactNode | ComponentType<unknown>;
|
icon?: ReactNode | ComponentType<unknown>;
|
||||||
featherIcon?: boolean;
|
featherIcon?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
|
description?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableTitle({
|
export function TableTitle({
|
||||||
|
@ -13,23 +14,27 @@ export function TableTitle({
|
||||||
featherIcon,
|
featherIcon,
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
|
description,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div className="toolBar">
|
<div className="toolBar flex-col">
|
||||||
<div className="toolBarTitle">
|
<div className="flex gap-1 p-0 w-full items-center">
|
||||||
{icon && (
|
<div className="toolBarTitle">
|
||||||
<div className="widget-icon">
|
{icon && (
|
||||||
<Icon
|
<div className="widget-icon">
|
||||||
icon={icon}
|
<Icon
|
||||||
feather={featherIcon}
|
icon={icon}
|
||||||
className="space-right feather"
|
feather={featherIcon}
|
||||||
/>
|
className="space-right feather"
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{label}
|
{label}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{description && description}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { confirmWarn } from '@/portainer/services/modal.service/confirm';
|
||||||
|
|
||||||
|
import { Datatable } from '@@/datatables';
|
||||||
|
import { Button, ButtonGroup } from '@@/buttons';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { IngressControllerClassMap } from '../types';
|
||||||
|
|
||||||
|
import { useColumns } from './columns';
|
||||||
|
import { createStore } from './datatable-store';
|
||||||
|
|
||||||
|
const useStore = createStore('ingressClasses');
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onChangeAvailability: (
|
||||||
|
controllerClassMap: IngressControllerClassMap[]
|
||||||
|
) => void; // angular function to save the ingress class list
|
||||||
|
description: string;
|
||||||
|
ingressControllers: IngressControllerClassMap[] | undefined;
|
||||||
|
noIngressControllerLabel: string;
|
||||||
|
view: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IngressClassDatatable({
|
||||||
|
onChangeAvailability,
|
||||||
|
description,
|
||||||
|
ingressControllers,
|
||||||
|
noIngressControllerLabel,
|
||||||
|
view,
|
||||||
|
}: Props) {
|
||||||
|
const [ingControllerFormValues, setIngControllerFormValues] =
|
||||||
|
useState(ingressControllers);
|
||||||
|
const settings = useStore();
|
||||||
|
const columns = useColumns();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="-mx-[15px]">
|
||||||
|
<Datatable
|
||||||
|
dataset={ingControllerFormValues || []}
|
||||||
|
storageKey="ingressClasses"
|
||||||
|
columns={columns}
|
||||||
|
settingsStore={settings}
|
||||||
|
isLoading={!ingControllerFormValues}
|
||||||
|
emptyContentLabel={noIngressControllerLabel}
|
||||||
|
titleOptions={{
|
||||||
|
icon: 'database',
|
||||||
|
title: 'Ingress controllers',
|
||||||
|
featherIcon: true,
|
||||||
|
}}
|
||||||
|
getRowId={(row) => `${row.Name}-${row.ClassName}-${row.Type}`}
|
||||||
|
renderTableActions={(selectedRows) => renderTableActions(selectedRows)}
|
||||||
|
description={renderIngressClassDescription()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderTableActions(selectedRows: IngressControllerClassMap[]) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
selectedRows.filter((row) => row.Availability === true).length ===
|
||||||
|
0
|
||||||
|
}
|
||||||
|
color="dangerlight"
|
||||||
|
size="small"
|
||||||
|
onClick={() =>
|
||||||
|
updateIngressControllers(
|
||||||
|
selectedRows,
|
||||||
|
ingControllerFormValues || [],
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Disallow selected
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
selectedRows.filter((row) => row.Availability === false)
|
||||||
|
.length === 0
|
||||||
|
}
|
||||||
|
color="default"
|
||||||
|
size="small"
|
||||||
|
onClick={() =>
|
||||||
|
updateIngressControllers(
|
||||||
|
selectedRows,
|
||||||
|
ingControllerFormValues || [],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Allow selected
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIngressClassDescription() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col !text-xs text-muted w-full">
|
||||||
|
<div className="mt-1">{description}</div>
|
||||||
|
{ingressControllers &&
|
||||||
|
ingControllerFormValues &&
|
||||||
|
isUnsavedChanges(ingressControllers, ingControllerFormValues) && (
|
||||||
|
<span className="flex items-center text-warning mt-1">
|
||||||
|
<Icon icon="alert-triangle" feather className="!mr-1" />
|
||||||
|
<span className="text-warning">Unsaved changes.</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIngressControllers(
|
||||||
|
selectedRows: IngressControllerClassMap[],
|
||||||
|
ingControllerFormValues: IngressControllerClassMap[],
|
||||||
|
availability: boolean
|
||||||
|
) {
|
||||||
|
const updatedIngressControllers = getUpdatedIngressControllers(
|
||||||
|
selectedRows,
|
||||||
|
ingControllerFormValues || [],
|
||||||
|
availability
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ingressControllers && ingressControllers.length) {
|
||||||
|
const newAllowed = updatedIngressControllers.map(
|
||||||
|
(ingController) => ingController.Availability
|
||||||
|
);
|
||||||
|
if (view === 'namespace') {
|
||||||
|
setIngControllerFormValues(updatedIngressControllers);
|
||||||
|
onChangeAvailability(updatedIngressControllers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedControllersToDisallow = ingressControllers.filter(
|
||||||
|
(ingController, index) => {
|
||||||
|
// if any of the current controllers are allowed, and are used, then become disallowed, then add the controller to a new list
|
||||||
|
if (
|
||||||
|
ingController.Availability &&
|
||||||
|
ingController.Used &&
|
||||||
|
!newAllowed[index]
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (usedControllersToDisallow.length > 0) {
|
||||||
|
const usedControllerHtmlListItems = usedControllersToDisallow.map(
|
||||||
|
(controller) => `<li>${controller.ClassName}</li>`
|
||||||
|
);
|
||||||
|
const usedControllerHtmlList = `<ul class="ml-6">${usedControllerHtmlListItems.join(
|
||||||
|
''
|
||||||
|
)}</ul>`;
|
||||||
|
confirmWarn({
|
||||||
|
title: 'Disallow in-use ingress controllers?',
|
||||||
|
message: `
|
||||||
|
<div>
|
||||||
|
<p>There are ingress controllers you want to disallow that are in use:</p>
|
||||||
|
${usedControllerHtmlList}
|
||||||
|
<p>No new ingress rules can be created for the disallowed controllers.</p>
|
||||||
|
</div>`,
|
||||||
|
buttons: {
|
||||||
|
cancel: {
|
||||||
|
label: 'Cancel',
|
||||||
|
className: 'btn-default',
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
label: 'Disallow',
|
||||||
|
className: 'btn-warning',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
setIngControllerFormValues(updatedIngressControllers);
|
||||||
|
onChangeAvailability(updatedIngressControllers);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIngControllerFormValues(updatedIngressControllers);
|
||||||
|
onChangeAvailability(updatedIngressControllers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnsavedChanges(
|
||||||
|
oldIngressControllers: IngressControllerClassMap[],
|
||||||
|
newIngressControllers: IngressControllerClassMap[]
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < oldIngressControllers.length; i += 1) {
|
||||||
|
if (
|
||||||
|
oldIngressControllers[i].Availability !==
|
||||||
|
newIngressControllers[i].Availability
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdatedIngressControllers(
|
||||||
|
selectedRows: IngressControllerClassMap[],
|
||||||
|
allRows: IngressControllerClassMap[],
|
||||||
|
allow: boolean
|
||||||
|
) {
|
||||||
|
const selectedRowClassNames = selectedRows.map((row) => row.ClassName);
|
||||||
|
const updatedIngressControllers = allRows?.map((row) => {
|
||||||
|
if (selectedRowClassNames.includes(row.ClassName)) {
|
||||||
|
return { ...row, Availability: allow };
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
return updatedIngressControllers;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { createRowContext } from '@@/datatables/RowContext';
|
||||||
|
|
||||||
|
interface RowContextState {
|
||||||
|
environment: Environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
||||||
|
|
||||||
|
export { RowProvider, useRowContext };
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Badge } from '@@/Badge';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import type { IngressControllerClassMap } from '../../types';
|
||||||
|
|
||||||
|
export const availability: Column<IngressControllerClassMap> = {
|
||||||
|
Header: 'Availability',
|
||||||
|
accessor: 'Availability',
|
||||||
|
Cell: AvailailityCell,
|
||||||
|
id: 'availability',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
sortInverted: true,
|
||||||
|
sortType: 'basic',
|
||||||
|
Filter: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function AvailailityCell({ value }: CellProps<IngressControllerClassMap>) {
|
||||||
|
return (
|
||||||
|
<Badge type={value ? 'success' : 'danger'}>
|
||||||
|
<Icon icon={value ? 'check' : 'x'} feather className="!mr-1" />
|
||||||
|
{value ? 'Allowed' : 'Disallowed'}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { availability } from './availability';
|
||||||
|
import { type } from './type';
|
||||||
|
import { name } from './name';
|
||||||
|
|
||||||
|
export function useColumns() {
|
||||||
|
return useMemo(() => [name, type, availability], []);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Badge } from '@@/Badge';
|
||||||
|
|
||||||
|
import type { IngressControllerClassMap } from '../../types';
|
||||||
|
|
||||||
|
export const name: Column<IngressControllerClassMap> = {
|
||||||
|
Header: 'Ingress class',
|
||||||
|
accessor: 'ClassName',
|
||||||
|
Cell: NameCell,
|
||||||
|
id: 'name',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
Filter: () => null,
|
||||||
|
sortType: 'string',
|
||||||
|
};
|
||||||
|
|
||||||
|
function NameCell({ row }: CellProps<IngressControllerClassMap>) {
|
||||||
|
return (
|
||||||
|
<span className="flex flex-nowrap">
|
||||||
|
{row.original.ClassName}
|
||||||
|
{row.original.New && <Badge className="ml-1">Newly detected</Badge>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
|
import type { IngressControllerClassMap } from '../../types';
|
||||||
|
|
||||||
|
export const type: Column<IngressControllerClassMap> = {
|
||||||
|
Header: 'Ingress controller type',
|
||||||
|
accessor: 'Type',
|
||||||
|
Cell: ({ row }: CellProps<IngressControllerClassMap>) =>
|
||||||
|
row.original.Type || '-',
|
||||||
|
id: 'type',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
Filter: () => null,
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
import create from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { keyBuilder } from '@/portainer/hooks/useLocalStorage';
|
||||||
|
import {
|
||||||
|
paginationSettings,
|
||||||
|
sortableSettings,
|
||||||
|
} from '@/react/components/datatables/types';
|
||||||
|
|
||||||
|
import { TableSettings } from './types';
|
||||||
|
|
||||||
|
export const TRUNCATE_LENGTH = 32;
|
||||||
|
|
||||||
|
export function createStore(storageKey: string) {
|
||||||
|
return create<TableSettings>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...sortableSettings(set),
|
||||||
|
...paginationSettings(set),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: keyBuilder(storageKey),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './IngressClassDatatable';
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {
|
||||||
|
PaginationTableSettings,
|
||||||
|
SortableTableSettings,
|
||||||
|
} from '@/react/components/datatables/types';
|
||||||
|
|
||||||
|
export interface TableSettings
|
||||||
|
extends SortableTableSettings,
|
||||||
|
PaginationTableSettings {}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import {
|
||||||
|
KubernetesApiListResponse,
|
||||||
|
V1IngressClass,
|
||||||
|
} from '@/react/kubernetes/services/kubernetes/types';
|
||||||
|
|
||||||
|
export async function getAllIngressClasses(environmentId: EnvironmentId) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { items },
|
||||||
|
} = await axios.get<KubernetesApiListResponse<V1IngressClass[]>>(
|
||||||
|
urlBuilder(environmentId)
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlBuilder(environmentId: EnvironmentId) {
|
||||||
|
return `endpoints/${environmentId}/kubernetes/apis/networking.k8s.io/v1/ingressclasses`;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
type SupportedIngControllerNames = 'nginx' | 'traefik' | 'unknown';
|
||||||
|
|
||||||
|
export interface IngressControllerClassMap extends Record<string, unknown> {
|
||||||
|
Name: string;
|
||||||
|
ClassName: string;
|
||||||
|
Type: SupportedIngControllerNames;
|
||||||
|
Availability: boolean;
|
||||||
|
New: boolean;
|
||||||
|
Used: boolean; // if the controller is used by any ingress in the cluster
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import PortainerError from '@/portainer/error';
|
||||||
|
import axios from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
import { IngressControllerClassMap } from '../types';
|
||||||
|
|
||||||
|
// get all supported ingress classes and controllers for the cluster
|
||||||
|
// allowedOnly set to true will hide globally disallowed ingresscontrollers
|
||||||
|
export async function getIngressControllerClassMap({
|
||||||
|
environmentId,
|
||||||
|
namespace,
|
||||||
|
allowedOnly,
|
||||||
|
}: {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
namespace?: string;
|
||||||
|
allowedOnly?: boolean;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const { data: controllerMaps } = await axios.get<
|
||||||
|
IngressControllerClassMap[]
|
||||||
|
>(
|
||||||
|
buildUrl(environmentId, namespace),
|
||||||
|
allowedOnly ? { params: { allowedOnly: true } } : undefined
|
||||||
|
);
|
||||||
|
return controllerMaps;
|
||||||
|
} catch (e) {
|
||||||
|
throw new PortainerError('Unable to get ingress controllers.', e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all supported ingress classes and controllers for the cluster
|
||||||
|
export async function updateIngressControllerClassMap(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
ingressControllerClassMap: IngressControllerClassMap[],
|
||||||
|
namespace?: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: controllerMaps } = await axios.put<
|
||||||
|
IngressControllerClassMap[]
|
||||||
|
>(buildUrl(environmentId, namespace), ingressControllerClassMap);
|
||||||
|
return controllerMaps;
|
||||||
|
} catch (e) {
|
||||||
|
throw new PortainerError(
|
||||||
|
'Unable to update ingress controllers.',
|
||||||
|
e as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
|
||||||
|
let url = `kubernetes/${environmentId}/`;
|
||||||
|
if (namespace) {
|
||||||
|
url += `namespaces/${namespace}/`;
|
||||||
|
}
|
||||||
|
url += 'ingresscontrollers';
|
||||||
|
return url;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export * from './v1IngressClass';
|
||||||
|
export * from './v1ObjectMeta';
|
||||||
|
|
||||||
|
export type KubernetesApiListResponse<T> = {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
items: T;
|
||||||
|
metadata: {
|
||||||
|
resourceVersion?: string;
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { V1ObjectMeta } from './v1ObjectMeta';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IngressClassParametersReference identifies an API object. This can be used to specify a cluster or namespace-scoped resource.
|
||||||
|
*/
|
||||||
|
type V1IngressClassParametersReference = {
|
||||||
|
/**
|
||||||
|
* APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.
|
||||||
|
*/
|
||||||
|
apiGroup?: string;
|
||||||
|
/**
|
||||||
|
* Kind is the type of resource being referenced.
|
||||||
|
*/
|
||||||
|
kind: string;
|
||||||
|
/**
|
||||||
|
* Name is the name of resource being referenced.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Namespace is the namespace of the resource being referenced. This field is required when scope is set to \"Namespace\" and must be unset when scope is set to \"Cluster\".
|
||||||
|
*/
|
||||||
|
namespace?: string;
|
||||||
|
/**
|
||||||
|
* Scope represents if this refers to a cluster or namespace scoped resource. This may be set to \"Cluster\" (default) or \"Namespace\".
|
||||||
|
*/
|
||||||
|
scope?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type V1IngressClassSpec = {
|
||||||
|
controller?: string;
|
||||||
|
parameters?: V1IngressClassParametersReference;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IngressClass represents the class of the Ingress, referenced by the Ingress Spec. The `ingressclass.kubernetes.io/is-default-class` annotation can be used to indicate that an IngressClass should be considered default. When a single IngressClass resource has this annotation set to true, new Ingress resources without a class specified will be assigned this default class.
|
||||||
|
*/
|
||||||
|
export type V1IngressClass = {
|
||||||
|
/**
|
||||||
|
* APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
*/
|
||||||
|
apiVersion?: string;
|
||||||
|
/**
|
||||||
|
* Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
*/
|
||||||
|
kind?: string;
|
||||||
|
metadata?: V1ObjectMeta;
|
||||||
|
spec?: V1IngressClassSpec;
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
// type definitions taken from https://github.com/kubernetes-client/javascript/blob/master/src/gen/model/v1ObjectMeta.ts
|
||||||
|
// and simplified to only include the types we need
|
||||||
|
|
||||||
|
export type V1ObjectMeta = {
|
||||||
|
/**
|
||||||
|
* Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations
|
||||||
|
*/
|
||||||
|
annotations?: { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels
|
||||||
|
*/
|
||||||
|
labels?: { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* Deprecated: ClusterName is a legacy field that was always cleared by the system and never used; it will be removed completely in 1.25. The name in the go struct is changed to help clients detect accidental use.
|
||||||
|
*/
|
||||||
|
clusterName?: string;
|
||||||
|
/**
|
||||||
|
* Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. Must be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces
|
||||||
|
*/
|
||||||
|
namespace?: string;
|
||||||
|
/**
|
||||||
|
* An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
|
||||||
|
*/
|
||||||
|
resourceVersion?: string;
|
||||||
|
/**
|
||||||
|
* UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. Populated by the system. Read-only. More info: http://kubernetes.io/docs/user-guide/identifiers#uids
|
||||||
|
*/
|
||||||
|
uid?: string;
|
||||||
|
};
|
Loading…
Reference in New Issue