1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 07:19:41 +02:00

refactor(k8s): namespace core logic (#12142)

Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
This commit is contained in:
Steven Kang 2024-10-01 14:15:51 +13:00 committed by GitHub
parent da010f3d08
commit ea228c3d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
276 changed files with 9241 additions and 3361 deletions

View file

@ -2,183 +2,252 @@ 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) {
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{})
classeses, err := kcl.cli.NetworkingV1().IngressClasses().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()
ingresses, err := kcl.GetIngresses("")
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.
results := []models.K8sIngressController{}
for _, class := range classeses.Items {
ingressClass := parseIngressClass(class)
if _, ok := usedClasses[class.Name]; ok {
controller.Used = true
ingressClass.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)
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) {
// 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 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
}
for _, class := range classList.Items {
// Write the ingress classes "type" to our map.
classes[class.Name] = class.Spec.Controller
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
results := make([]models.K8sIngressInfo, 0)
for _, ingress := range ingresses {
if _, ok := nonAdminNamespaceSet[ingress.Namespace]; ok {
results = append(results, ingress)
}
}
// Fetch each ingress.
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
ingressList, err := ingressClient.List(context.Background(), metav1.ListOptions{})
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
}
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)
ingressClasses, err := kcl.fetchIngressClasses()
if err != nil {
return nil, err
}
return infos, nil
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 {
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 := kcl.convertToK8sIngress(info, owner)
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Create(context.Background(), &ingress, metav1.CreateOptions{})
if err != nil {
return err
}
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 {
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 {
result := netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: info.Name,
Namespace: info.Namespace,
Annotations: info.Annotations,
},
Spec: netv1.IngressSpec{
IngressClassName: &info.ClassName,
},
}
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: i.Hosts,
SecretName: i.SecretName,
Hosts: t.Hosts,
SecretName: t.SecretName,
})
}
ingress.Spec.TLS = tls
result.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)
@ -197,7 +266,7 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf
}
for rule, paths := range rules {
ingress.Spec.Rules = append(ingress.Spec.Rules, netv1.IngressRule{
result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
Host: rule,
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
@ -207,102 +276,86 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf
})
}
// 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{
result.Spec.Rules = append(result.Spec.Rules, netv1.IngressRule{
Host: host,
})
}
}
_, err := ingressClient.Create(context.Background(), &ingress, metav1.CreateOptions{})
return err
return result
}
// 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(
err := kcl.cli.NetworkingV1().Ingresses(namespace).Delete(
context.Background(),
ingress,
metav1.DeleteOptions{},
)
if err != nil {
return err
}
}
}
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 {
ingressClient := kcl.cli.NetworkingV1().Ingresses(namespace)
ingress, err := ingressClient.Get(context.Background(), info.Name, metav1.GetOptions{})
ingress := kcl.convertToK8sIngress(info, "")
_, err := kcl.cli.NetworkingV1().Ingresses(namespace).Update(context.Background(), &ingress, metav1.UpdateOptions{})
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
return nil
}
// 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),
},
},
},
})
// 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)
}
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,
})
serviceMap := kcl.buildServicesMap(services)
for pathIndex, path := range ingress.Paths {
if _, ok := serviceMap[path.ServiceName]; ok {
ingress.Paths[pathIndex].HasService = true
}
}
_, err = ingressClient.Update(context.Background(), ingress, metav1.UpdateOptions{})
return err
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
}