blob: c308b8290bc2f898024a102cc3248760fdb8e356 [file] [log] [blame]
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +04001package main
2
3import (
4 "context"
5 "flag"
6 "fmt"
7 "log"
8 "net/http"
9 "time"
10
11 "github.com/gorilla/mux"
12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14 "k8s.io/apimachinery/pkg/runtime/schema"
15 "k8s.io/client-go/dynamic"
16 "k8s.io/client-go/rest"
17 "k8s.io/client-go/tools/clientcmd"
18)
19
20var port = flag.Int("port", 8080, "Port to listen on")
21var kubeconfig = flag.String("kubeconfig", "", "Path to kubeconfig file")
22
23const reconcileAnnotation = "reconcile.fluxcd.io/requestedAt"
24const reconcileAtLayout = time.RFC3339Nano
25
26type Server struct {
27 port int
28 client dynamic.Interface
29}
30
31func NewServer(port int, client dynamic.Interface) *Server {
32 return &Server{port, client}
33}
34
35func (s *Server) Start() {
36 r := mux.NewRouter()
37 r.Path("/source/git/{namespace}/{name}/reconcile").Methods("GET").HandlerFunc(s.sourceGitReconcile)
38 r.Path("/kustomization/{namespace}/{name}/reconcile").Methods("GET").HandlerFunc(s.kustomizationReconcile)
39 http.Handle("/", r)
40 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
41}
42
43func getReconciledAt(obj *unstructured.Unstructured) (string, error) {
44 status, ok := obj.Object["status"]
45 if !ok {
46 return "", fmt.Errorf("status not found")
47 }
48 statusMap, ok := status.(map[string]interface{})
49 if !ok {
50 return "", fmt.Errorf("status not map")
51 }
52 val, ok := statusMap["lastHandledReconcileAt"]
53 if !ok {
54 return "", fmt.Errorf("lastHandledReconcileAt not found in status")
55 }
56 valStr, ok := val.(string)
57 if !ok {
58 return "", fmt.Errorf("lastHandledReconcileAt not string")
59 }
60 return valStr, nil
61}
62
63func reconcile(
64 client dynamic.Interface,
65 res schema.GroupVersionResource,
66 namespace string,
67 name string,
68) error {
gio43b0f422024-08-21 10:40:13 +040069 fmt.Printf("%+v %s %s\n", res, namespace, name)
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +040070 unstr, err := client.Resource(res).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
71 if err != nil {
72 return err
73 }
74 timeNowTime := time.Now()
75 annotations := unstr.GetAnnotations()
gio43b0f422024-08-21 10:40:13 +040076 if annotations == nil {
77 annotations = map[string]string{}
78 unstr.SetAnnotations(annotations)
79 }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +040080 annotations[reconcileAnnotation] = timeNowTime.Format(reconcileAtLayout)
gio43b0f422024-08-21 10:40:13 +040081 fmt.Printf("New reconciled at: %+v\n", annotations[reconcileAnnotation])
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +040082 unstr.SetAnnotations(annotations)
83 unstr, err = client.Resource(res).Namespace(namespace).Update(context.TODO(), unstr, metav1.UpdateOptions{})
84 if err != nil {
85 return err
86 }
87 for {
88 unstr, err := client.Resource(res).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
89 if err != nil {
90 return err
91 }
92 reconciledAt, err := getReconciledAt(unstr)
93 if err != nil {
gio43b0f422024-08-21 10:40:13 +040094 continue
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +040095 }
96 reconciledAtTime, err := time.Parse(reconcileAtLayout, reconciledAt)
97 if err != nil {
98 return err
99 }
gio43b0f422024-08-21 10:40:13 +0400100 fmt.Printf("Current reconciled at: %s\n", reconciledAtTime.Format(reconcileAtLayout))
101 if reconciledAtTime.Equal(timeNowTime) || reconciledAtTime.After(timeNowTime) {
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400102 return nil
103 }
104 }
105}
106
107func (s *Server) sourceGitReconcile(w http.ResponseWriter, r *http.Request) {
108 vars := mux.Vars(r)
109 namespace, ok := vars["namespace"]
110 if !ok {
111 http.Error(w, "namespace missing", http.StatusBadRequest)
112 return
113 }
114 name, ok := vars["name"]
115 if !ok {
116 http.Error(w, "name missing", http.StatusBadRequest)
117 return
118 }
119 res := schema.GroupVersionResource{
120 Group: "source.toolkit.fluxcd.io",
121 Version: "v1",
122 Resource: "gitrepositories",
123 }
124 if err := reconcile(s.client, res, namespace, name); err != nil {
gio43b0f422024-08-21 10:40:13 +0400125 fmt.Println(err)
126 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400127 return
128 }
129}
130
131func (s *Server) kustomizationReconcile(w http.ResponseWriter, r *http.Request) {
132 vars := mux.Vars(r)
133 namespace, ok := vars["namespace"]
134 if !ok {
135 http.Error(w, "namespace missing", http.StatusBadRequest)
136 return
137 }
138 name, ok := vars["name"]
139 if !ok {
140 http.Error(w, "name missing", http.StatusBadRequest)
141 return
142 }
143 res := schema.GroupVersionResource{
144 Group: "kustomize.toolkit.fluxcd.io",
145 Version: "v1",
146 Resource: "kustomizations",
147 }
148 if err := reconcile(s.client, res, namespace, name); err != nil {
gio43b0f422024-08-21 10:40:13 +0400149 fmt.Println(err)
150 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400151 return
152 }
153}
154
155func NewKubeClient(kubeconfig string) (dynamic.Interface, error) {
156 if kubeconfig == "" {
157 config, err := rest.InClusterConfig()
158 if err != nil {
159 return nil, err
160 }
161 return dynamic.NewForConfig(config)
162 } else {
163 config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
164 if err != nil {
165 return nil, err
166 }
167 return dynamic.NewForConfig(config)
168 }
169}
170
171func main() {
172 flag.Parse()
173 client, err := NewKubeClient(*kubeconfig)
174 if err != nil {
175 log.Fatal(err)
176 }
177 NewServer(*port, client).Start()
178}