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:
parent
da010f3d08
commit
ea228c3d6d
276 changed files with 9241 additions and 3361 deletions
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue