AppManager: Implement ingress monitoring

Change-Id: I156236c3f062a616cfd5de9821aeccbf686e0c22
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 677ba95..95f1407 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -1005,6 +1005,7 @@
 }
 
 #WithOut: {
+	id:       uuid
 	cluster?: #Cluster
 	if input.cluster != _|_ {
 		cluster: #Cluster | *input.cluster
diff --git a/core/installer/status/helm.go b/core/installer/status/helm.go
index 8b4162c..dc5b3ba 100644
--- a/core/installer/status/helm.go
+++ b/core/installer/status/helm.go
@@ -14,12 +14,8 @@
 )
 
 type HelmRelease struct {
-	Metadata struct {
-		Name        string            `json:"name"`
-		Namespace   string            `json:"namespace"`
-		Annotations map[string]string `json:"annotations"`
-	} `json:"metadata"`
-	Status struct {
+	Metadata Metadata `json:"metadata"`
+	Status   struct {
 		Conditions []struct {
 			Type   string `json:"type"`
 			Status string `json:"status"`
@@ -32,7 +28,6 @@
 }
 
 func (m *helmReleaseMonitor) Get(ref ResourceRef) (Status, error) {
-	fmt.Printf("--- %+v\n", ref)
 	ctx := context.Background()
 	res, err := m.d.Resource(
 		schema.GroupVersionResource{
@@ -57,7 +52,8 @@
 	}
 	id, ok := hr.Metadata.Annotations["dodo.cloud/id"]
 	if !ok {
-		return StatusNoStatus, fmt.Errorf("missing annotation: dodo.cloud/id")
+		fmt.Printf("## missing dodo.cloud/id: %+v\n", ref)
+		return StatusNoStatus, nil
 	}
 	if id != ref.Id {
 		return StatusNotFound, nil
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
+}
diff --git a/core/installer/status/instance.go b/core/installer/status/instance.go
index 9ad2ff0..8263803 100644
--- a/core/installer/status/instance.go
+++ b/core/installer/status/instance.go
@@ -4,6 +4,7 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"strings"
 )
 
 type InstanceMonitor struct {
@@ -49,40 +50,54 @@
 // TODO(gio): handle volume
 func (m *InstanceMonitor) monitor(namespace string, resource DodoResource, out ResourceOut, ret map[DodoResource]Status) (Status, error) {
 	status := StatusNoStatus
-	for _, h := range out.Helm {
-		hs, err := m.m.Get(Resource{
-			Type: ResourceHelmRelease,
+	if resource.Type == "ingress" {
+		var err error
+		status, err = m.m.Get(Resource{
+			Type: ResourceIngress,
 			ResourceRef: ResourceRef{
-				Id:        h.Id,
-				Name:      h.Name,
+				Id:        out.Id,
+				Name:      fmt.Sprintf("ingress-%s", strings.TrimPrefix(resource.Name, "https://")),
 				Namespace: namespace,
 			},
 		})
-		fmt.Println(hs, err)
 		if err != nil {
 			return StatusNoStatus, err
 		}
-		status = mergeStatus(status, hs)
+	} else {
+		for _, h := range out.Helm {
+			hs, err := m.m.Get(Resource{
+				Type: ResourceHelmRelease,
+				ResourceRef: ResourceRef{
+					Id:        h.Id,
+					Name:      h.Name,
+					Namespace: namespace,
+				},
+			})
+			if err != nil {
+				return StatusNoStatus, err
+			}
+			status = mergeStatus(status, hs)
+		}
 	}
 	for _, i := range out.Volume {
 		if s, err := m.monitor(namespace, DodoResource{"volume", i.Name}, i, ret); err != nil {
 			return StatusNoStatus, err
 		} else {
-			s = mergeStatus(status, s)
+			status = mergeStatus(status, s)
 		}
 	}
 	for _, i := range out.PostgreSQL {
 		if s, err := m.monitor(namespace, DodoResource{"postgresql", i.Name}, i, ret); err != nil {
 			return StatusNoStatus, err
 		} else {
-			s = mergeStatus(status, s)
+			status = mergeStatus(status, s)
 		}
 	}
 	for _, i := range out.MongoDB {
 		if s, err := m.monitor(namespace, DodoResource{"mongodb", i.Name}, i, ret); err != nil {
 			return StatusNoStatus, err
 		} else {
-			s = mergeStatus(status, s)
+			status = mergeStatus(status, s)
 		}
 	}
 	for _, i := range out.Ingress {
@@ -90,14 +105,14 @@
 		if s, err := m.monitor(namespace, DodoResource{"ingress", name}, i.ResourceOut, ret); err != nil {
 			return StatusNoStatus, err
 		} else {
-			s = mergeStatus(status, s)
+			status = mergeStatus(status, s)
 		}
 	}
 	for _, i := range out.Services {
 		if s, err := m.monitor(namespace, DodoResource{"service", i.Name}, i, ret); err != nil {
 			return StatusNoStatus, err
 		} else {
-			s = mergeStatus(status, s)
+			status = mergeStatus(status, s)
 		}
 	}
 	ret[resource] = status
@@ -118,6 +133,7 @@
 }
 
 type ResourceOut struct {
+	Id         string                     `json:"id"`
 	Name       string                     `json:"name"`
 	Volume     map[string]ResourceOut     `json:"volume"`
 	PostgreSQL map[string]ResourceOut     `json:"postgresql"`
diff --git a/core/installer/status/metadata.go b/core/installer/status/metadata.go
new file mode 100644
index 0000000..9007f19
--- /dev/null
+++ b/core/installer/status/metadata.go
@@ -0,0 +1,18 @@
+package status
+
+type OwnerReference struct {
+	APIVersion string `json:"apiVersion"`
+	Kind       string `json:"kind"`
+	Name       string `json:"name"`
+}
+
+type Metadata struct {
+	Name            string            `json:"name"`
+	Namespace       string            `json:"namespace"`
+	Annotations     map[string]string `json:"annotations"`
+	OwnerReferences []OwnerReference  `json:"ownerReferences"`
+}
+
+type ResourceWithOwnerReferences interface {
+	OwnerReferences() []OwnerReference
+}
diff --git a/core/installer/status/status.go b/core/installer/status/status.go
index f093a93..c9f7939 100644
--- a/core/installer/status/status.go
+++ b/core/installer/status/status.go
@@ -42,6 +42,7 @@
 
 const (
 	ResourceHelmRelease ResourceType = iota
+	ResourceIngress
 )
 
 type ResourceRef struct {
@@ -72,9 +73,14 @@
 	if err != nil {
 		return nil, err
 	}
+	ingress, err := NewIngressMonitor(kubeconfig)
+	if err != nil {
+		return nil, err
+	}
 	return delegatingMonitor{
 		map[ResourceType]ResourceMonitor{
 			ResourceHelmRelease: helm,
+			ResourceIngress:     ingress,
 		},
 	}, nil
 }