portainer/api/kubernetes/cli/ingress.go

309 lines
8.6 KiB
Go

package cli
import (
"context"
"strings"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
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, err
}
// We want to know which of these controllers is in use.
var ingresses []models.K8sIngressInfo
namespaces, err := kcl.GetNamespaces()
if err != nil {
return nil, err
}
for namespace := range namespaces {
t, err := kcl.GetIngresses(namespace)
if err != nil {
// 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
}
ingresses = append(ingresses, t...)
}
usedClasses := make(map[string]struct{})
for _, ingress := range ingresses {
usedClasses[ingress.ClassName] = struct{}{}
}
for _, class := range classList.Items {
var controller models.K8sIngressController
controller.Name = class.Spec.Controller
controller.ClassName = class.Name
// If the class is used mark it as such.
if _, ok := usedClasses[class.Name]; ok {
controller.Used = true
}
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, nil
}
// 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
ingressClass := ingress.Spec.IngressClassName
info.ClassName = ""
if ingressClass != nil {
info.ClassName = *ingressClass
}
info.Type = classes[info.ClassName]
info.Annotations = ingress.Annotations
info.Labels = ingress.Labels
info.CreationDate = ingress.CreationTimestamp.Time
// 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 {
// 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{}{}
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
if p.PathType != nil {
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, owner string) error {
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
var ingress netv1.Ingress
ingress.Name = info.Name
ingress.Namespace = info.Namespace
if info.ClassName != "" {
ingress.Spec.IngressClassName = &info.ClassName
}
ingress.Annotations = info.Annotations
if ingress.Labels == nil {
ingress.Labels = make(map[string]string)
}
ingress.Labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)
// 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,
},
},
})
}
// 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{
Host: host,
})
}
}
_, 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)
ingress, err := ingressClient.Get(context.Background(), info.Name, metav1.GetOptions{})
if err != nil {
return err
}
ingress.Name = info.Name
ingress.Namespace = info.Namespace
if info.ClassName != "" {
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),
},
},
},
})
}
ingress.Spec.Rules = make([]netv1.IngressRule, 0)
for rule, paths := range rules {
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
Host: rule,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: paths,
},
},
})
}
// 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{
Host: host,
})
}
}
_, err = ingressClient.Update(context.Background(), ingress, metav1.UpdateOptions{})
return err
}