You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
portainer/api/kubernetes/cli/ingress.go

365 lines
11 KiB

package cli
import (
"context"
"fmt"
"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"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) {
classeses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
ingresses, err := kcl.GetIngresses("")
if err != nil {
return nil, err
}
usedClasses := make(map[string]struct{})
for _, ingress := range ingresses {
usedClasses[ingress.ClassName] = struct{}{}
}
results := []models.K8sIngressController{}
for _, class := range classeses.Items {
ingressClass := parseIngressClass(class)
if _, ok := usedClasses[class.Name]; ok {
ingressClass.Used = true
}
results = append(results, ingressClass)
}
return results, nil
}
// fetchIngressClasses fetches all the ingress classes in a k8s endpoint.
func (kcl *KubeClient) fetchIngressClasses() ([]models.K8sIngressController, error) {
ingressClasses, err := kcl.cli.NetworkingV1().IngressClasses().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
var controllers []models.K8sIngressController
for _, ingressClass := range ingressClasses.Items {
controllers = append(controllers, parseIngressClass(ingressClass))
}
return controllers, nil
}
// parseIngressClass converts a k8s native ingress class object to a Portainer K8sIngressController object.
func parseIngressClass(ingressClasses netv1.IngressClass) models.K8sIngressController {
ingressContoller := models.K8sIngressController{
Name: ingressClasses.Spec.Controller,
ClassName: ingressClasses.Name,
}
switch {
case strings.Contains(ingressContoller.Name, "nginx"):
ingressContoller.Type = "nginx"
case strings.Contains(ingressContoller.Name, "traefik"):
ingressContoller.Type = "traefik"
default:
ingressContoller.Type = "other"
}
return ingressContoller
}
// GetIngress gets an ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetIngress(namespace, ingressName string) (models.K8sIngressInfo, error) {
ingress, err := kcl.cli.NetworkingV1().Ingresses(namespace).Get(context.Background(), ingressName, metav1.GetOptions{})
if err != nil {
return models.K8sIngressInfo{}, err
}
return parseIngress(*ingress), nil
}
// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
if kcl.IsKubeAdmin {
return kcl.fetchIngresses(namespace)
}
return kcl.fetchIngressesForNonAdmin(namespace)
}
// fetchIngressesForNonAdmin gets all the ingresses for non-admin users in a k8s endpoint.
func (kcl *KubeClient) fetchIngressesForNonAdmin(namespace string) ([]models.K8sIngressInfo, error) {
log.Debug().Msgf("Fetching ingresses for non-admin user: %v", kcl.NonAdminNamespaces)
if len(kcl.NonAdminNamespaces) == 0 {
return nil, nil
}
ingresses, err := kcl.fetchIngresses(namespace)
if err != nil {
return nil, err
}
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
results := make([]models.K8sIngressInfo, 0)
for _, ingress := range ingresses {
if _, ok := nonAdminNamespaceSet[ingress.Namespace]; ok {
results = append(results, ingress)
}
}
return results, nil
}
// fetchIngresses fetches all the ingresses for a given namespace in a k8s endpoint.
func (kcl *KubeClient) fetchIngresses(namespace string) ([]models.K8sIngressInfo, error) {
ingresses, err := kcl.cli.NetworkingV1().Ingresses(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
ingressClasses, err := kcl.fetchIngressClasses()
if err != nil {
return nil, err
}
results := []models.K8sIngressInfo{}
if len(ingresses.Items) == 0 {
return results, nil
}
for _, ingress := range ingresses.Items {
result := parseIngress(ingress)
if ingress.Spec.IngressClassName != nil {
result.Type = findUsedIngressFromIngressClasses(ingressClasses, *ingress.Spec.IngressClassName).Name
}
results = append(results, result)
}
return results, nil
}
// parseIngress converts a k8s native ingress object to a Portainer K8sIngressInfo object.
func parseIngress(ingress netv1.Ingress) models.K8sIngressInfo {
ingressClassName := ""
if ingress.Spec.IngressClassName != nil {
ingressClassName = *ingress.Spec.IngressClassName
}
result := models.K8sIngressInfo{
Name: ingress.Name,
Namespace: ingress.Namespace,
UID: string(ingress.UID),
Annotations: ingress.Annotations,
Labels: ingress.Labels,
CreationDate: ingress.CreationTimestamp.Time,
ClassName: ingressClassName,
}
for _, tls := range ingress.Spec.TLS {
result.TLS = append(result.TLS, models.K8sIngressTLS{
Hosts: tls.Hosts,
SecretName: tls.SecretName,
})
}
hosts := make(map[string]struct{})
for _, r := range ingress.Spec.Rules {
hosts[r.Host] = struct{}{}
if r.HTTP == nil {
continue
}
for _, p := range r.HTTP.Paths {
var path models.K8sIngressPath
path.IngressName = result.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)
result.Paths = append(result.Paths, path)
}
}
for host := range hosts {
result.Hosts = append(result.Hosts, host)
}
return result
}
// findUsedIngressFromIngressClasses searches for an ingress in a slice of ingress classes and returns the ingress if found.
func findUsedIngressFromIngressClasses(ingressClasses []models.K8sIngressController, className string) models.K8sIngressController {
for _, ingressClass := range ingressClasses {
if ingressClass.ClassName == className {
return ingressClass
}
}
return models.K8sIngressController{}
}
// 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 {
ingress := kcl.convertToK8sIngress(info, owner)
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Create(context.Background(), &ingress, metav1.CreateOptions{})
if err != nil {
return err
}
return nil
}
// convertToK8sIngress converts a Portainer K8sIngressInfo object to a k8s native Ingress object.
// this is required for create and update operations.
func (kcl *KubeClient) convertToK8sIngress(info models.K8sIngressInfo, owner string) netv1.Ingress {
ingressSpec := netv1.IngressSpec{}
if info.ClassName != "" {
ingressSpec.IngressClassName = &info.ClassName
}
result := netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: info.Name,
Namespace: info.Namespace,
Annotations: info.Annotations,
},
Spec: ingressSpec,
}
labels := make(map[string]string)
labels["io.portainer.kubernetes.ingress.owner"] = stackutils.SanitizeLabel(owner)
result.Labels = labels
tls := []netv1.IngressTLS{}
for _, t := range info.TLS {
tls = append(tls, netv1.IngressTLS{
Hosts: t.Hosts,
SecretName: t.SecretName,
})
}
result.Spec.TLS = tls
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 {
result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
Host: rule,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: paths,
},
},
})
}
for _, host := range info.Hosts {
if _, ok := rules[host]; !ok {
result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
Host: host,
})
}
}
return result
}
// DeleteIngresses processes a K8sIngressDeleteRequest by deleting each ingress
// in its given namespace.
func (kcl *KubeClient) DeleteIngresses(reqs models.K8sIngressDeleteRequests) error {
for namespace := range reqs {
for _, ingress := range reqs[namespace] {
err := kcl.cli.NetworkingV1().Ingresses(namespace).Delete(
context.Background(),
ingress,
metav1.DeleteOptions{},
)
if err != nil {
return err
}
}
}
return nil
}
// UpdateIngress updates an existing ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInfo) error {
ingress := kcl.convertToK8sIngress(info, "")
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Update(context.Background(), &ingress, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}
// CombineIngressWithService combines an ingress with a service that is being used by the ingress.
// this is required to display the service that is being used by the ingress in the UI edit view.
func (kcl *KubeClient) CombineIngressWithService(ingress models.K8sIngressInfo) (models.K8sIngressInfo, error) {
services, err := kcl.GetServices(ingress.Namespace)
if err != nil {
return models.K8sIngressInfo{}, fmt.Errorf("an error occurred during the CombineIngressWithService operation, unable to retrieve services from the Kubernetes for a namespace level user. Error: %w", err)
}
serviceMap := kcl.buildServicesMap(services)
for pathIndex, path := range ingress.Paths {
if _, ok := serviceMap[path.ServiceName]; ok {
ingress.Paths[pathIndex].HasService = true
}
}
return ingress, nil
}
// CombineIngressesWithServices combines a list of ingresses with a list of services that are being used by the ingresses.
// this is required to display the services that are being used by the ingresses in the UI list view.
func (kcl *KubeClient) CombineIngressesWithServices(ingresses []models.K8sIngressInfo) ([]models.K8sIngressInfo, error) {
services, err := kcl.GetServices("")
if err != nil {
if k8serrors.IsUnauthorized(err) {
return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unauthorized access to the Kubernetes API. Error: %w", err)
}
return nil, fmt.Errorf("an error occurred during the CombineIngressesWithServices operation, unable to retrieve services from the Kubernetes for a cluster level user. Error: %w", err)
}
serviceMap := kcl.buildServicesMap(services)
for ingressIndex, ingress := range ingresses {
for pathIndex, path := range ingress.Paths {
if _, ok := serviceMap[path.ServiceName]; ok {
(ingresses)[ingressIndex].Paths[pathIndex].HasService = true
}
}
}
return ingresses, nil
}