mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(ingress): ingresses datatable with add/edit ingresses EE-2615 (#7672)
This commit is contained in:
parent
393d1fc91d
commit
ef1d648c07
68 changed files with 4938 additions and 61 deletions
17
api/database/models/configmaps_and_secrets.go
Normal file
17
api/database/models/configmaps_and_secrets.go
Normal file
|
@ -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"`
|
||||
}
|
||||
)
|
77
api/database/models/ingress.go
Normal file
77
api/database/models/ingress.go
Normal file
|
@ -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
|
||||
}
|
12
api/database/models/namespaces.go
Normal file
12
api/database/models/namespaces.go
Normal file
|
@ -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
|
||||
}
|
64
api/database/models/services.go
Normal file
64
api/database/models/services.go
Normal file
|
@ -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,
|
||||
"Kubernetes": {
|
||||
"Configuration": {
|
||||
"EnableResourceOverCommit": false,
|
||||
"IngressClasses": null,
|
||||
"ResourceOverCommitPercentage": 0,
|
||||
"RestrictDefaultNamespace": false,
|
||||
"StorageClasses": null,
|
||||
"UseLoadBalancer": false,
|
||||
|
|
33
api/http/handler/kubernetes/configmaps_and_secrets.go
Normal file
33
api/http/handler/kubernetes/configmaps_and_secrets.go
Normal file
|
@ -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 (
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"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"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
@ -23,10 +27,11 @@ type Handler struct {
|
|||
jwtService dataservices.JWTService
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
kubeClusterAccessService kubernetes.KubeClusterAccessService
|
||||
KubernetesClient portainer.KubeClient
|
||||
}
|
||||
|
||||
// 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{
|
||||
Router: mux.NewRouter(),
|
||||
authorizationService: authorizationService,
|
||||
|
@ -34,6 +39,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
|||
jwtService: jwtService,
|
||||
kubeClusterAccessService: kubeClusterAccessService,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
KubernetesClient: kubernetesClient,
|
||||
}
|
||||
|
||||
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
|
||||
|
@ -45,15 +51,32 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
|||
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
endpointRouter.Use(kubeOnlyMiddleware)
|
||||
endpointRouter.Use(h.kubeClient)
|
||||
|
||||
endpointRouter.PathPrefix("/nodes_limits").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).Methods(http.MethodGet)
|
||||
endpointRouter.PathPrefix("/nodes_limits").Handler(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
|
||||
// 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.
|
||||
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||
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
|
||||
}
|
||||
|
@ -75,3 +98,51 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler {
|
|||
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)
|
||||
})
|
||||
}
|
||||
|
|
410
api/http/handler/kubernetes/ingresses.go
Normal file
410
api/http/handler/kubernetes/ingresses.go
Normal file
|
@ -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
|
||||
}
|
97
api/http/handler/kubernetes/namespaces.go
Normal file
97
api/http/handler/kubernetes/namespaces.go
Normal file
|
@ -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
|
||||
}
|
121
api/http/handler/kubernetes/services.go
Normal file
121
api/http/handler/kubernetes/services.go
Normal file
|
@ -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.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)
|
||||
|
||||
|
|
64
api/kubernetes/cli/configmaps_and_secrets.go
Normal file
64
api/kubernetes/cli/configmaps_and_secrets.go
Normal file
|
@ -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
|
||||
}
|
243
api/kubernetes/cli/ingress.go
Normal file
243
api/kubernetes/cli/ingress.go
Normal file
|
@ -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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
v1 "k8s.io/api/core/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 {
|
||||
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
|
||||
if hasSystemLabel {
|
||||
|
@ -72,3 +106,34 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
|
|||
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)
|
||||
}
|
||||
|
|
153
api/kubernetes/cli/service.go
Normal file
153
api/kubernetes/cli/service.go
Normal file
|
@ -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/volume"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
@ -511,6 +512,11 @@ type (
|
|||
// JobType represents a job type
|
||||
JobType int
|
||||
|
||||
K8sNamespaceInfo struct {
|
||||
IsSystem bool `json:"IsSystem"`
|
||||
IsDefault bool `json:"IsDefault"`
|
||||
}
|
||||
|
||||
K8sNodeLimits struct {
|
||||
CPU int64 `json:"CPU"`
|
||||
Memory int64 `json:"Memory"`
|
||||
|
@ -540,11 +546,13 @@ type (
|
|||
|
||||
// KubernetesConfiguration represents the configuration of a Kubernetes environment(endpoint)
|
||||
KubernetesConfiguration struct {
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
EnableResourceOverCommit bool `json:"EnableResourceOverCommit"`
|
||||
ResourceOverCommitPercentage int `json:"ResourceOverCommitPercentage"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"`
|
||||
}
|
||||
|
||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||
|
@ -557,8 +565,10 @@ type (
|
|||
|
||||
// KubernetesIngressClassConfig represents a Kubernetes Ingress Class configuration
|
||||
KubernetesIngressClassConfig struct {
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
Blocked bool `json:"Blocked"`
|
||||
BlockedNamespaces []string `json:"BlockedNamespaces"`
|
||||
}
|
||||
|
||||
// KubernetesShellPod represents a Kubectl Shell details to facilitate pod exec functionality
|
||||
|
@ -1330,6 +1340,22 @@ type (
|
|||
GetServiceAccountBearerToken(userID int) (string, 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)
|
||||
|
||||
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)
|
||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||
GetNodesLimits() (K8sNodesLimits, error)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue