diff --git a/cmd/cli/osm.go b/cmd/cli/osm.go index 9c04de8f56..4416444a1e 100644 --- a/cmd/cli/osm.go +++ b/cmd/cli/osm.go @@ -46,6 +46,7 @@ func newRootCmd(config *action.Configuration, stdin io.Reader, stdout io.Writer, newPolicyCmd(stdout, stderr), newSupportCmd(config, stdout, stderr), newUninstallCmd(config, stdin, stdout), + newVerifyCmd(stdout, stderr), ) // Add subcommands related to unmanaged environments diff --git a/cmd/cli/verify.go b/cmd/cli/verify.go new file mode 100644 index 0000000000..02a9d7f798 --- /dev/null +++ b/cmd/cli/verify.go @@ -0,0 +1,24 @@ +package main + +import ( + "io" + + "github.com/spf13/cobra" +) + +const verifyDescription = ` +This command consists of multiple subcommands related to verifying +mesh configurations. +` + +func newVerifyCmd(stdout io.Writer, stderr io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "verify", + Short: "verify mesh configurations", + Long: verifyDescription, + Args: cobra.NoArgs, + } + cmd.AddCommand(newVerifyConnectivityCmd(stdout, stderr)) + + return cmd +} diff --git a/cmd/cli/verify_connectivity.go b/cmd/cli/verify_connectivity.go new file mode 100644 index 0000000000..6f6f248352 --- /dev/null +++ b/cmd/cli/verify_connectivity.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "io" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + + "github.com/openservicemesh/osm/pkg/cli/verifier" + "github.com/openservicemesh/osm/pkg/constants" + "github.com/openservicemesh/osm/pkg/k8s" +) + +const verifyConnectivityDescription = ` +This command consists of multiple subcommands related to verifying +connectivity related configurations. +` + +var ( + fromPod string + toPod string +) + +type verifyConnectCmd struct { + stdout io.Writer + stderr io.Writer + kubeClient kubernetes.Interface + srcPod types.NamespacedName + dstPod types.NamespacedName + appProtocol string + meshName string +} + +func newVerifyConnectivityCmd(stdout io.Writer, stderr io.Writer) *cobra.Command { + verifyCmd := &verifyConnectCmd{ + stdout: stdout, + stderr: stderr, + } + + cmd := &cobra.Command{ + Use: "connectivity", + Short: "verify connectivity between a pod and a destination", + Long: verifyConnectivityDescription, + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + config, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + return errors.Errorf("Error fetching kubeconfig: %s", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return errors.Errorf("Could not access Kubernetes cluster, check kubeconfig: %s", err) + } + verifyCmd.kubeClient = clientset + + namespacedName, err := k8s.NamespacedNameFrom(fromPod) + if err != nil { + return errors.Errorf("Source must be a namespaced name of the form /, got %s", fromPod) + } + verifyCmd.srcPod = namespacedName + namespacedName, err = k8s.NamespacedNameFrom(toPod) + if err != nil { + return errors.Errorf("Destination must be a namespaced name of the form /, got %s", toPod) + } + verifyCmd.dstPod = namespacedName + + return verifyCmd.run() + }, + } + + f := cmd.Flags() + f.StringVar(&fromPod, "from-pod", "", "Namespaced name of client pod: /") + //nolint: errcheck + //#nosec G104: Errors unhandled + cmd.MarkFlagRequired("from-pod") + f.StringVar(&toPod, "to-pod", "", "Namespaced name of destination pod: /") + //nolint: errcheck + //#nosec G104: Errors unhandled + cmd.MarkFlagRequired("to-pod") + f.StringVar(&verifyCmd.appProtocol, "app-protocol", constants.ProtocolHTTP, "Application protocol") + f.StringVar(&verifyCmd.meshName, "mesh-name", defaultMeshName, "Mesh name") + + return cmd +} + +func (cmd *verifyConnectCmd) run() error { + podConnectivityVerifier := verifier.NewPodConnectivityVerifier(cmd.stdout, cmd.stderr, cmd.kubeClient, + cmd.srcPod, cmd.dstPod, cmd.appProtocol, cmd.meshName) + result := podConnectivityVerifier.Run() + + fmt.Fprintln(cmd.stdout, "---------------------------------------------") + verifier.Print(result, cmd.stdout) + fmt.Fprintln(cmd.stdout, "---------------------------------------------") + + return nil +} diff --git a/pkg/cli/verifier/connectivity_pod_to_pod.go b/pkg/cli/verifier/connectivity_pod_to_pod.go new file mode 100644 index 0000000000..ab6257b13a --- /dev/null +++ b/pkg/cli/verifier/connectivity_pod_to_pod.go @@ -0,0 +1,50 @@ +package verifier + +import ( + "fmt" + "io" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" +) + +// PodConnectivityVerifier implements the Verifier interface for pod connectivity +type PodConnectivityVerifier struct { + stdout io.Writer + stderr io.Writer + kubeClient kubernetes.Interface + srcPod types.NamespacedName + dstPod types.NamespacedName + appProtocol string + meshName string +} + +// NewPodConnectivityVerifier implements verification for pod connectivity +func NewPodConnectivityVerifier(stdout io.Writer, stderr io.Writer, kubeClient kubernetes.Interface, + srcPod types.NamespacedName, dstPod types.NamespacedName, appProtocol string, meshName string) Verifier { + return &PodConnectivityVerifier{ + stdout: stdout, + stderr: stderr, + kubeClient: kubeClient, + srcPod: srcPod, + dstPod: dstPod, + appProtocol: appProtocol, + meshName: meshName, + } +} + +// Run executes the pod connectivity verifier +func (v *PodConnectivityVerifier) Run() Result { + ctx := fmt.Sprintf("Verify if pod %q can access pod %q for app protocol %q", v.srcPod, v.dstPod, v.appProtocol) + + verifiers := Set{ + // --- + // Verify prerequisites + // + // Namespace monitor verification + NewNamespaceMonitorVerifier(v.stdout, v.stderr, v.kubeClient, v.srcPod.Namespace, v.meshName), + NewNamespaceMonitorVerifier(v.stdout, v.stderr, v.kubeClient, v.dstPod.Namespace, v.meshName), + } + + return verifiers.Run(ctx) +} diff --git a/pkg/cli/verifier/connectivity_pod_to_pod_test.go b/pkg/cli/verifier/connectivity_pod_to_pod_test.go new file mode 100644 index 0000000000..ef90b62033 --- /dev/null +++ b/pkg/cli/verifier/connectivity_pod_to_pod_test.go @@ -0,0 +1,120 @@ +package verifier + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" + + "github.com/openservicemesh/osm/pkg/constants" +) + +func TestRun(t *testing.T) { + testMeshName := "test" + + testCases := []struct { + name string + resources []runtime.Object + srcPod types.NamespacedName + dstPod types.NamespacedName + expected Result + }{ + { + name: "pods have config to communicate", + resources: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns1", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "ns2", + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns1", + Labels: map[string]string{ + constants.OSMKubeResourceMonitorAnnotation: testMeshName, + }, + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns2", + Labels: map[string]string{ + constants.OSMKubeResourceMonitorAnnotation: testMeshName, + }, + }, + }, + }, + srcPod: types.NamespacedName{Namespace: "ns1", Name: "pod1"}, + dstPod: types.NamespacedName{Namespace: "ns2", Name: "pod2"}, + expected: Result{ + Status: Success, + }, + }, + { + name: "pod doesn't belong to monitored namespace", + resources: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns1", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "ns2", + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns1", + Labels: map[string]string{ + constants.OSMKubeResourceMonitorAnnotation: testMeshName, + }, + }, + }, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns2", // not monitored + }, + }, + }, + srcPod: types.NamespacedName{Namespace: "ns1", Name: "pod1"}, + dstPod: types.NamespacedName{Namespace: "ns2", Name: "pod2"}, + expected: Result{ + Status: Failure, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := assert.New(t) + + fakeClient := fake.NewSimpleClientset(tc.resources...) + v := &PodConnectivityVerifier{ + srcPod: tc.srcPod, + dstPod: tc.dstPod, + kubeClient: fakeClient, + meshName: testMeshName, + } + + actual := v.Run() + out := new(bytes.Buffer) + Print(actual, out) + a.Equal(tc.expected.Status, actual.Status, out) + }) + } +} diff --git a/pkg/cli/verifier/namespace.go b/pkg/cli/verifier/namespace.go new file mode 100644 index 0000000000..4833d02818 --- /dev/null +++ b/pkg/cli/verifier/namespace.go @@ -0,0 +1,63 @@ +package verifier + +import ( + "context" + "fmt" + "io" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/openservicemesh/osm/pkg/constants" +) + +// NamespaceMonitorVerifier implements the Verifier interface for pod connectivity +type NamespaceMonitorVerifier struct { + stdout io.Writer + stderr io.Writer + kubeClient kubernetes.Interface + namespace string + meshName string +} + +// NewNamespaceMonitorVerifier implements verification for namespace monitoring +func NewNamespaceMonitorVerifier(stdout io.Writer, stderr io.Writer, kubeClient kubernetes.Interface, namespace string, meshName string) Verifier { + return &NamespaceMonitorVerifier{ + stdout: stdout, + stderr: stderr, + kubeClient: kubeClient, + namespace: namespace, + meshName: meshName, + } +} + +// Run executes the namespace monitor verification +func (v *NamespaceMonitorVerifier) Run() Result { + result := Result{ + Context: fmt.Sprintf("Verify if namespace %q is monitored", v.namespace), + } + + ns, err := v.kubeClient.CoreV1().Namespaces().Get(context.Background(), v.namespace, metav1.GetOptions{}) + if err != nil { + result.Status = Failure + result.Reason = fmt.Sprintf("Error fetching namespace %q", v.namespace) + return result + } + + annotatedMeshName, ok := ns.Labels[constants.OSMKubeResourceMonitorAnnotation] + if !ok { + result.Status = Failure + result.Reason = fmt.Sprintf("Missing label %q on namespace %q", constants.OSMKubeResourceMonitorAnnotation, v.namespace) + result.Suggestion = fmt.Sprintf("Add label %q on namespace %q to include it in the mesh and restart the app", constants.OSMKubeResourceMonitorAnnotation, v.namespace) + return result + } + if annotatedMeshName != v.meshName { + result.Status = Failure + result.Reason = fmt.Sprintf("Expected label %q to have value %q, got %q", + constants.OSMKubeResourceMonitorAnnotation, v.meshName, annotatedMeshName) + return result + } + + result.Status = Success + return result +} diff --git a/pkg/cli/verifier/verifier.go b/pkg/cli/verifier/verifier.go new file mode 100644 index 0000000000..5f15b16b97 --- /dev/null +++ b/pkg/cli/verifier/verifier.go @@ -0,0 +1,93 @@ +package verifier + +import ( + "fmt" + "io" + + "github.com/fatih/color" +) + +// Status is a type describing the status of a verification +type Status string + +const ( + // Success indicates the verification succeeded + Success Status = "Success" + + // Failure indicates the verification failed + Failure Status = "Failure" + + // Unknown indicates the result of the verification could not be determined + Unknown Status = "Unknown" +) + +// Result defines the result returned by a Verifier instance +type Result struct { + Context string + Status Status + Reason string + Suggestion string + NestedResults []*Result +} + +// Verifier defines the interface to perform a verification +type Verifier interface { + Run() Result +} + +// Print prints the Result +func Print(result Result, w io.Writer) { + fmt.Fprintf(w, "[+] Context: %s\n", result.Context) + fmt.Fprintf(w, "Status: %s\n", result.Status.Color()) + if result.Reason != "" { + fmt.Fprintf(w, "Reason: %s\n", result.Reason) + } + if result.Suggestion != "" { + fmt.Fprintf(w, "Suggestion: %s\n", result.Suggestion) + } + fmt.Fprintln(w) + + if result.Status == Success { + return + } + + for _, res := range result.NestedResults { + Print(*res, w) + } +} + +// Color returns a color coded string for the verification status +func (s Status) Color() string { + if s == Success { + return color.GreenString("%s", s) + } else if s == Failure { + return color.RedString("%s", s) + } else { + return color.YellowString("%s", s) + } +} + +// Set is a collection of Verifier objects +type Set []Verifier + +// Run executes runs the verification for each verifier in the list +func (verifiers Set) Run(ctx string) Result { + result := Result{ + Context: ctx, + } + for _, verification := range verifiers { + res := verification.Run() + if res.Status == Failure { + result.Status = Failure + result.Reason = "A verification step failed" + result.Suggestion = "Please follow the suggestions listed in the failed steps below to resolve the issue" + } + result.NestedResults = append(result.NestedResults, &res) + } + + if result.Status != Failure && result.Status != Unknown { + result.Status = Success + } + + return result +}