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
}