diff --git a/controllers/configurationpolicy_controller_test.go b/controllers/configurationpolicy_controller_test.go index d68f9dd4..f5066b8b 100644 --- a/controllers/configurationpolicy_controller_test.go +++ b/controllers/configurationpolicy_controller_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" @@ -754,3 +755,88 @@ func TestShouldEvaluatePolicy(t *testing.T) { ) } } + +func TestShouldHandleSingleKeyFalse(t *testing.T) { + t.Parallel() + + var unstruct unstructured.Unstructured + var unstructObj unstructured.Unstructured + + var update, skip bool + + tests := [][]map[string]interface{}{ + { + { + "hostIPC": false, + "container": "test", + }, + { + "container": "test", + }, + { + "key": "hostIPC", + "expect": false, + }, + }, + { + { + "container": map[string]interface{}{ + "image": "nginx1.7.9", + "name": "nginx", + "hostIPC": false, + }, + }, + { + "container": map[string]interface{}{ + "image": "nginx1.7.9", + "name": "nginx", + }, + }, + { + "key": "container", + "expect": false, + }, + }, + { + { + "hostIPC": true, + "container": "test", + }, + { + "container": "test", + }, + { + "key": "hostIPC", + "expect": true, + }, + }, + { + { + "container": map[string]interface{}{ + "image": "nginx1.7.9", + "name": "nginx", + "hostIPC": true, + }, + }, + { + "container": map[string]interface{}{ + "image": "nginx1.7.9", + "name": "nginx", + }, + }, + { + "key": "container", + "expect": true, + }, + }, + } + + for _, test := range tests { + unstruct.Object = test[0] + unstructObj.Object = test[1] + key := test[2]["key"] + _, update, _, skip = handleSingleKey(key.(string), unstruct, &unstructObj, "musthave") + assert.Equal(t, update, test[2]["expect"]) + assert.False(t, skip) + } +} diff --git a/controllers/configurationpolicy_utils.go b/controllers/configurationpolicy_utils.go index da5e4bb6..f4a9179f 100644 --- a/controllers/configurationpolicy_utils.go +++ b/controllers/configurationpolicy_utils.go @@ -119,6 +119,16 @@ func equalObjWithSort(mergedObj interface{}, oldObj interface{}) (areEqual bool) return false } default: + // NOTE: when type is string, int, bool + var oVal interface{} + + if oldObj == nil && mergedObj != nil { + ref := reflect.ValueOf(mergedObj) + oVal = reflect.Zero(ref.Type()).Interface() + + return fmt.Sprint(oVal) == fmt.Sprint(mergedObj) + } + if !reflect.DeepEqual(fmt.Sprint(mergedObj), fmt.Sprint(oldObj)) { return false } @@ -175,8 +185,12 @@ func checkFieldsWithSort(mergedObj map[string]interface{}, oldObj map[string]int // extra check to see if value is a byte value mQty, err := apiRes.ParseQuantity(mVal) if err != nil { + oVal := oldObj[i] + if oVal == nil { + oVal = "" + } // An error indicates the value is a regular string, so check equality normally - if fmt.Sprint(oldObj[i]) != fmt.Sprint(mVal) { + if fmt.Sprint(oVal) != fmt.Sprint(mVal) { return false } } else { @@ -194,6 +208,12 @@ func checkFieldsWithSort(mergedObj map[string]interface{}, oldObj map[string]int default: // if field is not an object, just do a basic compare to check for a match oVal := oldObj[i] + // When oVal value omitted because of omitempty + if oVal == nil && mVal != nil { + ref := reflect.ValueOf(mVal) + oVal = reflect.Zero(ref.Type()).Interface() + } + if fmt.Sprint(oVal) != fmt.Sprint(mVal) { return false } diff --git a/controllers/configurationpolicy_utils_test.go b/controllers/configurationpolicy_utils_test.go index 07b92051..371757fe 100644 --- a/controllers/configurationpolicy_utils_test.go +++ b/controllers/configurationpolicy_utils_test.go @@ -118,3 +118,34 @@ func TestCheckFieldsWithSort(t *testing.T) { assert.True(t, checkFieldsWithSort(mergedObj, oldObj)) } + +func TestEqualObjWithSort(t *testing.T) { + t.Parallel() + + oldObj := map[string]interface{}{ + "nonResourceURLs": []string{"/version", "/healthz"}, + "verbs": []string{"get"}, + } + mergedObj := map[string]interface{}{ + "nonResourceURLs": []string{"/version", "/healthz"}, + "verbs": []string{"get"}, + "apiGroups": []interface{}{}, + "resources": []interface{}{}, + } + + assert.True(t, equalObjWithSort(mergedObj, oldObj)) + assert.False(t, equalObjWithSort(mergedObj, nil)) + + oldObj = map[string]interface{}{ + "nonResourceURLs": []string{"/version", "/healthz"}, + "verbs": []string{"get"}, + } + mergedObj = map[string]interface{}{ + "nonResourceURLs": []string{"/version", "/healthz"}, + "verbs": []string{"post"}, + "apiGroups": []interface{}{}, + "resources": []interface{}{}, + } + + assert.False(t, equalObjWithSort(mergedObj, oldObj)) +} diff --git a/go.mod b/go.mod index 515cb3d1..071699d3 100644 --- a/go.mod +++ b/go.mod @@ -103,4 +103,5 @@ replace ( golang.org/x/text => golang.org/x/text v0.3.8 // CVE-2022-32149 gopkg.in/yaml.v2 => gopkg.in/yaml.v2 v2.4.0 // CVE-2022-3064 k8s.io/client-go => k8s.io/client-go v0.23.9 + open-cluster-management.io/governance-policy-propagator => github.com/stolostron/governance-policy-propagator v0.0.0-20220727212642-86a318ab17cf ) diff --git a/policy-security.yml b/policy-security.yml new file mode 100644 index 00000000..e69de29b diff --git a/test/e2e/case12_list_compare_test.go b/test/e2e/case12_list_compare_test.go index 301e785a..2f0b3110 100644 --- a/test/e2e/case12_list_compare_test.go +++ b/test/e2e/case12_list_compare_test.go @@ -356,7 +356,18 @@ var _ = Describe("Test list handling for musthave", func() { deleteConfigPolicies(policies) }) }) - Describe("Create a statefulset object with a byte quantity field on managed cluster in ns:"+testNamespace, func() { + Describe("Create a statefulset object with a byte quantity field "+ + "on managed cluster in ns:"+testNamespace, Ordered, func() { + cleanup := func() { + // Delete the policies and ignore any errors (in case it was deleted previously) + policies := []string{ + case12ByteCreate, + case12ByteInform, + } + + deleteConfigPolicies(policies) + } + BeforeAll(cleanup) It("should only add the list item with the rounded byte value once", func() { By("Creating " + case12ByteCreate + " and " + case12ByteInform + " on managed") utils.Kubectl("apply", "-f", case12ByteCreateYaml, "-n", testNamespace) @@ -389,14 +400,6 @@ var _ = Describe("Test list handling for musthave", func() { return utils.GetComplianceState(managedPlc) }, defaultTimeoutSeconds, 1).Should(Equal("Compliant")) }) - - It("Cleans up", func() { - policies := []string{ - case12ByteCreate, - case12ByteInform, - } - - deleteConfigPolicies(policies) - }) + AfterAll(cleanup) }) }) diff --git a/test/e2e/case20_delete_objects_test.go b/test/e2e/case20_delete_objects_test.go index 2115a06a..6a01a26c 100644 --- a/test/e2e/case20_delete_objects_test.go +++ b/test/e2e/case20_delete_objects_test.go @@ -527,7 +527,8 @@ var _ = Describe("Test objects that should be deleted are actually being deleted return nil }, defaultTimeoutSeconds, 1).Should(BeNil()) }) - It("deletes the pod after the policy is deleted", func() { + AfterAll(func() { + By("deletes the pod after the policy is deleted") deleteConfigPolicies([]string{case20ConfigPolicyNameCreate}) Eventually(func() interface{} { pod := utils.GetWithTimeout(clientManagedDynamic, gvrPod, diff --git a/test/e2e/case25_related_object_metric_test.go b/test/e2e/case25_related_object_metric_test.go index b455eed1..048019a0 100644 --- a/test/e2e/case25_related_object_metric_test.go +++ b/test/e2e/case25_related_object_metric_test.go @@ -56,23 +56,19 @@ var _ = Describe("Test related object metrics", Ordered, func() { // Delete the policies and ignore any errors (in case it was deleted previously) cmd := exec.Command("kubectl", "delete", "-f", policyYaml, - "-n", testNamespace) + "-n", testNamespace, "--ignore-not-found") _, _ = cmd.CombinedOutput() opt := metav1.ListOptions{} utils.ListWithTimeout( clientManagedDynamic, gvrConfigPolicy, opt, 0, false, defaultTimeoutSeconds) utils.GetWithTimeout( clientManagedDynamic, gvrConfigMap, relatedObject, "default", false, defaultTimeoutSeconds) - } - - It("should clean up", cleanup) - It("should have no common related object metrics after clean up", func() { By("Checking metric endpoint for related object gauges") Eventually(func() interface{} { return utils.GetMetrics("common_related_objects") }, defaultTimeoutSeconds, 1).Should(Equal([]string{})) - }) + } AfterAll(cleanup) }) diff --git a/test/e2e/case26_user_error_metric_test.go b/test/e2e/case26_user_error_metric_test.go index 594c3880..344a5567 100644 --- a/test/e2e/case26_user_error_metric_test.go +++ b/test/e2e/case26_user_error_metric_test.go @@ -15,6 +15,21 @@ var _ = Describe("Test related object metrics", Ordered, func() { policy1Name = "case26-test-policy-1" policyYaml = "../resources/case26_user_error_metric/case26-missing-crd.yaml" ) + cleanup := func() { + // Delete the policies and ignore any errors (in case it was deleted previously) + cmd := exec.Command("kubectl", "delete", + "-f", policyYaml, + "-n", testNamespace, "--ignore-not-found") + _, _ = cmd.CombinedOutput() + opt := metav1.ListOptions{} + utils.ListWithTimeout( + clientManagedDynamic, gvrConfigPolicy, opt, 0, false, defaultTimeoutSeconds) + By("Checking metric endpoint for related object gauges") + Eventually(func() interface{} { + return utils.GetMetrics("policy_user_errors") + }, defaultTimeoutSeconds, 1).Should(Equal([]string{})) + } + It("should create policy", func() { By("Creating " + policyYaml) utils.Kubectl("apply", @@ -36,25 +51,5 @@ var _ = Describe("Test related object metrics", Ordered, func() { }, defaultTimeoutSeconds, 1).Should(Equal([]string{"policies", "counter", "1"})) }) - cleanup := func() { - // Delete the policies and ignore any errors (in case it was deleted previously) - cmd := exec.Command("kubectl", "delete", - "-f", policyYaml, - "-n", testNamespace) - _, _ = cmd.CombinedOutput() - opt := metav1.ListOptions{} - utils.ListWithTimeout( - clientManagedDynamic, gvrConfigPolicy, opt, 0, false, defaultTimeoutSeconds) - } - - It("should clean up", cleanup) - - It("should have no common related object metrics after clean up", func() { - By("Checking metric endpoint for related object gauges") - Eventually(func() interface{} { - return utils.GetMetrics("policy_user_errors") - }, defaultTimeoutSeconds, 1).Should(Equal([]string{})) - }) - AfterAll(cleanup) }) diff --git a/test/e2e/case28_evauluation_metric_test.go b/test/e2e/case28_evauluation_metric_test.go index cc32f789..f4ba2d76 100644 --- a/test/e2e/case28_evauluation_metric_test.go +++ b/test/e2e/case28_evauluation_metric_test.go @@ -102,7 +102,6 @@ var _ = Describe("Test config policy evaluation metrics", Ordered, func() { opt := metav1.ListOptions{} utils.ListWithTimeout( clientManagedDynamic, gvrConfigPolicy, opt, 0, false, defaultTimeoutSeconds) - Eventually(func() interface{} { return utils.GetMetrics( "config_policy_evaluation_total", fmt.Sprintf(`name=\"%s\"`, policyName)) diff --git a/test/e2e/case31_policy_history_test.go b/test/e2e/case31_policy_history_test.go new file mode 100644 index 00000000..827a5adb --- /dev/null +++ b/test/e2e/case31_policy_history_test.go @@ -0,0 +1,155 @@ +// Copyright (c) 2021 Red Hat, Inc. +// Copyright Contributors to the Open Cluster Management project + +package e2e + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "open-cluster-management.io/config-policy-controller/test/utils" +) + +const ( + case31Policy = "../resources/case31_policy_history/pod-policy.yaml" + case31ConfigPolicy = "../resources/case31_policy_history/pod-config-policy.yaml" + case31PolicyName = "test-policy-security" + case31ConfigPolicyName = "config-policy-pod" + case31PolicyNumber = "../resources/case31_policy_history/pod-policy-number.yaml" + case31ConfigPolicyNumber = "../resources/case31_policy_history/pod-config-policy-number.yaml" + case31PolicyNumberName = "test-policy-security-number" + case31ConfigPolicyNumberName = "config-policy-pod-number" +) + +var _ = Describe("Test policy history message when KubeAPI return "+ + "omits values in the returned object", Ordered, func() { + Describe("status toggling should not be generated When Policy include default value,", Ordered, func() { + It("creates the policyconfiguration "+case31Policy, func() { + utils.Kubectl("apply", "-f", case31Policy, "-n", "managed") + }) + + It("verifies the policy "+case31PolicyName+" in "+testNamespace, func() { + By("bind policy and configurationpolicy") + parent := utils.GetWithTimeout(clientManagedDynamic, gvrPolicy, + case31PolicyName, testNamespace, true, defaultTimeoutSeconds) + Expect(parent).NotTo(BeNil()) + + plcDef := utils.ParseYaml(case31ConfigPolicy) + ownerRefs := plcDef.GetOwnerReferences() + ownerRefs[0].UID = parent.GetUID() + plcDef.SetOwnerReferences(ownerRefs) + _, err := clientManagedDynamic.Resource(gvrConfigPolicy).Namespace(testNamespace). + Create(context.TODO(), plcDef, metav1.CreateOptions{}) + Expect(err).To(BeNil()) + + By("check configurationpolicy exist") + plc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case31ConfigPolicyName, testNamespace, true, defaultTimeoutSeconds) + Expect(plc).NotTo(BeNil()) + }) + + It("check history toggling", func() { + By("wait until pod is up") + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case31ConfigPolicyName, testNamespace, true, defaultTimeoutSeconds) + + return utils.GetComplianceState(managedPlc) + }, defaultTimeoutSeconds, 1).Should(Equal("Compliant")) + + By("check events") + Consistently(func() int { + eventlen := len(utils.GetMatchingEvents(clientManaged, testNamespace, + case31ConfigPolicyName, case31ConfigPolicyName, "NonCompliant;", defaultTimeoutSeconds)) + + return eventlen + }, 30, 5).Should(BeNumerically("<", 2)) + + Consistently(func() int { + eventlen := len(utils.GetMatchingEvents(clientManaged, testNamespace, + case31PolicyName, case31ConfigPolicyName, "NonCompliant;", defaultTimeoutSeconds)) + + return eventlen + }, 30, 5).Should(BeNumerically("<", 2)) + }) + AfterAll(func() { + utils.Kubectl("delete", "policy", case31PolicyName, "-n", + "managed", "--ignore-not-found") + configlPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case31ConfigPolicyName, "managed", false, defaultTimeoutSeconds, + ) + utils.Kubectl("delete", "event", + "--field-selector=involvedObject.name="+case31PolicyName, "-n", "managed") + utils.Kubectl("delete", "event", + "--field-selector=involvedObject.name="+case31ConfigPolicyName, "-n", "managed") + ExpectWithOffset(1, configlPlc).To(BeNil()) + }) + }) + Describe("status should not toggle When Policy include default value of number", Ordered, func() { + It("creates the policyconfiguration "+case31PolicyNumber, func() { + utils.Kubectl("apply", "-f", case31PolicyNumber, "-n", "managed") + }) + + It("verifies the policy "+case31PolicyNumberName+" in "+testNamespace, func() { + By("bind policy and configurationpolicy") + parent := utils.GetWithTimeout(clientManagedDynamic, gvrPolicy, + case31PolicyNumberName, testNamespace, true, defaultTimeoutSeconds) + Expect(parent).NotTo(BeNil()) + + plcDef := utils.ParseYaml(case31ConfigPolicyNumber) + ownerRefs := plcDef.GetOwnerReferences() + ownerRefs[0].UID = parent.GetUID() + plcDef.SetOwnerReferences(ownerRefs) + _, err := clientManagedDynamic.Resource(gvrConfigPolicy).Namespace(testNamespace). + Create(context.TODO(), plcDef, metav1.CreateOptions{}) + Expect(err).To(BeNil()) + + By("check configurationpolicy exist") + plc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case31ConfigPolicyNumberName, testNamespace, true, defaultTimeoutSeconds) + Expect(plc).NotTo(BeNil()) + }) + + It("check history toggling", func() { + By("wait until pod is up") + Eventually(func() interface{} { + managedPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case31ConfigPolicyNumberName, testNamespace, true, defaultTimeoutSeconds) + + return utils.GetComplianceState(managedPlc) + }, defaultTimeoutSeconds, 1).Should(Equal("Compliant")) + + By("check events") + Consistently(func() int { + eventLen := len(utils.GetMatchingEvents(clientManaged, testNamespace, case31ConfigPolicyNumberName, + case31ConfigPolicyNumberName, "NonCompliant;", defaultTimeoutSeconds)) + + return eventLen + }, 30, 5).Should(BeNumerically("<", 2)) + + // NOTE: pick policy event, these event's reason include ConfigPolicyName + Consistently(func() int { + eventLen := len(utils.GetMatchingEvents(clientManaged, testNamespace, + case31PolicyNumberName, case31ConfigPolicyNumberName, "NonCompliant;", defaultTimeoutSeconds)) + + return eventLen + }, 30, 5).Should(BeNumerically("<", 2)) + }) + AfterAll(func() { + utils.Kubectl("delete", "policy", case31PolicyNumberName, "-n", + "managed", "--ignore-not-found") + configlPlc := utils.GetWithTimeout(clientManagedDynamic, gvrConfigPolicy, + case31ConfigPolicyName, "managed", false, defaultTimeoutSeconds, + ) + utils.Kubectl("delete", "event", + "--field-selector=involvedObject.name="+case31PolicyNumberName, "-n", "managed") + utils.Kubectl("delete", "event", + "--field-selector=involvedObject.name="+case31ConfigPolicyNumberName, "-n", "managed") + + ExpectWithOffset(1, configlPlc).To(BeNil()) + }) + }) +}) diff --git a/test/e2e/case8_status_check_test.go b/test/e2e/case8_status_check_test.go index 4ac3993e..1e41fd99 100644 --- a/test/e2e/case8_status_check_test.go +++ b/test/e2e/case8_status_check_test.go @@ -144,7 +144,6 @@ var _ = Describe("Test pod obj template handling", func() { policies := []string{ case8ConfigPolicyStatusPod, } - deleteConfigPolicies(policies) }) }) diff --git a/test/resources/case31_policy_history/pod-config-policy-number.yaml b/test/resources/case31_policy_history/pod-config-policy-number.yaml new file mode 100644 index 00000000..b12f1f5c --- /dev/null +++ b/test/resources/case31_policy_history/pod-config-policy-number.yaml @@ -0,0 +1,36 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: ConfigurationPolicy +metadata: + name: config-policy-pod-number + labels: + policy.open-cluster-management.io/policy: test-policy-security + ownerReferences: + - apiVersion: policy.open-cluster-management.io/v1 + blockOwnerDeletion: false + controller: true + kind: Policy + name: test-policy-security-number + uid: 08bae967-4262-498a-84e9-d1f0e321b41e +spec: + pruneObjectBehavior: DeleteAll + remediationAction: enforce + namespaceSelector: + exclude: + - kube-* + include: + - default + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Pod + metadata: + name: case31-pod-policy-number + spec: + priority: 0 + containers: + - image: nginx:1.7.9 + imagePullPolicy: Never + name: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/test/resources/case31_policy_history/pod-config-policy.yaml b/test/resources/case31_policy_history/pod-config-policy.yaml new file mode 100644 index 00000000..4b51897c --- /dev/null +++ b/test/resources/case31_policy_history/pod-config-policy.yaml @@ -0,0 +1,36 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: ConfigurationPolicy +metadata: + name: config-policy-pod + labels: + policy.open-cluster-management.io/policy: test-policy-security + ownerReferences: + - apiVersion: policy.open-cluster-management.io/v1 + blockOwnerDeletion: false + controller: true + kind: Policy + name: test-policy-security + uid: 08bae967-4262-498a-84e9-d1f0e321b41e +spec: + pruneObjectBehavior: DeleteAll + remediationAction: enforce + namespaceSelector: + exclude: + - kube-* + include: + - default + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Pod + metadata: + name: case31-pod-policy + spec: + hostIPC: false + containers: + - image: nginx:1.7.9 + imagePullPolicy: Never + name: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/test/resources/case31_policy_history/pod-policy-number.yaml b/test/resources/case31_policy_history/pod-policy-number.yaml new file mode 100644 index 00000000..5e2e4e6a --- /dev/null +++ b/test/resources/case31_policy_history/pod-policy-number.yaml @@ -0,0 +1,39 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: Policy +metadata: + name: test-policy-security-number + annotations: + policy.open-cluster.management.io/standards: NIST-CSF + policy.open-cluster.management.io/categories: PR.PT Protective Technology + policy.open-cluster.management.io/controls: PR.PT-3 Least Functionality +spec: + remediationAction: enforce + disabled: false + policy-templates: + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: config-policy-pod-number + spec: + remediationAction: enforce + namespaceSelector: + exclude: + - kube-* + include: + - default + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Pod + metadata: + name: case31-pod-policy-number + spec: + priority: 0 + containers: + - image: nginx:1.7.9 + imagePullPolicy: Never + name: nginx + ports: + - containerPort: 80 \ No newline at end of file diff --git a/test/resources/case31_policy_history/pod-policy.yaml b/test/resources/case31_policy_history/pod-policy.yaml new file mode 100644 index 00000000..362c3530 --- /dev/null +++ b/test/resources/case31_policy_history/pod-policy.yaml @@ -0,0 +1,39 @@ +apiVersion: policy.open-cluster-management.io/v1 +kind: Policy +metadata: + name: test-policy-security + annotations: + policy.open-cluster.management.io/standards: NIST-CSF + policy.open-cluster.management.io/categories: PR.PT Protective Technology + policy.open-cluster.management.io/controls: PR.PT-3 Least Functionality +spec: + remediationAction: enforce + disabled: false + policy-templates: + - objectDefinition: + apiVersion: policy.open-cluster-management.io/v1 + kind: ConfigurationPolicy + metadata: + name: config-policy-pod + spec: + remediationAction: enforce + namespaceSelector: + exclude: + - kube-* + include: + - default + object-templates: + - complianceType: musthave + objectDefinition: + apiVersion: v1 + kind: Pod + metadata: + name: case31-pod-policy + spec: + hostIPC: false + containers: + - image: nginx:1.7.9 + imagePullPolicy: Never + name: nginx + ports: + - containerPort: 80 \ No newline at end of file