Skip to content

Commit

Permalink
API to use cli-runtime Info objects as source for K8s manifests (#304)
Browse files Browse the repository at this point in the history
* Working with Info objects
* Add test for bad yamls
* Use netpol-analyzer's fsscanner to get Info objs
* Adjust tests to new error types
* lint, documentation, remove dead code
* No manifests is now a critical error - fix test
* Using fsscanner in testing as well
* testing + hardening

Signed-off-by: ZIV NEVO <[email protected]>
  • Loading branch information
zivnevo authored Nov 16, 2023
1 parent 69cf7a3 commit b81abb2
Show file tree
Hide file tree
Showing 12 changed files with 441 additions and 197 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:
- name: Check Docker image - help
run: docker run ${{ steps.build_docker.outputs.digest }} -h
- name: Check Docker image - scan
run: docker run ${{ steps.build_docker.outputs.digest }} -dirpath /
run: docker run --rm -v $PWD/tests/bookinfo:/bookinfo ${{ steps.build_docker.outputs.digest }} -dirpath /bookinfo
17 changes: 14 additions & 3 deletions cmd/nettop/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,15 @@ var (
true,
nil,
},
{
"badYamls",
[][]string{{"bad_yamls"}},
JSONFormat,
true,
[]string{"-v"},
false,
nil,
},
}

