diff --git a/cmd/capacitor/main.go b/cmd/capacitor/main.go index dd711b0..1cf5d15 100644 --- a/cmd/capacitor/main.go +++ b/cmd/capacitor/main.go @@ -1,35 +1,23 @@ package main import ( - "context" - "encoding/json" "flag" "fmt" "net/http" "os" "os/signal" - "strings" "syscall" - kustomizationv1 "github.com/fluxcd/kustomize-controller/api/v1" - sourcev1 "github.com/fluxcd/source-controller/api/v1" + "github.com/gimlet-io/capacitor/pkg/api" "github.com/gimlet-io/capacitor/pkg/controllers" "github.com/gimlet-io/capacitor/pkg/streaming" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/clientcmd" ) func main() { - fmt.Println("Capacitor starting..") - fmt.Println("Connecting to Kubernetes..") - var kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") flag.Parse() @@ -39,61 +27,21 @@ func main() { panic(err.Error()) } - fmt.Println("--- Flux custom resources ---") dynamicClient, err := dynamic.NewForConfig(config) if err != nil { panic(err.Error()) } - fluxState, err := getFluxState(dynamicClient) - if err != nil { - panic(err.Error()) - } - fluxStateBytes, err := json.Marshal(fluxState) - if err != nil { - panic(err.Error()) - } - fmt.Println(string(fluxStateBytes)) - - var gitRepositoryResource = schema.GroupVersionResource{ - Group: "source.toolkit.fluxcd.io", - Version: "v1", - Resource: "gitrepositories", - } - stopCh := make(chan struct{}) defer close(stopCh) clientHub := streaming.NewClientHub() go clientHub.Run() - ctrl := controllers.NewDynamicController( - "gitrepositories.source.toolkit.fluxcd.io", - dynamicClient, - gitRepositoryResource, - func(informerEvent controllers.Event, objectMeta metav1.ObjectMeta, obj interface{}) error { - switch informerEvent.EventType { - case "create": - fallthrough - case "update": - fallthrough - case "delete": - fmt.Printf("Changes in %s\n", objectMeta.Name) - fluxState, err := getFluxState(dynamicClient) - if err != nil { - panic(err.Error()) - } - fluxStateBytes, err := json.Marshal(fluxState) - if err != nil { - panic(err.Error()) - } - clientHub.Broadcast <- fluxStateBytes - } - return nil - }) - go ctrl.Run(1, stopCh) + gitRepositoryController := controllers.GitRepositoryController(dynamicClient, clientHub) + go gitRepositoryController.Run(1, stopCh) - r := setupRouter(dynamicClient, clientHub) + r := api.SetupRouter(dynamicClient, clientHub) go func() { err = http.ListenAndServe(":9000", r) if err != nil { @@ -118,116 +66,3 @@ func main() { <-done logrus.Info("Exiting") } - -type fluxState struct { - GitRepositories []sourcev1.GitRepository `json:"gitRepositories"` - Kustomizations []kustomizationv1.Kustomization `json:"kustomizations"` -} - -func getFluxState(dc *dynamic.DynamicClient) (*fluxState, error) { - fluxState := &fluxState{ - GitRepositories: []sourcev1.GitRepository{}, - Kustomizations: []kustomizationv1.Kustomization{}, - } - - gitRepositories, err := dc.Resource(schema.GroupVersionResource{ - Group: "source.toolkit.fluxcd.io", - Version: "v1", - Resource: "gitrepositories", - }). - Namespace(""). - List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return nil, err - } - - for _, repo := range gitRepositories.Items { - unstructured := repo.UnstructuredContent() - var gitRepository sourcev1.GitRepository - err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &gitRepository) - if err != nil { - return nil, err - } - fluxState.GitRepositories = append(fluxState.GitRepositories, gitRepository) - } - - kustomizations, err := dc.Resource(schema.GroupVersionResource{ - Group: "kustomize.toolkit.fluxcd.io", - Version: "v1", - Resource: "kustomizations", - }). - Namespace(""). - List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return nil, err - } - for _, k := range kustomizations.Items { - unstructured := k.UnstructuredContent() - var kustomization kustomizationv1.Kustomization - err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &kustomization) - if err != nil { - return nil, err - } - fluxState.Kustomizations = append(fluxState.Kustomizations, kustomization) - } - - return fluxState, nil -} - -func setupRouter( - dynamicClient *dynamic.DynamicClient, - clientHub *streaming.ClientHub, -) *chi.Mux { - r := chi.NewRouter() - r.Use(middleware.WithValue("dynamicClient", dynamicClient)) - - r.Get("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - r.Get("/api/fluxState", fluxStateHandler) - r.Get("/ws/", func(w http.ResponseWriter, r *http.Request) { - streaming.ServeWs(clientHub, w, r) - }) - - filesDir := http.Dir("./web/build") - fileServer(r, "/", filesDir) - - return r -} - -// static files from a http.FileSystem -func fileServer(r chi.Router, path string, root http.FileSystem) { - if strings.ContainsAny(path, "{}*") { - //TODO: serve all React routes https://github.com/go-chi/chi/issues/403 - panic("FileServer does not permit any URL parameters.") - } - - if path != "/" && path[len(path)-1] != '/' { - r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) - path += "/" - } - path += "*" - - r.Get(path, func(w http.ResponseWriter, r *http.Request) { - ctx := chi.RouteContext(r.Context()) - pathPrefix := strings.TrimSuffix(ctx.RoutePattern(), "/*") - fs := http.StripPrefix(pathPrefix, http.FileServer(root)) - fs.ServeHTTP(w, r) - }) -} - -func fluxStateHandler(w http.ResponseWriter, r *http.Request) { - dynamicClient, _ := r.Context().Value("dynamicClient").(*dynamic.DynamicClient) - - fluxState, err := getFluxState(dynamicClient) - if err != nil { - panic(err.Error()) - } - fluxStateBytes, err := json.Marshal(fluxState) - if err != nil { - panic(err.Error()) - } - - w.WriteHeader(http.StatusOK) - w.Write(fluxStateBytes) -} diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..270c728 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,25 @@ +package api + +import ( + "encoding/json" + "net/http" + + "k8s.io/client-go/dynamic" + "github.com/gimlet-io/capacitor/pkg/flux" +) + +func fluxStateHandler(w http.ResponseWriter, r *http.Request) { + dynamicClient, _ := r.Context().Value("dynamicClient").(*dynamic.DynamicClient) + + fluxState, err := flux.GetFluxState(dynamicClient) + if err != nil { + panic(err.Error()) + } + fluxStateBytes, err := json.Marshal(fluxState) + if err != nil { + panic(err.Error()) + } + + w.WriteHeader(http.StatusOK) + w.Write(fluxStateBytes) +} diff --git a/pkg/api/router.go b/pkg/api/router.go new file mode 100644 index 0000000..38e4de9 --- /dev/null +++ b/pkg/api/router.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + "strings" + + "github.com/gimlet-io/capacitor/pkg/streaming" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "k8s.io/client-go/dynamic" +) + +func SetupRouter( + dynamicClient *dynamic.DynamicClient, + clientHub *streaming.ClientHub, +) *chi.Mux { + r := chi.NewRouter() + r.Use(middleware.WithValue("dynamicClient", dynamicClient)) + + r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + r.Get("/api/fluxState", fluxStateHandler) + r.Get("/ws/", func(w http.ResponseWriter, r *http.Request) { + streaming.ServeWs(clientHub, w, r) + }) + + filesDir := http.Dir("./web/build") + fileServer(r, "/", filesDir) + + return r +} + +// static files from a http.FileSystem +func fileServer(r chi.Router, path string, root http.FileSystem) { + if strings.ContainsAny(path, "{}*") { + //TODO: serve all React routes https://github.com/go-chi/chi/issues/403 + panic("FileServer does not permit any URL parameters.") + } + + if path != "/" && path[len(path)-1] != '/' { + r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) + path += "/" + } + path += "*" + + r.Get(path, func(w http.ResponseWriter, r *http.Request) { + ctx := chi.RouteContext(r.Context()) + pathPrefix := strings.TrimSuffix(ctx.RoutePattern(), "/*") + fs := http.StripPrefix(pathPrefix, http.FileServer(root)) + fs.ServeHTTP(w, r) + }) +} diff --git a/pkg/controllers/gitrepositoryController.go b/pkg/controllers/gitrepositoryController.go new file mode 100644 index 0000000..43bc84b --- /dev/null +++ b/pkg/controllers/gitrepositoryController.go @@ -0,0 +1,48 @@ +package controllers + +import ( + "encoding/json" + "fmt" + + "github.com/gimlet-io/capacitor/pkg/flux" + "github.com/gimlet-io/capacitor/pkg/streaming" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +var gitRepositoryResource = schema.GroupVersionResource{ + Group: "source.toolkit.fluxcd.io", + Version: "v1", + Resource: "gitrepositories", +} + +func GitRepositoryController( + dynamicClient *dynamic.DynamicClient, + clientHub *streaming.ClientHub, +) *Controller { + return NewDynamicController( + "gitrepositories.source.toolkit.fluxcd.io", + dynamicClient, + gitRepositoryResource, + func(informerEvent Event, objectMeta metav1.ObjectMeta, obj interface{}) error { + switch informerEvent.EventType { + case "create": + fallthrough + case "update": + fallthrough + case "delete": + fmt.Printf("Changes in %s\n", objectMeta.Name) + fluxState, err := flux.GetFluxState(dynamicClient) + if err != nil { + panic(err.Error()) + } + fluxStateBytes, err := json.Marshal(fluxState) + if err != nil { + panic(err.Error()) + } + clientHub.Broadcast <- fluxStateBytes + } + return nil + }) +} diff --git a/pkg/flux/flux.go b/pkg/flux/flux.go new file mode 100644 index 0000000..314451a --- /dev/null +++ b/pkg/flux/flux.go @@ -0,0 +1,67 @@ +package flux + +import ( + "context" + + kustomizationv1 "github.com/fluxcd/kustomize-controller/api/v1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +type FluxState struct { + GitRepositories []sourcev1.GitRepository `json:"gitRepositories"` + Kustomizations []kustomizationv1.Kustomization `json:"kustomizations"` +} + +func GetFluxState(dc *dynamic.DynamicClient) (*FluxState, error) { + fluxState := &FluxState{ + GitRepositories: []sourcev1.GitRepository{}, + Kustomizations: []kustomizationv1.Kustomization{}, + } + + gitRepositories, err := dc.Resource(schema.GroupVersionResource{ + Group: "source.toolkit.fluxcd.io", + Version: "v1", + Resource: "gitrepositories", + }). + Namespace(""). + List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + + for _, repo := range gitRepositories.Items { + unstructured := repo.UnstructuredContent() + var gitRepository sourcev1.GitRepository + err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &gitRepository) + if err != nil { + return nil, err + } + fluxState.GitRepositories = append(fluxState.GitRepositories, gitRepository) + } + + kustomizations, err := dc.Resource(schema.GroupVersionResource{ + Group: "kustomize.toolkit.fluxcd.io", + Version: "v1", + Resource: "kustomizations", + }). + Namespace(""). + List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, k := range kustomizations.Items { + unstructured := k.UnstructuredContent() + var kustomization kustomizationv1.Kustomization + err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &kustomization) + if err != nil { + return nil, err + } + fluxState.Kustomizations = append(fluxState.Kustomizations, kustomization) + } + + return fluxState, nil +}