Skip to content

Commit

Permalink
fix(validate): get non-namespace scoped resources (#585)
Browse files Browse the repository at this point in the history
* fix(validate): get non-namespace scoped resources

* fix(validate): alternate cluster-scoped resource get, schema mods

* fix: updated conditional statement

* docs: updated k8s domain doc
  • Loading branch information
meganwolf0 authored Aug 9, 2024
1 parent 5e75714 commit a5b8857
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 45 deletions.
Binary file added __debug_bin2095146582
Binary file not shown.
27 changes: 25 additions & 2 deletions docs/reference/domains/kubernetes-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ domain:
group: # Optional - empty or "" for core group
version: v1 # Required - Version of resource
resource: pods # Required - Resource type (API-recognized type, not Kind)
namespaces: [validation-test] # Optional - Namespaces to validate the above resources in. Empty or "" for all namespace pr non-namespaced resources
namespaces: [validation-test] # Optional - Namespaces to validate the above resources in. Empty or "" for all namespace or non-namespaced resources
field: # Optional - Field to grab in a resource if it is in an unusable type, e.g., string json data. Must specify named resource to use.
jsonpath: # Required - Jsonpath specifier of where to find the field from the top level object
type: # Optional - Accepts "json" or "yaml". Default is "json".
Expand Down Expand Up @@ -68,7 +68,7 @@ domain:

## Lists vs Named Resource

When Lula retrieves all targeted resources (bounded by namespace when applicable), the payload is a list of resources. When a resource Name is specified - with a target Namespace - the payload will be a single object.
When Lula retrieves all targeted resources (bounded by namespace when applicable), the payload is a list of resources. When a resource Name is specified - the payload will be a single object.

### Example

Expand Down Expand Up @@ -132,6 +132,29 @@ provider:
> [!IMPORTANT]
> Note how the rego now evaluates a single object called `podvt`. This is the name of the resource that is being validated.

We can also retrieve a single cluster-scoped resource as follows, where the rego evaluates a single object called `namespaceVt`.

```yaml
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: namespaceVt
resource-rule:
name: validation-test
version: v1
resource: namespaces
provider:
type: opa
opa-spec:
rego: |
package validate
validate {
input.namespaceVt.metadata.name == "validation-test"
}
```

## Extracting Resource Field Data
Many of the tool-specific configuration data is stored as json or yaml text inside configmaps and secrets. Some valuable data may also be stored in json or yaml strings in other resource locations, such as annotations. The `field` parameter of the `resource-rule` allows this data to be extracted and used by the Rego.

Expand Down
17 changes: 12 additions & 5 deletions src/pkg/common/schemas/validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,18 @@
"description": "Resource type (API-recognized type, not Kind)"
},
"namespaces": {
"type": "array",
"items": {
"type": "string"
},
"description": "Namespaces to validate the above resources in. Empty or \"\" for all namespace or non-namespaced resources. Required if name is specified"
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Namespaces to validate the above resources in. Empty or \"\" for all namespace or non-namespaced resources. Required if name is specified for namespaced resources"
},
"field": {
"$ref": "#/definitions/field"
Expand Down
66 changes: 28 additions & 38 deletions src/pkg/domains/kubernetes/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,38 +66,40 @@ func GetResourcesDynamically(ctx context.Context,
}
collection := make([]map[string]interface{}, 0)

namespaces := []string{""}
if len(resource.Namespaces) != 0 {
namespaces = resource.Namespaces
}
for _, namespace := range namespaces {
list, err := dynamic.Resource(resourceId).Namespace(namespace).
List(ctx, metav1.ListOptions{})

// Depending on resource-rule, either a single item or list of items will be appended to collection
namespaces := resource.Namespaces
if len(namespaces) == 0 {
namespaces = []string{""}
}

if resource.Name != "" && len(namespaces) > 1 {
return nil, fmt.Errorf("named resource requested cannot be returned from multiple namespaces")
} else if resource.Name != "" {
// Extracting named resources can only occur here
var itemObj *unstructured.Unstructured
itemObj, err := dynamic.Resource(resourceId).Namespace(namespaces[0]).Get(ctx, resource.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
item := itemObj.Object

// Reduce if named resource
if resource.Name != "" {
// requires single specified namespace
if len(resource.Namespaces) == 1 {
item, err := reduceByName(resource.Name, list.Items)
if err != nil {
return nil, err
}
// If field is specified, get the field data
if resource.Field != nil && resource.Field.Jsonpath != "" {
item, err = getFieldValue(item, resource.Field)
if err != nil {
return nil, err
}
}

collection = append(collection, item)
// If field is specified, get the field data; can only occur when a single named resource is specified
if resource.Field != nil && resource.Field.Jsonpath != "" {
item, err = getFieldValue(item, resource.Field)
if err != nil {
return nil, err
}
}

collection = append(collection, item)
} else {
for _, namespace := range namespaces {
list, err := dynamic.Resource(resourceId).Namespace(namespace).
List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}

} else {
for _, item := range list.Items {
collection = append(collection, item.Object)
}
Expand Down Expand Up @@ -142,18 +144,6 @@ func getGroupVersionResource(kind string) (gvr *schema.GroupVersionResource, err
return nil, fmt.Errorf("kind %s not found", kind)
}

// reduceByName() takes a name and loops over all items to return the first match
func reduceByName(name string, items []unstructured.Unstructured) (map[string]interface{}, error) {

for _, item := range items {
if item.GetName() == name {
return item.Object, nil
}
}

return nil, fmt.Errorf("no resource found with name %s", name)
}

// getFieldValue() looks up the field from a resource and returns a map[string]interface{} representation of the data
func getFieldValue(item map[string]interface{}, field *Field) (map[string]interface{}, error) {
if field == nil {
Expand Down
62 changes: 62 additions & 0 deletions src/test/e2e/scenarios/pod-label/oscal-component-all-bad.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ component-definition:
- href: "#3d170c49-41a7-4677-9b3f-xxxxxxxxxxxx"
rel: lula
text: Href does not exist
- uuid: 7c73052c-0764-4063-a6f1-65db773a8e3e
control-id: ID-7
description: >-
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
links:
- href: "#3d0c3020-7e92-47c1-944e-5667e68fdef8"
rel: lula
text: Named resource, namespace-scoped, no namespace
- uuid: 4dac6c27-db71-441a-8704-26b0d313ffa4
control-id: ID-8
description: >-
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
links:
- href: "#97d41576-66c5-448e-a0b1-040020be89a0"
rel: lula
text: Named resource, cluster-scoped, with namespace
back-matter:
resources:
- uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C
Expand Down Expand Up @@ -276,4 +296,46 @@ component-definition:
package validate
default validate = false
- uuid: 3d0c3020-7e92-47c1-944e-5667e68fdef8
description: |
metadata:
name: Validate pods with label foo=bar
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
name: test-pod-label
group:
version: v1
resource: pods
provider:
type: opa
opa-spec:
rego: |
package validate
default validate = false
- uuid: 97d41576-66c5-448e-a0b1-040020be89a0
description: |
metadata:
name: Validate pods with label foo=bar
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
name: validation-test
group:
version: v1
resource: namespaces
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: |
package validate
default validate = false
6 changes: 6 additions & 0 deletions src/test/e2e/scenarios/resource-data/oscal-component.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ component-definition:
field:
jsonpath: .metadata.annotations.annotation.io/simple
type: json
- name: validationTestNamespace
resource-rule:
name: validation-test
version: v1
resource: namespaces
provider:
type: opa
opa-spec:
Expand All @@ -99,4 +104,5 @@ component-definition:
input.yamlcm.logging.level == "INFO"
input.secret.username == "username"
"item1" in input.pod.items
input.validationTestNamespace.metadata.name == "validation-test"
}

0 comments on commit a5b8857

Please sign in to comment.