diff --git a/cyclops-ctrl/cmd/main/main.go b/cyclops-ctrl/cmd/main/main.go index feb7766f4..f73e0b98a 100644 --- a/cyclops-ctrl/cmd/main/main.go +++ b/cyclops-ctrl/cmd/main/main.go @@ -103,7 +103,7 @@ func main() { prometheus.StartCacheMetricsUpdater(&monitor, templatesRepo.ReturnCache(), 10*time.Second, setupLog) - helmReleaseClient := helm.NewReleaseClient(helmWatchNamespace) + helmReleaseClient := helm.NewReleaseClient(helmWatchNamespace, k8sClient) gitWriteClient := git.NewWriteClient(credsResolver, getCommitMessageTemplate(), setupLog) handler, err := handler.New(templatesRepo, k8sClient, helmReleaseClient, renderer, gitWriteClient, moduleTargetNamespace, telemetryClient, monitor) diff --git a/cyclops-ctrl/internal/controller/helm.go b/cyclops-ctrl/internal/controller/helm.go index a879ee5fa..8385556ca 100644 --- a/cyclops-ctrl/internal/controller/helm.go +++ b/cyclops-ctrl/internal/controller/helm.go @@ -117,9 +117,10 @@ func (h *Helm) UninstallRelease(ctx *gin.Context) { func (h *Helm) GetReleaseResources(ctx *gin.Context) { ctx.Header("Access-Control-Allow-Origin", "*") + namespace := ctx.Param("namespace") name := ctx.Param("name") - resources, err := h.kubernetesClient.GetResourcesForRelease(name) + resources, err := h.releaseClient.ListResources(namespace, name) if err != nil { fmt.Println(err) ctx.JSON(http.StatusBadRequest, dto.NewError("Error fetching Helm release resources", err.Error())) diff --git a/cyclops-ctrl/internal/controller/sse/resources.go b/cyclops-ctrl/internal/controller/sse/resources.go index 608833544..59147f6c5 100644 --- a/cyclops-ctrl/internal/controller/sse/resources.go +++ b/cyclops-ctrl/internal/controller/sse/resources.go @@ -25,7 +25,7 @@ func (s *Server) Resources(ctx *gin.Context) { } func (s *Server) ReleaseResources(ctx *gin.Context) { - resources, err := s.k8sClient.GetWorkloadsForRelease(ctx.Param("name")) + resources, err := s.releaseClient.ListWorkloadsForRelease(ctx.Param("namespace"), ctx.Param("name")) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return diff --git a/cyclops-ctrl/internal/controller/sse/server.go b/cyclops-ctrl/internal/controller/sse/server.go index efe63612c..d41cd4e13 100644 --- a/cyclops-ctrl/internal/controller/sse/server.go +++ b/cyclops-ctrl/internal/controller/sse/server.go @@ -1,19 +1,22 @@ package sse import ( + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/integrations/helm" "github.com/gin-gonic/gin" "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/cluster/k8sclient" ) type Server struct { - k8sClient k8sclient.IKubernetesClient + k8sClient k8sclient.IKubernetesClient + releaseClient *helm.ReleaseClient } // Initialize event and Start procnteessing requests -func NewServer(k8sClient k8sclient.IKubernetesClient) *Server { +func NewServer(k8sClient k8sclient.IKubernetesClient, releaseClient *helm.ReleaseClient) *Server { server := &Server{ - k8sClient: k8sClient, + k8sClient: k8sClient, + releaseClient: releaseClient, } return server diff --git a/cyclops-ctrl/internal/handler/handler.go b/cyclops-ctrl/internal/handler/handler.go index a8a295e33..7fc28b1b5 100644 --- a/cyclops-ctrl/internal/handler/handler.go +++ b/cyclops-ctrl/internal/handler/handler.go @@ -62,10 +62,10 @@ func (h *Handler) Start() error { h.router = gin.New() - server := sse.NewServer(h.k8sClient) + server := sse.NewServer(h.k8sClient, h.releaseClient) h.router.GET("/stream/resources/:name", sse.HeadersMiddleware(), server.Resources) - h.router.GET("/stream/releases/resources/:name", sse.HeadersMiddleware(), server.ReleaseResources) + h.router.GET("/stream/releases/:namespace/:name/resources", sse.HeadersMiddleware(), server.ReleaseResources) h.router.POST("/stream/resources", sse.HeadersMiddleware(), server.SingleResource) h.router.GET("/ping", h.pong()) diff --git a/cyclops-ctrl/internal/integrations/helm/helm.go b/cyclops-ctrl/internal/integrations/helm/helm.go index 41223eba8..c74dafe01 100644 --- a/cyclops-ctrl/internal/integrations/helm/helm.go +++ b/cyclops-ctrl/internal/integrations/helm/helm.go @@ -2,8 +2,13 @@ package helm import ( "fmt" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/models/dto" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/cluster/k8sclient" "io" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "log" + "sort" "strings" "github.com/pkg/errors" @@ -16,14 +21,18 @@ import ( type ReleaseClient struct { namespace string + k8sClient k8sclient.IKubernetesClient } -func NewReleaseClient(namespace string) *ReleaseClient { +func NewReleaseClient(namespace string, k8sClient k8sclient.IKubernetesClient) *ReleaseClient { return &ReleaseClient{ namespace: strings.TrimSpace(namespace), + k8sClient: k8sClient, } } +func noopLogger(format string, v ...interface{}) {} + func (r *ReleaseClient) ListReleases() ([]*release.Release, error) { settings := cli.New() @@ -119,3 +128,78 @@ func (r *ReleaseClient) UpgradeRelease( _, err = client.Run(name, current.Chart, values) return err } + +func (r *ReleaseClient) ListResources(namespace string, name string) ([]dto.Resource, error) { + if len(r.namespace) > 0 && namespace != r.namespace { + return nil, errors.New(fmt.Sprintf("invalid namespace provided: %v", namespace)) + } + + settings := cli.New() + settings.SetNamespace(namespace) + + actionConfig := new(action.Configuration) + if err := actionConfig.Init(settings.RESTClientGetter(), namespace, "", noopLogger); err != nil { + return nil, err + } + + client := action.NewStatus(actionConfig) + client.ShowResources = true + + releaseStatus, err := client.Run(name) + if err != nil { + return nil, err + } + + if releaseStatus.Info == nil { + return nil, errors.New("empty release info resources") + } + + out := make([]dto.Resource, 0, 0) + for gv, objs := range releaseStatus.Info.Resources { + if strings.HasSuffix(gv, "(related)") { + continue + } + + for _, obj := range objs { + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + + res, err := r.k8sClient.MapUnstructuredResource(unstructured.Unstructured{Object: u}) + if err != nil { + return nil, err + } + + out = append(out, res) + } + } + + sort.Slice(out, func(i, j int) bool { + if out[i].GetGroupVersionKind() != out[j].GetGroupVersionKind() { + return out[i].GetGroupVersionKind() < out[j].GetGroupVersionKind() + } + + return out[i].GetName() < out[j].GetName() + }) + + return out, nil +} + +func (r *ReleaseClient) ListWorkloadsForRelease(namespace, name string) ([]dto.Resource, error) { + resources, err := r.ListResources(namespace, name) + if err != nil { + return nil, err + } + + workloads := make([]dto.Resource, 0, 0) + for _, resource := range resources { + if resource.GetGroup() == "apps" && + resource.GetVersion() == "v1" && + (resource.GetKind() == "Deployment" || resource.GetKind() == "DaemonSet" || resource.GetKind() == "StatefulSet") { + workloads = append(workloads, resource) + } + } + + return workloads, nil +} diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/client.go b/cyclops-ctrl/pkg/cluster/k8sclient/client.go index 2ebb005ba..3749dac29 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go @@ -80,6 +80,7 @@ type IKubernetesClient interface { DeleteModule(name string) error GetModule(name string) (*cyclopsv1alpha1.Module, error) GetResourcesForModule(name string) ([]dto.Resource, error) + MapUnstructuredResource(u unstructured.Unstructured) (dto.Resource, error) GetWorkloadsForModule(name string) ([]dto.Resource, error) GetDeletedResources([]dto.Resource, string, string) ([]dto.Resource, error) GetModuleResourcesHealth(name string) (string, error) diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go index fd3c4d330..c88803496 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go @@ -94,20 +94,12 @@ func (k *KubernetesClient) GetResourcesForModule(name string) ([]dto.Resource, e } for _, o := range other { - status, err := k.getResourceStatus(o) + res, err := k.MapUnstructuredResource(o) if err != nil { return nil, err } - out = append(out, &dto.Other{ - Group: o.GroupVersionKind().Group, - Version: o.GroupVersionKind().Version, - Kind: o.GroupVersionKind().Kind, - Name: o.GetName(), - Namespace: o.GetNamespace(), - Status: status, - Deleted: false, - }) + out = append(out, res) } sort.Slice(out, func(i, j int) bool { @@ -121,6 +113,23 @@ func (k *KubernetesClient) GetResourcesForModule(name string) ([]dto.Resource, e return out, nil } +func (k *KubernetesClient) MapUnstructuredResource(u unstructured.Unstructured) (dto.Resource, error) { + status, err := k.getResourceStatus(u) + if err != nil { + return nil, err + } + + return &dto.Other{ + Group: u.GroupVersionKind().Group, + Version: u.GroupVersionKind().Version, + Kind: u.GroupVersionKind().Kind, + Name: u.GetName(), + Namespace: u.GetNamespace(), + Status: status, + Deleted: false, + }, nil +} + func (k *KubernetesClient) GetWorkloadsForModule(name string) ([]dto.Resource, error) { out := make([]dto.Resource, 0, 0) diff --git a/cyclops-ctrl/pkg/mocks/IKubernetesClient.go b/cyclops-ctrl/pkg/mocks/IKubernetesClient.go index 3b4d16903..d9dea77b6 100644 --- a/cyclops-ctrl/pkg/mocks/IKubernetesClient.go +++ b/cyclops-ctrl/pkg/mocks/IKubernetesClient.go @@ -1685,6 +1685,64 @@ func (_c *IKubernetesClient_ListTemplateStore_Call) RunAndReturn(run func() ([]v return _c } +// MapUnstructuredResource provides a mock function with given fields: u +func (_m *IKubernetesClient) MapUnstructuredResource(u unstructured.Unstructured) (dto.Resource, error) { + ret := _m.Called(u) + + if len(ret) == 0 { + panic("no return value specified for MapUnstructuredResource") + } + + var r0 dto.Resource + var r1 error + if rf, ok := ret.Get(0).(func(unstructured.Unstructured) (dto.Resource, error)); ok { + return rf(u) + } + if rf, ok := ret.Get(0).(func(unstructured.Unstructured) dto.Resource); ok { + r0 = rf(u) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dto.Resource) + } + } + + if rf, ok := ret.Get(1).(func(unstructured.Unstructured) error); ok { + r1 = rf(u) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IKubernetesClient_MapUnstructuredResource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MapUnstructuredResource' +type IKubernetesClient_MapUnstructuredResource_Call struct { + *mock.Call +} + +// MapUnstructuredResource is a helper method to define mock.On call +// - u unstructured.Unstructured +func (_e *IKubernetesClient_Expecter) MapUnstructuredResource(u interface{}) *IKubernetesClient_MapUnstructuredResource_Call { + return &IKubernetesClient_MapUnstructuredResource_Call{Call: _e.mock.On("MapUnstructuredResource", u)} +} + +func (_c *IKubernetesClient_MapUnstructuredResource_Call) Run(run func(u unstructured.Unstructured)) *IKubernetesClient_MapUnstructuredResource_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(unstructured.Unstructured)) + }) + return _c +} + +func (_c *IKubernetesClient_MapUnstructuredResource_Call) Return(_a0 dto.Resource, _a1 error) *IKubernetesClient_MapUnstructuredResource_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *IKubernetesClient_MapUnstructuredResource_Call) RunAndReturn(run func(unstructured.Unstructured) (dto.Resource, error)) *IKubernetesClient_MapUnstructuredResource_Call { + _c.Call.Return(run) + return _c +} + // Restart provides a mock function with given fields: group, _a1, kind, name, namespace func (_m *IKubernetesClient) Restart(group string, _a1 string, kind string, name string, namespace string) error { ret := _m.Called(group, _a1, kind, name, namespace) diff --git a/cyclops-ui/src/components/shared/HelmReleaseDetails/HelmReleaseDetails.tsx b/cyclops-ui/src/components/shared/HelmReleaseDetails/HelmReleaseDetails.tsx index fe3a80522..be58dbf3d 100644 --- a/cyclops-ui/src/components/shared/HelmReleaseDetails/HelmReleaseDetails.tsx +++ b/cyclops-ui/src/components/shared/HelmReleaseDetails/HelmReleaseDetails.tsx @@ -266,7 +266,7 @@ export const HelmReleaseDetails = ({ useEffect(() => { if (isStreamingEnabled()) { resourcesStream( - `/stream/releases/resources/${releaseName}`, + `/stream/releases/${releaseNamespace}/${releaseName}/resources`, (r: any) => { let resourceRef: ResourceRef = { group: r.group, @@ -281,7 +281,7 @@ export const HelmReleaseDetails = ({ resourceStreamImplementation, ); } - }, [releaseName, resourceStreamImplementation]); + }, [releaseNamespace, releaseName, resourceStreamImplementation]); const resourceLoading = () => { if (!loadModule) {