2022-09-21 04:49:42 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"strings"
|
|
|
|
|
2022-11-13 19:33:57 +00:00
|
|
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
2022-12-20 03:46:51 +00:00
|
|
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
2022-10-17 21:46:27 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2022-09-21 04:49:42 +00:00
|
|
|
netv1 "k8s.io/api/networking/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
)
|
|
|
|
|
2022-10-17 21:46:27 +00:00
|
|
|
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
|
2022-09-21 04:49:42 +00:00
|
|
|
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 {
|
2022-10-17 21:46:27 +00:00
|
|
|
return nil, err
|
2022-09-21 04:49:42 +00:00
|
|
|
}
|
|
|
|
|
2022-09-26 19:43:24 +00:00
|
|
|
// We want to know which of these controllers is in use.
|
|
|
|
var ingresses []models.K8sIngressInfo
|
|
|
|
namespaces, err := kcl.GetNamespaces()
|
|
|
|
if err != nil {
|
2022-10-17 21:46:27 +00:00
|
|
|
return nil, err
|
2022-09-26 19:43:24 +00:00
|
|
|
}
|
|
|
|
for namespace := range namespaces {
|
|
|
|
t, err := kcl.GetIngresses(namespace)
|
|
|
|
if err != nil {
|
2022-10-17 21:46:27 +00:00
|
|
|
// User might not be able to list ingresses in system/not allowed
|
|
|
|
// namespaces.
|
|
|
|
log.Debug().Err(err).Msg("failed to list ingresses for the current user, skipped sending ingress")
|
|
|
|
continue
|
2022-09-26 19:43:24 +00:00
|
|
|
}
|
|
|
|
ingresses = append(ingresses, t...)
|
|
|
|
}
|
|
|
|
usedClasses := make(map[string]struct{})
|
|
|
|
for _, ingress := range ingresses {
|
|
|
|
usedClasses[ingress.ClassName] = struct{}{}
|
|
|
|
}
|
|
|
|
|
2022-09-21 04:49:42 +00:00
|
|
|
for _, class := range classList.Items {
|
|
|
|
var controller models.K8sIngressController
|
|
|
|
controller.Name = class.Spec.Controller
|
|
|
|
controller.ClassName = class.Name
|
2022-09-26 19:43:24 +00:00
|
|
|
|
|
|
|
// If the class is used mark it as such.
|
|
|
|
if _, ok := usedClasses[class.Name]; ok {
|
|
|
|
controller.Used = true
|
|
|
|
}
|
|
|
|
|
2022-09-21 04:49:42 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-10-17 21:46:27 +00:00
|
|
|
return controllers, nil
|
2022-09-21 04:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
var info models.K8sIngressInfo
|
|
|
|
info.Name = ingress.Name
|
|
|
|
info.UID = string(ingress.UID)
|
|
|
|
info.Namespace = namespace
|
2022-10-24 20:41:30 +00:00
|
|
|
ingressClass := ingress.Spec.IngressClassName
|
2022-09-21 04:49:42 +00:00
|
|
|
info.ClassName = ""
|
|
|
|
if ingressClass != nil {
|
|
|
|
info.ClassName = *ingressClass
|
|
|
|
}
|
|
|
|
info.Type = classes[info.ClassName]
|
|
|
|
info.Annotations = ingress.Annotations
|
2023-03-02 19:45:19 +00:00
|
|
|
info.Labels = ingress.Labels
|
|
|
|
info.CreationDate = ingress.CreationTimestamp.Time
|
2022-09-21 04:49:42 +00:00
|
|
|
|
|
|
|
// 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 {
|
2022-10-24 20:41:30 +00:00
|
|
|
// 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{}{}
|
|
|
|
|
2022-09-21 04:49:42 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
path.Path = p.Path
|
2022-10-24 20:41:30 +00:00
|
|
|
if p.PathType != nil {
|
|
|
|
path.PathType = string(*p.PathType)
|
|
|
|
}
|
2022-09-21 04:49:42 +00:00
|
|
|
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.
|
2022-12-20 03:46:51 +00:00
|
|
|
func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInfo, owner string) error {
|
2022-09-21 04:49:42 +00:00
|
|
|
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
|
|
|
|
var ingress netv1.Ingress
|
|
|
|
|
|
|
|
ingress.Name = info.Name
|
|
|
|
ingress.Namespace = info.Namespace
|
2022-10-24 20:41:30 +00:00
|
|
|
if info.ClassName != "" {
|
|
|
|
ingress.Spec.IngressClassName = &info.ClassName
|
|
|
|
}
|
2022-09-21 04:49:42 +00:00
|
|
|
ingress.Annotations = info.Annotations
|
2022-12-20 03:46:51 +00:00
|
|
|
if ingress.Labels == nil {
|
|
|
|
ingress.Labels = make(map[string]string)
|
|
|
|
}
|
2023-01-18 00:29:59 +00:00
|
|
|
ingress.Labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)
|
2022-09-21 04:49:42 +00:00
|
|
|
|
|
|
|
// 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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-14 01:45:25 +00:00
|
|
|
// Add rules for hosts that does not have paths.
|
|
|
|
// e.g. dafault ingress rule without path to support what we had in 2.15
|
|
|
|
for _, host := range info.Hosts {
|
|
|
|
if _, ok := rules[host]; !ok {
|
|
|
|
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
2023-06-13 00:38:00 +00:00
|
|
|
Host: host,
|
2023-06-14 01:45:25 +00:00
|
|
|
})
|
2023-06-13 00:38:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-21 04:49:42 +00:00
|
|
|
_, 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)
|
2024-02-05 03:30:31 +00:00
|
|
|
ingress, err := ingressClient.Get(context.Background(), info.Name, metav1.GetOptions{})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-09-21 04:49:42 +00:00
|
|
|
|
|
|
|
ingress.Name = info.Name
|
|
|
|
ingress.Namespace = info.Namespace
|
2022-10-24 20:41:30 +00:00
|
|
|
if info.ClassName != "" {
|
|
|
|
ingress.Spec.IngressClassName = &info.ClassName
|
|
|
|
}
|
2022-09-21 04:49:42 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-14 01:45:25 +00:00
|
|
|
// Add rules for hosts that does not have paths.
|
|
|
|
// e.g. dafault ingress rule without path to support what we had in 2.15
|
|
|
|
for _, host := range info.Hosts {
|
|
|
|
if _, ok := rules[host]; !ok {
|
|
|
|
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
|
2023-06-13 00:38:00 +00:00
|
|
|
Host: host,
|
2023-06-14 01:45:25 +00:00
|
|
|
})
|
2023-06-13 00:38:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-05 03:30:31 +00:00
|
|
|
_, err = ingressClient.Update(context.Background(), ingress, metav1.UpdateOptions{})
|
2022-09-21 04:49:42 +00:00
|
|
|
return err
|
|
|
|
}
|