mirror of https://github.com/portainer/portainer
feat(ingress): ingresses datatable with add/edit ingresses EE-2615 (#7672)
parent
393d1fc91d
commit
ef1d648c07
|
@ -0,0 +1,17 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sConfigMapOrSecret struct {
|
||||||
|
UID string `json:"UID"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Namespace string `json:"Namespace"`
|
||||||
|
CreationDate string `json:"CreationDate"`
|
||||||
|
Annotations map[string]string `json:"Annotations"`
|
||||||
|
Data map[string]string `json:"Data"`
|
||||||
|
Applications []string `json:"Applications"`
|
||||||
|
IsSecret bool `json:"IsSecret"`
|
||||||
|
|
||||||
|
// SecretType will be an empty string for config maps.
|
||||||
|
SecretType string `json:"SecretType"`
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,77 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sIngressController struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
ClassName string `json:"ClassName"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Availability bool `json:"Availability"`
|
||||||
|
New bool `json:"New"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sIngressControllers []K8sIngressController
|
||||||
|
|
||||||
|
K8sIngressInfo struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
UID string `json:"UID"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Namespace string `json:"Namespace"`
|
||||||
|
ClassName string `json:"ClassName"`
|
||||||
|
Annotations map[string]string `json:"Annotations"`
|
||||||
|
Hosts []string `json:"Hosts"`
|
||||||
|
Paths []K8sIngressPath `json:"Paths"`
|
||||||
|
TLS []K8sIngressTLS `json:"TLS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sIngressTLS struct {
|
||||||
|
Hosts []string `json:"Hosts"`
|
||||||
|
SecretName string `json:"SecretName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sIngressPath struct {
|
||||||
|
IngressName string `json:"IngressName"`
|
||||||
|
Host string `json:"Host"`
|
||||||
|
ServiceName string `json:"ServiceName"`
|
||||||
|
Port int `json:"Port"`
|
||||||
|
Path string `json:"Path"`
|
||||||
|
PathType string `json:"PathType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// K8sIngressDeleteRequests is a mapping of namespace names to a slice of
|
||||||
|
// ingress names.
|
||||||
|
K8sIngressDeleteRequests map[string][]string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r K8sIngressControllers) Validate(request *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r K8sIngressInfo) Validate(request *http.Request) error {
|
||||||
|
if r.Name == "" {
|
||||||
|
return errors.New("missing ingress name from the request payload")
|
||||||
|
}
|
||||||
|
if r.Namespace == "" {
|
||||||
|
return errors.New("missing ingress Namespace from the request payload")
|
||||||
|
}
|
||||||
|
if r.ClassName == "" {
|
||||||
|
return errors.New("missing ingress ClassName from the request payload")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r K8sIngressDeleteRequests) Validate(request *http.Request) error {
|
||||||
|
if len(r) == 0 {
|
||||||
|
return errors.New("missing deletion request list in payload")
|
||||||
|
}
|
||||||
|
for ns := range r {
|
||||||
|
if len(ns) == 0 {
|
||||||
|
return errors.New("deletion given with empty namespace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type K8sNamespaceInfo struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Annotations map[string]string `json:"Annotations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *K8sNamespaceInfo) Validate(request *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
K8sServiceInfo struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
UID string `json:"UID"`
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Namespace string `json:"Namespace"`
|
||||||
|
Annotations map[string]string `json:"Annotations"`
|
||||||
|
CreationTimestamp string `json:"CreationTimestamp"`
|
||||||
|
Labels map[string]string `json:"Labels"`
|
||||||
|
AllocateLoadBalancerNodePorts *bool `json:"AllocateLoadBalancerNodePorts,omitempty"`
|
||||||
|
Ports []K8sServicePort `json:"Ports"`
|
||||||
|
Selector map[string]string `json:"Selector"`
|
||||||
|
IngressStatus []K8sServiceIngress `json:"IngressStatus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sServicePort struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
NodePort int `json:"NodePort"`
|
||||||
|
Port int `json:"Port"`
|
||||||
|
Protocol string `json:"Protocol"`
|
||||||
|
TargetPort int `json:"TargetPort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sServiceIngress struct {
|
||||||
|
IP string `json:"IP"`
|
||||||
|
Host string `json:"Host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// K8sServiceDeleteRequests is a mapping of namespace names to a slice of
|
||||||
|
// service names.
|
||||||
|
K8sServiceDeleteRequests map[string][]string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *K8sServiceInfo) Validate(request *http.Request) error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("missing service name from the request payload")
|
||||||
|
}
|
||||||
|
if s.Namespace == "" {
|
||||||
|
return errors.New("missing service namespace from the request payload")
|
||||||
|
}
|
||||||
|
if s.Ports == nil {
|
||||||
|
return errors.New("missing service ports from the request payload")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r K8sServiceDeleteRequests) Validate(request *http.Request) error {
|
||||||
|
if len(r) == 0 {
|
||||||
|
return errors.New("missing deletion request list in payload")
|
||||||
|
}
|
||||||
|
for ns := range r {
|
||||||
|
if len(ns) == 0 {
|
||||||
|
return errors.New("deletion given with empty namespace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -52,7 +52,9 @@
|
||||||
"IsEdgeDevice": false,
|
"IsEdgeDevice": false,
|
||||||
"Kubernetes": {
|
"Kubernetes": {
|
||||||
"Configuration": {
|
"Configuration": {
|
||||||
|
"EnableResourceOverCommit": false,
|
||||||
"IngressClasses": null,
|
"IngressClasses": null,
|
||||||
|
"ResourceOverCommitPercentage": 0,
|
||||||
"RestrictDefaultNamespace": false,
|
"RestrictDefaultNamespace": false,
|
||||||
"StorageClasses": null,
|
"StorageClasses": null,
|
||||||
"UseLoadBalancer": false,
|
"UseLoadBalancer": false,
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, configmaps)
|
||||||
|
}
|
|
@ -2,11 +2,15 @@ package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/portainer/portainer/api/kubernetes"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/http/middlewares"
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
@ -23,10 +27,11 @@ type Handler struct {
|
||||||
jwtService dataservices.JWTService
|
jwtService dataservices.JWTService
|
||||||
kubernetesClientFactory *cli.ClientFactory
|
kubernetesClientFactory *cli.ClientFactory
|
||||||
kubeClusterAccessService kubernetes.KubeClusterAccessService
|
kubeClusterAccessService kubernetes.KubeClusterAccessService
|
||||||
|
KubernetesClient portainer.KubeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
||||||
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubeClusterAccessService kubernetes.KubeClusterAccessService, kubernetesClientFactory *cli.ClientFactory) *Handler {
|
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, jwtService dataservices.JWTService, kubeClusterAccessService kubernetes.KubeClusterAccessService, kubernetesClientFactory *cli.ClientFactory, kubernetesClient portainer.KubeClient) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
authorizationService: authorizationService,
|
authorizationService: authorizationService,
|
||||||
|
@ -34,6 +39,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
kubeClusterAccessService: kubeClusterAccessService,
|
kubeClusterAccessService: kubeClusterAccessService,
|
||||||
kubernetesClientFactory: kubernetesClientFactory,
|
kubernetesClientFactory: kubernetesClientFactory,
|
||||||
|
KubernetesClient: kubernetesClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
|
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
|
||||||
|
@ -45,15 +51,32 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
||||||
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
|
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
|
||||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||||
endpointRouter.Use(kubeOnlyMiddleware)
|
endpointRouter.Use(kubeOnlyMiddleware)
|
||||||
|
endpointRouter.Use(h.kubeClient)
|
||||||
|
|
||||||
endpointRouter.PathPrefix("/nodes_limits").Handler(
|
endpointRouter.PathPrefix("/nodes_limits").Handler(httperror.LoggerHandler(h.getKubernetesNodesLimits)).Methods(http.MethodGet)
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).Methods(http.MethodGet)
|
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllers)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllers)).Methods(http.MethodPut)
|
||||||
|
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
|
||||||
|
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
|
||||||
|
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
|
||||||
|
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
|
||||||
|
endpointRouter.Path("/namespaces").Handler(httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
|
||||||
|
endpointRouter.Path("/namespace/{namespace}").Handler(httperror.LoggerHandler(h.deleteKubernetesNamespaces)).Methods(http.MethodDelete)
|
||||||
|
|
||||||
// namespaces
|
// namespaces
|
||||||
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
|
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
|
||||||
// to keep it simple, we've decided to leave it like this.
|
// to keep it simple, we've decided to leave it like this.
|
||||||
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||||
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
||||||
|
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
|
||||||
|
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
|
||||||
|
namespaceRouter.Handle("/configmaps", httperror.LoggerHandler(h.getKubernetesConfigMaps)).Methods(http.MethodGet)
|
||||||
|
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.createKubernetesIngress)).Methods(http.MethodPost)
|
||||||
|
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.updateKubernetesIngress)).Methods(http.MethodPut)
|
||||||
|
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.getKubernetesIngresses)).Methods(http.MethodGet)
|
||||||
|
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.createKubernetesService)).Methods(http.MethodPost)
|
||||||
|
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.updateKubernetesService)).Methods(http.MethodPut)
|
||||||
|
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.getKubernetesServices)).Methods(http.MethodGet)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
@ -75,3 +98,51 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler {
|
||||||
next.ServeHTTP(rw, request)
|
next.ServeHTTP(rw, request)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) kubeClient(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteError(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Invalid environment identifier route variable",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
httperror.WriteError(
|
||||||
|
w,
|
||||||
|
http.StatusNotFound,
|
||||||
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteError(
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
"Unable to find an environment with the specified identifier inside the database",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler.kubernetesClientFactory == nil {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kubeCli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteError(
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
"Unable to create Kubernetes client",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
handler.KubernetesClient = kubeCli
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to create Kubernetes client",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers := cli.GetIngressControllers()
|
||||||
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
for i := range controllers {
|
||||||
|
controllers[i].Availability = true
|
||||||
|
controllers[i].New = true
|
||||||
|
|
||||||
|
// Check if the controller is blocked globally.
|
||||||
|
for _, a := range existingClasses {
|
||||||
|
controllers[i].New = false
|
||||||
|
if controllers[i].ClassName != a.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
controllers[i].New = false
|
||||||
|
|
||||||
|
// Skip over non-global blocks.
|
||||||
|
if len(a.BlockedNamespaces) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if controllers[i].ClassName == a.Name {
|
||||||
|
controllers[i].Availability = !a.Blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Update existingClasses to take care of New and remove no longer
|
||||||
|
// existing classes.
|
||||||
|
}
|
||||||
|
return response.JSON(w, controllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to create Kubernetes client",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers := cli.GetIngressControllers()
|
||||||
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
for i := range controllers {
|
||||||
|
controllers[i].Availability = true
|
||||||
|
controllers[i].New = true
|
||||||
|
|
||||||
|
// Check if the controller is blocked globally or in the current
|
||||||
|
// namespace.
|
||||||
|
for _, a := range existingClasses {
|
||||||
|
if controllers[i].ClassName != a.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
controllers[i].New = false
|
||||||
|
|
||||||
|
// If it's not blocked we're all done!
|
||||||
|
if !a.Blocked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global blocks.
|
||||||
|
if len(a.BlockedNamespaces) == 0 {
|
||||||
|
controllers[i].Availability = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check the current namespace.
|
||||||
|
for _, ns := range a.BlockedNamespaces {
|
||||||
|
if namespace == ns {
|
||||||
|
controllers[i].Availability = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Update existingClasses to take care of New and remove no longer
|
||||||
|
// existing classes.
|
||||||
|
}
|
||||||
|
return response.JSON(w, controllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressControllers
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
classes := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
for _, p := range payload {
|
||||||
|
for i := range classes {
|
||||||
|
if p.ClassName == classes[i].Name {
|
||||||
|
classes[i].Blocked = !p.Availability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = classes
|
||||||
|
fmt.Printf("%#v\n", endpoint.Kubernetes.Configuration.IngressClasses)
|
||||||
|
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpointID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to update the BlockedIngressClasses inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressControllers
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
classes := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
PayloadLoop:
|
||||||
|
for _, p := range payload {
|
||||||
|
for i := range classes {
|
||||||
|
if p.ClassName == classes[i].Name {
|
||||||
|
if p.Availability == true {
|
||||||
|
classes[i].Blocked = false
|
||||||
|
classes[i].BlockedNamespaces = []string{}
|
||||||
|
continue PayloadLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
classes[i].Blocked = true
|
||||||
|
for _, ns := range classes[i].BlockedNamespaces {
|
||||||
|
if namespace == ns {
|
||||||
|
continue PayloadLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classes[i].BlockedNamespaces = append(
|
||||||
|
classes[i].BlockedNamespaces,
|
||||||
|
namespace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = classes
|
||||||
|
fmt.Printf("%#v\n", endpoint.Kubernetes.Configuration.IngressClasses)
|
||||||
|
err = handler.dataStore.Endpoint().UpdateEndpoint(
|
||||||
|
portainer.EndpointID(endpointID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to update the BlockedIngressClasses inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
ingresses, err := cli.GetIngresses(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, ingresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.CreateIngress(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sIngressDeleteRequests
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteIngresses(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.UpdateIngress(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
namespaces, err := cli.GetNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, namespaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sNamespaceInfo
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.CreateNamespace(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteNamespace(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sNamespaceInfo
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.UpdateNamespace(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
services, err := cli.GetServices(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve services",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, services)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sServiceInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.CreateService(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sServiceDeleteRequests
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteServices(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sServiceInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.UpdateService(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -186,7 +186,7 @@ func (server *Server) Start() error {
|
||||||
endpointProxyHandler.ProxyManager = server.ProxyManager
|
endpointProxyHandler.ProxyManager = server.ProxyManager
|
||||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
|
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory, nil)
|
||||||
|
|
||||||
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
|
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetConfigMapsAndSecrets gets all the ConfigMaps AND all the Secrets for a
|
||||||
|
// given namespace in a k8s endpoint. The result is a list of both config maps
|
||||||
|
// and secrets. The IsSecret boolean property indicates if a given struct is a
|
||||||
|
// secret or configmap.
|
||||||
|
func (kcl *KubeClient) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) {
|
||||||
|
mapsClient := kcl.cli.CoreV1().ConfigMaps(namespace)
|
||||||
|
mapsList, err := mapsClient.List(context.Background(), v1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Applications
|
||||||
|
var combined []models.K8sConfigMapOrSecret
|
||||||
|
for _, m := range mapsList.Items {
|
||||||
|
var cm models.K8sConfigMapOrSecret
|
||||||
|
cm.UID = string(m.UID)
|
||||||
|
cm.Name = m.Name
|
||||||
|
cm.Namespace = m.Namespace
|
||||||
|
cm.Annotations = m.Annotations
|
||||||
|
cm.Data = m.Data
|
||||||
|
cm.CreationDate = m.CreationTimestamp.Time.UTC().Format(time.RFC3339)
|
||||||
|
cm.IsSecret = false
|
||||||
|
combined = append(combined, cm)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretClient := kcl.cli.CoreV1().Secrets(namespace)
|
||||||
|
secretList, err := secretClient.List(context.Background(), v1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range secretList.Items {
|
||||||
|
var secret models.K8sConfigMapOrSecret
|
||||||
|
secret.UID = string(s.UID)
|
||||||
|
secret.Name = s.Name
|
||||||
|
secret.Namespace = s.Namespace
|
||||||
|
secret.Annotations = s.Annotations
|
||||||
|
secret.Data = msbToMss(s.Data)
|
||||||
|
secret.CreationDate = s.CreationTimestamp.Time.UTC().Format(time.RFC3339)
|
||||||
|
secret.IsSecret = true
|
||||||
|
secret.SecretType = string(s.Type)
|
||||||
|
combined = append(combined, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
return combined, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func msbToMss(msa map[string][]byte) map[string]string {
|
||||||
|
mss := make(map[string]string, len(msa))
|
||||||
|
for k, v := range msa {
|
||||||
|
mss[k] = string(v)
|
||||||
|
}
|
||||||
|
return mss
|
||||||
|
}
|
|
@ -0,0 +1,243 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
netv1 "k8s.io/api/networking/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers {
|
||||||
|
var controllers []models.K8sIngressController
|
||||||
|
|
||||||
|
// We know that each existing class points to a controller so we can start
|
||||||
|
// by collecting these easy ones.
|
||||||
|
classClient := kcl.cli.NetworkingV1().IngressClasses()
|
||||||
|
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, class := range classList.Items {
|
||||||
|
var controller models.K8sIngressController
|
||||||
|
controller.Name = class.Spec.Controller
|
||||||
|
controller.ClassName = class.Name
|
||||||
|
switch {
|
||||||
|
case strings.Contains(controller.Name, "nginx"):
|
||||||
|
controller.Type = "nginx"
|
||||||
|
case strings.Contains(controller.Name, "traefik"):
|
||||||
|
controller.Type = "traefik"
|
||||||
|
default:
|
||||||
|
controller.Type = "other"
|
||||||
|
}
|
||||||
|
controllers = append(controllers, controller)
|
||||||
|
}
|
||||||
|
return controllers
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
|
||||||
|
// Fetch ingress classes to build a map. We will later use the map to lookup
|
||||||
|
// each ingresses "type".
|
||||||
|
classes := make(map[string]string)
|
||||||
|
classClient := kcl.cli.NetworkingV1().IngressClasses()
|
||||||
|
classList, err := classClient.List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, class := range classList.Items {
|
||||||
|
// Write the ingress classes "type" to our map.
|
||||||
|
classes[class.Name] = class.Spec.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch each ingress.
|
||||||
|
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
||||||
|
ingressList, err := ingressClient.List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var infos []models.K8sIngressInfo
|
||||||
|
for _, ingress := range ingressList.Items {
|
||||||
|
ingressClass := ingress.Spec.IngressClassName
|
||||||
|
var info models.K8sIngressInfo
|
||||||
|
info.Name = ingress.Name
|
||||||
|
info.UID = string(ingress.UID)
|
||||||
|
info.Namespace = namespace
|
||||||
|
info.ClassName = ""
|
||||||
|
if ingressClass != nil {
|
||||||
|
info.ClassName = *ingressClass
|
||||||
|
}
|
||||||
|
info.Type = classes[info.ClassName]
|
||||||
|
info.Annotations = ingress.Annotations
|
||||||
|
|
||||||
|
// Gather TLS information.
|
||||||
|
for _, v := range ingress.Spec.TLS {
|
||||||
|
var tls models.K8sIngressTLS
|
||||||
|
tls.Hosts = v.Hosts
|
||||||
|
tls.SecretName = v.SecretName
|
||||||
|
info.TLS = append(info.TLS, tls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather list of paths and hosts.
|
||||||
|
hosts := make(map[string]struct{})
|
||||||
|
for _, r := range ingress.Spec.Rules {
|
||||||
|
if r.HTTP == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are multiple paths per rule. We want to flatten the list
|
||||||
|
// for our frontend.
|
||||||
|
for _, p := range r.HTTP.Paths {
|
||||||
|
var path models.K8sIngressPath
|
||||||
|
path.IngressName = info.Name
|
||||||
|
path.Host = r.Host
|
||||||
|
|
||||||
|
// We collect all exiting hosts in a map to avoid duplicates.
|
||||||
|
// Then, later convert it to a slice for the frontend.
|
||||||
|
hosts[r.Host] = struct{}{}
|
||||||
|
|
||||||
|
path.Path = p.Path
|
||||||
|
path.PathType = string(*p.PathType)
|
||||||
|
path.ServiceName = p.Backend.Service.Name
|
||||||
|
path.Port = int(p.Backend.Service.Port.Number)
|
||||||
|
info.Paths = append(info.Paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store list of hosts.
|
||||||
|
for host := range hosts {
|
||||||
|
info.Hosts = append(info.Hosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
infos = append(infos, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInfo) error {
|
||||||
|
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
||||||
|
var ingress netv1.Ingress
|
||||||
|
|
||||||
|
ingress.Name = info.Name
|
||||||
|
ingress.Namespace = info.Namespace
|
||||||
|
ingress.Spec.IngressClassName = &info.ClassName
|
||||||
|
ingress.Annotations = info.Annotations
|
||||||
|
|
||||||
|
// Store TLS information.
|
||||||
|
var tls []netv1.IngressTLS
|
||||||
|
for _, i := range info.TLS {
|
||||||
|
tls = append(tls, netv1.IngressTLS{
|
||||||
|
Hosts: i.Hosts,
|
||||||
|
SecretName: i.SecretName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ingress.Spec.TLS = tls
|
||||||
|
|
||||||
|
// Parse "paths" into rules with paths.
|
||||||
|
rules := make(map[string][]netv1.HTTPIngressPath)
|
||||||
|
for _, path := range info.Paths {
|
||||||
|
pathType := netv1.PathType(path.PathType)
|
||||||
|
rules[path.Host] = append(rules[path.Host], netv1.HTTPIngressPath{
|
||||||
|
Path: path.Path,
|
||||||
|
PathType: &pathType,
|
||||||
|
Backend: netv1.IngressBackend{
|
||||||
|
Service: &netv1.IngressServiceBackend{
|
||||||
|
Name: path.ServiceName,
|
||||||
|
Port: netv1.ServiceBackendPort{
|
||||||
|
Number: int32(path.Port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for rule, paths := range rules {
|
||||||
|
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
||||||
|
Host: rule,
|
||||||
|
IngressRuleValue: netv1.IngressRuleValue{
|
||||||
|
HTTP: &netv1.HTTPIngressRuleValue{
|
||||||
|
Paths: paths,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ingressClient.Create(context.Background(), &ingress, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteIngresses processes a K8sIngressDeleteRequest by deleting each ingress
|
||||||
|
// in its given namespace.
|
||||||
|
func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) error {
|
||||||
|
var err error
|
||||||
|
for namespace := range reqs {
|
||||||
|
for _, ingress := range reqs[namespace] {
|
||||||
|
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
||||||
|
err = ingressClient.Delete(
|
||||||
|
context.Background(),
|
||||||
|
ingress,
|
||||||
|
metav1.DeleteOptions{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
|
||||||
|
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
||||||
|
var ingress netv1.Ingress
|
||||||
|
|
||||||
|
ingress.Name = info.Name
|
||||||
|
ingress.Namespace = info.Namespace
|
||||||
|
ingress.Spec.IngressClassName = &info.ClassName
|
||||||
|
ingress.Annotations = info.Annotations
|
||||||
|
|
||||||
|
// Store TLS information.
|
||||||
|
var tls []netv1.IngressTLS
|
||||||
|
for _, i := range info.TLS {
|
||||||
|
tls = append(tls, netv1.IngressTLS{
|
||||||
|
Hosts: i.Hosts,
|
||||||
|
SecretName: i.SecretName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ingress.Spec.TLS = tls
|
||||||
|
|
||||||
|
// Parse "paths" into rules with paths.
|
||||||
|
rules := make(map[string][]netv1.HTTPIngressPath)
|
||||||
|
for _, path := range info.Paths {
|
||||||
|
pathType := netv1.PathType(path.PathType)
|
||||||
|
rules[path.Host] = append(rules[path.Host], netv1.HTTPIngressPath{
|
||||||
|
Path: path.Path,
|
||||||
|
PathType: &pathType,
|
||||||
|
Backend: netv1.IngressBackend{
|
||||||
|
Service: &netv1.IngressServiceBackend{
|
||||||
|
Name: path.ServiceName,
|
||||||
|
Port: netv1.ServiceBackendPort{
|
||||||
|
Number: int32(path.Port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for rule, paths := range rules {
|
||||||
|
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
||||||
|
Host: rule,
|
||||||
|
IngressRuleValue: netv1.IngressRuleValue{
|
||||||
|
HTTP: &netv1.HTTPIngressRuleValue{
|
||||||
|
Paths: paths,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ingressClient.Update(context.Background(), &ingress, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
|
@ -2,9 +2,12 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +25,37 @@ func defaultSystemNamespaces() map[string]struct{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNamespaces gets the namespaces in the current k8s environment(endpoint).
|
||||||
|
func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
|
||||||
|
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(map[string]portainer.K8sNamespaceInfo)
|
||||||
|
|
||||||
|
for _, ns := range namespaces.Items {
|
||||||
|
results[ns.Name] = portainer.K8sNamespaceInfo{
|
||||||
|
IsSystem: isSystemNamespace(ns),
|
||||||
|
IsDefault: ns.Name == defaultNamespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateIngress creates a new ingress in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceInfo) error {
|
||||||
|
client := kcl.cli.CoreV1().Namespaces()
|
||||||
|
|
||||||
|
var ns v1.Namespace
|
||||||
|
ns.Name = info.Name
|
||||||
|
ns.Annotations = info.Annotations
|
||||||
|
|
||||||
|
_, err := client.Create(context.Background(), &ns, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func isSystemNamespace(namespace v1.Namespace) bool {
|
func isSystemNamespace(namespace v1.Namespace) bool {
|
||||||
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
|
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
|
||||||
if hasSystemLabel {
|
if hasSystemLabel {
|
||||||
|
@ -72,3 +106,34 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceInfo) error {
|
||||||
|
client := kcl.cli.CoreV1().Namespaces()
|
||||||
|
|
||||||
|
var ns v1.Namespace
|
||||||
|
ns.Name = info.Name
|
||||||
|
ns.Annotations = info.Annotations
|
||||||
|
|
||||||
|
_, err := client.Update(context.Background(), &ns, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kcl *KubeClient) DeleteNamespace(namespace string) error {
|
||||||
|
client := kcl.cli.CoreV1().Namespaces()
|
||||||
|
namespaces, err := client.List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ns := range namespaces.Items {
|
||||||
|
if ns.Name == namespace {
|
||||||
|
return client.Delete(
|
||||||
|
context.Background(),
|
||||||
|
namespace,
|
||||||
|
metav1.DeleteOptions{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("namespace %s not found", namespace)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
models "github.com/portainer/portainer/api/database/models"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetServices gets all the services for a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) GetServices(namespace string) ([]models.K8sServiceInfo, error) {
|
||||||
|
client := kcl.cli.CoreV1().Services(namespace)
|
||||||
|
|
||||||
|
services, err := client.List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []models.K8sServiceInfo
|
||||||
|
|
||||||
|
for _, service := range services.Items {
|
||||||
|
servicePorts := make([]models.K8sServicePort, 0)
|
||||||
|
for _, port := range service.Spec.Ports {
|
||||||
|
servicePorts = append(servicePorts, models.K8sServicePort{
|
||||||
|
Name: port.Name,
|
||||||
|
NodePort: int(port.NodePort),
|
||||||
|
Port: int(port.Port),
|
||||||
|
Protocol: string(port.Protocol),
|
||||||
|
TargetPort: port.TargetPort.IntValue(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ingressStatus := make([]models.K8sServiceIngress, 0)
|
||||||
|
for _, status := range service.Status.LoadBalancer.Ingress {
|
||||||
|
ingressStatus = append(ingressStatus, models.K8sServiceIngress{
|
||||||
|
IP: status.IP,
|
||||||
|
Host: status.Hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, models.K8sServiceInfo{
|
||||||
|
Name: service.Name,
|
||||||
|
UID: string(service.GetUID()),
|
||||||
|
Type: string(service.Spec.Type),
|
||||||
|
Namespace: service.Namespace,
|
||||||
|
CreationTimestamp: service.GetCreationTimestamp().String(),
|
||||||
|
AllocateLoadBalancerNodePorts: service.Spec.AllocateLoadBalancerNodePorts,
|
||||||
|
Ports: servicePorts,
|
||||||
|
IngressStatus: ingressStatus,
|
||||||
|
Labels: service.GetLabels(),
|
||||||
|
Annotations: service.GetAnnotations(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateService creates a new service in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) CreateService(namespace string, info models.K8sServiceInfo) error {
|
||||||
|
ServiceClient := kcl.cli.CoreV1().Services(namespace)
|
||||||
|
var service v1.Service
|
||||||
|
|
||||||
|
service.Name = info.Name
|
||||||
|
service.Spec.Type = v1.ServiceType(info.Type)
|
||||||
|
service.Namespace = info.Namespace
|
||||||
|
service.Annotations = info.Annotations
|
||||||
|
service.Labels = info.Labels
|
||||||
|
service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
|
||||||
|
service.Spec.Selector = info.Selector
|
||||||
|
|
||||||
|
// Set ports.
|
||||||
|
for _, p := range info.Ports {
|
||||||
|
var port v1.ServicePort
|
||||||
|
port.Name = p.Name
|
||||||
|
port.NodePort = int32(p.NodePort)
|
||||||
|
port.Port = int32(p.Port)
|
||||||
|
port.Protocol = v1.Protocol(p.Protocol)
|
||||||
|
port.TargetPort = intstr.FromInt(p.TargetPort)
|
||||||
|
service.Spec.Ports = append(service.Spec.Ports, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ingresses.
|
||||||
|
for _, i := range info.IngressStatus {
|
||||||
|
var ing v1.LoadBalancerIngress
|
||||||
|
ing.IP = i.IP
|
||||||
|
ing.Hostname = i.Host
|
||||||
|
service.Status.LoadBalancer.Ingress = append(
|
||||||
|
service.Status.LoadBalancer.Ingress,
|
||||||
|
ing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ServiceClient.Create(context.Background(), &service, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteServices processes a K8sServiceDeleteRequest by deleting each service
|
||||||
|
// in its given namespace.
|
||||||
|
func (kcl *KubeClient) DeleteServices(reqs models.K8sServiceDeleteRequests) error {
|
||||||
|
var err error
|
||||||
|
for namespace := range reqs {
|
||||||
|
for _, service := range reqs[namespace] {
|
||||||
|
serviceClient := kcl.cli.CoreV1().Services(namespace)
|
||||||
|
err = serviceClient.Delete(
|
||||||
|
context.Background(),
|
||||||
|
service,
|
||||||
|
metav1.DeleteOptions{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateService updates service in a given namespace in a k8s endpoint.
|
||||||
|
func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInfo) error {
|
||||||
|
ServiceClient := kcl.cli.CoreV1().Services(namespace)
|
||||||
|
var service v1.Service
|
||||||
|
|
||||||
|
service.Name = info.Name
|
||||||
|
service.Spec.Type = v1.ServiceType(info.Type)
|
||||||
|
service.Namespace = info.Namespace
|
||||||
|
service.Annotations = info.Annotations
|
||||||
|
service.Labels = info.Labels
|
||||||
|
service.Spec.AllocateLoadBalancerNodePorts = info.AllocateLoadBalancerNodePorts
|
||||||
|
service.Spec.Selector = info.Selector
|
||||||
|
|
||||||
|
// Set ports.
|
||||||
|
for _, p := range info.Ports {
|
||||||
|
var port v1.ServicePort
|
||||||
|
port.Name = p.Name
|
||||||
|
port.NodePort = int32(p.NodePort)
|
||||||
|
port.Port = int32(p.Port)
|
||||||
|
port.Protocol = v1.Protocol(p.Protocol)
|
||||||
|
port.TargetPort = intstr.FromInt(p.TargetPort)
|
||||||
|
service.Spec.Ports = append(service.Spec.Ports, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ingresses.
|
||||||
|
for _, i := range info.IngressStatus {
|
||||||
|
var ing v1.LoadBalancerIngress
|
||||||
|
ing.IP = i.IP
|
||||||
|
ing.Hostname = i.Host
|
||||||
|
service.Status.LoadBalancer.Ingress = append(
|
||||||
|
service.Status.LoadBalancer.Ingress,
|
||||||
|
ing,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ServiceClient.Update(context.Background(), &service, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
@ -511,6 +512,11 @@ type (
|
||||||
// JobType represents a job type
|
// JobType represents a job type
|
||||||
JobType int
|
JobType int
|
||||||
|
|
||||||
|
K8sNamespaceInfo struct {
|
||||||
|
IsSystem bool `json:"IsSystem"`
|
||||||
|
IsDefault bool `json:"IsDefault"`
|
||||||
|
}
|
||||||
|
|
||||||
K8sNodeLimits struct {
|
K8sNodeLimits struct {
|
||||||
CPU int64 `json:"CPU"`
|
CPU int64 `json:"CPU"`
|
||||||
Memory int64 `json:"Memory"`
|
Memory int64 `json:"Memory"`
|
||||||
|
@ -542,6 +548,8 @@ type (
|
||||||
KubernetesConfiguration struct {
|
KubernetesConfiguration struct {
|
||||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||||
|
EnableResourceOverCommit bool `json:"EnableResourceOverCommit"`
|
||||||
|
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"`
|
||||||
|
@ -559,6 +567,8 @@ 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"`
|
||||||
|
BlockedNamespaces []string `json:"BlockedNamespaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesShellPod represents a Kubectl Shell details to facilitate pod exec functionality
|
// KubernetesShellPod represents a Kubectl Shell details to facilitate pod exec functionality
|
||||||
|
@ -1330,6 +1340,22 @@ type (
|
||||||
GetServiceAccountBearerToken(userID int) (string, error)
|
GetServiceAccountBearerToken(userID int) (string, error)
|
||||||
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
|
||||||
|
UpdateNamespace(info models.K8sNamespaceInfo) error
|
||||||
|
GetNamespaces() (map[string]K8sNamespaceInfo, error)
|
||||||
|
DeleteNamespace(namespace string) error
|
||||||
|
GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error)
|
||||||
|
CreateIngress(namespace string, info models.K8sIngressInfo) error
|
||||||
|
UpdateIngress(namespace string, info models.K8sIngressInfo) error
|
||||||
|
GetIngresses(namespace string) ([]models.K8sIngressInfo, error)
|
||||||
|
DeleteIngresses(reqs models.K8sIngressDeleteRequests) error
|
||||||
|
CreateService(namespace string, service models.K8sServiceInfo) error
|
||||||
|
UpdateService(namespace string, service models.K8sServiceInfo) error
|
||||||
|
GetServices(namespace string) ([]models.K8sServiceInfo, error)
|
||||||
|
DeleteServices(reqs models.K8sServiceDeleteRequests) error
|
||||||
|
GetIngressControllers() models.K8sIngressControllers
|
||||||
|
|
||||||
HasStackName(namespace string, stackName string) (bool, error)
|
HasStackName(namespace string, stackName string) (bool, error)
|
||||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||||
GetNodesLimits() (K8sNodesLimits, error)
|
GetNodesLimits() (K8sNodesLimits, error)
|
||||||
|
|
|
@ -159,3 +159,7 @@ a.hyperlink {
|
||||||
@apply text-blue-8 hover:text-blue-9;
|
@apply text-blue-8 hover:text-blue-9;
|
||||||
@apply hover:underline cursor-pointer;
|
@apply hover:underline cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.no-decoration {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
|
@ -67,6 +67,36 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ingresses = {
|
||||||
|
name: 'kubernetes.ingresses',
|
||||||
|
url: '/ingresses',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'kubernetesIngressesView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ingressesCreate = {
|
||||||
|
name: 'kubernetes.ingresses.create',
|
||||||
|
url: '/add',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'kubernetesIngressesCreateView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ingressesEdit = {
|
||||||
|
name: 'kubernetes.ingresses.edit',
|
||||||
|
url: '/:namespace/:name/edit',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'kubernetesIngressesCreateView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const applications = {
|
const applications = {
|
||||||
name: 'kubernetes.applications',
|
name: 'kubernetes.applications',
|
||||||
url: '/applications',
|
url: '/applications',
|
||||||
|
@ -376,5 +406,9 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
$stateRegistryProvider.register(registriesAccess);
|
$stateRegistryProvider.register(registriesAccess);
|
||||||
$stateRegistryProvider.register(endpointKubernetesConfiguration);
|
$stateRegistryProvider.register(endpointKubernetesConfiguration);
|
||||||
$stateRegistryProvider.register(endpointKubernetesSecurityConstraint);
|
$stateRegistryProvider.register(endpointKubernetesSecurityConstraint);
|
||||||
|
|
||||||
|
$stateRegistryProvider.register(ingresses);
|
||||||
|
$stateRegistryProvider.register(ingressesCreate);
|
||||||
|
$stateRegistryProvider.register(ingressesEdit);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -15,6 +15,7 @@ class KubernetesConfigurationConverter {
|
||||||
});
|
});
|
||||||
res.ConfigurationOwner = secret.ConfigurationOwner;
|
res.ConfigurationOwner = secret.ConfigurationOwner;
|
||||||
res.IsRegistrySecret = secret.IsRegistrySecret;
|
res.IsRegistrySecret = secret.IsRegistrySecret;
|
||||||
|
res.SecretType = secret.SecretType;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,8 @@ class KubernetesSecretConverter {
|
||||||
|
|
||||||
res.Yaml = yaml ? yaml.data : '';
|
res.Yaml = yaml ? yaml.data : '';
|
||||||
|
|
||||||
|
res.SecretType = payload.type;
|
||||||
|
|
||||||
res.Data = _.map(payload.data, (value, key) => {
|
res.Data = _.map(payload.data, (value, key) => {
|
||||||
const annotations = payload.metadata.annotations ? payload.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] : '';
|
const annotations = payload.metadata.annotations ? payload.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] : '';
|
||||||
const entry = new KubernetesConfigurationFormValuesEntry();
|
const entry = new KubernetesConfigurationFormValuesEntry();
|
||||||
|
|
|
@ -43,6 +43,7 @@ export class KubernetesIngressConverter {
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
res.Hosts.splice(idx, 1, '');
|
res.Hosts.splice(idx, 1, '');
|
||||||
}
|
}
|
||||||
|
res.TLS = data.spec.tls;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ export function KubernetesIngress() {
|
||||||
// PreviousHost: undefined, // only use for RP ingress host edit
|
// PreviousHost: undefined, // only use for RP ingress host edit
|
||||||
Paths: [],
|
Paths: [],
|
||||||
IngressClassName: '',
|
IngressClassName: '',
|
||||||
|
TLS: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ const _KubernetesConfiguration = Object.freeze({
|
||||||
Used: false,
|
Used: false,
|
||||||
Applications: [],
|
Applications: [],
|
||||||
Data: {},
|
Data: {},
|
||||||
|
SecretType: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesConfiguration {
|
export class KubernetesConfiguration {
|
||||||
|
|
|
@ -9,6 +9,7 @@ const _KubernetesApplicationSecret = Object.freeze({
|
||||||
ConfigurationOwner: '',
|
ConfigurationOwner: '',
|
||||||
Yaml: '',
|
Yaml: '',
|
||||||
Data: [],
|
Data: [],
|
||||||
|
SecretType: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesApplicationSecret {
|
export class KubernetesApplicationSecret {
|
||||||
|
|
|
@ -57,6 +57,7 @@ const _KubernetesIngressServiceRoute = Object.freeze({
|
||||||
IngressName: '',
|
IngressName: '',
|
||||||
Path: '',
|
Path: '',
|
||||||
ServiceName: '',
|
ServiceName: '',
|
||||||
|
TLSCert: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesIngressServiceRoute {
|
export class KubernetesIngressServiceRoute {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
export const viewsModule = angular.module(
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
'portainer.kubernetes.react.views',
|
import { IngressesDatatableView } from '@/kubernetes/react/views/networks/ingresses/IngressDatatable';
|
||||||
[]
|
import { CreateIngressView } from '@/kubernetes/react/views/networks/ingresses/CreateIngressView';
|
||||||
).name;
|
|
||||||
|
export const viewsModule = angular
|
||||||
|
.module('portainer.kubernetes.react.views', [])
|
||||||
|
.component('kubernetesIngressesView', r2a(IngressesDatatableView, []))
|
||||||
|
.component('kubernetesIngressesCreateView', r2a(CreateIngressView, [])).name;
|
||||||
|
|
|
@ -0,0 +1,659 @@
|
||||||
|
import { useState, useEffect, useMemo, ReactNode } from 'react';
|
||||||
|
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||||
|
import { useConfigurations } from '@/react/kubernetes/configs/queries';
|
||||||
|
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
|
||||||
|
import { Annotation } from '@/kubernetes/react/views/networks/ingresses/components/annotations/types';
|
||||||
|
import { useServices } from '@/kubernetes/react/views/networks/services/queries';
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Option } from '@@/form-components/Input/Select';
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
|
import { Ingress } from '../types';
|
||||||
|
import {
|
||||||
|
useCreateIngress,
|
||||||
|
useIngresses,
|
||||||
|
useUpdateIngress,
|
||||||
|
useIngressControllers,
|
||||||
|
} from '../queries';
|
||||||
|
|
||||||
|
import { Rule, Path, Host } from './types';
|
||||||
|
import { IngressForm } from './IngressForm';
|
||||||
|
import {
|
||||||
|
prepareTLS,
|
||||||
|
preparePaths,
|
||||||
|
prepareAnnotations,
|
||||||
|
prepareRuleFromIngress,
|
||||||
|
checkIfPathExistsWithHost,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export function CreateIngressView() {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const { params } = useCurrentStateAndParams();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const isEdit = !!params.namespace;
|
||||||
|
|
||||||
|
const [namespace, setNamespace] = useState<string>(params.namespace || '');
|
||||||
|
const [ingressRule, setIngressRule] = useState<Rule>({} as Rule);
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, ReactNode>>(
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
const namespacesResults = useNamespaces(environmentId);
|
||||||
|
|
||||||
|
const servicesResults = useServices(environmentId, namespace);
|
||||||
|
const configResults = useConfigurations(environmentId, namespace);
|
||||||
|
const ingressesResults = useIngresses(
|
||||||
|
environmentId,
|
||||||
|
namespacesResults.data ? Object.keys(namespacesResults?.data || {}) : []
|
||||||
|
);
|
||||||
|
const ingressControllersResults = useIngressControllers(
|
||||||
|
environmentId,
|
||||||
|
namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
const createIngressMutation = useCreateIngress();
|
||||||
|
const updateIngressMutation = useUpdateIngress();
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
(servicesResults.isLoading &&
|
||||||
|
configResults.isLoading &&
|
||||||
|
namespacesResults.isLoading &&
|
||||||
|
ingressesResults.isLoading) ||
|
||||||
|
(isEdit && !ingressRule.IngressName);
|
||||||
|
|
||||||
|
const [ingressNames, ingresses, ruleCounterByNamespace, hostWithTLS] =
|
||||||
|
useMemo((): [
|
||||||
|
string[],
|
||||||
|
Ingress[],
|
||||||
|
Record<string, number>,
|
||||||
|
Record<string, string>
|
||||||
|
] => {
|
||||||
|
const ruleCounterByNamespace: Record<string, number> = {};
|
||||||
|
const hostWithTLS: Record<string, string> = {};
|
||||||
|
ingressesResults.data?.forEach((ingress) => {
|
||||||
|
ingress.TLS?.forEach((tls) => {
|
||||||
|
tls.Hosts.forEach((host) => {
|
||||||
|
hostWithTLS[host] = tls.SecretName;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const ingressNames: string[] = [];
|
||||||
|
ingressesResults.data?.forEach((ing) => {
|
||||||
|
ruleCounterByNamespace[ing.Namespace] =
|
||||||
|
ruleCounterByNamespace[ing.Namespace] || 0;
|
||||||
|
const n = ing.Name.match(/^(.*)-(\d+)$/);
|
||||||
|
if (n?.length === 3) {
|
||||||
|
ruleCounterByNamespace[ing.Namespace] = Math.max(
|
||||||
|
ruleCounterByNamespace[ing.Namespace],
|
||||||
|
Number(n[2])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ing.Namespace === namespace) {
|
||||||
|
ingressNames.push(ing.Name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
ingressNames || [],
|
||||||
|
ingressesResults.data || [],
|
||||||
|
ruleCounterByNamespace,
|
||||||
|
hostWithTLS,
|
||||||
|
];
|
||||||
|
}, [ingressesResults.data, namespace]);
|
||||||
|
|
||||||
|
const namespacesOptions: Option<string>[] = [
|
||||||
|
{ label: 'Select a namespace', value: '' },
|
||||||
|
];
|
||||||
|
Object.entries(namespacesResults?.data || {}).forEach(([ns, val]) => {
|
||||||
|
if (!val.IsSystem) {
|
||||||
|
namespacesOptions.push({
|
||||||
|
label: ns,
|
||||||
|
value: ns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const clusterIpServices = useMemo(
|
||||||
|
() => servicesResults.data?.filter((s) => s.Type === 'ClusterIP'),
|
||||||
|
[servicesResults.data]
|
||||||
|
);
|
||||||
|
const servicesOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
clusterIpServices?.map((service) => ({
|
||||||
|
label: service.Name,
|
||||||
|
value: service.Name,
|
||||||
|
})),
|
||||||
|
[clusterIpServices]
|
||||||
|
);
|
||||||
|
|
||||||
|
const serviceOptions = [
|
||||||
|
{ label: 'Select a service', value: '' },
|
||||||
|
...(servicesOptions || []),
|
||||||
|
];
|
||||||
|
const servicePorts = clusterIpServices
|
||||||
|
? Object.fromEntries(
|
||||||
|
clusterIpServices?.map((service) => [
|
||||||
|
service.Name,
|
||||||
|
service.Ports.map((port) => ({
|
||||||
|
label: String(port.Port),
|
||||||
|
value: String(port.Port),
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const existingIngressClass = useMemo(
|
||||||
|
() =>
|
||||||
|
ingressControllersResults.data?.find(
|
||||||
|
(i) => i.ClassName === ingressRule.IngressClassName
|
||||||
|
),
|
||||||
|
[ingressControllersResults.data, ingressRule.IngressClassName]
|
||||||
|
);
|
||||||
|
const ingressClassOptions: Option<string>[] = [
|
||||||
|
{ label: 'Select an ingress class', value: '' },
|
||||||
|
...(ingressControllersResults.data?.map((cls) => ({
|
||||||
|
label: cls.ClassName,
|
||||||
|
value: cls.ClassName,
|
||||||
|
})) || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!existingIngressClass && ingressRule.IngressClassName) {
|
||||||
|
ingressClassOptions.push({
|
||||||
|
label: !ingressRule.IngressType
|
||||||
|
? `${ingressRule.IngressClassName} - NOT FOUND`
|
||||||
|
: `${ingressRule.IngressClassName} - DISALLOWED`,
|
||||||
|
value: ingressRule.IngressClassName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedConfigs = configResults?.data?.filter(
|
||||||
|
(config) =>
|
||||||
|
config.SecretType === 'kubernetes.io/tls' &&
|
||||||
|
config.Namespace === namespace
|
||||||
|
);
|
||||||
|
const tlsOptions: Option<string>[] = [
|
||||||
|
{ label: 'No TLS', value: '' },
|
||||||
|
...(matchedConfigs?.map((config) => ({
|
||||||
|
label: config.Name,
|
||||||
|
value: config.Name,
|
||||||
|
})) || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!params.name && ingressesResults.data && !ingressRule.IngressName) {
|
||||||
|
// if it is an edit screen, prepare the rule from the ingress
|
||||||
|
const ing = ingressesResults.data?.find(
|
||||||
|
(ing) => ing.Name === params.name && ing.Namespace === params.namespace
|
||||||
|
);
|
||||||
|
if (ing) {
|
||||||
|
const type = ingressControllersResults.data?.find(
|
||||||
|
(c) => c.ClassName === ing.ClassName
|
||||||
|
)?.Type;
|
||||||
|
const r = prepareRuleFromIngress(ing);
|
||||||
|
r.IngressType = type;
|
||||||
|
setIngressRule(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
params.name,
|
||||||
|
ingressesResults.data,
|
||||||
|
ingressControllersResults.data,
|
||||||
|
ingressRule.IngressName,
|
||||||
|
params.namespace,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (namespace.length > 0) {
|
||||||
|
validate(
|
||||||
|
ingressRule,
|
||||||
|
ingressNames || [],
|
||||||
|
servicesOptions || [],
|
||||||
|
!!existingIngressClass
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
ingressRule,
|
||||||
|
namespace,
|
||||||
|
ingressNames,
|
||||||
|
servicesOptions,
|
||||||
|
existingIngressClass,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title={isEdit ? 'Edit ingress' : 'Add ingress'}
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
link: 'kubernetes.ingresses',
|
||||||
|
label: 'Ingresses',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isEdit ? 'Edit ingress' : 'Add ingress',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="row ingress-rules">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<IngressForm
|
||||||
|
environmentID={environmentId}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isEdit={isEdit}
|
||||||
|
rule={ingressRule}
|
||||||
|
ingressClassOptions={ingressClassOptions}
|
||||||
|
errors={errors}
|
||||||
|
servicePorts={servicePorts}
|
||||||
|
tlsOptions={tlsOptions}
|
||||||
|
serviceOptions={serviceOptions}
|
||||||
|
addNewIngressHost={addNewIngressHost}
|
||||||
|
handleTLSChange={handleTLSChange}
|
||||||
|
handleHostChange={handleHostChange}
|
||||||
|
handleIngressChange={handleIngressChange}
|
||||||
|
handlePathChange={handlePathChange}
|
||||||
|
addNewIngressRoute={addNewIngressRoute}
|
||||||
|
removeIngressHost={removeIngressHost}
|
||||||
|
removeIngressRoute={removeIngressRoute}
|
||||||
|
addNewAnnotation={addNewAnnotation}
|
||||||
|
removeAnnotation={removeAnnotation}
|
||||||
|
reloadTLSCerts={reloadTLSCerts}
|
||||||
|
handleAnnotationChange={handleAnnotationChange}
|
||||||
|
namespace={namespace}
|
||||||
|
handleNamespaceChange={handleNamespaceChange}
|
||||||
|
namespacesOptions={namespacesOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{namespace && !isLoading && (
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleCreateIngressRules()}
|
||||||
|
disabled={Object.keys(errors).length > 0}
|
||||||
|
>
|
||||||
|
{isEdit ? 'Update' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
function validate(
|
||||||
|
ingressRule: Rule,
|
||||||
|
ingressNames: string[],
|
||||||
|
serviceOptions: Option<string>[],
|
||||||
|
existingIngressClass: boolean
|
||||||
|
) {
|
||||||
|
const errors: Record<string, ReactNode> = {};
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
// User cannot edit the namespace and the ingress name
|
||||||
|
if (!isEdit) {
|
||||||
|
if (!rule.Namespace) {
|
||||||
|
errors.namespace = 'Namespace is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameRegex = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||||
|
if (!rule.IngressName) {
|
||||||
|
errors.ingressName = 'Ingress name is required';
|
||||||
|
} else if (!nameRegex.test(rule.IngressName)) {
|
||||||
|
errors.ingressName =
|
||||||
|
"This field must consist of lower case alphanumeric characters or '-', contain at most 63 characters, start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').";
|
||||||
|
} else if (ingressNames.includes(rule.IngressName)) {
|
||||||
|
errors.ingressName = 'Ingress name already exists';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.IngressClassName) {
|
||||||
|
errors.className = 'Ingress class is required';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit && !ingressRule.IngressClassName) {
|
||||||
|
errors.className =
|
||||||
|
'No ingress class is currently set for this ingress - use of the Portainer UI requires one to be set.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit && !existingIngressClass && ingressRule.IngressClassName) {
|
||||||
|
if (!rule.IngressType) {
|
||||||
|
errors.className =
|
||||||
|
'Currently set to an ingress class that cannot be found in the cluster - you must select a valid class.';
|
||||||
|
} else {
|
||||||
|
errors.className =
|
||||||
|
'Currently set to an ingress class that you do not have access to - you must select a valid class.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicatedAnnotations: string[] = [];
|
||||||
|
rule.Annotations?.forEach((a, i) => {
|
||||||
|
if (!a.Key) {
|
||||||
|
errors[`annotations.key[${i}]`] = 'Annotation key is required';
|
||||||
|
} else if (duplicatedAnnotations.includes(a.Key)) {
|
||||||
|
errors[`annotations.key[${i}]`] = 'Annotation cannot be duplicated';
|
||||||
|
}
|
||||||
|
if (!a.Value) {
|
||||||
|
errors[`annotations.value[${i}]`] = 'Annotation value is required';
|
||||||
|
}
|
||||||
|
duplicatedAnnotations.push(a.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicatedHosts: string[] = [];
|
||||||
|
// Check if the paths are duplicates
|
||||||
|
rule.Hosts?.forEach((host, hi) => {
|
||||||
|
if (!host.NoHost) {
|
||||||
|
if (!host.Host) {
|
||||||
|
errors[`hosts[${hi}].host`] = 'Host is required';
|
||||||
|
} else if (duplicatedHosts.includes(host.Host)) {
|
||||||
|
errors[`hosts[${hi}].host`] = 'Host cannot be duplicated';
|
||||||
|
}
|
||||||
|
duplicatedHosts.push(host.Host);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate service
|
||||||
|
host.Paths?.forEach((path, pi) => {
|
||||||
|
if (!path.ServiceName) {
|
||||||
|
errors[`hosts[${hi}].paths[${pi}].servicename`] =
|
||||||
|
'Service name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isEdit &&
|
||||||
|
path.ServiceName &&
|
||||||
|
!serviceOptions.find((s) => s.value === path.ServiceName)
|
||||||
|
) {
|
||||||
|
errors[`hosts[${hi}].paths[${pi}].servicename`] = (
|
||||||
|
<span>
|
||||||
|
Currently set to {path.ServiceName}, which does not exist. You can
|
||||||
|
create a service with this name for a particular deployment via{' '}
|
||||||
|
<Link
|
||||||
|
to="kubernetes.applications"
|
||||||
|
params={{ id: environmentId }}
|
||||||
|
className="text-primary"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Applications
|
||||||
|
</Link>
|
||||||
|
, and on returning here it will be picked up.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.ServicePort) {
|
||||||
|
errors[`hosts[${hi}].paths[${pi}].serviceport`] =
|
||||||
|
'Service port is required';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Validate paths
|
||||||
|
const paths = host.Paths.map((path) => path.Route);
|
||||||
|
paths.forEach((item, idx) => {
|
||||||
|
if (!item) {
|
||||||
|
errors[`hosts[${hi}].paths[${idx}].path`] = 'Path cannot be empty';
|
||||||
|
} else if (paths.indexOf(item) !== idx) {
|
||||||
|
errors[`hosts[${hi}].paths[${idx}].path`] =
|
||||||
|
'Paths cannot be duplicated';
|
||||||
|
} else {
|
||||||
|
// Validate host and path combination globally
|
||||||
|
const isExists = checkIfPathExistsWithHost(
|
||||||
|
ingresses,
|
||||||
|
host.Host,
|
||||||
|
item,
|
||||||
|
params.name
|
||||||
|
);
|
||||||
|
if (isExists) {
|
||||||
|
errors[`hosts[${hi}].paths[${idx}].path`] =
|
||||||
|
'Path is already in use with the same host';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setErrors(errors);
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNamespaceChange(ns: string) {
|
||||||
|
setNamespace(ns);
|
||||||
|
if (!isEdit) {
|
||||||
|
addNewIngress(ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIngressChange(key: string, val: string) {
|
||||||
|
setIngressRule((prevRules) => {
|
||||||
|
const rule = { ...prevRules, [key]: val };
|
||||||
|
if (key === 'IngressClassName') {
|
||||||
|
rule.IngressType = ingressControllersResults.data?.find(
|
||||||
|
(c) => c.ClassName === val
|
||||||
|
)?.Type;
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTLSChange(hostIndex: number, tls: string) {
|
||||||
|
setIngressRule((prevRules) => {
|
||||||
|
const rule = { ...prevRules };
|
||||||
|
rule.Hosts[hostIndex] = { ...rule.Hosts[hostIndex], Secret: tls };
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHostChange(hostIndex: number, val: string) {
|
||||||
|
setIngressRule((prevRules) => {
|
||||||
|
const rule = { ...prevRules };
|
||||||
|
rule.Hosts[hostIndex] = { ...rule.Hosts[hostIndex], Host: val };
|
||||||
|
rule.Hosts[hostIndex].Secret =
|
||||||
|
hostWithTLS[val] || rule.Hosts[hostIndex].Secret;
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePathChange(
|
||||||
|
hostIndex: number,
|
||||||
|
pathIndex: number,
|
||||||
|
key: 'Route' | 'PathType' | 'ServiceName' | 'ServicePort',
|
||||||
|
val: string
|
||||||
|
) {
|
||||||
|
setIngressRule((prevRules) => {
|
||||||
|
const rule = { ...prevRules };
|
||||||
|
const h = { ...rule.Hosts[hostIndex] };
|
||||||
|
h.Paths[pathIndex] = {
|
||||||
|
...h.Paths[pathIndex],
|
||||||
|
[key]: key === 'ServicePort' ? Number(val) : val,
|
||||||
|
};
|
||||||
|
|
||||||
|
// set the first port of the service as the default port
|
||||||
|
if (
|
||||||
|
key === 'ServiceName' &&
|
||||||
|
servicePorts[val] &&
|
||||||
|
servicePorts[val].length > 0
|
||||||
|
) {
|
||||||
|
h.Paths[pathIndex].ServicePort = Number(servicePorts[val][0].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.Hosts[hostIndex] = h;
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAnnotationChange(
|
||||||
|
index: number,
|
||||||
|
key: 'Key' | 'Value',
|
||||||
|
val: string
|
||||||
|
) {
|
||||||
|
setIngressRule((prevRules) => {
|
||||||
|
const rules = { ...prevRules };
|
||||||
|
|
||||||
|
rules.Annotations = rules.Annotations || [];
|
||||||
|
rules.Annotations[index] = rules.Annotations[index] || {
|
||||||
|
Key: '',
|
||||||
|
Value: '',
|
||||||
|
};
|
||||||
|
rules.Annotations[index][key] = val;
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewIngress(namespace: string) {
|
||||||
|
const newKey = `${namespace}-ingress-${
|
||||||
|
(ruleCounterByNamespace[namespace] || 0) + 1
|
||||||
|
}`;
|
||||||
|
const path: Path = {
|
||||||
|
Key: uuidv4(),
|
||||||
|
ServiceName: '',
|
||||||
|
ServicePort: 0,
|
||||||
|
Route: '',
|
||||||
|
PathType: 'Prefix',
|
||||||
|
};
|
||||||
|
|
||||||
|
const host: Host = {
|
||||||
|
Host: '',
|
||||||
|
Secret: '',
|
||||||
|
Paths: [path],
|
||||||
|
Key: uuidv4(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const rule: Rule = {
|
||||||
|
Key: uuidv4(),
|
||||||
|
Namespace: namespace,
|
||||||
|
IngressName: newKey,
|
||||||
|
IngressClassName: '',
|
||||||
|
Hosts: [host],
|
||||||
|
};
|
||||||
|
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewIngressHost(noHost = false) {
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
const path: Path = {
|
||||||
|
ServiceName: '',
|
||||||
|
ServicePort: 0,
|
||||||
|
Route: '',
|
||||||
|
PathType: 'Prefix',
|
||||||
|
Key: uuidv4(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const host: Host = {
|
||||||
|
Host: '',
|
||||||
|
Secret: '',
|
||||||
|
Paths: [path],
|
||||||
|
NoHost: noHost,
|
||||||
|
Key: uuidv4(),
|
||||||
|
};
|
||||||
|
|
||||||
|
rule.Hosts.push(host);
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewIngressRoute(hostIndex: number) {
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
const path: Path = {
|
||||||
|
ServiceName: '',
|
||||||
|
ServicePort: 0,
|
||||||
|
Route: '',
|
||||||
|
PathType: 'Prefix',
|
||||||
|
Key: uuidv4(),
|
||||||
|
};
|
||||||
|
|
||||||
|
rule.Hosts[hostIndex].Paths.push(path);
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewAnnotation(type?: 'rewrite' | 'regex') {
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
const annotation: Annotation = {
|
||||||
|
Key: '',
|
||||||
|
Value: '',
|
||||||
|
ID: uuidv4(),
|
||||||
|
};
|
||||||
|
if (type === 'rewrite') {
|
||||||
|
annotation.Key = 'nginx.ingress.kubernetes.io/rewrite-target';
|
||||||
|
annotation.Value = '/$1';
|
||||||
|
}
|
||||||
|
if (type === 'regex') {
|
||||||
|
annotation.Key = 'nginx.ingress.kubernetes.io/use-regex';
|
||||||
|
annotation.Value = 'true';
|
||||||
|
}
|
||||||
|
rule.Annotations = rule.Annotations || [];
|
||||||
|
rule.Annotations?.push(annotation);
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAnnotation(index: number) {
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
rule.Annotations?.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIngressRoute(hostIndex: number, pathIndex: number) {
|
||||||
|
const rule = { ...ingressRule, Hosts: [...ingressRule.Hosts] };
|
||||||
|
if (hostIndex > -1 && pathIndex > -1) {
|
||||||
|
rule.Hosts[hostIndex].Paths.splice(pathIndex, 1);
|
||||||
|
}
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIngressHost(hostIndex: number) {
|
||||||
|
const rule = { ...ingressRule, Hosts: [...ingressRule.Hosts] };
|
||||||
|
if (hostIndex > -1) {
|
||||||
|
rule.Hosts.splice(hostIndex, 1);
|
||||||
|
}
|
||||||
|
setIngressRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadTLSCerts() {
|
||||||
|
configResults.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateIngressRules() {
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
const ingress: Ingress = {
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: rule.IngressName,
|
||||||
|
ClassName: rule.IngressClassName,
|
||||||
|
Hosts: rule.Hosts.map((host) => host.Host),
|
||||||
|
Paths: preparePaths(rule.IngressName, rule.Hosts),
|
||||||
|
TLS: prepareTLS(rule.Hosts),
|
||||||
|
Annotations: prepareAnnotations(rule.Annotations || []),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
updateIngressMutation.mutate(
|
||||||
|
{ environmentId, ingress },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess('Success', 'Ingress updated successfully');
|
||||||
|
router.stateService.go('kubernetes.ingresses');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createIngressMutation.mutate(
|
||||||
|
{ environmentId, ingress },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess('Success', 'Ingress created successfully');
|
||||||
|
router.stateService.go('kubernetes.ingresses');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,589 @@
|
||||||
|
import { ChangeEvent, ReactNode } from 'react';
|
||||||
|
import { Plus, RefreshCw, Trash2 } from 'react-feather';
|
||||||
|
|
||||||
|
import { Annotations } from '@/kubernetes/react/views/networks/ingresses/components/annotations';
|
||||||
|
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
import { Select, Option } from '@@/form-components/Input/Select';
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||||
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
|
import { Rule, ServicePorts } from './types';
|
||||||
|
|
||||||
|
import '../style.css';
|
||||||
|
|
||||||
|
const PathTypes: Record<string, string[]> = {
|
||||||
|
nginx: ['ImplementationSpecific', 'Prefix', 'Exact'],
|
||||||
|
traefik: ['Prefix', 'Exact'],
|
||||||
|
other: ['Prefix', 'Exact'],
|
||||||
|
};
|
||||||
|
const PlaceholderAnnotations: Record<string, string[]> = {
|
||||||
|
nginx: ['e.g. nginx.ingress.kubernetes.io/rewrite-target', '/$1'],
|
||||||
|
traefik: ['e.g. traefik.ingress.kubernetes.io/router.tls', 'true'],
|
||||||
|
other: ['e.g. app.kubernetes.io/name', 'examplename'],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
environmentID: number;
|
||||||
|
rule: Rule;
|
||||||
|
|
||||||
|
errors: Record<string, ReactNode>;
|
||||||
|
isLoading: boolean;
|
||||||
|
isEdit: boolean;
|
||||||
|
namespace: string;
|
||||||
|
|
||||||
|
servicePorts: ServicePorts;
|
||||||
|
ingressClassOptions: Option<string>[];
|
||||||
|
serviceOptions: Option<string>[];
|
||||||
|
tlsOptions: Option<string>[];
|
||||||
|
namespacesOptions: Option<string>[];
|
||||||
|
|
||||||
|
removeIngressRoute: (hostIndex: number, pathIndex: number) => void;
|
||||||
|
removeIngressHost: (hostIndex: number) => void;
|
||||||
|
removeAnnotation: (index: number) => void;
|
||||||
|
|
||||||
|
addNewIngressHost: (noHost?: boolean) => void;
|
||||||
|
addNewIngressRoute: (hostIndex: number) => void;
|
||||||
|
addNewAnnotation: (type?: 'rewrite' | 'regex') => void;
|
||||||
|
|
||||||
|
handleNamespaceChange: (val: string) => void;
|
||||||
|
handleHostChange: (hostIndex: number, val: string) => void;
|
||||||
|
handleTLSChange: (hostIndex: number, tls: string) => void;
|
||||||
|
handleIngressChange: (
|
||||||
|
key: 'IngressName' | 'IngressClassName',
|
||||||
|
value: string
|
||||||
|
) => void;
|
||||||
|
handleAnnotationChange: (
|
||||||
|
index: number,
|
||||||
|
key: 'Key' | 'Value',
|
||||||
|
val: string
|
||||||
|
) => void;
|
||||||
|
handlePathChange: (
|
||||||
|
hostIndex: number,
|
||||||
|
pathIndex: number,
|
||||||
|
key: 'Route' | 'PathType' | 'ServiceName' | 'ServicePort',
|
||||||
|
val: string
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
reloadTLSCerts: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IngressForm({
|
||||||
|
environmentID,
|
||||||
|
rule,
|
||||||
|
isLoading,
|
||||||
|
isEdit,
|
||||||
|
servicePorts,
|
||||||
|
tlsOptions,
|
||||||
|
handleTLSChange,
|
||||||
|
addNewIngressHost,
|
||||||
|
serviceOptions,
|
||||||
|
handleHostChange,
|
||||||
|
handleIngressChange,
|
||||||
|
handlePathChange,
|
||||||
|
addNewIngressRoute,
|
||||||
|
removeIngressRoute,
|
||||||
|
removeIngressHost,
|
||||||
|
addNewAnnotation,
|
||||||
|
removeAnnotation,
|
||||||
|
reloadTLSCerts,
|
||||||
|
handleAnnotationChange,
|
||||||
|
ingressClassOptions,
|
||||||
|
errors,
|
||||||
|
namespacesOptions,
|
||||||
|
handleNamespaceChange,
|
||||||
|
namespace,
|
||||||
|
}: Props) {
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
const hasNoHostRule = rule.Hosts?.some((host) => host.NoHost);
|
||||||
|
const placeholderAnnotation =
|
||||||
|
PlaceholderAnnotations[rule.IngressType || 'other'];
|
||||||
|
const pathTypes = PathTypes[rule.IngressType || 'other'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget>
|
||||||
|
<WidgetTitle icon="svg-route" title="Ingress" />
|
||||||
|
<WidgetBody key={rule.Key + rule.Namespace}>
|
||||||
|
<div className="row">
|
||||||
|
<div className="form-horizontal">
|
||||||
|
<div className="form-group">
|
||||||
|
<label
|
||||||
|
className="control-label text-muted col-sm-3 col-lg-2 required"
|
||||||
|
htmlFor="namespace"
|
||||||
|
>
|
||||||
|
Namespace
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-4">
|
||||||
|
{isEdit ? (
|
||||||
|
namespace
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
name="namespaces"
|
||||||
|
options={namespacesOptions || []}
|
||||||
|
onChange={(e) => handleNamespaceChange(e.target.value)}
|
||||||
|
defaultValue={namespace}
|
||||||
|
disabled={isEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{namespace && (
|
||||||
|
<div className="row">
|
||||||
|
<div className="form-horizontal">
|
||||||
|
<div className="form-group">
|
||||||
|
<label
|
||||||
|
className="control-label text-muted col-sm-3 col-lg-2 required"
|
||||||
|
htmlFor="ingress_name"
|
||||||
|
>
|
||||||
|
Ingress name
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-4">
|
||||||
|
{isEdit ? (
|
||||||
|
rule.IngressName
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
name="ingress_name"
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Ingress name"
|
||||||
|
defaultValue={rule.IngressName}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngressChange('IngressName', e.target.value)
|
||||||
|
}
|
||||||
|
disabled={isEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{errors.ingressName && !isEdit && (
|
||||||
|
<FormError className="mt-1 error-inline">
|
||||||
|
{errors.ingressName}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group" key={rule.IngressClassName}>
|
||||||
|
<label
|
||||||
|
className="control-label text-muted col-sm-3 col-lg-2 required"
|
||||||
|
htmlFor="ingress_class"
|
||||||
|
>
|
||||||
|
Ingress class
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-4">
|
||||||
|
<Select
|
||||||
|
name="ingress_class"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Ingress name"
|
||||||
|
defaultValue={rule.IngressClassName}
|
||||||
|
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
handleIngressChange('IngressClassName', e.target.value)
|
||||||
|
}
|
||||||
|
options={ingressClassOptions}
|
||||||
|
/>
|
||||||
|
{errors.className && (
|
||||||
|
<FormError className="mt-1 error-inline">
|
||||||
|
{errors.className}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-12 px-0 text-muted !mb-0">
|
||||||
|
<div className="mb-2">Annotations</div>
|
||||||
|
<p className="vertical-center text-muted small">
|
||||||
|
<Icon icon="info" mode="primary" feather />
|
||||||
|
<span>
|
||||||
|
You can specify{' '}
|
||||||
|
<a
|
||||||
|
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
||||||
|
target="_black"
|
||||||
|
>
|
||||||
|
annotations
|
||||||
|
</a>{' '}
|
||||||
|
for the object. See further Kubernetes documentation on{' '}
|
||||||
|
<a
|
||||||
|
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
|
||||||
|
target="_black"
|
||||||
|
>
|
||||||
|
well-known annotations
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rule?.Annotations && (
|
||||||
|
<Annotations
|
||||||
|
placeholder={placeholderAnnotation}
|
||||||
|
annotations={rule.Annotations}
|
||||||
|
handleAnnotationChange={handleAnnotationChange}
|
||||||
|
removeAnnotation={removeAnnotation}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="col-sm-12 p-0 anntation-actions">
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-light mb-2 !ml-0"
|
||||||
|
onClick={() => addNewAnnotation()}
|
||||||
|
icon={Plus}
|
||||||
|
title="Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type."
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
add annotation
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{rule.IngressType === 'nginx' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-light mb-2 ml-2"
|
||||||
|
onClick={() => addNewAnnotation('rewrite')}
|
||||||
|
icon={Plus}
|
||||||
|
title="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."
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
add rewrite annotation
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-light mb-2 ml-2"
|
||||||
|
onClick={() => addNewAnnotation('regex')}
|
||||||
|
icon={Plus}
|
||||||
|
title="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."
|
||||||
|
>
|
||||||
|
add regular expression annotation
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-12 px-0 text-muted">Rules</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{namespace &&
|
||||||
|
rule?.Hosts?.map((host, hostIndex) => (
|
||||||
|
<div className="row mb-5 rule bordered" key={host.Key}>
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<div className="row mt-5 rule-actions">
|
||||||
|
<div className="col-sm-3 p-0">
|
||||||
|
{!host.NoHost ? 'Rule' : 'Fallback rule'}
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-9 p-0 text-right">
|
||||||
|
{!host.NoHost && (
|
||||||
|
<Button
|
||||||
|
className="btn btn-light btn-sm"
|
||||||
|
onClick={() => reloadTLSCerts()}
|
||||||
|
icon={RefreshCw}
|
||||||
|
>
|
||||||
|
Reload TLS secrets
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-dangerlight ml-2"
|
||||||
|
type="button"
|
||||||
|
data-cy={`k8sAppCreate-rmHostButton_${hostIndex}`}
|
||||||
|
onClick={() => removeIngressHost(hostIndex)}
|
||||||
|
disabled={rule.Hosts.length === 1}
|
||||||
|
icon={Trash2}
|
||||||
|
>
|
||||||
|
Remove rule
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!host.NoHost && (
|
||||||
|
<div className="row">
|
||||||
|
<div className="form-group !pl-0 col-sm-6 col-lg-4 !pr-2">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">
|
||||||
|
Hostname
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
name={`ingress_host_${hostIndex}`}
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="e.g. example.com"
|
||||||
|
defaultValue={host.Host}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleHostChange(hostIndex, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[`hosts[${hostIndex}].host`] && (
|
||||||
|
<FormError className="mt-1 !mb-0">
|
||||||
|
{errors[`hosts[${hostIndex}].host`]}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group !pr-0 col-sm-6 col-lg-4 !pl-2">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon">TLS secret</span>
|
||||||
|
<Select
|
||||||
|
key={tlsOptions.toString() + host.Secret}
|
||||||
|
name={`ingress_tls_${hostIndex}`}
|
||||||
|
options={tlsOptions}
|
||||||
|
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
handleTLSChange(hostIndex, e.target.value)
|
||||||
|
}
|
||||||
|
defaultValue={host.Secret}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="vertical-center text-muted small whitespace-nowrap col-sm-12 !p-0">
|
||||||
|
<Icon icon="info" mode="primary" size="md" feather />
|
||||||
|
<span>
|
||||||
|
Add a secret via{' '}
|
||||||
|
<Link
|
||||||
|
to="kubernetes.configurations"
|
||||||
|
params={{ id: environmentID }}
|
||||||
|
className="text-primary"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
ConfigMaps & Secrets
|
||||||
|
</Link>
|
||||||
|
{', '}
|
||||||
|
then select 'Reload TLS secrets' above to
|
||||||
|
populate the dropdown with your changes.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{host.NoHost && (
|
||||||
|
<p className="vertical-center text-muted small whitespace-nowrap col-sm-12 !p-0">
|
||||||
|
<Icon icon="info" mode="primary" size="md" feather />A
|
||||||
|
fallback rule has no host specified. This rule only applies
|
||||||
|
when an inbound request has a hostname that does not match
|
||||||
|
with any of your other rules.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12 px-0 !mb-0 mt-2 text-muted">
|
||||||
|
Paths
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{host.Paths.map((path, pathIndex) => (
|
||||||
|
<div
|
||||||
|
className="mt-5 !mb-5 row path"
|
||||||
|
key={`path_${path.Key}}`}
|
||||||
|
>
|
||||||
|
<div className="form-group !pl-0 col-sm-3 col-xl-2 !m-0">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">
|
||||||
|
Service
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
key={serviceOptions.toString() + path.ServiceName}
|
||||||
|
name={`ingress_service_${hostIndex}_${pathIndex}`}
|
||||||
|
options={serviceOptions}
|
||||||
|
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
handlePathChange(
|
||||||
|
hostIndex,
|
||||||
|
pathIndex,
|
||||||
|
'ServiceName',
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
defaultValue={path.ServiceName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].servicename`
|
||||||
|
] && (
|
||||||
|
<FormError className="mt-1 !mb-0 error-inline">
|
||||||
|
{
|
||||||
|
errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].servicename`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group !pl-0 col-sm-2 col-xl-2 !m-0">
|
||||||
|
{servicePorts && (
|
||||||
|
<>
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">
|
||||||
|
Service port
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
key={servicePorts.toString() + path.ServicePort}
|
||||||
|
name={`ingress_servicePort_${hostIndex}_${pathIndex}`}
|
||||||
|
options={
|
||||||
|
path.ServiceName &&
|
||||||
|
servicePorts[path.ServiceName]
|
||||||
|
? servicePorts[path.ServiceName]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: 'Select port',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
handlePathChange(
|
||||||
|
hostIndex,
|
||||||
|
pathIndex,
|
||||||
|
'ServicePort',
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
defaultValue={path.ServicePort}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].serviceport`
|
||||||
|
] && (
|
||||||
|
<FormError className="mt-1 !mb-0">
|
||||||
|
{
|
||||||
|
errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].serviceport`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group !pl-0 col-sm-3 col-xl-2 !m-0">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">
|
||||||
|
Path type
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
key={servicePorts.toString() + path.PathType}
|
||||||
|
name={`ingress_pathType_${hostIndex}_${pathIndex}`}
|
||||||
|
options={
|
||||||
|
pathTypes
|
||||||
|
? pathTypes.map((type) => ({
|
||||||
|
label: type,
|
||||||
|
value: type,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
handlePathChange(
|
||||||
|
hostIndex,
|
||||||
|
pathIndex,
|
||||||
|
'PathType',
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
defaultValue={path.PathType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].pathType`
|
||||||
|
] && (
|
||||||
|
<FormError className="mt-1 !mb-0">
|
||||||
|
{
|
||||||
|
errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].pathType`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group !pl-0 col-sm-3 col-xl-3 !m-0">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">Path</span>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
name={`ingress_route_${hostIndex}-${pathIndex}`}
|
||||||
|
placeholder="/example"
|
||||||
|
data-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
|
||||||
|
data-cy={`k8sAppCreate-route_${hostIndex}-${pathIndex}`}
|
||||||
|
defaultValue={path.Route}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handlePathChange(
|
||||||
|
hostIndex,
|
||||||
|
pathIndex,
|
||||||
|
'Route',
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].path`
|
||||||
|
] && (
|
||||||
|
<FormError className="mt-1 !mb-0">
|
||||||
|
{
|
||||||
|
errors[
|
||||||
|
`hosts[${hostIndex}].paths[${pathIndex}].path`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group !pl-0 col-sm-1 !m-0">
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-dangerlight btn-only-icon !ml-0 vertical-center"
|
||||||
|
type="button"
|
||||||
|
data-cy={`k8sAppCreate-rmPortButton_${hostIndex}-${pathIndex}`}
|
||||||
|
onClick={() => removeIngressRoute(hostIndex, pathIndex)}
|
||||||
|
disabled={host.Paths.length === 1}
|
||||||
|
icon={Trash2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="row mt-5">
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-light !ml-0"
|
||||||
|
type="button"
|
||||||
|
onClick={() => addNewIngressRoute(hostIndex)}
|
||||||
|
icon={Plus}
|
||||||
|
>
|
||||||
|
Add path
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{namespace && (
|
||||||
|
<div className="row p-0 rules-action">
|
||||||
|
<div className="col-sm-12 p-0 vertical-center">
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-light !ml-0"
|
||||||
|
type="button"
|
||||||
|
onClick={() => addNewIngressHost()}
|
||||||
|
icon={Plus}
|
||||||
|
>
|
||||||
|
Add new host
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="btn btn-sm btn-light ml-2"
|
||||||
|
type="button"
|
||||||
|
onClick={() => addNewIngressHost(true)}
|
||||||
|
disabled={hasNoHostRule}
|
||||||
|
icon={Plus}
|
||||||
|
>
|
||||||
|
Add fallback rule
|
||||||
|
</Button>
|
||||||
|
<Tooltip message="A fallback rule will be applied to all requests that do not match any of the defined hosts." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { CreateIngressView } from './CreateIngressView';
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Annotation } from '@/kubernetes/react/views/networks/ingresses/components/annotations/types';
|
||||||
|
|
||||||
|
import { Option } from '@@/form-components/Input/Select';
|
||||||
|
|
||||||
|
export interface Path {
|
||||||
|
Key: string;
|
||||||
|
Route: string;
|
||||||
|
ServiceName: string;
|
||||||
|
ServicePort: number;
|
||||||
|
PathType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Host {
|
||||||
|
Key: string;
|
||||||
|
Host: string;
|
||||||
|
Secret: string;
|
||||||
|
Paths: Path[];
|
||||||
|
NoHost?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rule {
|
||||||
|
Key: string;
|
||||||
|
IngressName: string;
|
||||||
|
Namespace: string;
|
||||||
|
IngressClassName: string;
|
||||||
|
Hosts: Host[];
|
||||||
|
Annotations?: Annotation[];
|
||||||
|
IngressType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServicePorts {
|
||||||
|
[serviceName: string]: Option<string>[];
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { Annotation } from '@/kubernetes/react/views/networks/ingresses/components/annotations/types';
|
||||||
|
|
||||||
|
import { TLS, Ingress } from '../types';
|
||||||
|
|
||||||
|
import { Host, Rule } from './types';
|
||||||
|
|
||||||
|
const ignoreAnnotationsForEdit = [
|
||||||
|
'kubectl.kubernetes.io/last-applied-configuration',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function prepareTLS(hosts: Host[]) {
|
||||||
|
const tls: TLS[] = [];
|
||||||
|
hosts.forEach((host) => {
|
||||||
|
if (host.Secret && host.Host) {
|
||||||
|
tls.push({
|
||||||
|
Hosts: [host.Host],
|
||||||
|
SecretName: host.Secret,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preparePaths(ingressName: string, hosts: Host[]) {
|
||||||
|
return hosts.flatMap((host) =>
|
||||||
|
host.Paths.map((p) => ({
|
||||||
|
ServiceName: p.ServiceName,
|
||||||
|
Host: host.Host,
|
||||||
|
Path: p.Route,
|
||||||
|
Port: p.ServicePort,
|
||||||
|
PathType: p.PathType || 'Prefix',
|
||||||
|
IngressName: ingressName,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareAnnotations(annotations: Annotation[]) {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
annotations.forEach((a) => {
|
||||||
|
result[a.Key] = a.Value;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecretByHost(host: string, tls?: TLS[]) {
|
||||||
|
let secret = '';
|
||||||
|
if (tls) {
|
||||||
|
tls.forEach((t) => {
|
||||||
|
if (t.Hosts.indexOf(host) !== -1) {
|
||||||
|
secret = t.SecretName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareRuleHostsFromIngress(ing: Ingress) {
|
||||||
|
const hosts = ing.Hosts?.map((host) => {
|
||||||
|
const h: Host = {} as Host;
|
||||||
|
h.Host = host;
|
||||||
|
h.Secret = getSecretByHost(host, ing.TLS);
|
||||||
|
h.Paths = [];
|
||||||
|
ing.Paths.forEach((path) => {
|
||||||
|
if (path.Host === host) {
|
||||||
|
h.Paths.push({
|
||||||
|
Route: path.Path,
|
||||||
|
ServiceName: path.ServiceName,
|
||||||
|
ServicePort: path.Port,
|
||||||
|
PathType: path.PathType,
|
||||||
|
Key: Math.random().toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!host) {
|
||||||
|
h.NoHost = true;
|
||||||
|
}
|
||||||
|
h.Key = uuidv4();
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnnotationsForEdit(
|
||||||
|
annotations: Record<string, string>
|
||||||
|
): Annotation[] {
|
||||||
|
const result: Annotation[] = [];
|
||||||
|
Object.keys(annotations).forEach((k) => {
|
||||||
|
if (ignoreAnnotationsForEdit.indexOf(k) === -1) {
|
||||||
|
result.push({
|
||||||
|
Key: k,
|
||||||
|
Value: annotations[k],
|
||||||
|
ID: uuidv4(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareRuleFromIngress(ing: Ingress): Rule {
|
||||||
|
return {
|
||||||
|
Key: uuidv4(),
|
||||||
|
IngressName: ing.Name,
|
||||||
|
Namespace: ing.Namespace,
|
||||||
|
IngressClassName: ing.ClassName,
|
||||||
|
Hosts: prepareRuleHostsFromIngress(ing) || [],
|
||||||
|
Annotations: ing.Annotations ? getAnnotationsForEdit(ing.Annotations) : [],
|
||||||
|
IngressType: ing.Type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkIfPathExistsWithHost(
|
||||||
|
ingresses: Ingress[],
|
||||||
|
host: string,
|
||||||
|
path: string,
|
||||||
|
ingressName?: string
|
||||||
|
) {
|
||||||
|
let exists = false;
|
||||||
|
ingresses.forEach((ingress) => {
|
||||||
|
if (ingressName && ingress.Name === ingressName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ingress.Paths?.forEach((p) => {
|
||||||
|
if (p.Host === host && p.Path === path) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return exists;
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { Plus, Trash2 } from 'react-feather';
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||||
|
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
|
||||||
|
import { Authorized } from '@/portainer/hooks/useUser';
|
||||||
|
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||||
|
|
||||||
|
import { Datatable } from '@@/datatables';
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
import { DeleteIngressesRequest, Ingress } from '../types';
|
||||||
|
import { useDeleteIngresses, useIngresses } from '../queries';
|
||||||
|
|
||||||
|
import { createStore } from './datatable-store';
|
||||||
|
import { useColumns } from './columns';
|
||||||
|
|
||||||
|
import '../style.css';
|
||||||
|
|
||||||
|
interface SelectedIngress {
|
||||||
|
Namespace: string;
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStore = createStore('ingresses');
|
||||||
|
|
||||||
|
export function IngressDataTable() {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
|
||||||
|
const nsResult = useNamespaces(environmentId);
|
||||||
|
const result = useIngresses(environmentId, Object.keys(nsResult?.data || {}));
|
||||||
|
|
||||||
|
const settings = useStore();
|
||||||
|
|
||||||
|
const columns = useColumns();
|
||||||
|
const deleteIngressesMutation = useDeleteIngresses();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Datatable
|
||||||
|
dataset={result.data || []}
|
||||||
|
storageKey="ingressClassesNameSpace"
|
||||||
|
columns={columns}
|
||||||
|
settingsStore={settings}
|
||||||
|
isLoading={result.isLoading}
|
||||||
|
emptyContentLabel="No supported ingresses found"
|
||||||
|
titleOptions={{
|
||||||
|
icon: 'svg-route',
|
||||||
|
title: 'Ingresses',
|
||||||
|
}}
|
||||||
|
getRowId={(row) => row.Name + row.Type + row.Namespace}
|
||||||
|
renderTableActions={tableActions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
function tableActions(selectedFlatRows: Ingress[]) {
|
||||||
|
return (
|
||||||
|
<div className="ingressDatatable-actions">
|
||||||
|
<Authorized authorizations="AzureContainerGroupDelete">
|
||||||
|
<Button
|
||||||
|
className="btn-wrapper"
|
||||||
|
color="dangerlight"
|
||||||
|
disabled={selectedFlatRows.length === 0}
|
||||||
|
onClick={() =>
|
||||||
|
handleRemoveClick(
|
||||||
|
selectedFlatRows.map((row) => ({
|
||||||
|
Name: row.Name,
|
||||||
|
Namespace: row.Namespace,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
icon={Trash2}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Authorized>
|
||||||
|
|
||||||
|
<Authorized authorizations="K8sIngressesAdd">
|
||||||
|
<Link to="kubernetes.ingresses.create" className="space-left">
|
||||||
|
<Button
|
||||||
|
icon={Plus}
|
||||||
|
className="btn-wrapper vertical-center"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Add with form
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Authorized>
|
||||||
|
<Authorized authorizations="K8sApplicationsW">
|
||||||
|
<Link to="kubernetes.deploy" className="space-left">
|
||||||
|
<Button icon={Plus} className="btn-wrapper">
|
||||||
|
Create from manifest
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Authorized>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveClick(ingresses: SelectedIngress[]) {
|
||||||
|
const confirmed = await confirmDeletionAsync(
|
||||||
|
'Are you sure you want to delete the selected ingresses?'
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: DeleteIngressesRequest = {} as DeleteIngressesRequest;
|
||||||
|
ingresses.forEach((ingress) => {
|
||||||
|
payload[ingress.Namespace] = payload[ingress.Namespace] || [];
|
||||||
|
payload[ingress.Namespace].push(ingress.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteIngressesMutation.mutate(
|
||||||
|
{ environmentId, data: payload },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
router.stateService.reload();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return ingresses;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
|
export const className: Column<Ingress> = {
|
||||||
|
Header: 'Class Name',
|
||||||
|
accessor: 'ClassName',
|
||||||
|
id: 'className',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { name } from './name';
|
||||||
|
import { type } from './type';
|
||||||
|
import { namespace } from './namespace';
|
||||||
|
import { className } from './className';
|
||||||
|
import { ingressRules } from './ingressRules';
|
||||||
|
|
||||||
|
export function useColumns() {
|
||||||
|
return useMemo(() => [name, namespace, className, type, ingressRules], []);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { Ingress, TLS, Path } from '../../types';
|
||||||
|
|
||||||
|
function isHTTP(TLSs: TLS[], host: string) {
|
||||||
|
return TLSs.filter((t) => t.Hosts.indexOf(host) !== -1).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function link(host: string, path: string, isHttp: boolean) {
|
||||||
|
if (!host) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`${isHttp ? 'http' : 'https'}://${host}${path}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{`${isHttp ? 'http' : 'https'}://${host}${path}`}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ingressRules: Column<Ingress> = {
|
||||||
|
Header: 'Rules and Paths',
|
||||||
|
accessor: 'Paths',
|
||||||
|
Cell: ({ row }: CellProps<Ingress, Path[]>) => {
|
||||||
|
const results = row.original.Paths?.map((path: Path) => {
|
||||||
|
const isHttp = isHTTP(row.original.TLS || [], path.Host);
|
||||||
|
return (
|
||||||
|
<div key={`${path.Host}${path.Path}${path.ServiceName}:${path.Port}`}>
|
||||||
|
{link(path.Host, path.Path, isHttp)}
|
||||||
|
<span className="px-2">
|
||||||
|
<Icon icon="arrow-right" feather />
|
||||||
|
</span>
|
||||||
|
{`${path.ServiceName}:${path.Port}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return results || <div />;
|
||||||
|
},
|
||||||
|
id: 'ingressRules',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
|
export const name: Column<Ingress> = {
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'Name',
|
||||||
|
Cell: ({ row }: CellProps<Ingress>) => (
|
||||||
|
<Link
|
||||||
|
to="kubernetes.ingresses.edit"
|
||||||
|
params={{
|
||||||
|
uid: row.original.UID,
|
||||||
|
namespace: row.original.Namespace,
|
||||||
|
name: row.original.Name,
|
||||||
|
}}
|
||||||
|
title={row.original.Name}
|
||||||
|
>
|
||||||
|
{row.original.Name}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
id: 'name',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { CellProps, Column, Row } from 'react-table';
|
||||||
|
|
||||||
|
import { filterHOC } from '@/react/components/datatables/Filter';
|
||||||
|
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
|
export const namespace: Column<Ingress> = {
|
||||||
|
Header: 'Namespace',
|
||||||
|
accessor: 'Namespace',
|
||||||
|
Cell: ({ row }: CellProps<Ingress>) => (
|
||||||
|
<Link
|
||||||
|
to="kubernetes.resourcePools.resourcePool"
|
||||||
|
params={{
|
||||||
|
id: row.original.Namespace,
|
||||||
|
}}
|
||||||
|
title={row.original.Namespace}
|
||||||
|
>
|
||||||
|
{row.original.Namespace}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
id: 'namespace',
|
||||||
|
disableFilters: false,
|
||||||
|
canHide: true,
|
||||||
|
Filter: filterHOC('Filter by namespace'),
|
||||||
|
filter: (rows: Row<Ingress>[], filterValue, filters) => {
|
||||||
|
if (filters.length === 0) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
return rows.filter((r) => filters.includes(r.original.Namespace));
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
|
export const type: Column<Ingress> = {
|
||||||
|
Header: 'Type',
|
||||||
|
accessor: 'Type',
|
||||||
|
id: 'type',
|
||||||
|
disableFilters: true,
|
||||||
|
canHide: true,
|
||||||
|
};
|
|
@ -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,20 @@
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
|
import { IngressDataTable } from './IngressDataTable';
|
||||||
|
|
||||||
|
export function IngressesDatatableView() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Ingresses"
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
label: 'Ingresses',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
reload
|
||||||
|
/>
|
||||||
|
<IngressDataTable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { ChangeEvent, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
|
||||||
|
import { Annotation } from './types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
annotations: Annotation[];
|
||||||
|
handleAnnotationChange: (
|
||||||
|
index: number,
|
||||||
|
key: 'Key' | 'Value',
|
||||||
|
val: string
|
||||||
|
) => void;
|
||||||
|
removeAnnotation: (index: number) => void;
|
||||||
|
errors: Record<string, ReactNode>;
|
||||||
|
placeholder: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Annotations({
|
||||||
|
annotations,
|
||||||
|
handleAnnotationChange,
|
||||||
|
removeAnnotation,
|
||||||
|
errors,
|
||||||
|
placeholder,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{annotations.map((annotation, i) => (
|
||||||
|
<div className="row" key={annotation.ID}>
|
||||||
|
<div className="form-group !pl-0 col-sm-4 !m-0">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">Key</span>
|
||||||
|
<input
|
||||||
|
name={`annotation_key_${i}`}
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder={placeholder[0]}
|
||||||
|
defaultValue={annotation.Key}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleAnnotationChange(i, 'Key', e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[`annotations.key[${i}]`] && (
|
||||||
|
<FormError className="mt-1 !mb-0">
|
||||||
|
{errors[`annotations.key[${i}]`]}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form-group !pl-0 col-sm-4 !m-0">
|
||||||
|
<div className="input-group input-group-sm">
|
||||||
|
<span className="input-group-addon required">Value</span>
|
||||||
|
<input
|
||||||
|
name={`annotation_value_${i}`}
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder={placeholder[1]}
|
||||||
|
defaultValue={annotation.Value}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleAnnotationChange(i, 'Value', e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors[`annotations.value[${i}]`] && (
|
||||||
|
<FormError className="mt-1 !mb-0">
|
||||||
|
{errors[`annotations.value[${i}]`]}
|
||||||
|
</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-3 !pl-0 !m-0">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-dangerlight btn-only-icon !ml-0"
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeAnnotation(i)}
|
||||||
|
>
|
||||||
|
<Icon icon="trash-2" size="md" feather />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface Annotation {
|
||||||
|
Key: string;
|
||||||
|
Value: string;
|
||||||
|
ID: string;
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import {
|
||||||
|
mutationOptions,
|
||||||
|
withError,
|
||||||
|
withInvalidate,
|
||||||
|
} from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getIngresses,
|
||||||
|
getIngress,
|
||||||
|
createIngress,
|
||||||
|
deleteIngresses,
|
||||||
|
updateIngress,
|
||||||
|
getIngressControllers,
|
||||||
|
} from './service';
|
||||||
|
import { DeleteIngressesRequest, Ingress } from './types';
|
||||||
|
|
||||||
|
const ingressKeys = {
|
||||||
|
all: ['environments', 'kubernetes', 'namespace', 'ingress'] as const,
|
||||||
|
namespace: (
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string,
|
||||||
|
ingress: string
|
||||||
|
) => [...ingressKeys.all, String(environmentId), namespace, ingress] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useIngress(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string,
|
||||||
|
name: string
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
[
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'namespace',
|
||||||
|
namespace,
|
||||||
|
'ingress',
|
||||||
|
name,
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
const ing = await getIngress(environmentId, namespace, name);
|
||||||
|
return ing;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...withError('Unable to get ingress'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIngresses(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespaces: string[]
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
[
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'namespace',
|
||||||
|
namespaces,
|
||||||
|
'ingress',
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
const ingresses: Ingress[] = [];
|
||||||
|
for (let i = 0; i < namespaces.length; i += 1) {
|
||||||
|
const ings = await getIngresses(environmentId, namespaces[i]);
|
||||||
|
if (ings) {
|
||||||
|
ingresses.push(...ings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ingresses;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: namespaces.length > 0,
|
||||||
|
...withError('Unable to get ingresses'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateIngress() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
({
|
||||||
|
environmentId,
|
||||||
|
ingress,
|
||||||
|
}: {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
ingress: Ingress;
|
||||||
|
}) => createIngress(environmentId, ingress),
|
||||||
|
mutationOptions(
|
||||||
|
withError('Unable to create ingress controller'),
|
||||||
|
withInvalidate(queryClient, [ingressKeys.all])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateIngress() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
({
|
||||||
|
environmentId,
|
||||||
|
ingress,
|
||||||
|
}: {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
ingress: Ingress;
|
||||||
|
}) => updateIngress(environmentId, ingress),
|
||||||
|
mutationOptions(
|
||||||
|
withError('Unable to update ingress controller'),
|
||||||
|
withInvalidate(queryClient, [ingressKeys.all])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteIngresses() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
({
|
||||||
|
environmentId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
data: DeleteIngressesRequest;
|
||||||
|
}) => deleteIngresses(environmentId, data),
|
||||||
|
mutationOptions(
|
||||||
|
withError('Unable to update ingress controller'),
|
||||||
|
withInvalidate(queryClient, [ingressKeys.all])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ingress Controllers
|
||||||
|
*/
|
||||||
|
export function useIngressControllers(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
[
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'namespace',
|
||||||
|
namespace,
|
||||||
|
'ingresscontrollers',
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
const ing = await getIngressControllers(environmentId, namespace);
|
||||||
|
return ing;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!namespace,
|
||||||
|
...withError('Unable to get ingress controllers'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { Ingress, DeleteIngressesRequest, IngressController } from './types';
|
||||||
|
|
||||||
|
export async function getIngress(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string,
|
||||||
|
ingressName: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: ingress } = await axios.get<Ingress[]>(
|
||||||
|
buildUrl(environmentId, namespace, ingressName)
|
||||||
|
);
|
||||||
|
return ingress[0];
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve the ingress');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIngresses(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: ingresses } = await axios.get<Ingress[]>(
|
||||||
|
buildUrl(environmentId, namespace)
|
||||||
|
);
|
||||||
|
return ingresses;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve ingresses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIngressControllers(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: ingresscontrollers } = await axios.get<IngressController[]>(
|
||||||
|
`kubernetes/${environmentId}/namespaces/${namespace}/ingresscontrollers`
|
||||||
|
);
|
||||||
|
return ingresscontrollers;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve ingresses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createIngress(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
ingress: Ingress
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await axios.post(
|
||||||
|
buildUrl(environmentId, ingress.Namespace),
|
||||||
|
ingress
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to create an ingress');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateIngress(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
ingress: Ingress
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await axios.put(buildUrl(environmentId, ingress.Namespace), ingress);
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to update an ingress');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteIngresses(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
data: DeleteIngressesRequest
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await axios.post(
|
||||||
|
`kubernetes/${environmentId}/ingresses/delete`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to delete ingresses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string,
|
||||||
|
ingressName?: string
|
||||||
|
) {
|
||||||
|
let url = `kubernetes/${environmentId}/namespaces/${namespace}/ingresses`;
|
||||||
|
|
||||||
|
if (ingressName) {
|
||||||
|
url += `/${ingressName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
.ingress-rules .bordered {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingress-rules .rule {
|
||||||
|
background-color: var(--bg-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingressDatatable-actions button > span,
|
||||||
|
.anntation-actions button > span,
|
||||||
|
.rules-action button > span,
|
||||||
|
.rule button > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-inline {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-inline svg {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-inline svg,
|
||||||
|
.error-inline span {
|
||||||
|
display: inline;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {
|
||||||
|
PaginationTableSettings,
|
||||||
|
SortableTableSettings,
|
||||||
|
} from '@/react/components/datatables/types';
|
||||||
|
|
||||||
|
export interface TableSettings
|
||||||
|
extends SortableTableSettings,
|
||||||
|
PaginationTableSettings {}
|
||||||
|
|
||||||
|
export interface Path {
|
||||||
|
IngressName: string;
|
||||||
|
ServiceName: string;
|
||||||
|
Host: string;
|
||||||
|
Port: number;
|
||||||
|
Path: string;
|
||||||
|
PathType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TLS {
|
||||||
|
Hosts: string[];
|
||||||
|
SecretName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Ingress = {
|
||||||
|
Name: string;
|
||||||
|
UID?: string;
|
||||||
|
Namespace: string;
|
||||||
|
ClassName: string;
|
||||||
|
Annotations?: Record<string, string>;
|
||||||
|
Hosts?: string[];
|
||||||
|
Paths: Path[];
|
||||||
|
TLS?: TLS[];
|
||||||
|
Type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DeleteIngressesRequest {
|
||||||
|
[key: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngressController {
|
||||||
|
Name: string;
|
||||||
|
ClassName: string;
|
||||||
|
Availability: string;
|
||||||
|
Type: string;
|
||||||
|
New: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { getServices } from './service';
|
||||||
|
import { Service } from './types';
|
||||||
|
|
||||||
|
export function useServices(environmentId: EnvironmentId, namespace: string) {
|
||||||
|
return useQuery(
|
||||||
|
[
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'namespaces',
|
||||||
|
namespace,
|
||||||
|
'services',
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
namespace ? getServices(environmentId, namespace) : ([] as Service[]),
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
notifyError('Failure', err as Error, 'Unable to get services');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { Service } from './types';
|
||||||
|
|
||||||
|
export async function getServices(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: services } = await axios.get<Service[]>(
|
||||||
|
buildUrl(environmentId, namespace)
|
||||||
|
);
|
||||||
|
return services;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve services');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(environmentId: EnvironmentId, namespace: string) {
|
||||||
|
const url = `kubernetes/${environmentId}/namespaces/${namespace}/services`;
|
||||||
|
return url;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
export interface Port {
|
||||||
|
Name: string;
|
||||||
|
Protocol: string;
|
||||||
|
Port: number;
|
||||||
|
TargetPort: number;
|
||||||
|
NodePort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngressIP {
|
||||||
|
IP: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadBalancer {
|
||||||
|
Ingress: IngressIP[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
LoadBalancer: LoadBalancer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
Annotations?: Document;
|
||||||
|
CreationTimestamp?: string;
|
||||||
|
Labels?: Document;
|
||||||
|
Name: string;
|
||||||
|
Namespace: string;
|
||||||
|
UID: string;
|
||||||
|
AllocateLoadBalancerNodePorts?: boolean;
|
||||||
|
Ports: Port[];
|
||||||
|
Selector?: Document;
|
||||||
|
Type: string;
|
||||||
|
Status?: Status;
|
||||||
|
}
|
|
@ -57,8 +57,24 @@ export interface KubernetesSnapshot {
|
||||||
NodeCount: number;
|
NodeCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IngressClass = {
|
||||||
|
Name: string;
|
||||||
|
Type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface KubernetesConfiguration {
|
||||||
|
UseLoadBalancer?: boolean;
|
||||||
|
UseServerMetrics?: boolean;
|
||||||
|
EnableResourceOverCommit?: boolean;
|
||||||
|
ResourceOverCommitPercentage?: number;
|
||||||
|
RestrictDefaultNamespace?: boolean;
|
||||||
|
IngressClasses: IngressClass[];
|
||||||
|
IngressAvailabilityPerNamespace: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface KubernetesSettings {
|
export interface KubernetesSettings {
|
||||||
Snapshots?: KubernetesSnapshot[] | null;
|
Snapshots?: KubernetesSnapshot[] | null;
|
||||||
|
Configuration: KubernetesConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EnvironmentEdge = {
|
export type EnvironmentEdge = {
|
||||||
|
|
|
@ -70,7 +70,13 @@ export function createMockEnvironment(): Environment {
|
||||||
Status: 1,
|
Status: 1,
|
||||||
URL: 'url',
|
URL: 'url',
|
||||||
Snapshots: [],
|
Snapshots: [],
|
||||||
Kubernetes: { Snapshots: [] },
|
Kubernetes: {
|
||||||
|
Snapshots: [],
|
||||||
|
Configuration: {
|
||||||
|
IngressClasses: [],
|
||||||
|
IngressAvailabilityPerNamespace: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
EdgeKey: '',
|
EdgeKey: '',
|
||||||
Id: 3,
|
Id: 3,
|
||||||
UserTrusted: false,
|
UserTrusted: false,
|
||||||
|
@ -92,8 +98,6 @@ export function createMockEnvironment(): Environment {
|
||||||
enableHostManagementFeatures: false,
|
enableHostManagementFeatures: false,
|
||||||
},
|
},
|
||||||
Gpus: [],
|
Gpus: [],
|
||||||
Agent: {
|
Agent: { Version: '1.0.0' },
|
||||||
Version: '',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren, AnchorHTMLAttributes } from 'react';
|
||||||
import { UISref, UISrefProps } from '@uirouter/react';
|
import { UISref, UISrefProps } from '@uirouter/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link({
|
export function Link({
|
||||||
title = '',
|
title = '',
|
||||||
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<Props> & UISrefProps) {
|
}: PropsWithChildren<Props> & UISrefProps) {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<UISref {...props}>
|
<UISref className={clsx('no-decoration', className)} {...props}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
<a title={title}>{children}</a>
|
<a title={title} target={props.target}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
</UISref>
|
</UISref>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,11 @@ export function Datatable<
|
||||||
<TableSettingsProvider settings={settingsStore}>
|
<TableSettingsProvider settings={settingsStore}>
|
||||||
<Table.Container>
|
<Table.Container>
|
||||||
{isTitleVisible(titleOptions) && (
|
{isTitleVisible(titleOptions) && (
|
||||||
<Table.Title label={titleOptions.title} icon={titleOptions.icon}>
|
<Table.Title
|
||||||
|
label={titleOptions.title}
|
||||||
|
icon={titleOptions.icon}
|
||||||
|
featherIcon={titleOptions.featherIcon}
|
||||||
|
>
|
||||||
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
|
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
|
||||||
{renderTableActions && (
|
{renderTableActions && (
|
||||||
<Table.Actions>
|
<Table.Actions>
|
||||||
|
|
|
@ -3,42 +3,22 @@ import { useMemo } from 'react';
|
||||||
import { Menu, MenuButton, MenuPopover } from '@reach/menu-button';
|
import { Menu, MenuButton, MenuPopover } from '@reach/menu-button';
|
||||||
import { ColumnInstance } from 'react-table';
|
import { ColumnInstance } from 'react-table';
|
||||||
|
|
||||||
export function DefaultFilter({
|
export const DefaultFilter = filterHOC('Filter by state');
|
||||||
column: { filterValue, setFilter, preFilteredRows, id },
|
|
||||||
}: {
|
|
||||||
column: ColumnInstance;
|
|
||||||
}) {
|
|
||||||
const options = useMemo(() => {
|
|
||||||
const options = new Set<string>();
|
|
||||||
preFilteredRows.forEach((row) => {
|
|
||||||
options.add(row.values[id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(options);
|
|
||||||
}, [id, preFilteredRows]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MultipleSelectionFilter
|
|
||||||
options={options}
|
|
||||||
filterKey={id}
|
|
||||||
value={filterValue}
|
|
||||||
onChange={setFilter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MultipleSelectionFilterProps {
|
interface MultipleSelectionFilterProps {
|
||||||
options: string[];
|
options: string[];
|
||||||
value: string[];
|
value: string[];
|
||||||
filterKey: string;
|
filterKey: string;
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
|
menuTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MultipleSelectionFilter({
|
export function MultipleSelectionFilter({
|
||||||
options,
|
options,
|
||||||
value = [],
|
value = [],
|
||||||
filterKey,
|
filterKey,
|
||||||
onChange,
|
onChange,
|
||||||
|
menuTitle = 'Filter by state',
|
||||||
}: MultipleSelectionFilterProps) {
|
}: MultipleSelectionFilterProps) {
|
||||||
const enabled = value.length > 0;
|
const enabled = value.length > 0;
|
||||||
return (
|
return (
|
||||||
|
@ -59,7 +39,7 @@ function MultipleSelectionFilter({
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuPopover className="dropdown-menu">
|
<MenuPopover className="dropdown-menu">
|
||||||
<div className="tableMenu">
|
<div className="tableMenu">
|
||||||
<div className="menuHeader">Filter by state</div>
|
<div className="menuHeader">{menuTitle}</div>
|
||||||
<div className="menuContent">
|
<div className="menuContent">
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<div className="md-checkbox" key={index}>
|
<div className="md-checkbox" key={index}>
|
||||||
|
@ -91,3 +71,28 @@ function MultipleSelectionFilter({
|
||||||
onChange([...value, option]);
|
onChange([...value, option]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterHOC(menuTitle: string) {
|
||||||
|
return function Filter({
|
||||||
|
column: { filterValue, setFilter, preFilteredRows, id },
|
||||||
|
}: {
|
||||||
|
column: ColumnInstance;
|
||||||
|
}) {
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const options = new Set<string>();
|
||||||
|
preFilteredRows.forEach((row) => {
|
||||||
|
options.add(row.values[id]);
|
||||||
|
});
|
||||||
|
return Array.from(options);
|
||||||
|
}, [id, preFilteredRows]);
|
||||||
|
return (
|
||||||
|
<MultipleSelectionFilter
|
||||||
|
options={options}
|
||||||
|
filterKey={id}
|
||||||
|
value={filterValue}
|
||||||
|
onChange={setFilter}
|
||||||
|
menuTitle={menuTitle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
export function FormError({ children }: PropsWithChildren<unknown>) {
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormError({ children, className }: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div className="small text-warning vertical-center">
|
<p className={clsx(`text-muted small vertical-center`, className)}>
|
||||||
<Icon
|
<Icon icon="alert-triangle" className="icon-warning" feather />
|
||||||
icon="alert-triangle"
|
<span className="text-warning">{children}</span>
|
||||||
feather
|
</p>
|
||||||
className="icon icon-sm icon-warning"
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export function ContainersDatatable({
|
||||||
<RowProvider context={{ environment }}>
|
<RowProvider context={{ environment }}>
|
||||||
<Datatable
|
<Datatable
|
||||||
titleOptions={{
|
titleOptions={{
|
||||||
icon: 'fa-cubes',
|
icon: 'svg-cubes',
|
||||||
title: 'Containers',
|
title: 'Containers',
|
||||||
}}
|
}}
|
||||||
settingsStore={settings}
|
settingsStore={settings}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { getConfigMaps } from './service';
|
||||||
|
|
||||||
|
export function useConfigurations(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace?: string
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
[
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'namespaces',
|
||||||
|
namespace,
|
||||||
|
'configurations',
|
||||||
|
],
|
||||||
|
() => (namespace ? getConfigMaps(environmentId, namespace) : []),
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
notifyError('Failure', err as Error, 'Unable to get configurations');
|
||||||
|
},
|
||||||
|
enabled: !!namespace,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { Configuration } from './types';
|
||||||
|
|
||||||
|
export async function getConfigMaps(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: configmaps } = await axios.get<Configuration[]>(
|
||||||
|
`kubernetes/${environmentId}/namespaces/${namespace}/configmaps`
|
||||||
|
);
|
||||||
|
return configmaps;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve configmaps');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export interface Configuration {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
Type: number;
|
||||||
|
Namespace: string;
|
||||||
|
CreationDate: Date;
|
||||||
|
|
||||||
|
ConfigurationOwner: string;
|
||||||
|
|
||||||
|
Used: boolean;
|
||||||
|
// Applications: any[];
|
||||||
|
Data: Document;
|
||||||
|
Yaml: string;
|
||||||
|
|
||||||
|
SecretType?: string;
|
||||||
|
IsRegistrySecret?: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { getNamespaces, getNamespace } from './service';
|
||||||
|
|
||||||
|
export function useNamespaces(environmentId: EnvironmentId) {
|
||||||
|
return useQuery(
|
||||||
|
['environments', environmentId, 'kubernetes', 'namespaces'],
|
||||||
|
() => getNamespaces(environmentId),
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
notifyError('Failure', err as Error, 'Unable to get namespaces.');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNamespace(environmentId: EnvironmentId, namespace: string) {
|
||||||
|
return useQuery(
|
||||||
|
['environments', environmentId, 'kubernetes', 'namespaces', namespace],
|
||||||
|
() => getNamespace(environmentId, namespace),
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
notifyError('Failure', err as Error, 'Unable to get namespace.');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { Namespaces } from './types';
|
||||||
|
|
||||||
|
export async function getNamespace(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data: ingress } = await axios.get<Namespaces>(
|
||||||
|
buildUrl(environmentId, namespace)
|
||||||
|
);
|
||||||
|
return ingress;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve network details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNamespaces(environmentId: EnvironmentId) {
|
||||||
|
try {
|
||||||
|
const { data: ingresses } = await axios.get<Namespaces>(
|
||||||
|
buildUrl(environmentId)
|
||||||
|
);
|
||||||
|
return ingresses;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Unable to retrieve network details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
|
||||||
|
let url = `kubernetes/${environmentId}/namespaces`;
|
||||||
|
|
||||||
|
if (namespace) {
|
||||||
|
url += `/${namespace}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface Namespaces {
|
||||||
|
[key: string]: {
|
||||||
|
IsDefault: boolean;
|
||||||
|
IsSystem: boolean;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Common Services
|
||||||
|
|
||||||
|
This folder contains rest api services that are shared by different features within kubernetes.
|
||||||
|
|
||||||
|
This includes api requests to the portainer backend, and also requests to the kubernetes api.
|
|
@ -3,6 +3,7 @@ import { Box, Edit, Layers, Lock, Server } from 'react-feather';
|
||||||
import { EnvironmentId } from '@/portainer/environments/types';
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
import { Authorized } from '@/portainer/hooks/useUser';
|
import { Authorized } from '@/portainer/hooks/useUser';
|
||||||
import Helm from '@/assets/ico/vendor/helm.svg?c';
|
import Helm from '@/assets/ico/vendor/helm.svg?c';
|
||||||
|
import Route from '@/assets/ico/route.svg?c';
|
||||||
|
|
||||||
import { DashboardLink } from '../items/DashboardLink';
|
import { DashboardLink } from '../items/DashboardLink';
|
||||||
import { SidebarItem } from '../SidebarItem';
|
import { SidebarItem } from '../SidebarItem';
|
||||||
|
@ -69,6 +70,14 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
data-cy="k8sSidebar-applications"
|
data-cy="k8sSidebar-applications"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SidebarItem
|
||||||
|
to="kubernetes.ingresses"
|
||||||
|
params={{ endpointId: environmentId }}
|
||||||
|
label="Ingresses"
|
||||||
|
data-cy="k8sSidebar-ingresses"
|
||||||
|
icon={Route}
|
||||||
|
/>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.configurations"
|
to="kubernetes.configurations"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
|
@ -97,7 +106,7 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.cluster.setup"
|
to="kubernetes.cluster.setup"
|
||||||
params={{ id: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Setup"
|
label="Setup"
|
||||||
data-cy="k8sSidebar-setup"
|
data-cy="k8sSidebar-setup"
|
||||||
/>
|
/>
|
||||||
|
@ -110,7 +119,7 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.cluster.securityConstraint"
|
to="kubernetes.cluster.securityConstraint"
|
||||||
params={{ id: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Security constraints"
|
label="Security constraints"
|
||||||
data-cy="k8sSidebar-securityConstraints"
|
data-cy="k8sSidebar-securityConstraints"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesConfigMaps
|
||||||
|
// @summary Fetches a list of config maps for a given namespace
|
||||||
|
// @description Fetches a list of config maps for a given namespace
|
||||||
|
// classes from the kubernetes api
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/configmaps [get]
|
||||||
|
func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, configmaps)
|
||||||
|
}
|
|
@ -0,0 +1,550 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
portaineree "github.com/portainer/portainer-ee/api"
|
||||||
|
"github.com/portainer/portainer-ee/api/database/models"
|
||||||
|
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesIngressControllers
|
||||||
|
// @summary Fetches a list of ingress controllers with classes
|
||||||
|
// @description Fetches a list of ingress controllers which have associated
|
||||||
|
// classes from the kubernetes api
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/ingresscontrollers [get]
|
||||||
|
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portaineree.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to create Kubernetes client",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers := cli.GetIngressControllers()
|
||||||
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
for i := range controllers {
|
||||||
|
controllers[i].Availability = true
|
||||||
|
controllers[i].New = true
|
||||||
|
|
||||||
|
// Check if the controller is blocked globally.
|
||||||
|
for _, a := range existingClasses {
|
||||||
|
controllers[i].New = false
|
||||||
|
if controllers[i].ClassName != a.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
controllers[i].New = false
|
||||||
|
|
||||||
|
// Skip over non-global blocks.
|
||||||
|
if len(a.BlockedNamespaces) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if controllers[i].ClassName == a.Name {
|
||||||
|
controllers[i].Availability = !a.Blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Update existingClasses to take care of New and remove no longer
|
||||||
|
// existing classes.
|
||||||
|
}
|
||||||
|
return response.JSON(w, controllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesIngressControllersByNamespace
|
||||||
|
// @summary Fetches a list of ingress controllers with classes allowed in a
|
||||||
|
// namespace
|
||||||
|
// @description Fetches a list of ingress controllers which have associated
|
||||||
|
// classes from the kubernetes api and have been allowed in a given namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [get]
|
||||||
|
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portaineree.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to create Kubernetes client",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers := cli.GetIngressControllers()
|
||||||
|
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
for i := range controllers {
|
||||||
|
controllers[i].Availability = true
|
||||||
|
controllers[i].New = true
|
||||||
|
|
||||||
|
// Check if the controller is blocked globally or in the current
|
||||||
|
// namespace.
|
||||||
|
for _, a := range existingClasses {
|
||||||
|
if controllers[i].ClassName != a.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
controllers[i].New = false
|
||||||
|
|
||||||
|
// If it's not blocked we're all done!
|
||||||
|
if !a.Blocked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global blocks.
|
||||||
|
if len(a.BlockedNamespaces) == 0 {
|
||||||
|
controllers[i].Availability = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check the current namespace.
|
||||||
|
for _, ns := range a.BlockedNamespaces {
|
||||||
|
if namespace == ns {
|
||||||
|
controllers[i].Availability = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Update existingClasses to take care of New and remove no longer
|
||||||
|
// existing classes.
|
||||||
|
}
|
||||||
|
return response.JSON(w, controllers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id UpdateKubernetesIngressControllers
|
||||||
|
// @summary Updates a list of ingress controller permissions globally in a
|
||||||
|
// cluster
|
||||||
|
// @description Updates a list of ingress controller permissions to deny or
|
||||||
|
// allow their usage in a given cluster
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sIngressControllers true "list of controllers to update"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/ingresscontrollers [put]
|
||||||
|
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portaineree.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressControllers
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
classes := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
for _, p := range payload {
|
||||||
|
for i := range classes {
|
||||||
|
if p.ClassName == classes[i].Name {
|
||||||
|
classes[i].Blocked = !p.Availability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = classes
|
||||||
|
fmt.Printf("%#v\n", endpoint.Kubernetes.Configuration.IngressClasses)
|
||||||
|
err = handler.DataStore.Endpoint().UpdateEndpoint(
|
||||||
|
portaineree.EndpointID(endpointID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to update the BlockedIngressClasses inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id UpdateKubernetesIngressControllers
|
||||||
|
// @summary Updates a list of ingress controller permissions in a particular
|
||||||
|
// namespace in a particular cluster
|
||||||
|
// @description Updates a list of ingress controller permissions in a particular
|
||||||
|
// namespace in a particular cluster
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sIngressControllers true "list of controllers to update"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [put]
|
||||||
|
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid environment identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portaineree.EndpointID(endpointID))
|
||||||
|
if err == portainerDsErrors.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to find an environment with the specified identifier inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressControllers
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
classes := endpoint.Kubernetes.Configuration.IngressClasses
|
||||||
|
PayloadLoop:
|
||||||
|
for _, p := range payload {
|
||||||
|
for i := range classes {
|
||||||
|
if p.ClassName == classes[i].Name {
|
||||||
|
if p.Availability == true {
|
||||||
|
classes[i].Blocked = false
|
||||||
|
classes[i].BlockedNamespaces = []string{}
|
||||||
|
continue PayloadLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
classes[i].Blocked = true
|
||||||
|
for _, ns := range classes[i].BlockedNamespaces {
|
||||||
|
if namespace == ns {
|
||||||
|
continue PayloadLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classes[i].BlockedNamespaces = append(
|
||||||
|
classes[i].BlockedNamespaces,
|
||||||
|
namespace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoint.Kubernetes.Configuration.IngressClasses = classes
|
||||||
|
fmt.Printf("%#v\n", endpoint.Kubernetes.Configuration.IngressClasses)
|
||||||
|
err = handler.DataStore.Endpoint().UpdateEndpoint(
|
||||||
|
portaineree.EndpointID(endpointID),
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to update the BlockedIngressClasses inside the database",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id GetKubernetesIngresses
|
||||||
|
// @summary Fetches a list of ingresses in a namespace
|
||||||
|
// @description Fetches a list of ingresses in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [get]
|
||||||
|
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
ingresses, err := cli.GetIngresses(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, ingresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id CreateKubernetesIngresses
|
||||||
|
// @summary Creates an ingress in a namespace
|
||||||
|
// @description Creates an ingress in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sIngressInfo true "ingress to create"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [post]
|
||||||
|
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.CreateIngress(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id DeleteKubernetesIngresses
|
||||||
|
// @summary Deletes an ingress in a namespace
|
||||||
|
// @description Fetches a list of ingresses in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sIngressDeleteRequests true "ingress to delete"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses/delete [post]
|
||||||
|
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sIngressDeleteRequests
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteIngresses(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id UpdateKubernetesIngresses
|
||||||
|
// @summary Updates an ingress in a namespace
|
||||||
|
// @description Fetches a list of ingresses in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sIngressInfo true "ingress to update"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [put]
|
||||||
|
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sIngressInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.UpdateIngress(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer-ee/api/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesNamespaces
|
||||||
|
// @summary Fetches a list of namespaces for a given cluster
|
||||||
|
// @description Fetches a list of namespaces for a given cluster
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces [get]
|
||||||
|
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
namespaces, err := cli.GetNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, namespaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id CreateKubernetesNamespace
|
||||||
|
// @summary Creates a namespace in a given cluster
|
||||||
|
// @description Creates a namespace in a given cluster
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sNamespaceInfo true "namespace to create"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces [post]
|
||||||
|
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sNamespaceInfo
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.CreateNamespace(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id DeleteKubernetesNamespaces
|
||||||
|
// @summary Delete a namespace from a given cluster
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespace/{namespace} [delete]
|
||||||
|
func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteNamespace(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id UpdateKubernetesNamespace
|
||||||
|
// @summary Updates a namespace in a given cluster
|
||||||
|
// @description Updates a namespace in a given cluster
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sNamespaceInfo true "namespace to update"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces [put]
|
||||||
|
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sNamespaceInfo
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.UpdateNamespace(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer-ee/api/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @id GetKubernetesServices
|
||||||
|
// @summary Fetches a list of services in a namespace
|
||||||
|
// @description Fetches a list of services in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/services [get]
|
||||||
|
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
services, err := cli.GetServices(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve services",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, services)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id CreateKubernetesService
|
||||||
|
// @summary Creates an service in a namespace
|
||||||
|
// @description Creates an service in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sServiceInfo true "service to create"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/services [post]
|
||||||
|
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sServiceInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.CreateService(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id DeleteKubernetesServices
|
||||||
|
// @summary Deletes a service in a namespace
|
||||||
|
// @description Deletes a service in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sServiceDeleteRequests true "service to delete"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/services/delete [post]
|
||||||
|
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
|
||||||
|
var payload models.K8sServiceDeleteRequests
|
||||||
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.DeleteServices(payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id UpdateKubernetesService
|
||||||
|
// @summary Updates an service in a namespace
|
||||||
|
// @description Updates an service in a namespace
|
||||||
|
// @description **Access policy**: authenticated
|
||||||
|
// @tags kubernetes
|
||||||
|
// @security ApiKeyAuth
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param body body models.K8sServiceInfo true "service to update"
|
||||||
|
// @success 200 "Success"
|
||||||
|
// @failure 400 "Invalid request"
|
||||||
|
// @failure 401 "Unauthorized"
|
||||||
|
// @failure 403 "Permission denied"
|
||||||
|
// @failure 404 "Environment(Endpoint) not found"
|
||||||
|
// @failure 500 "Server error"
|
||||||
|
// @router /kubernetes/{id}/namespaces/{namespace}/services [put]
|
||||||
|
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid namespace identifier route variable",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload models.K8sServiceInfo
|
||||||
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request payload",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := handler.KubernetesClient
|
||||||
|
err = cli.UpdateService(namespace, payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to retrieve nodes limits",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue