diff --git a/pkg/analyzer/resources.go b/pkg/analyzer/resources.go index 96447d7..fee1e17 100644 --- a/pkg/analyzer/resources.go +++ b/pkg/analyzer/resources.go @@ -7,19 +7,20 @@ SPDX-License-Identifier: Apache-2.0 package analyzer import ( - "io" + "bytes" "k8s.io/apimachinery/pkg/util/yaml" ) const yamlParseBufferSize = 200 -func parseResource[T interface{}](r io.Reader) *T { - if r == nil { +func parseResource[T interface{}](objDataBuf []byte) *T { + reader := bytes.NewReader(objDataBuf) + if reader == nil { return nil } var rc T - err := yaml.NewYAMLOrJSONDecoder(r, yamlParseBufferSize).Decode(&rc) + err := yaml.NewYAMLOrJSONDecoder(reader, yamlParseBufferSize).Decode(&rc) if err != nil { return nil } diff --git a/pkg/analyzer/scan.go b/pkg/analyzer/scan.go index 125c4a3..a159ac3 100644 --- a/pkg/analyzer/scan.go +++ b/pkg/analyzer/scan.go @@ -7,7 +7,6 @@ SPDX-License-Identifier: Apache-2.0 package analyzer import ( - "bytes" "fmt" "net/url" "strconv" @@ -32,27 +31,27 @@ func ScanK8sWorkloadObject(kind string, objDataBuf []byte) (*common.Resource, er resourceCtx.Resource.Kind = kind switch kind { // TODO: handle Pod case "ReplicaSet": - obj := parseResource[appsv1.ReplicaSet](bytes.NewReader(objDataBuf)) + obj := parseResource[appsv1.ReplicaSet](objDataBuf) podSpecV1 = &obj.Spec.Template metaObj = obj case "ReplicationController": - obj := parseResource[v1.ReplicationController](bytes.NewReader(objDataBuf)) + obj := parseResource[v1.ReplicationController](objDataBuf) podSpecV1 = obj.Spec.Template metaObj = obj case "Deployment": - obj := parseResource[appsv1.Deployment](bytes.NewReader(objDataBuf)) + obj := parseResource[appsv1.Deployment](objDataBuf) podSpecV1 = &obj.Spec.Template metaObj = obj case "DaemonSet": - obj := parseResource[appsv1.DaemonSet](bytes.NewReader(objDataBuf)) + obj := parseResource[appsv1.DaemonSet](objDataBuf) podSpecV1 = &obj.Spec.Template metaObj = obj case "StatefulSet": - obj := parseResource[appsv1.StatefulSet](bytes.NewReader(objDataBuf)) + obj := parseResource[appsv1.StatefulSet](objDataBuf) podSpecV1 = &obj.Spec.Template metaObj = obj case "Job": - obj := parseResource[batchv1.Job](bytes.NewReader(objDataBuf)) + obj := parseResource[batchv1.Job](objDataBuf) podSpecV1 = &obj.Spec.Template metaObj = obj default: @@ -71,8 +70,8 @@ func matchLabelSelectorToStrLabels(labels map[string]string) []string { return res } -func ScanK8sConfigmapObject(kind string, objDataBuf []byte) (*common.CfgMap, error) { - obj := parseResource[v1.ConfigMap](bytes.NewReader(objDataBuf)) +func ScanK8sConfigmapObject(objDataBuf []byte) (*common.CfgMap, error) { + obj := parseResource[v1.ConfigMap](objDataBuf) if obj == nil { return nil, fmt.Errorf("unable to parse configmap") } @@ -82,19 +81,15 @@ func ScanK8sConfigmapObject(kind string, objDataBuf []byte) (*common.CfgMap, err } // Create a common.Service object from a k8s Service object -func ScanK8sServiceObject(kind string, objDataBuf []byte) (*common.Service, error) { - if kind != "Service" { - return nil, fmt.Errorf("expected parsing a Service resource, but got `%s`", kind) - } - - svcObj := parseResource[v1.Service](bytes.NewReader(objDataBuf)) +func ScanK8sServiceObject(objDataBuf []byte) (*common.Service, error) { + svcObj := parseResource[v1.Service](objDataBuf) if svcObj == nil { return nil, fmt.Errorf("failed to parse Service resource") } var serviceCtx common.Service serviceCtx.Resource.Name = svcObj.GetName() serviceCtx.Resource.Namespace = svcObj.Namespace - serviceCtx.Resource.Kind = kind + serviceCtx.Resource.Kind = svcObj.Kind serviceCtx.Resource.Type = svcObj.Spec.Type serviceCtx.Resource.Selectors = matchLabelSelectorToStrLabels(svcObj.Spec.Selector) serviceCtx.Resource.ExposeExternally = (svcObj.Spec.Type == v1.ServiceTypeLoadBalancer || svcObj.Spec.Type == v1.ServiceTypeNodePort) @@ -109,12 +104,8 @@ func ScanK8sServiceObject(kind string, objDataBuf []byte) (*common.Service, erro } // Scan an OpenShift Route object and mark the services it uses to be exposed inside the cluster -func ScanOCRouteObject(kind string, objDataBuf []byte, servicesToExpose common.ServicesToExpose) error { - if kind != "Route" { - return fmt.Errorf("expected parsing a Route resource, but got `%s`", kind) - } - - routeObj := parseResource[ocroutev1.Route](bytes.NewReader(objDataBuf)) +func ScanOCRouteObject(objDataBuf []byte, servicesToExpose common.ServicesToExpose) error { + routeObj := parseResource[ocroutev1.Route](objDataBuf) if routeObj == nil { return fmt.Errorf("failed to parse Route resource") } @@ -133,12 +124,8 @@ func ScanOCRouteObject(kind string, objDataBuf []byte, servicesToExpose common.S } // Scan an Ingress object and mark the services it uses to be exposed inside the cluster -func ScanIngressObject(kind string, objDataBuf []byte, servicesToExpose common.ServicesToExpose) error { - if kind != "Ingress" { - return fmt.Errorf("expected parsing a Ingress resource, but got `%s`", kind) - } - - ingressObj := parseResource[networkv1.Ingress](bytes.NewReader(objDataBuf)) +func ScanIngressObject(objDataBuf []byte, servicesToExpose common.ServicesToExpose) error { + ingressObj := parseResource[networkv1.Ingress](objDataBuf) if ingressObj == nil { return fmt.Errorf("failed to parse Ingress resource") } diff --git a/pkg/analyzer/scan_test.go b/pkg/analyzer/scan_test.go new file mode 100644 index 0000000..223e7c4 --- /dev/null +++ b/pkg/analyzer/scan_test.go @@ -0,0 +1,123 @@ +package analyzer + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/np-guard/cluster-topology-analyzer/pkg/common" +) + +func TestNetworkAddressValue(t *testing.T) { + type strBoolPair struct { + str string + b bool + } + + valuesToCheck := map[string]strBoolPair{ + "svc": {"svc", true}, + "svc:500": {"svc:500", true}, + "http://svc:500": {"svc:500", true}, + "fttps://svc:500/something#abc": {"svc:500", true}, + strings.Repeat("abc", 500): {"", false}, + "not%a*url": {"", false}, + "123": {"", false}, + } + + for val, expectedAnswer := range valuesToCheck { + strRes, boolRes := NetworkAddressValue(val) + require.Equal(t, expectedAnswer.b, boolRes) + require.Equal(t, expectedAnswer.str, strRes) + } +} + +func TestScanningSvc(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"k8s_guestbook", "frontend-service.yaml"}) + require.Nil(t, err) + res, err := ScanK8sServiceObject(resourceBuf) + require.Nil(t, err) + require.Equal(t, "frontend", res.Resource.Name) + require.Len(t, res.Resource.Selectors, 2) + require.Len(t, res.Resource.Network, 1) + require.Equal(t, 80, res.Resource.Network[0].Port) +} + +func TestScanningDeploymentWithArgs(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"sockshop", "manifests", "01-carts-dep.yaml"}) + require.Nil(t, err) + res, err := ScanK8sWorkloadObject("Deployment", resourceBuf) + require.Nil(t, err) + require.Equal(t, "carts", res.Resource.Name) + require.Len(t, res.Resource.NetworkAddrs, 1) + require.Equal(t, "carts-db:27017", res.Resource.NetworkAddrs[0]) + require.Len(t, res.Resource.Labels, 1) + require.Equal(t, "carts", res.Resource.Labels["name"]) +} + +func TestScanningDeploymentWithEnvs(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"k8s_guestbook", "frontend-deployment.yaml"}) + require.Nil(t, err) + res, err := ScanK8sWorkloadObject("Deployment", resourceBuf) + require.Nil(t, err) + require.Equal(t, "frontend", res.Resource.Name) + require.Len(t, res.Resource.NetworkAddrs, 4) + require.Len(t, res.Resource.Labels, 2) +} + +func TestScanningDeploymentWithConfigMapRef(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"acs-security-demos", "frontend", "webapp", "deployment.yaml"}) + require.Nil(t, err) + res, err := ScanK8sWorkloadObject("Deployment", resourceBuf) + require.Nil(t, err) + require.Equal(t, "webapp", res.Resource.Name) + require.Len(t, res.Resource.ConfigMapRefs, 1) + require.Empty(t, res.Resource.NetworkAddrs) // extracting network addresses from configmaps happens later + require.Len(t, res.Resource.Labels, 1) +} + +func TestScanningReplicaSet(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"k8s_guestbook", "redis-leader-deployment.yaml"}) + require.Nil(t, err) + res, err := ScanK8sWorkloadObject("ReplicaSet", resourceBuf) + require.Nil(t, err) + require.Equal(t, "redis-leader", res.Resource.Name) + require.Len(t, res.Resource.NetworkAddrs, 0) + require.Len(t, res.Resource.Labels, 3) +} + +func TestScanningConfigMap(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"qotd", "qotd_usecase.yaml"}) + require.Nil(t, err) + res, err := ScanK8sConfigmapObject(resourceBuf) + require.Nil(t, err) + require.Equal(t, res.FullName, "qotd-load/qotd-usecase-library") + require.Len(t, res.Data, 5) +} + +func TestScanningIngress(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"bookinfo", "bookinfo-ingress.yaml"}) + require.Nil(t, err) + toExpose := common.ServicesToExpose{} + err = ScanIngressObject(resourceBuf, toExpose) + require.Nil(t, err) + require.Len(t, toExpose, 1) +} + +func TestScanningRoute(t *testing.T) { + resourceBuf, err := loadResourceAsByteArray([]string{"acs-security-demos", "frontend", "webapp", "route.yaml"}) + require.Nil(t, err) + toExpose := common.ServicesToExpose{} + err = ScanOCRouteObject(resourceBuf, toExpose) + require.Nil(t, err) + require.Len(t, toExpose, 1) +} + +func loadResourceAsByteArray(resourceDirs []string) ([]byte, error) { + currentDir, _ := os.Getwd() + resourceRelPath := filepath.Join(resourceDirs...) + resourcePath := filepath.Join(currentDir, "..", "..", "tests", resourceRelPath) + return os.ReadFile(resourcePath) +} diff --git a/pkg/controller/resource_finder.go b/pkg/controller/resource_finder.go index 4b739e5..4fa79fd 100644 --- a/pkg/controller/resource_finder.go +++ b/pkg/controller/resource_finder.go @@ -186,24 +186,24 @@ func (rf *resourceFinder) parseK8sYaml(mfp, relMfp string) []FileProcessingError func (rf *resourceFinder) parseResource(kind string, yamlDoc []byte, manifestFilePath string) error { switch kind { case service: - res, err := analyzer.ScanK8sServiceObject(kind, yamlDoc) + res, err := analyzer.ScanK8sServiceObject(yamlDoc) if err != nil { return err } res.Resource.FilePath = manifestFilePath rf.services = append(rf.services, res) case route: - err := analyzer.ScanOCRouteObject(kind, yamlDoc, rf.servicesToExpose) + err := analyzer.ScanOCRouteObject(yamlDoc, rf.servicesToExpose) if err != nil { return err } case ingress: - err := analyzer.ScanIngressObject(kind, yamlDoc, rf.servicesToExpose) + err := analyzer.ScanIngressObject(yamlDoc, rf.servicesToExpose) if err != nil { return err } case configmap: - res, err := analyzer.ScanK8sConfigmapObject(kind, yamlDoc) + res, err := analyzer.ScanK8sConfigmapObject(yamlDoc) if err != nil { return err }