currentDir, _ = os.Getwd()
Expand All @@ -211,7 +220,9 @@ func (td *TestDetails) runTest(t *testing.T) {
outFileName, err := getTempOutputFile()
require.Nil(t, err)

err = _main(getTestArgs(td, outFileName))
testArgs := getTestArgs(td, outFileName)
t.Logf("Test args: %v", testArgs)
err = _main(testArgs)

if td.expectError {
require.NotNil(t, err)
Expand Down Expand Up @@ -296,8 +307,8 @@ func compareFiles(expectedFile, actualFile string) (bool, error) {
for i := 0; i < len(expectedLines); i++ {
lineExpected := expectedLines[i]
lineActual := actualLines[i]
if lineExpected != lineActual && !strings.Contains(lineExpected, "\"filepath\"") {
fmt.Printf("Gap in line %d: expected(%s): %s, actual(%s): %s", i, expectedFile, lineExpected, actualFile, lineActual)
if lineExpected != lineActual && !strings.Contains(lineExpected, "filepath") {
fmt.Printf("Gap in line %d:\n expected(%s): %s\n actual(%s): %s\n", i, expectedFile, lineExpected, actualFile, lineActual)
return false, nil
}
}
Expand Down
36 changes: 34 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,61 @@ module github.com/np-guard/cluster-topology-analyzer
go 1.20

require (
github.com/np-guard/netpol-analyzer v1.0.1
github.com/openshift/api v0.0.0-20230502160752-c71432710382
github.com/stretchr/testify v1.8.3
github.com/stretchr/testify v1.8.4
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.28.2
k8s.io/apimachinery v0.28.2
k8s.io/cli-runtime v0.28.2
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.2.4 // 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.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.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/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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/client-go v0.28.2 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
161 changes: 157 additions & 4 deletions go.sum

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions pkg/analyzer/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ SPDX-License-Identifier: Apache-2.0
package analyzer

import (
"bytes"

"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
)

const yamlParseBufferSize = 200

func parseResource[T interface{}](objDataBuf []byte) *T {
reader := bytes.NewReader(objDataBuf)
if reader == nil {
func parseResourceFromInfo[T interface{}](info *resource.Info) *T {
obj, ok := info.Object.(*unstructured.Unstructured)
if !ok {
return nil
}

var rc T
err := yaml.NewYAMLOrJSONDecoder(reader, yamlParseBufferSize).Decode(&rc)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &rc)
if err != nil {
return nil
}
Expand Down
46 changes: 24 additions & 22 deletions pkg/analyzer/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,44 @@ import (
networkv1 "k8s.io/api/networking/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/cli-runtime/pkg/resource"

"github.com/np-guard/cluster-topology-analyzer/pkg/common"
)

// Create a common.Resource object from a k8s Workload object
func ScanK8sWorkloadObject(kind string, objDataBuf []byte) (*common.Resource, error) {
// ScanK8sWorkloadObjectFromInfo creates a common.Resource object from an Info object
func ScanK8sWorkloadObjectFromInfo(info *resource.Info) (*common.Resource, error) {
var podSpecV1 *v1.PodTemplateSpec
var resourceCtx common.Resource
var metaObj metaV1.Object
resourceCtx.Resource.Kind = kind
switch kind { // TODO: handle Pod
resourceCtx.Resource.Kind = info.Object.GetObjectKind().GroupVersionKind().Kind
switch resourceCtx.Resource.Kind { // TODO: handle Pod
case "ReplicaSet":
obj := parseResource[appsv1.ReplicaSet](objDataBuf)
obj := parseResourceFromInfo[appsv1.ReplicaSet](info)
podSpecV1 = &obj.Spec.Template
metaObj = obj
case "ReplicationController":
obj := parseResource[v1.ReplicationController](objDataBuf)
obj := parseResourceFromInfo[v1.ReplicationController](info)
podSpecV1 = obj.Spec.Template
metaObj = obj
case "Deployment":
obj := parseResource[appsv1.Deployment](objDataBuf)
obj := parseResourceFromInfo[appsv1.Deployment](info)
podSpecV1 = &obj.Spec.Template
metaObj = obj
case "DaemonSet":
obj := parseResource[appsv1.DaemonSet](objDataBuf)
obj := parseResourceFromInfo[appsv1.DaemonSet](info)
podSpecV1 = &obj.Spec.Template
metaObj = obj
case "StatefulSet":
obj := parseResource[appsv1.StatefulSet](objDataBuf)
obj := parseResourceFromInfo[appsv1.StatefulSet](info)
podSpecV1 = &obj.Spec.Template
metaObj = obj
case "Job":
obj := parseResource[batchv1.Job](objDataBuf)
obj := parseResourceFromInfo[batchv1.Job](info)
podSpecV1 = &obj.Spec.Template
metaObj = obj
default:
return nil, fmt.Errorf("unsupported object type: `%s`", kind)
return nil, fmt.Errorf("unsupported object type: `%s`", resourceCtx.Resource.Kind)
}

parseDeployResource(podSpecV1, metaObj, &resourceCtx)
Expand All @@ -70,8 +71,9 @@ func matchLabelSelectorToStrLabels(labels map[string]string) []string {
return res
}

func ScanK8sConfigmapObject(objDataBuf []byte) (*common.CfgMap, error) {
obj := parseResource[v1.ConfigMap](objDataBuf)
// ScanK8sConfigmapInfo creates a common.CfgMap object from a k8s ConfigMap object
func ScanK8sConfigmapInfo(info *resource.Info) (*common.CfgMap, error) {
obj := parseResourceFromInfo[v1.ConfigMap](info)
if obj == nil {
return nil, fmt.Errorf("unable to parse configmap")
}
Expand All @@ -80,9 +82,9 @@ func ScanK8sConfigmapObject(objDataBuf []byte) (*common.CfgMap, error) {
return &common.CfgMap{FullName: fullName, Data: obj.Data}, nil
}

// Create a common.Service object from a k8s Service object
func ScanK8sServiceObject(objDataBuf []byte) (*common.Service, error) {
svcObj := parseResource[v1.Service](objDataBuf)
// ScanK8sServiceInfo creates a common.Service object from a k8s Service object
func ScanK8sServiceInfo(info *resource.Info) (*common.Service, error) {
svcObj := parseResourceFromInfo[v1.Service](info)
if svcObj == nil {
return nil, fmt.Errorf("failed to parse Service resource")
}
Expand All @@ -103,9 +105,9 @@ func ScanK8sServiceObject(objDataBuf []byte) (*common.Service, error) {
return &serviceCtx, nil
}

// Scan an OpenShift Route object and mark the services it uses to be exposed inside the cluster
func ScanOCRouteObject(objDataBuf []byte, servicesToExpose common.ServicesToExpose) error {
routeObj := parseResource[ocroutev1.Route](objDataBuf)
// ScanOCRouteObjectFromInfo updates servicesToExpose based on an OpenShift Route object
func ScanOCRouteObjectFromInfo(info *resource.Info, servicesToExpose common.ServicesToExpose) error {
routeObj := parseResourceFromInfo[ocroutev1.Route](info)
if routeObj == nil {
return fmt.Errorf("failed to parse Route resource")
}
Expand All @@ -123,9 +125,9 @@ func ScanOCRouteObject(objDataBuf []byte, servicesToExpose common.ServicesToExpo
return nil
}

// Scan an Ingress object and mark the services it uses to be exposed inside the cluster
func ScanIngressObject(objDataBuf []byte, servicesToExpose common.ServicesToExpose) error {
ingressObj := parseResource[networkv1.Ingress](objDataBuf)
// ScanIngressObjectFromInfo updates servicesToExpose based on an K8s Ingress object
func ScanIngressObjectFromInfo(info *resource.Info, servicesToExpose common.ServicesToExpose) error {
ingressObj := parseResourceFromInfo[networkv1.Ingress](info)
if ingressObj == nil {
return fmt.Errorf("failed to parse Ingress resource")
}
Expand Down
45 changes: 27 additions & 18 deletions pkg/analyzer/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"testing"

"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/resource"

"github.com/np-guard/netpol-analyzer/pkg/netpol/manifests/fsscanner"

"github.com/np-guard/cluster-topology-analyzer/pkg/common"
)
Expand Down Expand Up @@ -35,9 +38,9 @@ func TestNetworkAddressValue(t *testing.T) {
}

func TestScanningSvc(t *testing.T) {
resourceBuf, err := loadResourceAsByteArray([]string{"k8s_guestbook", "frontend-service.yaml"})
resourceInfo, err := loadResourceAsInfo([]string{"k8s_guestbook", "frontend-service.yaml"})
require.Nil(t, err)
res, err := ScanK8sServiceObject(resourceBuf)
res, err := ScanK8sServiceInfo(resourceInfo)
require.Nil(t, err)
require.Equal(t, "frontend", res.Resource.Name)
require.Len(t, res.Resource.Selectors, 2)
Expand All @@ -46,9 +49,9 @@ func TestScanningSvc(t *testing.T) {
}

func TestScanningDeploymentWithArgs(t *testing.T) {
resourceBuf, err := loadResourceAsByteArray([]string{"sockshop", "manifests", "01-carts-dep.yaml"})
resourceInfo, err := loadResourceAsInfo([]string{"sockshop", "manifests", "01-carts-dep.yaml"})
require.Nil(t, err)
res, err := ScanK8sWorkloadObject("Deployment", resourceBuf)
res, err := ScanK8sWorkloadObjectFromInfo(resourceInfo)
require.Nil(t, err)
require.Equal(t, "carts", res.Resource.Name)
require.Len(t, res.Resource.NetworkAddrs, 1)
Expand All @@ -58,19 +61,19 @@ func TestScanningDeploymentWithArgs(t *testing.T) {
}

func TestScanningDeploymentWithEnvs(t *testing.T) {
resourceBuf, err := loadResourceAsByteArray([]string{"k8s_guestbook", "frontend-deployment.yaml"})
resourceInfo, err := loadResourceAsInfo([]string{"k8s_guestbook", "frontend-deployment.yaml"})
require.Nil(t, err)
res, err := ScanK8sWorkloadObject("Deployment", resourceBuf)
res, err := ScanK8sWorkloadObjectFromInfo(resourceInfo)
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"})
resourceInfo, err := loadResourceAsInfo([]string{"acs-security-demos", "frontend", "webapp", "deployment.yaml"})
require.Nil(t, err)
res, err := ScanK8sWorkloadObject("Deployment", resourceBuf)
res, err := ScanK8sWorkloadObjectFromInfo(resourceInfo)
require.Nil(t, err)
require.Equal(t, "webapp", res.Resource.Name)
require.Len(t, res.Resource.ConfigMapRefs, 1)
Expand All @@ -79,45 +82,51 @@ func TestScanningDeploymentWithConfigMapRef(t *testing.T) {
}

func TestScanningReplicaSet(t *testing.T) {
resourceBuf, err := loadResourceAsByteArray([]string{"k8s_guestbook", "redis-leader-deployment.yaml"})
resourceInfo, err := loadResourceAsInfo([]string{"k8s_guestbook", "redis-leader-deployment.yaml"})
require.Nil(t, err)
res, err := ScanK8sWorkloadObject("ReplicaSet", resourceBuf)
res, err := ScanK8sWorkloadObjectFromInfo(resourceInfo)
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"})
resourceInfo, err := loadResourceAsInfo([]string{"qotd", "qotd_usecase.yaml"})
require.Nil(t, err)
res, err := ScanK8sConfigmapObject(resourceBuf)
res, err := ScanK8sConfigmapInfo(resourceInfo)
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"})
resourceInfo, err := loadResourceAsInfo([]string{"bookinfo", "bookinfo-ingress.yaml"})
require.Nil(t, err)
toExpose := common.ServicesToExpose{}
err = ScanIngressObject(resourceBuf, toExpose)
err = ScanIngressObjectFromInfo(resourceInfo, 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"})
resourceInfo, err := loadResourceAsInfo([]string{"acs-security-demos", "frontend", "webapp", "route.yaml"})
require.Nil(t, err)
toExpose := common.ServicesToExpose{}
err = ScanOCRouteObject(resourceBuf, toExpose)
err = ScanOCRouteObjectFromInfo(resourceInfo, toExpose)
require.Nil(t, err)
require.Len(t, toExpose, 1)
}

func loadResourceAsByteArray(resourceDirs []string) ([]byte, error) {
func loadResourceAsInfo(resourceDirs []string) (*resource.Info, error) {
currentDir, _ := os.Getwd()
resourceRelPath := filepath.Join(resourceDirs...)
resourcePath := filepath.Join(currentDir, "..", "..", "tests", resourceRelPath)
return os.ReadFile(resourcePath)

infos, errs := fsscanner.GetResourceInfosFromDirPath([]string{resourcePath}, true, true)
if len(errs) > 0 {
return nil, errs[0]
}

return infos[0], nil
}
Loading

0 comments on commit b81abb2

Please sign in to comment.