From 6d209093a54d5d93a86785e8ccdaf94579163ec0 Mon Sep 17 00:00:00 2001 From: Tejal Desai Date: Tue, 10 Mar 2020 01:27:51 -0700 Subject: [PATCH] [Diagnostics] Add validator interface. Add resource interface and PodValidator (#3742) * Add validator interface. Add resource interface and change pod.go to implement to Validator interface * wip --- pkg/diag/validator/pod.go | 64 +++++++++++++++------- pkg/diag/validator/pod_test.go | 96 ++++++++++++--------------------- pkg/diag/validator/resource.go | 57 ++++++++++++++++++++ pkg/diag/validator/validator.go | 31 +++++++++++ 4 files changed, 166 insertions(+), 82 deletions(-) create mode 100644 pkg/diag/validator/resource.go create mode 100644 pkg/diag/validator/validator.go diff --git a/pkg/diag/validator/pod.go b/pkg/diag/validator/pod.go index ce3637199d0..b471508ecd7 100644 --- a/pkg/diag/validator/pod.go +++ b/pkg/diag/validator/pod.go @@ -17,6 +17,7 @@ limitations under the License. package validator import ( + "context" "fmt" v1 "k8s.io/api/core/v1" @@ -36,36 +37,59 @@ var ( waitingContainerStatus = getWaitingContainerStatus ) -type PodStatus struct { - phase string - err *PodErr +// PodValidator implements the Validator interface for Pods +type PodValidator struct { + k kubernetes.Interface } -type PodErr struct { +// NewPodValidator initializes a PodValidator +func NewPodValidator(k kubernetes.Interface) *PodValidator { + return &PodValidator{k: k} +} + +// Validate implements the Validate method for Validator interface +func (p *PodValidator) Validate(ctx context.Context, ns string, opts meta_v1.ListOptions) ([]Resource, error) { + pods, err := p.k.CoreV1().Pods(ns).List(opts) + if err != nil { + return nil, err + } + rs := []Resource{} + for _, po := range pods.Items { + ps := p.getPodStatus(&po) + rs = append(rs, NewResourceFromObject(&po, Status(ps.phase), ps.reason.String())) + } + return rs, nil +} + +type podStatus struct { + phase string + reason *podReason +} + +type podReason struct { reason string message string } -func (e *PodErr) Error() string { - return fmt.Sprintf("pod in error due to reason: %s, message: %s", e.reason, e.message) +func (r *podReason) String() string { + if r == nil { + return "" + } + return fmt.Sprintf("pod unstable due to reason: %s, message: %s", r.reason, r.message) } -func GetPodDetails(client kubernetes.Interface, ns string, podName string) PodStatus { - pod, err := client.CoreV1().Pods(ns).Get(podName, meta_v1.GetOptions{}) - if err != nil { - return PodStatus{err: &PodErr{message: err.Error()}} - } +func (p *PodValidator) getPodStatus(pod *v1.Pod) podStatus { switch pod.Status.Phase { case v1.PodSucceeded: - return PodStatus{phase: success} + return podStatus{phase: success} case v1.PodRunning: - return PodStatus{phase: running} + return podStatus{phase: running} default: return getPendingDetails(pod) } } -func getPendingDetails(pod *v1.Pod) PodStatus { +func getPendingDetails(pod *v1.Pod) podStatus { // See https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions for _, c := range pod.Status.Conditions { switch c.Status { @@ -91,20 +115,20 @@ func getWaitingContainerStatus(cs []v1.ContainerStatus) (string, string) { return success, success } -func newPendingStatus(r string, d string) PodStatus { - return PodStatus{ +func newPendingStatus(r string, d string) podStatus { + return podStatus{ phase: pending, - err: &PodErr{ + reason: &podReason{ reason: r, message: d, }, } } -func newUnknownStatus() PodStatus { - return PodStatus{ +func newUnknownStatus() podStatus { + return podStatus{ phase: unknown, - err: &PodErr{ + reason: &podReason{ reason: unknown, message: unknown, }, diff --git a/pkg/diag/validator/pod_test.go b/pkg/diag/validator/pod_test.go index 8ee30965aff..3f77594ba63 100644 --- a/pkg/diag/validator/pod_test.go +++ b/pkg/diag/validator/pod_test.go @@ -17,44 +17,37 @@ limitations under the License. package validator import ( + "context" "testing" "github.com/google/go-cmp/cmp" v1 "k8s.io/api/core/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" fakekubeclientset "k8s.io/client-go/kubernetes/fake" "github.com/GoogleContainerTools/skaffold/testutil" ) -func TestGetPodDetails(t *testing.T) { +func TestRun(t *testing.T) { tests := []struct { description string - pod v1.Pod - name string - expected PodStatus - shouldErr bool + pods []*v1.Pod + expected []Resource }{ { - description: "pod does not exist", - pod: v1.Pod{ + description: "pod don't exist in test namespace", + pods: []*v1.Pod{{ ObjectMeta: meta_v1.ObjectMeta{ Name: "foo", - Namespace: "test", - }, + Namespace: "foo-ns", + }}, }, - name: "not-there", - expected: PodStatus{ - err: &PodErr{ - message: "pods \"not-there\" not found", - }, - }, - shouldErr: true, + expected: []Resource{}, }, { description: "pod is Waiting conditions with reason and message", - name: "foo", - pod: v1.Pod{ + pods: []*v1.Pod{{ ObjectMeta: meta_v1.ObjectMeta{ Name: "foo", Namespace: "test", @@ -74,20 +67,13 @@ func TestGetPodDetails(t *testing.T) { }, }, }, - }, - shouldErr: true, - expected: PodStatus{ - phase: pending, - err: &PodErr{ - reason: "ErrImgPull", - message: "could not pull the container image", - }, - }, + }}, + expected: []Resource{NewResource("test", "", "foo", "Pending", + "pod unstable due to reason: ErrImgPull, message: could not pull the container image")}, }, { description: "pod is Waiting conditions with reason but no message", - name: "foo", - pod: v1.Pod{ + pods: []*v1.Pod{{ ObjectMeta: meta_v1.ObjectMeta{ Name: "foo", Namespace: "test", @@ -106,19 +92,12 @@ func TestGetPodDetails(t *testing.T) { }, }, }, - }, - shouldErr: true, - expected: PodStatus{ - phase: pending, - err: &PodErr{ - reason: "Unschedulable", - }, - }, + }}, + expected: []Resource{NewResource("test", "", "foo", "Pending", "pod unstable due to reason: Unschedulable, message: ")}, }, { description: "pod is in Terminated State", - name: "foo", - pod: v1.Pod{ + pods: []*v1.Pod{{ ObjectMeta: meta_v1.ObjectMeta{ Name: "foo", Namespace: "test", @@ -127,15 +106,12 @@ func TestGetPodDetails(t *testing.T) { Phase: v1.PodSucceeded, Conditions: []v1.PodCondition{{Status: v1.ConditionTrue}}, }, - }, - expected: PodStatus{ - phase: "Succeeded", - }, + }}, + expected: []Resource{NewResource("test", "", "foo", "Succeeded", "")}, }, { description: "pod is in Stable State", - name: "foo", - pod: v1.Pod{ + pods: []*v1.Pod{{ ObjectMeta: meta_v1.ObjectMeta{ Name: "foo", Namespace: "test", @@ -150,15 +126,12 @@ func TestGetPodDetails(t *testing.T) { }, }, }, - }, - expected: PodStatus{ - phase: running, - }, + }}, + expected: []Resource{NewResource("test", "", "foo", "Running", "")}, }, { description: "pod condition unknown", - name: "foo", - pod: v1.Pod{ + pods: []*v1.Pod{{ ObjectMeta: meta_v1.ObjectMeta{ Name: "foo", Namespace: "test", @@ -170,22 +143,21 @@ func TestGetPodDetails(t *testing.T) { Message: "could not determine", }}, }, - }, - expected: PodStatus{ - phase: pending, - err: &PodErr{ - reason: "Unknown", - message: "could not determine", - }, - }, + }}, + expected: []Resource{NewResource("test", "", "foo", "Pending", "pod unstable due to reason: Unknown, message: could not determine")}, }, } for _, test := range tests { testutil.Run(t, test.description, func(t *testutil.T) { - client := fakekubeclientset.NewSimpleClientset(&test.pod) - actual := GetPodDetails(client, "test", test.name) - t.CheckDeepEqual(test.expected, actual, cmp.AllowUnexported(PodStatus{}, PodErr{})) + rs := make([]runtime.Object, len(test.pods)) + for i, p := range test.pods { + rs[i] = p + } + f := fakekubeclientset.NewSimpleClientset(rs...) + actual, err := NewPodValidator(f).Validate(context.Background(), "test", meta_v1.ListOptions{}) + t.CheckNoError(err) + t.CheckDeepEqual(test.expected, actual, cmp.AllowUnexported(Resource{})) }) } } diff --git a/pkg/diag/validator/resource.go b/pkg/diag/validator/resource.go new file mode 100644 index 00000000000..2a1e10be7c5 --- /dev/null +++ b/pkg/diag/validator/resource.go @@ -0,0 +1,57 @@ +/* +Copyright 2020 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validator + +import ( + "fmt" + + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type Resource struct { + namespace string + kind string + name string + reason string + status Status +} + +func (r Resource) Kind() string { return r.kind } +func (r Resource) Name() string { return r.name } +func (r Resource) Reason() string { return r.reason } +func (r Resource) Namespace() string { return r.namespace } +func (r Resource) Status() Status { return r.status } +func (r Resource) String() string { + return fmt.Sprintf("{%s:%s/%s}", r.kind, r.namespace, r.name) +} + +// NewResource creates new Resource of kind +func NewResource(namespace, kind, name string, status Status, reason string) Resource { + return Resource{namespace: namespace, kind: kind, name: name, status: status, reason: reason} +} + +// objectWithMetadata is any k8s object that has kind and object metadata. +type objectWithMetadata interface { + runtime.Object + meta_v1.Object +} + +// NewResourceFromObject creates new Resource with fields populated from object metadata. +func NewResourceFromObject(object objectWithMetadata, status Status, reason string) Resource { + return NewResource(object.GetNamespace(), object.GetObjectKind().GroupVersionKind().Kind, object.GetName(), status, reason) +} diff --git a/pkg/diag/validator/validator.go b/pkg/diag/validator/validator.go new file mode 100644 index 00000000000..89f45a080bc --- /dev/null +++ b/pkg/diag/validator/validator.go @@ -0,0 +1,31 @@ +/* +Copyright 2020 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validator + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type Status string + +type Validator interface { + // Validate runs the validator and returns the list of resources with status. + Validate(ctx context.Context, client kubernetes.Interface, ns string, opts metav1.ListOptions) ([]Resource, error) +}