AppManager: Implement ingress monitoring

Change-Id: I156236c3f062a616cfd5de9821aeccbf686e0c22
diff --git a/core/installer/status/ingress.go b/core/installer/status/ingress.go
new file mode 100644
index 0000000..b76c8e0
--- /dev/null
+++ b/core/installer/status/ingress.go
@@ -0,0 +1,183 @@
+package status
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/giolekva/pcloud/core/installer/kube"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+type ingressMonitor struct {
+	d dynamic.Interface
+}
+
+type ingress struct {
+	APIVersion string   `json:"apiVersion"`
+	Kind       string   `json:"kind"`
+	Metadata   Metadata `json:"metadata"`
+	Spec       struct {
+		TLS []struct {
+			SecretName string `json:"secretName"`
+		} `json:"tls"`
+	} `json:"spec"`
+}
+
+type certificate struct {
+	APIVersion string   `json:"apiVersion"`
+	Kind       string   `json:"kind"`
+	Metadata   Metadata `json:"metadata"`
+	Spec       struct {
+		SecretName string `json:"secretName"`
+	} `json:"spec"`
+}
+
+func (c certificate) OwnerReferences() []OwnerReference {
+	return c.Metadata.OwnerReferences
+}
+
+type certificateRequest struct {
+	Metadata Metadata `json:"metadata"`
+	Status   struct {
+		Conditions []struct {
+			Type   string `json:"type"`
+			Status string `json:"status"`
+		} `json:"conditions"`
+	} `json:"status"`
+}
+
+func (c certificateRequest) OwnerReferences() []OwnerReference {
+	return c.Metadata.OwnerReferences
+}
+
+func (c certificateRequest) IsReady() bool {
+	for _, cond := range c.Status.Conditions {
+		if cond.Type == "Ready" && cond.Status == "True" {
+			return true
+		}
+	}
+	return false
+}
+
+func (m *ingressMonitor) Get(ref ResourceRef) (Status, error) {
+	ctx := context.Background()
+	res, err := m.d.Resource(
+		schema.GroupVersionResource{
+			Group:    "networking.k8s.io",
+			Version:  "v1",
+			Resource: "ingresses",
+		},
+	).Namespace(ref.Namespace).Get(ctx, ref.Name, metav1.GetOptions{})
+	if err != nil {
+		if errors.IsNotFound(err) {
+			return StatusNotFound, nil
+		}
+		return StatusNoStatus, err
+	}
+	b, err := res.MarshalJSON()
+	if err != nil {
+		return StatusNoStatus, err
+	}
+	var r ingress
+	if err := json.Unmarshal(b, &r); err != nil {
+		return StatusNoStatus, err
+	}
+	id, ok := r.Metadata.Annotations["dodo.cloud/id"]
+	if !ok {
+		fmt.Printf("## missing dodo.cloud/id: %+v\n", ref)
+		// TODO(gio): pass down annotations from helm release to resources
+		// return StatusNoStatus, nil
+	} else if id != ref.Id {
+		return StatusNotFound, nil
+	}
+	certs, err := decodeResources[certificate](m.d.Resource(
+		schema.GroupVersionResource{
+			Group:    "cert-manager.io",
+			Version:  "v1",
+			Resource: "certificates",
+		},
+	).Namespace(ref.Namespace).List(ctx, metav1.ListOptions{}))
+	certs = filterByOwner(certs, OwnerReference{r.APIVersion, r.Kind, r.Metadata.Name})
+	if err != nil {
+		return StatusNotFound, nil
+	}
+	certReqs, err := decodeResources[certificateRequest](m.d.Resource(
+		schema.GroupVersionResource{
+			Group:    "cert-manager.io",
+			Version:  "v1",
+			Resource: "certificaterequests",
+		},
+	).Namespace(ref.Namespace).List(ctx, metav1.ListOptions{}))
+	if err != nil {
+		return StatusNotFound, nil
+	}
+	if len(r.Spec.TLS) != len(certs) {
+		return StatusProcessing, nil
+	}
+	for _, tls := range r.Spec.TLS {
+		var cert *certificate
+		for _, c := range certs {
+			if tls.SecretName == c.Spec.SecretName {
+				cert = &c
+				break
+			}
+		}
+		if cert == nil {
+			return StatusProcessing, nil
+		}
+		reqs := filterByOwner(certReqs, OwnerReference{cert.APIVersion, cert.Kind, cert.Metadata.Name})
+		for _, req := range reqs {
+			if !req.IsReady() {
+				return StatusProcessing, nil
+			}
+		}
+	}
+	return StatusSuccess, nil
+}
+
+func NewIngressMonitor(kubeconfig string) (ResourceMonitor, error) {
+	c, err := kube.NewKubeClient(kube.KubeConfigOpts{KubeConfigPath: kubeconfig})
+	if err != nil {
+		return nil, err
+	}
+	d := dynamic.New(c.RESTClient())
+	return &ingressMonitor{d}, nil
+}
+
+func filterByOwner[T ResourceWithOwnerReferences](certs []T, owner OwnerReference) []T {
+	ret := []T{}
+	for _, i := range certs {
+		for _, o := range i.OwnerReferences() {
+			if owner.APIVersion == o.APIVersion && owner.Kind == o.Kind && owner.Name == o.Name {
+				ret = append(ret, i)
+				break
+			}
+		}
+	}
+	return ret
+}
+
+func decodeResources[T any](list *unstructured.UnstructuredList, err error) ([]T, error) {
+	if err != nil {
+		return nil, err
+	}
+	ret := []T{}
+	for _, i := range list.Items {
+		b, err := i.MarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		var r T
+		if err := json.Unmarshal(b, &r); err != nil {
+			return nil, err
+		}
+		ret = append(ret, r)
+	}
+	return ret, nil
+}