From 40417d066249db8af4d0b3364aa0f1b3e1ccb489 Mon Sep 17 00:00:00 2001 From: Victor Dorneanu Date: Tue, 3 Sep 2024 12:36:51 +0200 Subject: [PATCH] feat(scanner): Implement K8s scanner (#184) * Implement K8s scanner * Automatic application of license header * Clean up * Signing previous commits * Sign with SSH key * Sign with SSH key (again) * Sign with SSH key (again testing) * Implement K8s scanner * Automatic application of license header * Clean up * Signing previous commits * Sign with SSH key * Sign with SSH key (again) * Sign with SSH key (again testing) * Signing previous commits * Sign with SSH key * Sign with SSH key (again) * Sign with SSH key (again testing) * Signing previous commits * Sign with SSH key * Sign with SSH key (again) * Sign with SSH key (again testing) * WIP * Restore go.mod * Wip * Add tests * Automatic application of license header * Wip * Improve logic * Factorize KubeConfig * Fixes #discussion_r1740426730 * Fixes https://github.com/cloudoperators/heureka/pull/184#discussion_r1733993763 --------- Co-authored-by: License Bot Co-authored-by: David Rochow --- scanner/k8s-assets/client/generated.go | 701 ++++++++++++++++++ scanner/k8s-assets/client/genqlient.yaml | 11 + .../client/query/component_query.graphql | 19 + .../query/componentinstance_create.graphql | 15 + .../query/componentinstance_query.graphql | 15 + .../query/componentversion_create.graphql | 12 + .../query/componentversion_query.graphql | 19 + .../client/query/service_create.graphql | 12 + .../client/query/service_query.graphql | 15 + scanner/k8s-assets/config/kubeconfig.go | 88 +++ scanner/k8s-assets/go.mod | 61 +- scanner/k8s-assets/go.sum | 168 +++++ scanner/k8s-assets/main.go | 197 ++++- scanner/k8s-assets/processor/config.go | 10 + scanner/k8s-assets/processor/processor.go | 257 +++++++ .../processor/processor_suite_test.go | 16 + .../k8s-assets/processor/processor_test.go | 198 +++++ scanner/k8s-assets/scanner/config.go | 13 + scanner/k8s-assets/scanner/scanner.go | 187 +++++ 19 files changed, 2012 insertions(+), 2 deletions(-) create mode 100644 scanner/k8s-assets/client/generated.go create mode 100644 scanner/k8s-assets/client/genqlient.yaml create mode 100644 scanner/k8s-assets/client/query/component_query.graphql create mode 100644 scanner/k8s-assets/client/query/componentinstance_create.graphql create mode 100644 scanner/k8s-assets/client/query/componentinstance_query.graphql create mode 100644 scanner/k8s-assets/client/query/componentversion_create.graphql create mode 100644 scanner/k8s-assets/client/query/componentversion_query.graphql create mode 100644 scanner/k8s-assets/client/query/service_create.graphql create mode 100644 scanner/k8s-assets/client/query/service_query.graphql create mode 100644 scanner/k8s-assets/config/kubeconfig.go create mode 100644 scanner/k8s-assets/go.sum create mode 100644 scanner/k8s-assets/processor/config.go create mode 100644 scanner/k8s-assets/processor/processor.go create mode 100644 scanner/k8s-assets/processor/processor_suite_test.go create mode 100644 scanner/k8s-assets/processor/processor_test.go create mode 100644 scanner/k8s-assets/scanner/config.go create mode 100644 scanner/k8s-assets/scanner/scanner.go diff --git a/scanner/k8s-assets/client/generated.go b/scanner/k8s-assets/client/generated.go new file mode 100644 index 0000000..a7bd7e0 --- /dev/null +++ b/scanner/k8s-assets/client/generated.go @@ -0,0 +1,701 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package client + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// Component includes the requested fields of the GraphQL type Component. +type Component struct { + Id string `json:"id"` + Name string `json:"name"` + Type ComponentTypeValues `json:"type"` +} + +// GetId returns Component.Id, and is useful for accessing the field via an interface. +func (v *Component) GetId() string { return v.Id } + +// GetName returns Component.Name, and is useful for accessing the field via an interface. +func (v *Component) GetName() string { return v.Name } + +// GetType returns Component.Type, and is useful for accessing the field via an interface. +func (v *Component) GetType() ComponentTypeValues { return v.Type } + +// ComponentConnection includes the requested fields of the GraphQL type ComponentConnection. +type ComponentConnection struct { + TotalCount int `json:"totalCount"` + Edges []*ComponentConnectionEdgesComponentEdge `json:"edges"` +} + +// GetTotalCount returns ComponentConnection.TotalCount, and is useful for accessing the field via an interface. +func (v *ComponentConnection) GetTotalCount() int { return v.TotalCount } + +// GetEdges returns ComponentConnection.Edges, and is useful for accessing the field via an interface. +func (v *ComponentConnection) GetEdges() []*ComponentConnectionEdgesComponentEdge { return v.Edges } + +// ComponentConnectionEdgesComponentEdge includes the requested fields of the GraphQL type ComponentEdge. +type ComponentConnectionEdgesComponentEdge struct { + Node *Component `json:"node"` +} + +// GetNode returns ComponentConnectionEdgesComponentEdge.Node, and is useful for accessing the field via an interface. +func (v *ComponentConnectionEdgesComponentEdge) GetNode() *Component { return v.Node } + +type ComponentFilter struct { + ComponentName []string `json:"componentName"` +} + +// GetComponentName returns ComponentFilter.ComponentName, and is useful for accessing the field via an interface. +func (v *ComponentFilter) GetComponentName() []string { return v.ComponentName } + +// ComponentInstance includes the requested fields of the GraphQL type ComponentInstance. +type ComponentInstance struct { + Id string `json:"id"` + Ccrn string `json:"ccrn"` + Count int `json:"count"` + ComponentVersionId string `json:"componentVersionId"` + ServiceId string `json:"serviceId"` +} + +// GetId returns ComponentInstance.Id, and is useful for accessing the field via an interface. +func (v *ComponentInstance) GetId() string { return v.Id } + +// GetCcrn returns ComponentInstance.Ccrn, and is useful for accessing the field via an interface. +func (v *ComponentInstance) GetCcrn() string { return v.Ccrn } + +// GetCount returns ComponentInstance.Count, and is useful for accessing the field via an interface. +func (v *ComponentInstance) GetCount() int { return v.Count } + +// GetComponentVersionId returns ComponentInstance.ComponentVersionId, and is useful for accessing the field via an interface. +func (v *ComponentInstance) GetComponentVersionId() string { return v.ComponentVersionId } + +// GetServiceId returns ComponentInstance.ServiceId, and is useful for accessing the field via an interface. +func (v *ComponentInstance) GetServiceId() string { return v.ServiceId } + +type ComponentInstanceFilter struct { + IssueMatchId []string `json:"issueMatchId"` +} + +// GetIssueMatchId returns ComponentInstanceFilter.IssueMatchId, and is useful for accessing the field via an interface. +func (v *ComponentInstanceFilter) GetIssueMatchId() []string { return v.IssueMatchId } + +type ComponentInstanceInput struct { + Ccrn string `json:"ccrn"` + Count int `json:"count"` + ComponentVersionId string `json:"componentVersionId"` + ServiceId string `json:"serviceId"` +} + +// GetCcrn returns ComponentInstanceInput.Ccrn, and is useful for accessing the field via an interface. +func (v *ComponentInstanceInput) GetCcrn() string { return v.Ccrn } + +// GetCount returns ComponentInstanceInput.Count, and is useful for accessing the field via an interface. +func (v *ComponentInstanceInput) GetCount() int { return v.Count } + +// GetComponentVersionId returns ComponentInstanceInput.ComponentVersionId, and is useful for accessing the field via an interface. +func (v *ComponentInstanceInput) GetComponentVersionId() string { return v.ComponentVersionId } + +// GetServiceId returns ComponentInstanceInput.ServiceId, and is useful for accessing the field via an interface. +func (v *ComponentInstanceInput) GetServiceId() string { return v.ServiceId } + +type ComponentTypeValues string + +const ( + ComponentTypeValuesContainerimage ComponentTypeValues = "containerImage" + ComponentTypeValuesVirtualmachineimage ComponentTypeValues = "virtualMachineImage" + ComponentTypeValuesRepository ComponentTypeValues = "repository" +) + +// ComponentVersion includes the requested fields of the GraphQL type ComponentVersion. +type ComponentVersion struct { + Id string `json:"id"` + Version string `json:"version"` + ComponentId string `json:"componentId"` +} + +// GetId returns ComponentVersion.Id, and is useful for accessing the field via an interface. +func (v *ComponentVersion) GetId() string { return v.Id } + +// GetVersion returns ComponentVersion.Version, and is useful for accessing the field via an interface. +func (v *ComponentVersion) GetVersion() string { return v.Version } + +// GetComponentId returns ComponentVersion.ComponentId, and is useful for accessing the field via an interface. +func (v *ComponentVersion) GetComponentId() string { return v.ComponentId } + +// ComponentVersionConnection includes the requested fields of the GraphQL type ComponentVersionConnection. +type ComponentVersionConnection struct { + TotalCount int `json:"totalCount"` + Edges []*ComponentVersionConnectionEdgesComponentVersionEdge `json:"edges"` +} + +// GetTotalCount returns ComponentVersionConnection.TotalCount, and is useful for accessing the field via an interface. +func (v *ComponentVersionConnection) GetTotalCount() int { return v.TotalCount } + +// GetEdges returns ComponentVersionConnection.Edges, and is useful for accessing the field via an interface. +func (v *ComponentVersionConnection) GetEdges() []*ComponentVersionConnectionEdgesComponentVersionEdge { + return v.Edges +} + +// ComponentVersionConnectionEdgesComponentVersionEdge includes the requested fields of the GraphQL type ComponentVersionEdge. +type ComponentVersionConnectionEdgesComponentVersionEdge struct { + Node *ComponentVersion `json:"node"` +} + +// GetNode returns ComponentVersionConnectionEdgesComponentVersionEdge.Node, and is useful for accessing the field via an interface. +func (v *ComponentVersionConnectionEdgesComponentVersionEdge) GetNode() *ComponentVersion { + return v.Node +} + +type ComponentVersionFilter struct { + IssueId []string `json:"issueId"` + Version []string `json:"version"` +} + +// GetIssueId returns ComponentVersionFilter.IssueId, and is useful for accessing the field via an interface. +func (v *ComponentVersionFilter) GetIssueId() []string { return v.IssueId } + +// GetVersion returns ComponentVersionFilter.Version, and is useful for accessing the field via an interface. +func (v *ComponentVersionFilter) GetVersion() []string { return v.Version } + +type ComponentVersionInput struct { + Version string `json:"version"` + ComponentId string `json:"componentId"` +} + +// GetVersion returns ComponentVersionInput.Version, and is useful for accessing the field via an interface. +func (v *ComponentVersionInput) GetVersion() string { return v.Version } + +// GetComponentId returns ComponentVersionInput.ComponentId, and is useful for accessing the field via an interface. +func (v *ComponentVersionInput) GetComponentId() string { return v.ComponentId } + +// CreateComponentInstanceResponse is returned by CreateComponentInstance on success. +type CreateComponentInstanceResponse struct { + CreateComponentInstance *ComponentInstance `json:"createComponentInstance"` +} + +// GetCreateComponentInstance returns CreateComponentInstanceResponse.CreateComponentInstance, and is useful for accessing the field via an interface. +func (v *CreateComponentInstanceResponse) GetCreateComponentInstance() *ComponentInstance { + return v.CreateComponentInstance +} + +// CreateComponentVersionCreateComponentVersion includes the requested fields of the GraphQL type ComponentVersion. +type CreateComponentVersionCreateComponentVersion struct { + Id string `json:"id"` + Version string `json:"version"` + ComponentId string `json:"componentId"` +} + +// GetId returns CreateComponentVersionCreateComponentVersion.Id, and is useful for accessing the field via an interface. +func (v *CreateComponentVersionCreateComponentVersion) GetId() string { return v.Id } + +// GetVersion returns CreateComponentVersionCreateComponentVersion.Version, and is useful for accessing the field via an interface. +func (v *CreateComponentVersionCreateComponentVersion) GetVersion() string { return v.Version } + +// GetComponentId returns CreateComponentVersionCreateComponentVersion.ComponentId, and is useful for accessing the field via an interface. +func (v *CreateComponentVersionCreateComponentVersion) GetComponentId() string { return v.ComponentId } + +// CreateComponentVersionResponse is returned by CreateComponentVersion on success. +type CreateComponentVersionResponse struct { + CreateComponentVersion *CreateComponentVersionCreateComponentVersion `json:"createComponentVersion"` +} + +// GetCreateComponentVersion returns CreateComponentVersionResponse.CreateComponentVersion, and is useful for accessing the field via an interface. +func (v *CreateComponentVersionResponse) GetCreateComponentVersion() *CreateComponentVersionCreateComponentVersion { + return v.CreateComponentVersion +} + +// CreateServiceResponse is returned by CreateService on success. +type CreateServiceResponse struct { + CreateService *Service `json:"createService"` +} + +// GetCreateService returns CreateServiceResponse.CreateService, and is useful for accessing the field via an interface. +func (v *CreateServiceResponse) GetCreateService() *Service { return v.CreateService } + +// ListComponentInstancesComponentInstancesComponentInstanceConnection includes the requested fields of the GraphQL type ComponentInstanceConnection. +type ListComponentInstancesComponentInstancesComponentInstanceConnection struct { + TotalCount int `json:"totalCount"` + Edges []*ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdge `json:"edges"` +} + +// GetTotalCount returns ListComponentInstancesComponentInstancesComponentInstanceConnection.TotalCount, and is useful for accessing the field via an interface. +func (v *ListComponentInstancesComponentInstancesComponentInstanceConnection) GetTotalCount() int { + return v.TotalCount +} + +// GetEdges returns ListComponentInstancesComponentInstancesComponentInstanceConnection.Edges, and is useful for accessing the field via an interface. +func (v *ListComponentInstancesComponentInstancesComponentInstanceConnection) GetEdges() []*ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdge { + return v.Edges +} + +// ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdge includes the requested fields of the GraphQL type ComponentInstanceEdge. +type ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdge struct { + Node *ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdgeNodeComponentInstance `json:"node"` +} + +// GetNode returns ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdge.Node, and is useful for accessing the field via an interface. +func (v *ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdge) GetNode() *ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdgeNodeComponentInstance { + return v.Node +} + +// ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdgeNodeComponentInstance includes the requested fields of the GraphQL type ComponentInstance. +type ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdgeNodeComponentInstance struct { + Id string `json:"id"` +} + +// GetId returns ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdgeNodeComponentInstance.Id, and is useful for accessing the field via an interface. +func (v *ListComponentInstancesComponentInstancesComponentInstanceConnectionEdgesComponentInstanceEdgeNodeComponentInstance) GetId() string { + return v.Id +} + +// ListComponentInstancesResponse is returned by ListComponentInstances on success. +type ListComponentInstancesResponse struct { + ComponentInstances *ListComponentInstancesComponentInstancesComponentInstanceConnection `json:"ComponentInstances"` +} + +// GetComponentInstances returns ListComponentInstancesResponse.ComponentInstances, and is useful for accessing the field via an interface. +func (v *ListComponentInstancesResponse) GetComponentInstances() *ListComponentInstancesComponentInstancesComponentInstanceConnection { + return v.ComponentInstances +} + +// ListComponentVersionsResponse is returned by ListComponentVersions on success. +type ListComponentVersionsResponse struct { + ComponentVersions *ComponentVersionConnection `json:"ComponentVersions"` +} + +// GetComponentVersions returns ListComponentVersionsResponse.ComponentVersions, and is useful for accessing the field via an interface. +func (v *ListComponentVersionsResponse) GetComponentVersions() *ComponentVersionConnection { + return v.ComponentVersions +} + +// ListComponentsResponse is returned by ListComponents on success. +type ListComponentsResponse struct { + Components *ComponentConnection `json:"Components"` +} + +// GetComponents returns ListComponentsResponse.Components, and is useful for accessing the field via an interface. +func (v *ListComponentsResponse) GetComponents() *ComponentConnection { return v.Components } + +// ListServicesResponse is returned by ListServices on success. +type ListServicesResponse struct { + Services *ListServicesServicesServiceConnection `json:"Services"` +} + +// GetServices returns ListServicesResponse.Services, and is useful for accessing the field via an interface. +func (v *ListServicesResponse) GetServices() *ListServicesServicesServiceConnection { + return v.Services +} + +// ListServicesServicesServiceConnection includes the requested fields of the GraphQL type ServiceConnection. +type ListServicesServicesServiceConnection struct { + TotalCount int `json:"totalCount"` + Edges []*ListServicesServicesServiceConnectionEdgesServiceEdge `json:"edges"` +} + +// GetTotalCount returns ListServicesServicesServiceConnection.TotalCount, and is useful for accessing the field via an interface. +func (v *ListServicesServicesServiceConnection) GetTotalCount() int { return v.TotalCount } + +// GetEdges returns ListServicesServicesServiceConnection.Edges, and is useful for accessing the field via an interface. +func (v *ListServicesServicesServiceConnection) GetEdges() []*ListServicesServicesServiceConnectionEdgesServiceEdge { + return v.Edges +} + +// ListServicesServicesServiceConnectionEdgesServiceEdge includes the requested fields of the GraphQL type ServiceEdge. +type ListServicesServicesServiceConnectionEdgesServiceEdge struct { + Node *ListServicesServicesServiceConnectionEdgesServiceEdgeNodeService `json:"node"` +} + +// GetNode returns ListServicesServicesServiceConnectionEdgesServiceEdge.Node, and is useful for accessing the field via an interface. +func (v *ListServicesServicesServiceConnectionEdgesServiceEdge) GetNode() *ListServicesServicesServiceConnectionEdgesServiceEdgeNodeService { + return v.Node +} + +// ListServicesServicesServiceConnectionEdgesServiceEdgeNodeService includes the requested fields of the GraphQL type Service. +type ListServicesServicesServiceConnectionEdgesServiceEdgeNodeService struct { + Id string `json:"id"` +} + +// GetId returns ListServicesServicesServiceConnectionEdgesServiceEdgeNodeService.Id, and is useful for accessing the field via an interface. +func (v *ListServicesServicesServiceConnectionEdgesServiceEdgeNodeService) GetId() string { + return v.Id +} + +// Service includes the requested fields of the GraphQL type Service. +type Service struct { + Id string `json:"id"` + Name string `json:"name"` +} + +// GetId returns Service.Id, and is useful for accessing the field via an interface. +func (v *Service) GetId() string { return v.Id } + +// GetName returns Service.Name, and is useful for accessing the field via an interface. +func (v *Service) GetName() string { return v.Name } + +type ServiceFilter struct { + ServiceName []string `json:"serviceName"` + UniqueUserId []string `json:"uniqueUserId"` + Type []int `json:"type"` + UserName []string `json:"userName"` + SupportGroupName []string `json:"supportGroupName"` +} + +// GetServiceName returns ServiceFilter.ServiceName, and is useful for accessing the field via an interface. +func (v *ServiceFilter) GetServiceName() []string { return v.ServiceName } + +// GetUniqueUserId returns ServiceFilter.UniqueUserId, and is useful for accessing the field via an interface. +func (v *ServiceFilter) GetUniqueUserId() []string { return v.UniqueUserId } + +// GetType returns ServiceFilter.Type, and is useful for accessing the field via an interface. +func (v *ServiceFilter) GetType() []int { return v.Type } + +// GetUserName returns ServiceFilter.UserName, and is useful for accessing the field via an interface. +func (v *ServiceFilter) GetUserName() []string { return v.UserName } + +// GetSupportGroupName returns ServiceFilter.SupportGroupName, and is useful for accessing the field via an interface. +func (v *ServiceFilter) GetSupportGroupName() []string { return v.SupportGroupName } + +type ServiceInput struct { + Name string `json:"name"` +} + +// GetName returns ServiceInput.Name, and is useful for accessing the field via an interface. +func (v *ServiceInput) GetName() string { return v.Name } + +// __CreateComponentInstanceInput is used internally by genqlient +type __CreateComponentInstanceInput struct { + Input *ComponentInstanceInput `json:"input,omitempty"` +} + +// GetInput returns __CreateComponentInstanceInput.Input, and is useful for accessing the field via an interface. +func (v *__CreateComponentInstanceInput) GetInput() *ComponentInstanceInput { return v.Input } + +// __CreateComponentVersionInput is used internally by genqlient +type __CreateComponentVersionInput struct { + Input *ComponentVersionInput `json:"input,omitempty"` +} + +// GetInput returns __CreateComponentVersionInput.Input, and is useful for accessing the field via an interface. +func (v *__CreateComponentVersionInput) GetInput() *ComponentVersionInput { return v.Input } + +// __CreateServiceInput is used internally by genqlient +type __CreateServiceInput struct { + Input *ServiceInput `json:"input,omitempty"` +} + +// GetInput returns __CreateServiceInput.Input, and is useful for accessing the field via an interface. +func (v *__CreateServiceInput) GetInput() *ServiceInput { return v.Input } + +// __ListComponentInstancesInput is used internally by genqlient +type __ListComponentInstancesInput struct { + Filter *ComponentInstanceFilter `json:"filter,omitempty"` +} + +// GetFilter returns __ListComponentInstancesInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListComponentInstancesInput) GetFilter() *ComponentInstanceFilter { return v.Filter } + +// __ListComponentVersionsInput is used internally by genqlient +type __ListComponentVersionsInput struct { + Filter *ComponentVersionFilter `json:"filter,omitempty"` +} + +// GetFilter returns __ListComponentVersionsInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListComponentVersionsInput) GetFilter() *ComponentVersionFilter { return v.Filter } + +// __ListComponentsInput is used internally by genqlient +type __ListComponentsInput struct { + Filter *ComponentFilter `json:"filter,omitempty"` +} + +// GetFilter returns __ListComponentsInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListComponentsInput) GetFilter() *ComponentFilter { return v.Filter } + +// __ListServicesInput is used internally by genqlient +type __ListServicesInput struct { + Filter *ServiceFilter `json:"filter,omitempty"` +} + +// GetFilter returns __ListServicesInput.Filter, and is useful for accessing the field via an interface. +func (v *__ListServicesInput) GetFilter() *ServiceFilter { return v.Filter } + +// The query or mutation executed by CreateComponentInstance. +const CreateComponentInstance_Operation = ` +mutation CreateComponentInstance ($input: ComponentInstanceInput!) { + createComponentInstance(input: $input) { + id + ccrn + count + componentVersionId + serviceId + } +} +` + +func CreateComponentInstance( + ctx_ context.Context, + client_ graphql.Client, + input *ComponentInstanceInput, +) (*CreateComponentInstanceResponse, error) { + req_ := &graphql.Request{ + OpName: "CreateComponentInstance", + Query: CreateComponentInstance_Operation, + Variables: &__CreateComponentInstanceInput{ + Input: input, + }, + } + var err_ error + + var data_ CreateComponentInstanceResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by CreateComponentVersion. +const CreateComponentVersion_Operation = ` +mutation CreateComponentVersion ($input: ComponentVersionInput!) { + createComponentVersion(input: $input) { + id + version + componentId + } +} +` + +func CreateComponentVersion( + ctx_ context.Context, + client_ graphql.Client, + input *ComponentVersionInput, +) (*CreateComponentVersionResponse, error) { + req_ := &graphql.Request{ + OpName: "CreateComponentVersion", + Query: CreateComponentVersion_Operation, + Variables: &__CreateComponentVersionInput{ + Input: input, + }, + } + var err_ error + + var data_ CreateComponentVersionResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by CreateService. +const CreateService_Operation = ` +mutation CreateService ($input: ServiceInput!) { + createService(input: $input) { + id + name + } +} +` + +func CreateService( + ctx_ context.Context, + client_ graphql.Client, + input *ServiceInput, +) (*CreateServiceResponse, error) { + req_ := &graphql.Request{ + OpName: "CreateService", + Query: CreateService_Operation, + Variables: &__CreateServiceInput{ + Input: input, + }, + } + var err_ error + + var data_ CreateServiceResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListComponentInstances. +const ListComponentInstances_Operation = ` +query ListComponentInstances ($filter: ComponentInstanceFilter) { + ComponentInstances(filter: $filter) { + totalCount + edges { + node { + id + } + } + } +} +` + +func ListComponentInstances( + ctx_ context.Context, + client_ graphql.Client, + filter *ComponentInstanceFilter, +) (*ListComponentInstancesResponse, error) { + req_ := &graphql.Request{ + OpName: "ListComponentInstances", + Query: ListComponentInstances_Operation, + Variables: &__ListComponentInstancesInput{ + Filter: filter, + }, + } + var err_ error + + var data_ ListComponentInstancesResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListComponentVersions. +const ListComponentVersions_Operation = ` +query ListComponentVersions ($filter: ComponentVersionFilter) { + ComponentVersions(filter: $filter) { + totalCount + edges { + node { + id + version + componentId + } + } + } +} +` + +func ListComponentVersions( + ctx_ context.Context, + client_ graphql.Client, + filter *ComponentVersionFilter, +) (*ListComponentVersionsResponse, error) { + req_ := &graphql.Request{ + OpName: "ListComponentVersions", + Query: ListComponentVersions_Operation, + Variables: &__ListComponentVersionsInput{ + Filter: filter, + }, + } + var err_ error + + var data_ ListComponentVersionsResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListComponents. +const ListComponents_Operation = ` +query ListComponents ($filter: ComponentFilter) { + Components(filter: $filter) { + totalCount + edges { + node { + id + name + type + } + } + } +} +` + +func ListComponents( + ctx_ context.Context, + client_ graphql.Client, + filter *ComponentFilter, +) (*ListComponentsResponse, error) { + req_ := &graphql.Request{ + OpName: "ListComponents", + Query: ListComponents_Operation, + Variables: &__ListComponentsInput{ + Filter: filter, + }, + } + var err_ error + + var data_ ListComponentsResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + +// The query or mutation executed by ListServices. +const ListServices_Operation = ` +query ListServices ($filter: ServiceFilter) { + Services(filter: $filter) { + totalCount + edges { + node { + id + } + } + } +} +` + +func ListServices( + ctx_ context.Context, + client_ graphql.Client, + filter *ServiceFilter, +) (*ListServicesResponse, error) { + req_ := &graphql.Request{ + OpName: "ListServices", + Query: ListServices_Operation, + Variables: &__ListServicesInput{ + Filter: filter, + }, + } + var err_ error + + var data_ ListServicesResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} diff --git a/scanner/k8s-assets/client/genqlient.yaml b/scanner/k8s-assets/client/genqlient.yaml new file mode 100644 index 0000000..abb271a --- /dev/null +++ b/scanner/k8s-assets/client/genqlient.yaml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: ../../../internal/api/graphql/graph/schema/*.graphqls +operations: +- ./query/*.graphql +generated: generated.go +package: client +use_struct_references: true diff --git a/scanner/k8s-assets/client/query/component_query.graphql b/scanner/k8s-assets/client/query/component_query.graphql new file mode 100644 index 0000000..9b52a73 --- /dev/null +++ b/scanner/k8s-assets/client/query/component_query.graphql @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListComponents($filter: ComponentFilter) { + # @genqlient(typename: "ComponentConnection") + Components ( + filter: $filter, + ) { + totalCount + edges { + # @genqlient(typename: "Component") + node { + id + name + type + } + } + } +} diff --git a/scanner/k8s-assets/client/query/componentinstance_create.graphql b/scanner/k8s-assets/client/query/componentinstance_create.graphql new file mode 100644 index 0000000..8ee6112 --- /dev/null +++ b/scanner/k8s-assets/client/query/componentinstance_create.graphql @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation CreateComponentInstance ($input: ComponentInstanceInput!) { + # @genqlient(typename: "ComponentInstance") + createComponentInstance ( + input: $input + ) { + id + ccrn + count + componentVersionId + serviceId + } +} \ No newline at end of file diff --git a/scanner/k8s-assets/client/query/componentinstance_query.graphql b/scanner/k8s-assets/client/query/componentinstance_query.graphql new file mode 100644 index 0000000..38e436b --- /dev/null +++ b/scanner/k8s-assets/client/query/componentinstance_query.graphql @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListComponentInstances ($filter: ComponentInstanceFilter) { + ComponentInstances ( + filter: $filter, + ) { + totalCount + edges { + node { + id + } + } + } +} diff --git a/scanner/k8s-assets/client/query/componentversion_create.graphql b/scanner/k8s-assets/client/query/componentversion_create.graphql new file mode 100644 index 0000000..3f5bb72 --- /dev/null +++ b/scanner/k8s-assets/client/query/componentversion_create.graphql @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation CreateComponentVersion ($input: ComponentVersionInput!) { + createComponentVersion ( + input: $input + ) { + id + version + componentId + } +} diff --git a/scanner/k8s-assets/client/query/componentversion_query.graphql b/scanner/k8s-assets/client/query/componentversion_query.graphql new file mode 100644 index 0000000..5f9288e --- /dev/null +++ b/scanner/k8s-assets/client/query/componentversion_query.graphql @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListComponentVersions($filter: ComponentVersionFilter) { + # @genqlient(typename: "ComponentVersionConnection") + ComponentVersions ( + filter: $filter, + ) { + totalCount + edges { + # @genqlient(typename: "ComponentVersion") + node { + id + version + componentId + } + } + } +} diff --git a/scanner/k8s-assets/client/query/service_create.graphql b/scanner/k8s-assets/client/query/service_create.graphql new file mode 100644 index 0000000..052a4cd --- /dev/null +++ b/scanner/k8s-assets/client/query/service_create.graphql @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +mutation CreateService ($input: ServiceInput!) { + # @genqlient(typename: "Service") + createService ( + input: $input + ) { + id + name + } +} \ No newline at end of file diff --git a/scanner/k8s-assets/client/query/service_query.graphql b/scanner/k8s-assets/client/query/service_query.graphql new file mode 100644 index 0000000..d92ced4 --- /dev/null +++ b/scanner/k8s-assets/client/query/service_query.graphql @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +query ListServices ($filter: ServiceFilter) { + Services ( + filter: $filter, + ) { + totalCount + edges { + node { + id + } + } + } +} diff --git a/scanner/k8s-assets/config/kubeconfig.go b/scanner/k8s-assets/config/kubeconfig.go new file mode 100644 index 0000000..1e4f457 --- /dev/null +++ b/scanner/k8s-assets/config/kubeconfig.go @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "fmt" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// KubeConfigFactory interface +type KubeConfigFactory interface { + CreateConfig() (*rest.Config, error) +} + +// OIDCConfigFactory implements KubeConfigFactory for OIDC-based configs +type OIDCConfigFactory struct { + path string + context string +} + +func NewOIDCConfigFactory(path, context string) *OIDCConfigFactory { + return &OIDCConfigFactory{path: path, context: context} +} + +func (f *OIDCConfigFactory) CreateConfig() (*rest.Config, error) { + config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: f.path}, + &clientcmd.ConfigOverrides{CurrentContext: f.context}, + ).ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to load OIDC kubeconfig: %w", err) + } + return config, nil +} + +// InClusterConfigFactory implements KubeConfigFactory for in-cluster configs +type InClusterConfigFactory struct{} + +func NewInClusterConfigFactory() *InClusterConfigFactory { + return &InClusterConfigFactory{} +} + +func (f *InClusterConfigFactory) CreateConfig() (*rest.Config, error) { + return rest.InClusterConfig() +} + +// DefaultConfigFactory implements KubeConfigFactory for default configs +type DefaultConfigFactory struct { + path string +} + +func NewDefaultConfigFactory(path string) *DefaultConfigFactory { + return &DefaultConfigFactory{path: path} +} + +func (f *DefaultConfigFactory) CreateConfig() (*rest.Config, error) { + config, err := clientcmd.BuildConfigFromFlags("", f.path) + if err != nil { + return nil, fmt.Errorf("failed to load default kubeconfig: %w", err) + } + return config, nil +} + +// createConfigFactory creates the appropriate KubeConfigFactory based on the type and parameters +func createConfigFactory(configType, path, context string) (KubeConfigFactory, error) { + switch configType { + case "oidc": + return NewOIDCConfigFactory(path, context), nil + case "in-cluster": + return NewInClusterConfigFactory(), nil + case "default": + return NewDefaultConfigFactory(path), nil + default: + return nil, fmt.Errorf("unknown KUBECONFIG_TYPE: %s", configType) + } +} + +// getKubeConfig is the main function to get the Kubernetes configuration +func GetKubeConfig(configType, path, context string) (*rest.Config, error) { + factory, err := createConfigFactory(configType, path, context) + if err != nil { + return nil, err + } + return factory.CreateConfig() +} diff --git a/scanner/k8s-assets/go.mod b/scanner/k8s-assets/go.mod index f50dba0..5c5d06f 100644 --- a/scanner/k8s-assets/go.mod +++ b/scanner/k8s-assets/go.mod @@ -1,3 +1,62 @@ module github.com/cloudoperators/heureka/scanners/k8s-assets -go 1.22.6 \ No newline at end of file +go 1.22.6 + +require ( + github.com/Khan/genqlient v0.7.0 + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 + k8s.io/client-go v0.31.0 +) + +require ( + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect + github.com/vektah/gqlparser/v2 v2.5.11 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kelseyhightower/envconfig v1.4.0 + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.31.0 + k8s.io/apimachinery v0.31.0 + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/scanner/k8s-assets/go.sum b/scanner/k8s-assets/go.sum new file mode 100644 index 0000000..d67084e --- /dev/null +++ b/scanner/k8s-assets/go.sum @@ -0,0 +1,168 @@ +github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= +github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= +github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/scanner/k8s-assets/main.go b/scanner/k8s-assets/main.go index 929e04b..43e5a42 100644 --- a/scanner/k8s-assets/main.go +++ b/scanner/k8s-assets/main.go @@ -4,9 +4,204 @@ package main import ( + "context" + "os" + "sync" + "time" "fmt" + + kubeconfig "github.com/cloudoperators/heureka/scanners/k8s-assets/config" + "github.com/cloudoperators/heureka/scanners/k8s-assets/processor" + "github.com/cloudoperators/heureka/scanners/k8s-assets/scanner" + "github.com/kelseyhightower/envconfig" + log "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" + "runtime" ) +type Config struct { + LogLevel string `envconfig:"LOG_LEVEL" default:"debug" required:"true" json:"-"` +} + +type WorkerResult struct { + Namespace string + PodCount int + Error error +} + +func init() { + // Log as JSON instead of the default ASCII formatter. + log.SetFormatter(&log.JSONFormatter{}) + + // Output to stdout instead of the default stderr + // Can be any io.Writer, see below for File example + log.SetOutput(os.Stdout) + + var cfg Config + err := envconfig.Process("heureka", &cfg) + if err != nil { + log.WithError(err).Fatal("Error while reading env config") + } + + level, err := log.ParseLevel(cfg.LogLevel) + + if err != nil { + log.WithError(err).Fatal("Error while parsing log level") + } + + // Only log the warning severity or above. + log.SetLevel(level) +} + +func processNamespace(ctx context.Context, s *scanner.Scanner, p *processor.Processor, namespace string) WorkerResult { + result := WorkerResult{Namespace: namespace} + + pods, err := s.GetPodsByNamespace(namespace, metav1.ListOptions{}) + if err != nil { + result.Error = fmt.Errorf("failed to get pods for namespace %s: %w", namespace, err) + return result + } + + result.PodCount = len(pods) + podReplicas := s.GroupPodsByGenerateName(pods) + + for _, podReplica := range podReplicas { + if len(podReplica.Pods) == 0 { + continue + } + // TODO + serviceInfo := s.GetServiceInfo(podReplica.Pods[0]) + + serviceId, err := p.ProcessService(ctx, namespace, serviceInfo) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "namespace": namespace, + "serviceName": serviceInfo.Name, + }).Error("Failed to process service") + continue + } + + err = p.ProcessPodReplicaSet(ctx, namespace, serviceId, podReplica) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "namespace": namespace, + "serviceName": serviceInfo.Name, + "podName": podReplica.GenerateName, + }).Error("Failed to process pod") + } + } + return result +} + +func processConcurrently(ctx context.Context, s *scanner.Scanner, p *processor.Processor, namespaces []v1.Namespace) { + maxConcurrency := runtime.GOMAXPROCS(0) + + // sem is an unbuffered channel meaning that sending onto it will block + // until there's a corresponding receive operation + sem := make(chan struct{}) + results := make(chan WorkerResult, len(namespaces)) + var wg sync.WaitGroup + + // Start maxConcurrency number of worker goroutines + for i := 0; i < maxConcurrency; i++ { + go func() { + for { + select { + case <-ctx.Done(): + // Context cancelled! + return + case sem <- struct{}{}: + // Go routines will constantly try to send this empty struct to this channel. This will block until + // there is a corresponding receive operation. + } + } + }() + } + + // Process namespaces concurrently (in own Go routine). There can be only "maxConcurrency" worker go routines + // processing data at a given time. Any additional Go routines will be blocked (waiting for a slot to become + // available) + for _, ns := range namespaces { + wg.Add(1) + go func(namespace string) { + defer wg.Done() + <-sem // Wait for an available slot + result := processNamespace(ctx, s, p, namespace) + results <- result + }(ns.Name) + } + + // Close results channel when all goroutines are done + go func() { + wg.Wait() + close(results) + }() + + // Collect and process results + for result := range results { + if result.Error != nil { + log.WithFields(log.Fields{ + "namespace": result.Namespace, + "error": result.Error, + }).Error("Failed to process namespace") + } else { + log.WithFields(log.Fields{ + "namespace": result.Namespace, + "podCount": result.PodCount, + }).Info("Successfully processed namespace") + } + } +} + func main() { - fmt.Println("This is the k8s asset scanner") + var scannerCfg scanner.Config + err := envconfig.Process("heureka", &scannerCfg) + if err != nil { + log.WithFields(log.Fields{ + "errror": err, + }).Warn("Couldn't initialize scanner config") + } + + kubeConfig, err := kubeconfig.GetKubeConfig(scannerCfg.KubeconfigType, scannerCfg.KubeConfigPath, scannerCfg.KubeconfigContext) + if err != nil { + log.WithError(err).Fatal("couldn't load kubeConfig") + } + + // Create k8s client + k8sClient, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + log.WithError(err).Fatal("couldn't create k8sClient") + } + + // Create new k8s scanner + scanner := scanner.NewScanner(kubeConfig, k8sClient, scannerCfg) + + // Create new processor + var cfg processor.Config + err = envconfig.Process("heureka", &cfg) + if err != nil { + log.WithError(err).Fatal("Error while reading env config") + } + processor := processor.NewProcessor(cfg) + + // Create context with timeout (30min should be ok) + scanTimeout, _ := time.ParseDuration(scannerCfg.ScannerTimeout) + ctx, cancel := context.WithTimeout(context.Background(), scanTimeout*time.Minute) + defer cancel() + + // Get namespaces + namespaces, err := scanner.GetNamespaces(metav1.ListOptions{}) + if err != nil { + log.WithError(err).Fatal("no namespaces available") + } + + // Process namespaces concurrently + processConcurrently(ctx, &scanner, processor, namespaces) + + log.Info("Finished processing all namespaces") } diff --git a/scanner/k8s-assets/processor/config.go b/scanner/k8s-assets/processor/config.go new file mode 100644 index 0000000..d45ab10 --- /dev/null +++ b/scanner/k8s-assets/processor/config.go @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package processor + +type Config struct { + HeurekaUrl string `envconfig:"HEUREKA_URL" required:"true" json:"-"` + ClusterName string `envconfig:"HEUREKA_CLUSTER_NAME" required:"true" json:"-"` + RegionName string `envconfig:"HEUREKA_CLUSTER_REGION" required:"true" json:"-"` +} diff --git a/scanner/k8s-assets/processor/processor.go b/scanner/k8s-assets/processor/processor.go new file mode 100644 index 0000000..bfce910 --- /dev/null +++ b/scanner/k8s-assets/processor/processor.go @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package processor + +import ( + "context" + "fmt" + "net/http" + "strings" + + "bytes" + "text/template" + + "github.com/Khan/genqlient/graphql" + "github.com/cloudoperators/heureka/scanners/k8s-assets/client" + "github.com/cloudoperators/heureka/scanners/k8s-assets/scanner" + log "github.com/sirupsen/logrus" +) + +type Processor struct { + Client *graphql.Client + config Config +} + +type CCRN struct { + Region string + Domain string + Project string + Cluster string + Namespace string + Pod string + PodID string + Container string +} + +// UniqueContainerInfo extends scanner.ContainerInfo to represent a unique container +// configuration within a pod replica set, adding a count of occurrences. +// It is used by the CollectUniqueContainers function to aggregate information about +// distinct containers across multiple pods. +type UniqueContainerInfo struct { + scanner.ContainerInfo + PodName string + GenerateName string + Count int +} + +func (c CCRN) String() string { + ccrnTemplate := `rn.cloud.sap/ccrn/kubernetes/v1/{{.Region}}/-/-/-/{{.Cluster}}/{{.Namespace}}/{{.Pod}}/{{.Container}}` + + tmpl, err := template.New("ccrn").Parse(ccrnTemplate) + if err != nil { + return "" + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, c) + if err != nil { + log.Error("Couldn't create CCRN string") + return "" + } + + return buf.String() +} + +func NewProcessor(cfg Config) *Processor { + httpClient := http.Client{} + gClient := graphql.NewClient(cfg.HeurekaUrl, &httpClient) + return &Processor{ + config: cfg, + Client: &gClient, + } +} + +// ProcessService creates a service and processes all its pods +func (p *Processor) ProcessService(ctx context.Context, namespace string, serviceInfo scanner.ServiceInfo) (string, error) { + var serviceId string + + // The Service might already exist in the DB + // Let's try to fetch one Service by name + _serviceId, err := p.getService(ctx, serviceInfo) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "serviceName": serviceInfo.Name, + }).Error("failed to fetch service") + } else { + serviceId = _serviceId + return serviceId, nil + } + + // Create new Service + createServiceInput := &client.ServiceInput{ + Name: serviceInfo.Name, + } + + createServiceResp, err := client.CreateService(ctx, *p.Client, createServiceInput) + if err != nil { + return "", fmt.Errorf("failed to create Service %s: %w", serviceInfo.Name, err) + } else { + serviceId = createServiceResp.CreateService.Id + } + return serviceId, nil +} + +// getService returns (if any) a ServiceID +func (p *Processor) getService(ctx context.Context, serviceInfo scanner.ServiceInfo) (string, error) { + var serviceId string + + listServicesFilter := client.ServiceFilter{ + ServiceName: []string{serviceInfo.Name}, + } + listServicesResp, err := client.ListServices(ctx, *p.Client, &listServicesFilter) + if err != nil { + return "", fmt.Errorf("Couldn't list services") + } + + // Return the first item + if listServicesResp.Services.TotalCount > 0 { + for _, s := range listServicesResp.Services.Edges { + serviceId = s.Node.Id + break + } + } else { + return "", fmt.Errorf("ListServices returned no ServiceID") + } + + return serviceId, nil +} + +// CollectUniqueContainers processes a PodReplicaSet and returns a slice of +// unique container name, image name, and image ID combinations with their +// respective counts across all pods in the replica set. +// +// This function identifies unique combinations based on container name, image +// name, and image ID (referred to as ImageHash in the code). It counts how many +// times each unique combination appears across all pods in the replica set. +// +// Parameters: +// - podReplicaSet: A scanner.PodReplicaSet object representing a group of related pods. +// +// Returns: +// - []UniqueContainerInfo: A slice of UniqueContainerInfo structs. Each struct contains: +// - ContainerInfo: The original container information (Name, Image, ImageHash). +// - Count: The number of times this unique combination appears in the PodReplicaSet. +// +// The returned slice will contain one entry for each unique combination of +// container name, image name, and image ID found in the PodReplicaSet, along +// with a count of its occurrences. +func (p *Processor) CollectUniqueContainers(podReplicaSet scanner.PodReplicaSet) []UniqueContainerInfo { + uniqueContainers := make(map[string]*UniqueContainerInfo) + + for _, pod := range podReplicaSet.Pods { + for _, container := range pod.Containers { + key := fmt.Sprintf("%s-%s", container.Name, container.ImageHash) + if _, exists := uniqueContainers[key]; !exists { + uniqueContainers[key] = &UniqueContainerInfo{ + ContainerInfo: container, + PodName: pod.Name, + GenerateName: strings.TrimSuffix(pod.GenerateName, "-"), + Count: 0, + } + } + uniqueContainers[key].Count++ + } + } + + result := make([]UniqueContainerInfo, 0, len(uniqueContainers)) + for _, container := range uniqueContainers { + result = append(result, *container) + } + + return result +} + +func (p *Processor) ProcessPodReplicaSet(ctx context.Context, namespace string, serviceID string, podReplicaSet scanner.PodReplicaSet) error { + uniqueContainers := p.CollectUniqueContainers(podReplicaSet) + + for _, containerInfo := range uniqueContainers { + if err := p.ProcessContainer(ctx, namespace, serviceID, podReplicaSet.GenerateName, containerInfo); err != nil { + return fmt.Errorf("failed to process container %s in pod replica set %s: %w", containerInfo.Name, podReplicaSet.GenerateName, err) + } + } + return nil +} + +func (p *Processor) getComponentVersion(ctx context.Context, versionHash string) (string, error) { + var componentVersionId string + + listComponentVersionFilter := client.ComponentVersionFilter{ + Version: []string{versionHash}, + } + listCompoVersResp, err := client.ListComponentVersions(ctx, *p.Client, &listComponentVersionFilter) + if err != nil { + return "", fmt.Errorf("Couldn't list ComponentVersion") + } + + if listCompoVersResp.ComponentVersions.TotalCount > 0 { + for _, cv := range listCompoVersResp.ComponentVersions.Edges { + componentVersionId = cv.Node.Id + break + } + } else { + return "", fmt.Errorf("ListComponentVersion returned no ComponentVersion objects") + } + return componentVersionId, nil +} + +// ProcessContainer creates a ComponentVersion and ComponentInstance for a container +func (p *Processor) ProcessContainer( + ctx context.Context, + namespace string, + serviceID string, + podGroupName string, + containerInfo UniqueContainerInfo, +) error { + // Find component version by container image hash + componentVersionId, err := p.getComponentVersion(ctx, containerInfo.ImageHash) + if err != nil { + return fmt.Errorf("Couldn't find ComponentVersion (imageHash: %s): %w", containerInfo.ImageHash, err) + } + + // Create new CCRN + ccrn := CCRN{ + Region: p.config.RegionName, + Domain: "-", // Not used at the moment + Project: "-", // Not used at the moment + Cluster: p.config.ClusterName, + Namespace: namespace, + Pod: containerInfo.GenerateName, + PodID: containerInfo.PodName, + Container: containerInfo.Name, + } + + // Create new ComponentInstance + componentInstanceInput := &client.ComponentInstanceInput{ + Ccrn: ccrn.String(), + Count: containerInfo.Count, + ComponentVersionId: componentVersionId, + ServiceId: serviceID, + } + createCompInstResp, err := client.CreateComponentInstance(ctx, *p.Client, componentInstanceInput) + if err != nil { + return fmt.Errorf("failed to create ComponentInstance: %w", err) + } + componentInstanceID := createCompInstResp.CreateComponentInstance.Id + + // Do logging + log.WithFields(log.Fields{ + "componentVersionID": componentVersionId, + "componentInstanceID": componentInstanceID, + "podGroup": podGroupName, + "container": containerInfo.Name, + "count": containerInfo.Count, + }).Info("Created new entities") + + return nil +} diff --git a/scanner/k8s-assets/processor/processor_suite_test.go b/scanner/k8s-assets/processor/processor_suite_test.go new file mode 100644 index 0000000..8260950 --- /dev/null +++ b/scanner/k8s-assets/processor/processor_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package processor_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestProcessor(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Processor Suite") +} diff --git a/scanner/k8s-assets/processor/processor_test.go b/scanner/k8s-assets/processor/processor_test.go new file mode 100644 index 0000000..bb0eff8 --- /dev/null +++ b/scanner/k8s-assets/processor/processor_test.go @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package processor_test + +import ( + "github.com/cloudoperators/heureka/scanners/k8s-assets/processor" + "github.com/cloudoperators/heureka/scanners/k8s-assets/scanner" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Processor", func() { + var p *processor.Processor + + BeforeEach(func() { + p = &processor.Processor{} + }) + + Describe("CollectUniqueContainers", func() { + Context("with a single pod and single container", func() { + It("should return one unique container", func() { + podReplicaSet := scanner.PodReplicaSet{ + GenerateName: "test-pod", + Pods: []scanner.PodInfo{ + { + Name: "test-pod-1", + Containers: []scanner.ContainerInfo{ + {Name: "container1", Image: "image1", ImageHash: "hash1"}, + }, + }, + }, + } + + uniqueContainers := p.CollectUniqueContainers(podReplicaSet) + Expect(uniqueContainers).To(HaveLen(1)) + Expect(uniqueContainers[0].Name).To(Equal("container1")) + Expect(uniqueContainers[0].ImageHash).To(Equal("hash1")) + Expect(uniqueContainers[0].Count).To(Equal(1)) + }) + }) + + Context("with multiple pods having overlapping containers", func() { + It("should return all unique containers", func() { + podReplicaSet := scanner.PodReplicaSet{ + GenerateName: "test-pod", + Pods: []scanner.PodInfo{ + { + Name: "test-pod-1", + Containers: []scanner.ContainerInfo{ + {Name: "container1", Image: "image1", ImageHash: "hash1"}, + {Name: "container2", Image: "image2", ImageHash: "hash2"}, + }, + }, + { + Name: "test-pod-2", + Containers: []scanner.ContainerInfo{ + {Name: "container1", Image: "image1", ImageHash: "hash1"}, + {Name: "container3", Image: "image3", ImageHash: "hash3"}, + }, + }, + }, + } + + uniqueContainers := p.CollectUniqueContainers(podReplicaSet) + Expect(uniqueContainers).To(HaveLen(3)) + containerNames := []string{uniqueContainers[0].Name, uniqueContainers[1].Name, uniqueContainers[2].Name} + Expect(containerNames).To(ConsistOf("container1", "container2", "container3")) + + // Check counts + for _, c := range uniqueContainers { + if c.Name == "container1" { + Expect(c.Count).To(Equal(2)) + } else { + Expect(c.Count).To(Equal(1)) + } + } + }) + }) + + Context("with multiple pods having different image hashes", func() { + It("should return containers with different image hashes and correct counts", func() { + podReplicaSet := scanner.PodReplicaSet{ + GenerateName: "test-pod", + Pods: []scanner.PodInfo{ + { + Name: "test-pod-1", + Containers: []scanner.ContainerInfo{ + {Name: "container1", Image: "image1", ImageHash: "hash1"}, + }, + }, + { + Name: "test-pod-2", + Containers: []scanner.ContainerInfo{ + {Name: "container1", Image: "image1", ImageHash: "hash1"}, + {Name: "container1", Image: "image1", ImageHash: "hash2"}, + {Name: "container2", Image: "image1", ImageHash: "hash2"}, + {Name: "container3", Image: "image1", ImageHash: "hash2"}, + {Name: "container4", Image: "image1", ImageHash: "hash3"}, + }, + }, + }, + } + uniqueContainers := p.CollectUniqueContainers(podReplicaSet) + Expect(uniqueContainers).To(HaveLen(5)) + + // Helper function to find a container by name and hash + findContainer := func(name, hash string) *processor.UniqueContainerInfo { + for _, c := range uniqueContainers { + if c.Name == name && c.ImageHash == hash { + return &c + } + } + return nil + } + + // Check each unique container + c1 := findContainer("container1", "hash1") + Expect(c1).NotTo(BeNil()) + Expect(c1.Count).To(Equal(2)) + + c2 := findContainer("container1", "hash2") + Expect(c2).NotTo(BeNil()) + Expect(c2.Count).To(Equal(1)) + + c3 := findContainer("container2", "hash2") + Expect(c3).NotTo(BeNil()) + Expect(c3.Count).To(Equal(1)) + + c4 := findContainer("container3", "hash2") + Expect(c4).NotTo(BeNil()) + Expect(c4.Count).To(Equal(1)) + + c5 := findContainer("container4", "hash3") + Expect(c5).NotTo(BeNil()) + Expect(c5.Count).To(Equal(1)) + }) + }) + + Context("with edge cases", func() { + It("should handle empty pods, no containers, and empty replica sets", func() { + emptyPodReplicaSet := scanner.PodReplicaSet{ + GenerateName: "empty-pods", + Pods: []scanner.PodInfo{{}}, + } + result := p.CollectUniqueContainers(emptyPodReplicaSet) + Expect(result).To(BeEmpty()) + + noContainersPodReplicaSet := scanner.PodReplicaSet{ + GenerateName: "no-containers", + Pods: []scanner.PodInfo{ + {Name: "pod1", Containers: []scanner.ContainerInfo{}}, + {Name: "pod2", Containers: []scanner.ContainerInfo{}}, + }, + } + result = p.CollectUniqueContainers(noContainersPodReplicaSet) + Expect(result).To(BeEmpty()) + + emptyReplicaSet := scanner.PodReplicaSet{ + GenerateName: "empty-replica-set", + Pods: []scanner.PodInfo{}, + } + result = p.CollectUniqueContainers(emptyReplicaSet) + Expect(result).To(BeEmpty()) + }) + + It("should handle containers with empty names or image hashes", func() { + podReplicaSet := scanner.PodReplicaSet{ + GenerateName: "empty-fields", + Pods: []scanner.PodInfo{ + {Name: "pod1", Containers: []scanner.ContainerInfo{ + {Name: "", Image: "image1", ImageHash: "hash1"}, + {Name: "container2", Image: "image2", ImageHash: ""}, + }}, + }, + } + result := p.CollectUniqueContainers(podReplicaSet) + Expect(result).To(HaveLen(2)) + Expect(result[0].Count).To(Equal(1)) + Expect(result[1].Count).To(Equal(1)) + }) + + It("should handle when all containers across all pods are identical", func() { + container := scanner.ContainerInfo{Name: "container", Image: "image", ImageHash: "hash"} + podReplicaSet := scanner.PodReplicaSet{ + GenerateName: "all-identical", + Pods: []scanner.PodInfo{ + {Name: "pod1", Containers: []scanner.ContainerInfo{container, container}}, + {Name: "pod2", Containers: []scanner.ContainerInfo{container, container}}, + }, + } + result := p.CollectUniqueContainers(podReplicaSet) + Expect(result).To(HaveLen(1)) + Expect(result[0].Count).To(Equal(4)) + }) + }) + }) +}) diff --git a/scanner/k8s-assets/scanner/config.go b/scanner/k8s-assets/scanner/config.go new file mode 100644 index 0000000..c6162c8 --- /dev/null +++ b/scanner/k8s-assets/scanner/config.go @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package scanner + +type Config struct { + KubeConfigPath string `envconfig:"KUBE_CONFIG_PATH" default:"~/.kube/config" required:"true" json:"-"` + KubeconfigContext string `envconfig:"KUBE_CONFIG_CONTEXT"` + KubeconfigType string `envconfig:"KUBE_CONFIG_TYPE" default:"oidc"` + SupportGroupLabel string `envconfig:"SUPPORT_GROUP_LABEL" default:"ccloud/support-group" required:"true"` + ServiceNameLabel string `envconfig:"SERVICE_NAME_LABEL" default:"ccloud/service" required:"true"` + ScannerTimeout string `envconfig:"SCANNER_TIMEOUT" default:"30"` +} diff --git a/scanner/k8s-assets/scanner/scanner.go b/scanner/k8s-assets/scanner/scanner.go new file mode 100644 index 0000000..00c9782 --- /dev/null +++ b/scanner/k8s-assets/scanner/scanner.go @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package scanner + +import ( + "context" + "fmt" + + "strings" + + log "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// Some usefull structs +type Scanner struct { + Config Config + KubeConfig *rest.Config + ClientSet *kubernetes.Clientset +} + +type ServiceInfo struct { + Name string + SupportGroup string + Pods []PodInfo +} + +type PodReplicaSet struct { + GenerateName string + Pods []PodInfo +} + +type PodInfo struct { + Labels PodLabels + Name string + GenerateName string + Namespace string + UID string + Containers []ContainerInfo +} + +type ContainerInfo struct { + Name string + Image string + ImageHash string +} + +type PodLabels struct { + SupportGroup string + Owner string + ServiceName string +} + +func NewScanner(kubeConfig *rest.Config, clientSet *kubernetes.Clientset, cfg Config) Scanner { + return Scanner{ + KubeConfig: kubeConfig, + ClientSet: clientSet, + Config: cfg, + } +} + +// GetNamespaces fetches all available namespaces for a cluster +func (s *Scanner) GetNamespaces(listOptions metav1.ListOptions) ([]v1.Namespace, error) { + namespaces, err := s.ClientSet.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("couldn't list namespaces: %w", err) + } + return namespaces.Items, nil +} + +// GetRelevantLabels returns only specific/relevant pod labels +func (s *Scanner) GetRelevantLabels(pod v1.Pod) PodLabels { + podLabels := PodLabels{} + for labelName, labelValue := range pod.Labels { + switch labelName { + case s.Config.ServiceNameLabel: + podLabels.ServiceName = labelValue + case s.Config.SupportGroupLabel: + podLabels.SupportGroup = labelValue + default: + continue + } + } + return podLabels +} + +// ParseImageHash extracts the image ID hash (after the @) +func (s *Scanner) ParseImageHash(image string) (string, error) { + parts := strings.Split(image, "@") + if len(parts) != 2 { + return "", fmt.Errorf("image does not contain a manifest hash: %s", image) + } + return parts[1], nil +} + +// fetchImageId fetches the right imageId for a specific container +func (s *Scanner) fetchImageId(pod v1.Pod, container v1.Container) string { + for _, containerStatus := range pod.Status.ContainerStatuses { + if containerStatus.Image == container.Image { + return containerStatus.ImageID + } + } + return "" +} + +func (s *Scanner) GetPodInfo(pod v1.Pod) PodInfo { + podInfo := PodInfo{ + Name: pod.Name, + GenerateName: pod.GenerateName, + UID: string(pod.UID), + Namespace: pod.Namespace, + Labels: s.GetRelevantLabels(pod), + Containers: make([]ContainerInfo, 0, len(pod.Spec.Containers)), + } + + for _, container := range pod.Spec.Containers { + var imageHash, imageId string + + imageId = s.fetchImageId(pod, container) + if imageId == "" { + log.WithFields(log.Fields{ + "containerName": container.Name, + "podName": pod.Name, + "namespace": pod.Namespace, + }).Error("Couldn't find imageId") + } else { + hash, err := s.ParseImageHash(imageId) + if err != nil { + log.WithError(err).Error("Couldn't parse image hash in the image ID") + } + imageHash = hash + } + podInfo.Containers = append(podInfo.Containers, ContainerInfo{ + Name: container.Name, + Image: container.Image, + ImageHash: imageHash, + }) + } + + return podInfo +} + +// GroupPodsByGenerateName will group pod replicas by "GenerateName" +// wll return a list pf PodReplicaSet +func (s *Scanner) GroupPodsByGenerateName(pods []v1.Pod) []PodReplicaSet { + podGroups := make(map[string][]PodInfo) + + for _, pod := range pods { + podInfo := s.GetPodInfo(pod) + key := podInfo.GenerateName + if key == "" { + key = podInfo.Name // Use Name if GenerateName is empty + } + podGroups[key] = append(podGroups[key], podInfo) + } + + result := make([]PodReplicaSet, 0, len(podGroups)) + for generateName, pods := range podGroups { + result = append(result, PodReplicaSet{ + GenerateName: generateName, + Pods: pods, + }) + } + + return result +} + +// GetServiceInfo extracts meta information from a PodInfo object +func (s *Scanner) GetServiceInfo(podInfo PodInfo) ServiceInfo { + return ServiceInfo{ + Name: podInfo.Labels.ServiceName, + SupportGroup: podInfo.Labels.SupportGroup, + } +} + +// GetPodsByNamespace returns a list of pods for a given namespace +func (s *Scanner) GetPodsByNamespace(namespace string, listOptions metav1.ListOptions) ([]v1.Pod, error) { + pods, err := s.ClientSet.CoreV1().Pods(namespace).List(context.Background(), listOptions) + if err != nil { + return nil, fmt.Errorf("couldn't list pods: %w", err) + } + return pods.Items, nil +}