Skip to content

Commit

Permalink
Add gatekeeper mutation edges (#377)
Browse files Browse the repository at this point in the history
Ref: https://issues.redhat.com/browse/ACM-16115

Signed-off-by: yiraeChristineKim <yikim@redhat.com>
  • Loading branch information
yiraeChristineKim authored Jan 13, 2025
1 parent de1383e commit 80423fc
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 1 deletion.
47 changes: 47 additions & 0 deletions pkg/transforms/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,21 @@ func transformCommon(resource v1.Object) Node {
Properties: commonProperties(resource),
Metadata: make(map[string]string),
}

// When a resource is mutated by Gatekeeper, add this annotation
mutation, ok := resource.GetAnnotations()["gatekeeper.sh/mutations"]
if ok {
n.Metadata["gatekeeper.sh/mutations"] = mutation
}

n.Metadata["OwnerUID"] = ownerRefUID(resource.GetOwnerReferences())
// Adding OwnerReleaseName and Namespace to resources that doesn't have ownerRef but are deployed by a release.
if n.Metadata["OwnerUID"] == "" && resource.GetAnnotations()["meta.helm.sh/release-name"] != "" &&
resource.GetAnnotations()["meta.helm.sh/release-namespace"] != "" {
n.Metadata["OwnerReleaseName"] = resource.GetAnnotations()["meta.helm.sh/release-name"]
n.Metadata["OwnerReleaseNamespace"] = resource.GetAnnotations()["meta.helm.sh/release-namespace"]
}

return n
}

Expand Down Expand Up @@ -159,6 +167,8 @@ func CommonEdges(uid string, ns NodeStore) []Edge {

ret = edgesByKyverno(ret, currNode, ns)

ret = edgesByGatekeeperMutation(ret, currNode, ns)

return ret
}

Expand Down Expand Up @@ -206,6 +216,43 @@ func edgesByKyverno(ret []Edge, currNode Node, ns NodeStore) []Edge {
return ret
}

// Function to create an edge linking a resource to Gatekeeper mutations (e.g., Assign, AssignImage) that modify the resource.
func edgesByGatekeeperMutation(ret []Edge, currNode Node, ns NodeStore) []Edge {
mutationEntries, ok := currNode.Metadata["gatekeeper.sh/mutations"]
if !ok || mutationEntries == "" {
return ret
}

for _, kindNsName := range strings.Split(mutationEntries, ", ") {
parts := strings.Split(kindNsName, "/")
if len(parts) != 3 {
continue // Skip invalid entries that don't follow the "kind/namespace/name" format
}

mutationNs := parts[1]
if parts[1] == "" {
mutationNs = "_NONE"
}

// Extract the mutation name (ignoring any suffix after ':').
mutationName := strings.Split(parts[2], ":")[0]
mutationNode, ok := ns.ByKindNamespaceName[parts[0]][mutationNs][mutationName]
if !ok {
continue
}

ret = append(ret, Edge{
SourceKind: currNode.Properties["kind"].(string),
SourceUID: currNode.UID,
EdgeType: "mutatedBy",
DestUID: mutationNode.UID,
DestKind: mutationNode.Properties["kind"].(string),
})
}

return ret
}

// Prefixes the given UID with the cluster name from config and a /
func prefixedUID(uid apiTypes.UID) string {
return strings.Join([]string{config.Cfg.ClusterName, string(uid)}, "/")
Expand Down
99 changes: 99 additions & 0 deletions pkg/transforms/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,102 @@ func TestKyvernoPolicyEdges(t *testing.T) {

AssertDeepEqual("edge", edge, expectedEdge, t)
}

func TestGatekeeperMutationEdges(t *testing.T) {
// For testing common resource
pod := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "my-pod",
"uid": "2222",
"namespace": "test3",
"annotations": map[string]interface{}{
"gatekeeper.sh/mutations": "Assign//mutation-configmap:1, AssignMetadata/hello/mutation-meta-configmap:1",
},
},
}}

// For testing generic resource
configmap := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "zk-kafka-address",
"uid": "1111",
"namespace": "test2",
"annotations": map[string]interface{}{
"gatekeeper.sh/mutations": "Assign//mutation-configmap:1, AssignMetadata/hello/mutation-meta-configmap:1",
},
},
}}

// Case: Namespace is "default"
assign := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mutations.gatekeeper.sh/v1",
"kind": "Assign",
"metadata": map[string]interface{}{
"name": "mutation-configmap",
"uid": "683aaeb0-78b9-4d44-a737-59d621bc71f0",
},
},
}

assignMeta := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mutations.gatekeeper.sh/v1",
"kind": "AssignMetadata",
"metadata": map[string]interface{}{
"name": "mutation-meta-configmap",
"namespace": "hello",
"uid": "bc09fa5f-8ef7-4cd1-833a-4c79c26b03f3",
},
},
}

nodes := []Node{
GenericResourceBuilder(assign).BuildNode(),
GenericResourceBuilder(assignMeta).BuildNode(),
GenericResourceBuilder(configmap).BuildNode(),
GenericResourceBuilder(pod).BuildNode(),
}
nodeStore := BuildFakeNodeStore(nodes)

t.Log("Test Gatekeeper mutation")
configMapEdges := CommonEdges("local-cluster/1111", nodeStore)
if len(configMapEdges) != 2 {
t.Fatalf("Expected 2 edge but got %d", len(configMapEdges))
}

podEdges := CommonEdges("local-cluster/1111", nodeStore)
if len(configMapEdges) != 2 {
t.Fatalf("Expected 2 edge but got %d", len(configMapEdges))
}

edge := configMapEdges[0]
edge2 := podEdges[0]
expectedEdge := Edge{
EdgeType: "mutatedBy",
SourceUID: "local-cluster/1111",
DestUID: "local-cluster/683aaeb0-78b9-4d44-a737-59d621bc71f0",
SourceKind: "ConfigMap",
DestKind: "Assign",
}

AssertDeepEqual("edge", edge, expectedEdge, t)
AssertDeepEqual("edge", edge2, expectedEdge, t)

edge = configMapEdges[1]
edge2 = podEdges[1]
expectedEdge = Edge{
EdgeType: "mutatedBy",
SourceUID: "local-cluster/1111",
DestUID: "local-cluster/bc09fa5f-8ef7-4cd1-833a-4c79c26b03f3",
SourceKind: "ConfigMap",
DestKind: "AssignMetadata",
}

AssertDeepEqual("edge", edge, expectedEdge, t)
AssertDeepEqual("edge", edge2, expectedEdge, t)
}
8 changes: 7 additions & 1 deletion pkg/transforms/genericresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,18 @@ func genericProperties(r *unstructured.Unstructured) map[string]interface{} {
if r.GetAnnotations()["apps.open-cluster-management.io/hosting-deployable"] != "" {
ret["_hostingDeployable"] = r.GetAnnotations()["apps.open-cluster-management.io/hosting-deployable"]
}
return ret

return ret
}

func genericMetadata(r *unstructured.Unstructured) map[string]string {
metadata := make(map[string]string)
// When a resource is mutated by Gatekeeper, add this annotation
mutation, ok := r.GetAnnotations()["gatekeeper.sh/mutations"]
if ok {
metadata["gatekeeper.sh/mutations"] = mutation
}

metadata["OwnerUID"] = ownerRefUID(r.GetOwnerReferences())
// Adds OwnerReleaseName and Namespace to resources that don't have ownerRef, but are deployed by a release.
if metadata["OwnerUID"] == "" && r.GetAnnotations()["meta.helm.sh/release-name"] != "" &&
Expand Down

0 comments on commit 80423fc

Please sign in to comment.