Skip to content

Commit be45519

Browse files
authored
Add target PodTemplate which exposes the full Pod (not only the spec) (#801)
* Add `target PodTemplate` which exposes the full Pod (not only the spec) * Fix PotTemplate in conjunction with how pod-schema-checks are handled * Add test for GO template `Polaris` sub-keys, help `NewGenericResourceFromPod` to set `PodTemplate` in more cases * Clarify PldTemplate logic for `IsActionable()`
1 parent ccaa384 commit be45519

File tree

5 files changed

+118
-6
lines changed

5 files changed

+118
-6
lines changed

pkg/config/schema.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ const (
4141
TargetContainer TargetKind = "Container"
4242
// TargetPodSpec points to the pod spec
4343
TargetPodSpec TargetKind = "PodSpec"
44+
// TargetPodTemplate points to the pod template
45+
TargetPodTemplate TargetKind = "PodTemplate"
4446
)
4547

4648
// HandledTargets is a list of target names that are explicitly handled
4749
var HandledTargets = []TargetKind{
4850
TargetController,
4951
TargetContainer,
5052
TargetPodSpec,
53+
TargetPodTemplate,
5154
}
5255

5356
// MutationComment is the comments added to a mutated file
@@ -258,6 +261,11 @@ func (check SchemaCheck) CheckPodSpec(pod *corev1.PodSpec) (bool, []jsonschema.V
258261
return check.CheckObject(pod)
259262
}
260263

264+
// CheckPodTemplate checks a pod template against the schema
265+
func (check SchemaCheck) CheckPodTemplate(podTemplate interface{}) (bool, []jsonschema.ValError, error) {
266+
return check.CheckObject(podTemplate)
267+
}
268+
261269
// CheckController checks a controler's spec against the schema
262270
func (check SchemaCheck) CheckController(bytes []byte) (bool, []jsonschema.ValError, error) {
263271
errs, err := check.Validator.ValidateBytes(bytes)
@@ -304,6 +312,11 @@ func (check SchemaCheck) CheckAdditionalObjects(groupkind string, objects []inte
304312
// IsActionable decides if this check applies to a particular target
305313
func (check SchemaCheck) IsActionable(target TargetKind, kind string, isInit bool) bool {
306314
if funk.Contains(HandledTargets, target) {
315+
if check.Target == TargetPodTemplate && target == TargetPodSpec {
316+
// A target=PodSpec and check.Target=PodTemplate is expected
317+
// because applyPodSchemaChecks() explicitly sets check.Target
318+
return true
319+
}
307320
if check.Target != target {
308321
return false
309322
}

pkg/kube/resource.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type GenericResource struct {
3535
ObjectMeta kubeAPIMetaV1.Object
3636
Resource unstructured.Unstructured
3737
PodSpec *kubeAPICoreV1.PodSpec
38+
PodTemplate interface{}
3839
OriginalObjectJSON []byte
3940
}
4041

@@ -53,6 +54,7 @@ func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecM
5354
return workload, err
5455
}
5556
workload.ObjectMeta = objMeta
57+
workload.PodTemplate = GetPodTemplate(unst.UnstructuredContent())
5658

5759
b, err := json.Marshal(&unst)
5860
if err != nil {
@@ -84,10 +86,15 @@ func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecM
8486

8587
// NewGenericResourceFromPod builds a new workload for a given Pod without looking at parents
8688
func NewGenericResourceFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericResource, error) {
89+
podMap, err := SerializePod(&podResource)
90+
if err != nil {
91+
return GenericResource{}, err
92+
}
8793
workload := GenericResource{
88-
Kind: "Pod",
89-
PodSpec: &podResource.Spec,
90-
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
94+
Kind: "Pod",
95+
PodSpec: &podResource.Spec,
96+
PodTemplate: podMap,
97+
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
9198
}
9299
if originalObject != nil {
93100
bytes, err := json.Marshal(originalObject)
@@ -237,3 +244,22 @@ func GetPodSpec(yaml map[string]interface{}) interface{} {
237244
}
238245
return nil
239246
}
247+
248+
// GetPodTemplate looks inside arbitrary YAML for a Pod template, containing
249+
// fields `spec.containers`.
250+
// For example, it returns the `spec.template` level of a Kubernetes Deployment yaml.
251+
func GetPodTemplate(yaml map[string]interface{}) interface{} {
252+
if yamlSpec, ok := yaml["spec"]; ok {
253+
if yamlSpecMap, ok := yamlSpec.(map[string]interface{}); ok {
254+
if _, ok := yamlSpecMap["containers"]; ok {
255+
return yaml
256+
}
257+
}
258+
}
259+
for _, podSpecField := range podSpecFields {
260+
if childYaml, ok := yaml[podSpecField]; ok {
261+
return GetPodTemplate(childYaml.(map[string]interface{}))
262+
}
263+
}
264+
return nil
265+
}

pkg/kube/resources.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,22 @@ func (resources *ResourceProvider) addResourceFromString(contents string) error
477477
return err
478478
}
479479

480-
// SerializePod converts a typed PodSpec into a map[string]interface{}
481-
func SerializePod(pod *corev1.PodSpec) (map[string]interface{}, error) {
480+
// SerializePodSpec converts a typed PodSpec into a map[string]interface{}
481+
func SerializePodSpec(pod *corev1.PodSpec) (map[string]interface{}, error) {
482+
podJSON, err := json.Marshal(pod)
483+
if err != nil {
484+
return nil, err
485+
}
486+
podMap := make(map[string]interface{})
487+
err = json.Unmarshal(podJSON, &podMap)
488+
if err != nil {
489+
return nil, err
490+
}
491+
return podMap, nil
492+
}
493+
494+
// SerializePod converts a typed Pod into a map[string]interface{}
495+
func SerializePod(pod *corev1.Pod) (map[string]interface{}, error) {
482496
podJSON, err := json.Marshal(pod)
483497
if err != nil {
484498
return nil, err
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2019 FairwindsOps Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package validator
16+
17+
import (
18+
"testing"
19+
20+
conf "github.com/fairwindsops/polaris/pkg/config"
21+
"github.com/fairwindsops/polaris/pkg/kube"
22+
"github.com/fairwindsops/polaris/test"
23+
"github.com/stretchr/testify/require"
24+
25+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
)
27+
28+
func TestGetTemplateInputReturnsPolarisSubKeys(t *testing.T) {
29+
pod := test.MockPod() // Includes a container, required by GetPodSpec
30+
pod.Spec.NodeName = "testNodeName"
31+
pod.ObjectMeta.Name = "testpod"
32+
genRes, err := kube.NewGenericResourceFromPod(pod, pod)
33+
require.NoError(t, err, "creating new generic resource from a pod")
34+
schemaTest := schemaTestCase{
35+
Target: conf.TargetPodSpec, // ends up being set in the case of target: PodTemplate
36+
Resource: genRes,
37+
}
38+
39+
templateInput, err := getTemplateInput(schemaTest)
40+
require.NoError(t, err, "getting template input from a generic resource")
41+
require.NotNil(t, templateInput)
42+
nodeName, ok, err := unstructured.NestedString(templateInput, "Polaris", "PodSpec", "nodeName")
43+
require.NoError(t, err, "getting Polaris.PodSpec.nodeName from template input")
44+
require.True(t, ok, "getting Polaris.PodSpec.nodeName from template input")
45+
require.Equal(t, "testNodeName", nodeName, "the nodeName from template output")
46+
podName, ok, err := unstructured.NestedString(templateInput, "Polaris", "PodTemplate", "metadata", "name")
47+
require.NoError(t, err, "getting Polaris.PodTemplate.metadata.name from template input")
48+
require.True(t, ok, "getting Polaris.PodTemplate.metadata.name from template input")
49+
require.Equal(t, "testpod", podName, "the pod from template input")
50+
}

pkg/validator/schema.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,20 @@ func getTemplateInput(test schemaTestCase) (map[string]interface{}, error) {
8585
return nil, nil
8686
}
8787
if test.Target == config.TargetPodSpec {
88-
podSpecMap, err := kube.SerializePod(test.Resource.PodSpec)
88+
podSpecMap, err := kube.SerializePodSpec(test.Resource.PodSpec)
8989
if err != nil {
9090
return nil, err
9191
}
9292
err = unstructured.SetNestedMap(templateInput, podSpecMap, "Polaris", "PodSpec")
9393
if err != nil {
9494
return nil, err
9595
}
96+
if podTemplateMap, ok := test.Resource.PodTemplate.(map[string]interface{}); ok {
97+
err := unstructured.SetNestedMap(templateInput, podTemplateMap, "Polaris", "PodTemplate")
98+
if err != nil {
99+
return nil, err
100+
}
101+
}
96102
}
97103
return templateInput, nil
98104
}
@@ -314,6 +320,9 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
314320
} else if check.Target == config.TargetPodSpec {
315321
passes, issues, err = check.CheckPodSpec(test.Resource.PodSpec)
316322
prefix = getJSONSchemaPrefix(test.Resource.Kind)
323+
} else if check.Target == config.TargetPodTemplate {
324+
passes, issues, err = check.CheckPodTemplate(test.Resource.PodTemplate)
325+
prefix = getJSONSchemaPrefix(test.Resource.Kind)
317326
} else if check.Target == config.TargetContainer {
318327
containerIndex := funk.IndexOf(test.Resource.PodSpec.Containers, func(value corev1.Container) bool {
319328
return value.Name == test.Container.Name

0 commit comments

Comments
 (0)