package ingresswrapper import ( "context" "fmt" "reflect" rextv1beta1 "github.com/rancher/rancher/pkg/generated/norman/extensions/v1beta1" rnetworkingv1 "github.com/rancher/rancher/pkg/generated/norman/networking.k8s.io/v1" kextv1beta1 "k8s.io/api/extensions/v1beta1" knetworkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" clientv1beta1 "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" clientv1 "k8s.io/client-go/kubernetes/typed/networking/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) // Ingress is an interface with features of both metav1.Object and runtime.Object, // and can be used whenever the metadata of a resource is needed but the actual // spec data is not. type Ingress interface { client.Object } // ServerSupportsIngressV1 runs a discovery query to check whether the // networking.k8s.io/v1/Ingress resource is supported in the given cluster. func ServerSupportsIngressV1(k8sClient kubernetes.Interface) bool { resources, err := k8sClient.Discovery().ServerResourcesForGroupVersion("networking.k8s.io/v1") if err != nil || resources == nil { return false } for _, r := range resources.APIResources { if r.Kind == "Ingress" { return true } } return false } // CompatSyncV1 returns a controller handler for networking.k8s.io/v1/Ingress resources. func CompatSyncV1(fn func(string, Ingress) (runtime.Object, error)) func(string, *knetworkingv1.Ingress) (runtime.Object, error) { return func(key string, obj *knetworkingv1.Ingress) (runtime.Object, error) { return fn(key, obj) } } // CompatSyncV1Beta1 returns a controller handler for extensions/v1beta1/Ingress resources. func CompatSyncV1Beta1(fn func(string, Ingress) (runtime.Object, error)) func(string, *kextv1beta1.Ingress) (runtime.Object, error) { return func(key string, obj *kextv1beta1.Ingress) (runtime.Object, error) { return fn(key, obj) } } // ToUnstructured converts a resource Object into a unstructured map. func ToUnstructured(o interface{}) (map[string]interface{}, error) { obj := o.(client.Object) // Populate GVK explicitly. See https://github.com/kubernetes/kubernetes/issues/3030 gvks, _, err := scheme.Scheme.ObjectKinds(obj) if err != nil { return nil, fmt.Errorf("error finding APIVersion or Kind for ingress; %w", err) } for _, gvk := range gvks { if len(gvk.Kind) == 0 { continue } if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal { continue } obj.GetObjectKind().SetGroupVersionKind(gvk) break } return runtime.DefaultUnstructuredConverter.ToUnstructured(o) } // CompatIngress is an intermediary between networking.k8s.io/v1/Ingress and // extensions/v1beta1/Ingress. It inlines networking.k8s.io/v1/Ingress and so // it can be handled, and its attributes accessed, in the same way a v1 Ingress // resource would be handled. type CompatIngress struct { knetworkingv1.Ingress } // ToCompatIngress converts a networking.k8s.io/v1/Ingress or // extensions/v1beta1/Ingress object to a CompatIngress object. If the object // is already a CompatIngress object, it is returned unchanged. func ToCompatIngress(obj interface{}) (*CompatIngress, error) { switch o := obj.(type) { case *CompatIngress: return o, nil case *knetworkingv1.Ingress: if reflect.ValueOf(o).IsNil() { return &CompatIngress{}, nil } return &CompatIngress{*o}, nil case *kextv1beta1.Ingress: unst, err := ToUnstructured(o) if err != nil { return nil, err } spec := unst["spec"].(map[string]interface{}) if o.Spec.Backend != nil { spec["defaultBackend"] = map[string]interface{}{ "service": map[string]interface{}{ "name": o.Spec.Backend.ServiceName, "port": map[string]interface{}{ "number": o.Spec.Backend.ServicePort.IntVal, }, }, } delete(spec, "backend") } if spec["rules"] != nil { rules := spec["rules"].([]interface{}) for i, r := range o.Spec.Rules { rule := rules[i].(map[string]interface{}) if r.HTTP != nil { for j, p := range r.HTTP.Paths { path := map[string]interface{}{ "path": p.Path, "backend": map[string]interface{}{ "service": map[string]interface{}{ "name": p.Backend.ServiceName, "port": map[string]interface{}{ "number": p.Backend.ServicePort.IntVal, }, }, }, } rule["http"].(map[string]interface{})["paths"].([]interface{})[j] = path } } if rule["http"] != nil && rule["http"].(map[string]interface{})["backend"] != nil { delete(rule["http"].(map[string]interface{})["backend"].(map[string]interface{}), "serviceName") delete(rule["http"].(map[string]interface{})["backend"].(map[string]interface{}), "servicePort") } } } compat := CompatIngress{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(unst, &compat.Ingress) if err != nil { return nil, err } return &compat, nil default: return nil, fmt.Errorf("unexpected ingress type: %T", o) } } // ToIngressV1FromCompat converts a CompatIngress object to a networking.k8s.io/v1/Ingress object. func ToIngressV1FromCompat(obj *CompatIngress) (*knetworkingv1.Ingress, error) { unst, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return nil, err } ingress := knetworkingv1.Ingress{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(unst["ingress"].(map[string]interface{}), &ingress) if err != nil { return nil, fmt.Errorf("error converting CompatIngress to networking.k8s.io/v1/Ingress: %w", err) } return &ingress, nil } // ToIngressV1Beta1FromCompat converts a CompatIngress object to an extensions/v1beta1/Ingress object. func ToIngressV1Beta1FromCompat(obj *CompatIngress) (*kextv1beta1.Ingress, error) { unst, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return nil, err } spec := unst["ingress"].(map[string]interface{})["spec"].(map[string]interface{}) if spec["defaultBackend"] != nil { spec["backend"] = map[string]interface{}{ "serviceName": obj.Spec.DefaultBackend.Service.Name, "servicePort": intstr.IntOrString{ IntVal: obj.Spec.DefaultBackend.Service.Port.Number, }, } } delete(spec, "defaultBackend") if spec["rules"] != nil { rules := spec["rules"].([]interface{}) for i, r := range obj.Spec.Rules { rule := rules[i].(map[string]interface{}) if r.HTTP != nil { for j, p := range r.HTTP.Paths { backend := map[string]interface{}{ "serviceName": p.Backend.Service.Name, "servicePort": intstr.IntOrString{ IntVal: p.Backend.Service.Port.Number, }, } rule["http"].(map[string]interface{})["paths"].([]interface{})[j].(map[string]interface{})["backend"] = backend } } } } ingress := kextv1beta1.Ingress{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(unst["ingress"].(map[string]interface{}), &ingress) if err != nil { return nil, fmt.Errorf("error converting CompatIngress to extensions/v1beta1/Ingress: %w", err) } return &ingress, nil } // CompatInterface is a wrapper for the norman interfaces for // networking.k8s.io/v1/Ingress and extensions/v1beta1/Ingress. type CompatInterface struct { ingressInterface rnetworkingv1.IngressInterface ingressLegacyInterface rextv1beta1.IngressInterface ServerSupportsIngressV1 bool } // NewCompatInterface initializes either a networking.k8s.io/v1/Ingress client // or an extensions/v1beta1 client and returns a CompatInterface. func NewCompatInterface(networkingAPI rnetworkingv1.Interface, extensionsAPI rextv1beta1.Interface, k8sClient kubernetes.Interface) CompatInterface { c := CompatInterface{ ServerSupportsIngressV1: ServerSupportsIngressV1(k8sClient), } if c.ServerSupportsIngressV1 { c.ingressInterface = networkingAPI.Ingresses("") return c } c.ingressLegacyInterface = extensionsAPI.Ingresses("") return c } // Update calls the Update method for the active ingress interface. func (i *CompatInterface) Update(ingress Ingress) (runtime.Object, error) { obj := ingress.(*CompatIngress) if i.ServerSupportsIngressV1 { toUpdate, err := ToIngressV1FromCompat(obj) if err != nil { return toUpdate, err } return i.ingressInterface.Update(toUpdate) } toUpdate, err := ToIngressV1Beta1FromCompat(obj) if err != nil { return toUpdate, err } return i.ingressLegacyInterface.Update(toUpdate) } // CompatLister is a wrapper for the norman listers for // networking.k8s.io/v1/Ingress and extensions/v1beta1/Ingress. type CompatLister struct { ingressLister rnetworkingv1.IngressLister ingressLegacyLister rextv1beta1.IngressLister ServerSupportsIngressV1 bool } // NewCompatLister initializes either a networking.k8s.io/v1/Ingress watch or // an extensions/v1beta1/Ingress watch and returns a CompatLister. func NewCompatLister(networkingAPI rnetworkingv1.Interface, extensionsAPI rextv1beta1.Interface, k8sClient kubernetes.Interface) CompatLister { c := CompatLister{ ServerSupportsIngressV1: ServerSupportsIngressV1(k8sClient), } if c.ServerSupportsIngressV1 { c.ingressLister = networkingAPI.Ingresses("").Controller().Lister() return c } c.ingressLegacyLister = extensionsAPI.Ingresses("").Controller().Lister() return c } // List calls the List method for the active ingress lister. func (l *CompatLister) List(namespace string, selector labels.Selector) ([]*CompatIngress, error) { var list []*CompatIngress if l.ServerSupportsIngressV1 { ingresses, err := l.ingressLister.List(namespace, selector) if err != nil { return list, err } for _, i := range ingresses { ingressCompat, err := ToCompatIngress(i) if err != nil { return list, err } list = append(list, ingressCompat) } return list, nil } ingresses, err := l.ingressLegacyLister.List(namespace, selector) if err != nil { return list, err } for _, i := range ingresses { ingressCompat, err := ToCompatIngress(i) if err != nil { return list, err } list = append(list, ingressCompat) } return list, nil } // CompatClient is a wrapper for the client-go networking.k8s.io/v1/Ingress and // extensions/v1beta1/Ingress clients. type CompatClient struct { ingressClient clientv1.IngressInterface ingressLegacyClient clientv1beta1.IngressInterface ServerSupportsIngressV1 bool } // NewCompatClient initializes either a networking.k8s.io/v1/Ingress client-go client or // an extensions/v1beta1/Ingress client-go client and returns a CompatClient. func NewCompatClient(k8sClient kubernetes.Interface, namespace string) CompatClient { c := CompatClient{ ServerSupportsIngressV1: ServerSupportsIngressV1(k8sClient), } if c.ServerSupportsIngressV1 { c.ingressClient = k8sClient.NetworkingV1().Ingresses(namespace) return c } c.ingressLegacyClient = k8sClient.ExtensionsV1beta1().Ingresses(namespace) return c } // Get calls the Get method for the active ingress client. func (c *CompatClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*CompatIngress, error) { if c.ServerSupportsIngressV1 { ret, err := c.ingressClient.Get(ctx, name, opts) if err != nil { return nil, err } return ToCompatIngress(ret) } ret, err := c.ingressLegacyClient.Get(ctx, name, opts) if err != nil { return nil, err } return ToCompatIngress(ret) } // Create calls the Create method for the active ingress client. func (c *CompatClient) Create(ctx context.Context, ingress Ingress, opts metav1.CreateOptions) (*CompatIngress, error) { if c.ServerSupportsIngressV1 { ret, err := c.ingressClient.Create(ctx, ingress.(*knetworkingv1.Ingress), opts) if err != nil { return nil, err } return ToCompatIngress(ret) } ret, err := c.ingressLegacyClient.Create(ctx, ingress.(*kextv1beta1.Ingress), opts) if err != nil { return nil, err } return ToCompatIngress(ret) } // UpdateStatus calls the UpdateStatus method for the active ingress client. func (c *CompatClient) UpdateStatus(ctx context.Context, ingress Ingress, opts metav1.UpdateOptions) (*CompatIngress, error) { if c.ServerSupportsIngressV1 { var toUpdate *knetworkingv1.Ingress if o, ok := ingress.(*CompatIngress); ok { var err error toUpdate, err = ToIngressV1FromCompat(o) if err != nil { return nil, err } } else { toUpdate = ingress.(*knetworkingv1.Ingress) } ret, err := c.ingressClient.UpdateStatus(ctx, toUpdate, opts) if err != nil { return nil, err } return ToCompatIngress(ret) } var toUpdate *kextv1beta1.Ingress if o, ok := ingress.(*CompatIngress); ok { var err error toUpdate, err = ToIngressV1Beta1FromCompat(o) if err != nil { return nil, err } } else { toUpdate = ingress.(*kextv1beta1.Ingress) } ret, err := c.ingressLegacyClient.UpdateStatus(ctx, toUpdate, opts) if err != nil { return nil, err } return ToCompatIngress(ret) } // Update calls the Update method for the active ingress client. func (c *CompatClient) Update(ctx context.Context, ingress Ingress, opts metav1.UpdateOptions) (*CompatIngress, error) { if c.ServerSupportsIngressV1 { var toUpdate *knetworkingv1.Ingress if o, ok := ingress.(*CompatIngress); ok { var err error toUpdate, err = ToIngressV1FromCompat(o) if err != nil { return nil, err } } else { toUpdate = ingress.(*knetworkingv1.Ingress) } ret, err := c.ingressClient.Update(ctx, toUpdate, opts) if err != nil { return nil, err } return ToCompatIngress(ret) } var toUpdate *kextv1beta1.Ingress if o, ok := ingress.(*CompatIngress); ok { var err error toUpdate, err = ToIngressV1Beta1FromCompat(o) if err != nil { return nil, err } } else { toUpdate = ingress.(*kextv1beta1.Ingress) } ret, err := c.ingressLegacyClient.Update(ctx, toUpdate, opts) if err != nil { return nil, err } return ToCompatIngress(ret) }