mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +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
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue