diff --git a/go.mod b/go.mod index 6ac3fa3d1..ee3545265 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( k8s.io/api v0.17.3 k8s.io/apiextensions-apiserver v0.17.2 k8s.io/apimachinery v0.17.3 + k8s.io/cli-runtime v0.17.3 k8s.io/client-go v0.17.3 k8s.io/code-generator v0.17.3 k8s.io/component-base v0.17.3 diff --git a/pkg/kudoctl/cmd/diagnostics.go b/pkg/kudoctl/cmd/diagnostics.go new file mode 100644 index 000000000..8d8074674 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "fmt" + "time" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + + "github.com/kudobuilder/kudo/pkg/kudoctl/cmd/diagnostics" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" +) + +const ( + diagCollectExample = ` # collect diagnostics example + kubectl kudo diagnostics collect --instance flink +` +) + +func newDiagnosticsCmd(fs afero.Fs) *cobra.Command { + cmd := &cobra.Command{ + Use: "diagnostics", + Short: "collect diagnostics", + Long: "diagnostics provides functionality to collect and analyze diagnostics data", + } + cmd.AddCommand(newDiagnosticsCollectCmd(fs)) + return cmd +} + +func newDiagnosticsCollectCmd(fs afero.Fs) *cobra.Command { + var logSince time.Duration + var instance string + cmd := &cobra.Command{ + Use: "collect", + Short: "collect diagnostics", + Long: "collect data relevant for diagnostics of the provided instance's state", + Example: diagCollectExample, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := kudo.NewClient(Settings.KubeConfig, Settings.RequestTimeout, Settings.Validate) + if err != nil { + return fmt.Errorf("failed to create kudo client: %v", err) + } + return diagnostics.Collect(fs, instance, diagnostics.NewOptions(logSince), c, &Settings) + }, + } + cmd.Flags().StringVar(&instance, "instance", "", "The instance name.") + cmd.Flags().DurationVar(&logSince, "log-since", 0, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs.") + + return cmd +} diff --git a/pkg/kudoctl/cmd/diagnostics/collectors.go b/pkg/kudoctl/cmd/diagnostics/collectors.go new file mode 100644 index 000000000..750112955 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/collectors.go @@ -0,0 +1,132 @@ +package diagnostics + +import ( + "fmt" + "io" + "path/filepath" + "reflect" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/kudobuilder/kudo/pkg/kudoctl/clog" +) + +// Ensure collector is implemented +var _ collector = &resourceCollector{} + +// resourceCollector - collector interface implementation for Kubernetes resources (runtime objects) +type resourceCollector struct { + loadResourceFn func() (runtime.Object, error) + name string // object kind used to describe the error + parentDir stringGetter // parent dir to attach the printer's output + failOnError bool // define whether the collector should return the error + callback func(runtime.Object) // will be called with the retrieved resource after collection to update shared context + printMode printMode +} + +// collect - load a resource and send either the resource or collection error to printer +// return error if failOnError field is set to true +// if failOnError is true, finding no object(s) is treated as an error +func (c *resourceCollector) collect(printer *nonFailingPrinter) error { + clog.V(4).Printf("Collect Resource %s in parent dir %s", c.name, c.parentDir()) + obj, err := c._collect(c.failOnError) + if err != nil { + printer.printError(err, c.parentDir(), c.name) + if c.failOnError { + return err + } + } + if obj != nil { + printer.printObject(obj, c.parentDir(), c.printMode) + } + return nil +} + +func emptyResult(obj runtime.Object) bool { + return obj == nil || reflect.ValueOf(obj).IsNil() || (meta.IsListType(obj) && meta.LenList(obj) == 0) +} + +func (c *resourceCollector) _collect(failOnError bool) (runtime.Object, error) { + obj, err := c.loadResourceFn() + if err != nil { + return nil, fmt.Errorf("failed to retrieve object(s) of kind %s: %v", c.name, err) + } + if emptyResult(obj) { + if failOnError { + return nil, fmt.Errorf("no object(s) of kind %s retrieved", c.name) + } + return nil, nil + } + if c.callback != nil { + c.callback(obj) + } + return obj, nil +} + +// Ensure collector is implemented +var _ collector = &resourceCollectorGroup{} + +// resourceCollectorGroup - a composite collector for Kubernetes runtime objects whose loading and printing depend on +// each other's side-effects on the shared context +type resourceCollectorGroup struct { + collectors []resourceCollector +} + +// collect - collect resource and run callback for each collector, print all afterwards +// collection failures are treated as fatal regardless of the collectors failOnError flag setting +func (g resourceCollectorGroup) collect(printer *nonFailingPrinter) error { + clog.V(0).Printf("Collect ResourceGroup for %d collectors", len(g.collectors)) + objs := make([]runtime.Object, len(g.collectors)) + for i, c := range g.collectors { + obj, err := c._collect(true) + if err != nil { + printer.printError(err, c.parentDir(), c.name) + return err + } + objs[i] = obj + } + for i, c := range g.collectors { + printer.printObject(objs[i], c.parentDir(), c.printMode) + } + return nil +} + +// Ensure collector is implemented +var _ collector = &logsCollector{} + +type logsCollector struct { + loadLogFn func(string, string) (io.ReadCloser, error) + pods func() []v1.Pod + parentDir stringGetter +} + +func (c *logsCollector) collect(printer *nonFailingPrinter) error { + clog.V(0).Printf("Collect Logs for %d pods", len(c.pods())) + for _, pod := range c.pods() { + for _, container := range pod.Spec.Containers { + log, err := c.loadLogFn(pod.Name, container.Name) + if err != nil { + printer.printError(err, filepath.Join(c.parentDir(), fmt.Sprintf("pod_%s", pod.Name)), fmt.Sprintf("%s.log", container.Name)) + } else { + printer.printLog(log, filepath.Join(c.parentDir(), fmt.Sprintf("pod_%s", pod.Name)), container.Name) + _ = log.Close() + } + } + } + return nil +} + +var _ collector = &objCollector{} + +type objCollector struct { + obj interface{} + parentDir stringGetter + name string +} + +func (c *objCollector) collect(printer *nonFailingPrinter) error { + printer.printYaml(c.obj, c.parentDir(), c.name) + return nil +} diff --git a/pkg/kudoctl/cmd/diagnostics/diagnostics.go b/pkg/kudoctl/cmd/diagnostics/diagnostics.go new file mode 100644 index 000000000..6a4ecaa7e --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/diagnostics.go @@ -0,0 +1,55 @@ +package diagnostics + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/afero" + + "github.com/kudobuilder/kudo/pkg/kudoctl/env" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" + "github.com/kudobuilder/kudo/pkg/version" +) + +type Options struct { + LogSince *int64 +} + +func NewOptions(logSince time.Duration) *Options { + opts := Options{} + if logSince > 0 { + sec := int64(logSince.Round(time.Second).Seconds()) + opts.LogSince = &sec + } + return &opts +} + +func Collect(fs afero.Fs, instance string, options *Options, c *kudo.Client, s *env.Settings) error { + if err := verifyDiagDirNotExists(fs); err != nil { + return err + } + p := &nonFailingPrinter{fs: fs} + + if err := diagForInstance(instance, options, c, version.Get(), s, p); err != nil { + p.errors = append(p.errors, err.Error()) + } + if err := diagForKudoManager(options, c, p); err != nil { + p.errors = append(p.errors, err.Error()) + } + if len(p.errors) > 0 { + return fmt.Errorf(strings.Join(p.errors, "\n")) + } + return nil +} + +func verifyDiagDirNotExists(fs afero.Fs) error { + exists, err := afero.Exists(fs, DiagDir) + if err != nil { + return fmt.Errorf("failed to verify that target directory %s doesn't exist: %v", DiagDir, err) + } + if exists { + return fmt.Errorf("target directory %s already exists", DiagDir) + } + return nil +} diff --git a/pkg/kudoctl/cmd/diagnostics/diagnostics_test.go b/pkg/kudoctl/cmd/diagnostics/diagnostics_test.go new file mode 100644 index 000000000..7c6b39b43 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/diagnostics_test.go @@ -0,0 +1,489 @@ +package diagnostics + +import ( + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1beta1 "k8s.io/api/rbac/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + kubefake "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" + "sigs.k8s.io/yaml" + + "github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1" + "github.com/kudobuilder/kudo/pkg/client/clientset/versioned/fake" + "github.com/kudobuilder/kudo/pkg/kudoctl/env" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" +) + +const ( + fakeNamespace = "my-namespace" + fakeZkInstance = "zookeeper-instance" +) + +const ( + zkOperatorFile = "diag/operator_zookeeper/zookeeper.yaml" + zkOperatorVersionFile = "diag/operator_zookeeper/operatorversion_zookeeper-0.3.0/zookeeper-0.3.0.yaml" + zkPod2File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-2/zookeeper-instance-zookeeper-2.yaml" + zkLog2Container1File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-2/kubernetes-zookeeper.log.gz" + zkServicesFile = "diag/operator_zookeeper/instance_zookeeper-instance/servicelist.yaml" + zkPod0File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-0/zookeeper-instance-zookeeper-0.yaml" + zkLog0Container1File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-0/kubernetes-zookeeper.log.gz" + zkLog0Container2File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-0/pause-debug.log.gz" + zkInstanceFile = "diag/operator_zookeeper/instance_zookeeper-instance/zookeeper-instance.yaml" + zkPod1File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-1/zookeeper-instance-zookeeper-1.yaml" + zkLog1Container1File = "diag/operator_zookeeper/instance_zookeeper-instance/pod_zookeeper-instance-zookeeper-1/kubernetes-zookeeper.log.gz" + zkStatefulSetsFile = "diag/operator_zookeeper/instance_zookeeper-instance/statefulsetlist.yaml" + versionFile = "diag/version.yaml" + kmServicesFile = "diag/kudo/servicelist.yaml" + kmPodFile = "diag/kudo/pod_kudo-controller-manager-0/kudo-controller-manager-0.yaml" + kmLogFile = "diag/kudo/pod_kudo-controller-manager-0/manager.log.gz" + kmServiceAccountsFile = "diag/kudo/serviceaccountlist.yaml" + kmStatefulSetsFile = "diag/kudo/statefulsetlist.yaml" + settingsFile = "diag/settings.yaml" +) + +// defaultFileNames - all the files that should be created if no error happens +func defaultFileNames() map[string]struct{} { + return map[string]struct{}{ + zkOperatorFile: {}, + zkOperatorVersionFile: {}, + zkPod2File: {}, + zkLog2Container1File: {}, + zkServicesFile: {}, + zkPod0File: {}, + zkLog0Container1File: {}, + zkLog0Container2File: {}, + zkInstanceFile: {}, + zkPod1File: {}, + zkLog1Container1File: {}, + zkStatefulSetsFile: {}, + versionFile: {}, + kmServicesFile: {}, + kmPodFile: {}, + kmLogFile: {}, + kmServiceAccountsFile: {}, + kmStatefulSetsFile: {}, + settingsFile: {}, + } +} + +// resource to be loaded into fake clients +var ( + // resource of the instance for which diagnostics is run + pods corev1.PodList + serviceAccounts corev1.ServiceAccountList + services corev1.ServiceList + statefulsets appsv1.StatefulSetList + pvs corev1.PersistentVolumeList + pvcs corev1.PersistentVolumeClaimList + operator v1beta1.Operator + operatorVersion v1beta1.OperatorVersion + instance v1beta1.Instance + + // kudo-manager resources + kmNs corev1.Namespace + kmPod corev1.Pod + kmServices corev1.ServiceList + kmServiceAccounts corev1.ServiceAccountList + kmStatefulsets appsv1.StatefulSetList + + // resources unrelated to the diagnosed instance or kudo-manager, should not be collected + cowPod corev1.Pod + defaultServiceAccount corev1.ServiceAccount + clusterRole rbacv1beta1.ClusterRole +) + +var ( + kubeObjects objectList + kudoObjects objectList +) + +func check(err error) { + if err != nil { + log.Fatalln(err) + } +} + +func assertNilError(t *testing.T) func(error) { + return func(e error) { + assert.Nil(t, e) + } +} + +func mustReadObjectFromYaml(fs afero.Fs, fname string, object runtime.Object, checkFn func(error)) { + b, err := afero.ReadFile(fs, fname) + checkFn(err) + err = yaml.Unmarshal(b, object) + checkFn(err) +} + +type objectList []runtime.Object + +func (l objectList) append(obj runtime.Object) objectList { + if meta.IsListType(obj) { + objs, err := meta.ExtractList(obj) + check(err) + return append(l, objs...) + } + return append(l, obj) +} + +func init() { + osFs := afero.NewOsFs() + mustReadObjectFromYaml(osFs, "testdata/zk_pods.yaml", &pods, check) + mustReadObjectFromYaml(osFs, "testdata/zk_service_accounts.yaml", &serviceAccounts, check) + mustReadObjectFromYaml(osFs, "testdata/zk_services.yaml", &services, check) + mustReadObjectFromYaml(osFs, "testdata/zk_statefulsets.yaml", &statefulsets, check) + mustReadObjectFromYaml(osFs, "testdata/zk_pvs.yaml", &pvs, check) + mustReadObjectFromYaml(osFs, "testdata/zk_pvcs.yaml", &pvcs, check) + mustReadObjectFromYaml(osFs, "testdata/zk_operator.yaml", &operator, check) + mustReadObjectFromYaml(osFs, "testdata/zk_operatorversion.yaml", &operatorVersion, check) + mustReadObjectFromYaml(osFs, "testdata/zk_instance.yaml", &instance, check) + mustReadObjectFromYaml(osFs, "testdata/kudo_ns.yaml", &kmNs, check) + mustReadObjectFromYaml(osFs, "testdata/kudo_pod.yaml", &kmPod, check) + mustReadObjectFromYaml(osFs, "testdata/kudo_services.yaml", &kmServices, check) + mustReadObjectFromYaml(osFs, "testdata/kudo_serviceaccounts.yaml", &kmServiceAccounts, check) + mustReadObjectFromYaml(osFs, "testdata/kudo_statefulsets.yaml", &kmStatefulsets, check) + mustReadObjectFromYaml(osFs, "testdata/cow_pod.yaml", &cowPod, check) + mustReadObjectFromYaml(osFs, "testdata/kudo_default_serviceaccount.yaml", &defaultServiceAccount, check) + mustReadObjectFromYaml(osFs, "testdata/cluster_role.yaml", &clusterRole, check) + + // standard kube objects to be returned by kube clientset + kubeObjects = objectList{}. + append(&pods). + append(&serviceAccounts). + append(&services). + append(&statefulsets). + append(&pvs). + append(&pvcs). + append(&kmNs). + append(&kmPod). + append(&kmServices). + append(&kmServiceAccounts). + append(&kmStatefulsets). + append(&cowPod). + append(&defaultServiceAccount). + append(&clusterRole) + + // kudo custom resources to be returned by kudo clientset + kudoObjects = objectList{}. + append(&operator). + append(&operatorVersion). + append(&instance) +} + +func TestCollect_OK(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + client := kudo.NewClientFromK8s(kcs, k8cs) + + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + assert.Nil(t, err) + + // all files should be present and no other files + fileNames := defaultFileNames() + for name := range fileNames { + exists, _ := afero.Exists(fs, name) + assert.True(t, exists, "file %s not found", name) + } + _ = afero.Walk(fs, "diag", func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + _, ok := fileNames[path] + assert.True(t, ok, "unexpected file: %s", path) + } + return nil + }) + + var ( + collectedPod0 corev1.Pod + collectedPod1 corev1.Pod + collectedPod2 corev1.Pod + collectedKmPod corev1.Pod + collectedServices corev1.ServiceList + collectedStatefulsets appsv1.StatefulSetList + collectedOperator v1beta1.Operator + collectedOperatorVersion v1beta1.OperatorVersion + collectedInstance v1beta1.Instance + collectedKmServices corev1.ServiceList + collectedKmServiceAccounts corev1.ServiceAccountList + collectedKmStatefulsets appsv1.StatefulSetList + ) + + // read the created files and assert no error + mustReadObjectFromYaml(fs, zkOperatorFile, &collectedOperator, assertNilError(t)) + mustReadObjectFromYaml(fs, zkOperatorVersionFile, &collectedOperatorVersion, assertNilError(t)) + mustReadObjectFromYaml(fs, zkPod2File, &collectedPod2, assertNilError(t)) + mustReadObjectFromYaml(fs, zkServicesFile, &collectedServices, assertNilError(t)) + mustReadObjectFromYaml(fs, zkPod0File, &collectedPod0, assertNilError(t)) + mustReadObjectFromYaml(fs, zkInstanceFile, &collectedInstance, assertNilError(t)) + mustReadObjectFromYaml(fs, zkPod1File, &collectedPod1, assertNilError(t)) + mustReadObjectFromYaml(fs, zkStatefulSetsFile, &collectedStatefulsets, assertNilError(t)) + mustReadObjectFromYaml(fs, kmServicesFile, &collectedKmServices, assertNilError(t)) + mustReadObjectFromYaml(fs, kmPodFile, &collectedKmPod, assertNilError(t)) + mustReadObjectFromYaml(fs, kmServiceAccountsFile, &collectedKmServiceAccounts, assertNilError(t)) + mustReadObjectFromYaml(fs, kmStatefulSetsFile, &collectedKmStatefulsets, assertNilError(t)) + + // verify the correctness of the created files by comparison of the objects read from those to the original objects + assert.Equal(t, operator, collectedOperator) + assert.Equal(t, operatorVersion, collectedOperatorVersion) + assert.Equal(t, pods.Items[2], collectedPod2) + assert.Equal(t, services, collectedServices) + assert.Equal(t, pods.Items[0], collectedPod0) + assert.Equal(t, instance, collectedInstance) + assert.Equal(t, pods.Items[1], collectedPod1) + assert.Equal(t, statefulsets, collectedStatefulsets) + assert.Equal(t, kmServices, collectedKmServices) + assert.Equal(t, kmPod, collectedKmPod) + assert.Equal(t, kmServiceAccounts, collectedKmServiceAccounts) + assert.Equal(t, kmStatefulsets, collectedKmStatefulsets) +} + +// Fatal error +func TestCollect_InstanceNotFound(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + + // force kudo clientset to return no operator + reactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + if action.GetNamespace() == fakeNamespace { + return true, nil, nil + } + return + } + kcs.PrependReactor("get", "instances", reactor) + + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + + assert.Error(t, err) + exists, _ := afero.Exists(fs, "diag/instance.err") + assert.True(t, exists) +} + +// Fatal error +func TestCollect_FatalError(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + + // force kudo clientset to return no operator + reactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + if action.GetNamespace() == fakeNamespace { + return true, nil, nil + } + return + } + kcs.PrependReactor("get", "operators", reactor) + + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + + assert.Error(t, err) + exists, _ := afero.Exists(fs, "diag/operator.err") + assert.True(t, exists) +} + +// Fatal error - special case: api server returns "Not Found", api then returns (nil, nil) +func TestCollect_FatalNotFound(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + + // force kudo clientset to return no operator + reactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + if action.GetNamespace() == fakeNamespace { + err := errors.NewNotFound(schema.GroupResource{ + Group: "kudo.dev/v1beta1", + Resource: "operators", + }, "zookeeper") + return true, nil, err + } + return + } + kcs.PrependReactor("get", "operators", reactor) + + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + + assert.Error(t, err) +} + +// Client returns an error retrieving a resource that should not be wrapped into its own dir +// corresponding resource collector has failOnError = false +func TestCollect_NonFatalError(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + + // force kube clientset to return error when retrieving services + reactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + if action.GetNamespace() == fakeNamespace { + return true, nil, errFakeTestError + } + return + } + k8cs.PrependReactor("list", "services", reactor) + + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + + // no error returned, error is saved into the file in place of the corresponding resource file + assert.Nil(t, err) + exists, _ := afero.Exists(fs, zkServicesFile) + assert.False(t, exists) + exists, _ = afero.Exists(fs, "diag/operator_zookeeper/instance_zookeeper-instance/service.err") + assert.True(t, exists) +} + +// Client returns an error retrieving a resource to be printed in its own dir +// corresponding resource collector has failOnError = false +func TestCollect_NonFatalErrorWithDir(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + + // force kube clientset to return error when retrieving pods + reactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + if action.GetNamespace() == fakeNamespace { + return true, nil, errFakeTestError + } + return + } + k8cs.PrependReactor("list", "pods", reactor) + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + + // no error returned, no pods files present, error file present in the directory where otherwise pod dirs would have been + assert.Nil(t, err) + exists, _ := afero.Exists(fs, zkPod2File) + assert.False(t, exists) + exists, _ = afero.Exists(fs, zkPod0File) + assert.False(t, exists) + exists, _ = afero.Exists(fs, zkPod1File) + assert.False(t, exists) + exists, _ = afero.Exists(fs, "diag/operator_zookeeper/instance_zookeeper-instance/pod.err") + assert.True(t, exists) +} + +func TestCollect_KudoNameSpaceNotFound(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + + // force kudo clientset to return no operator + reactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + err = errors.NewNotFound(schema.GroupResource{ + Group: "kudo.dev/v1beta1", + Resource: "namespaces", + }, "kudo-system") + return true, nil, err + } + k8cs.PrependReactor("list", "namespaces", reactor) + + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := &afero.MemMapFs{} + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + + assert.Error(t, err) +} + +func TestCollect_PrintFailure(t *testing.T) { + k8cs := kubefake.NewSimpleClientset(kubeObjects...) + kcs := fake.NewSimpleClientset(kudoObjects...) + client := kudo.NewClientFromK8s(kcs, k8cs) + + a := &afero.MemMapFs{} + fs := &failingFs{Fs: a, failOn: zkPod2File} + + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + assert.Error(t, err) + + // all files should be present except the one failed to be printed, and no other files + fileNames := defaultFileNames() + delete(fileNames, zkPod2File) + + for name := range fileNames { + exists, _ := afero.Exists(fs, name) + assert.True(t, exists, "file %s not found", name) + } + _ = afero.Walk(fs, "diag", func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + _, ok := fileNames[path] + assert.True(t, ok, "unexpected file: %s", path) + } + return nil + }) +} + +func TestCollect_DiagDirExists(t *testing.T) { + k8cs := kubefake.NewSimpleClientset() + kcs := fake.NewSimpleClientset() + client := kudo.NewClientFromK8s(kcs, k8cs) + fs := afero.NewMemMapFs() + _ = fs.Mkdir(DiagDir, 0700) + err := Collect(fs, fakeZkInstance, &Options{}, client, &env.Settings{ + Namespace: fakeNamespace, + }) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("target directory %s already exists", DiagDir), err) +} + +func TestNewOptions(t *testing.T) { + tests := []struct { + desc string + logSince time.Duration + exp int64 + }{ + { + desc: "log-since provided and positive", + logSince: time.Second * 3600, + exp: 3600, + }, + { + desc: "log-since provided and negative", + logSince: time.Second * (-3600), + }, + { + desc: "log-since not provided", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.desc, func(t *testing.T) { + opts := NewOptions(tt.logSince) + assert.True(t, (tt.exp > 0) == (opts.LogSince != nil)) + if tt.exp > 0 { + assert.Equal(t, tt.exp, *opts.LogSince) + } + }) + } +} diff --git a/pkg/kudoctl/cmd/diagnostics/gziphelper.go b/pkg/kudoctl/cmd/diagnostics/gziphelper.go new file mode 100644 index 000000000..224bc8c55 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/gziphelper.go @@ -0,0 +1,37 @@ +package diagnostics + +import ( + "io" + + "compress/gzip" +) + +// streamGzipper - a helper for gzipping a stream +type streamGzipper struct { + w io.Writer +} + +func newGzipWriter(w io.Writer) *streamGzipper { + return &streamGzipper{ + w: w, + } +} + +// write - gzip the provided stream by sequential reads into the underlying bytes buffer and gzipping the bytes +func (z *streamGzipper) write(r io.ReadCloser) error { + zw := gzip.NewWriter(z.w) + var err error + var written int64 + for { + written, err = io.Copy(zw, r) + if err != nil || written == 0 { + _ = zw.Close() + _ = r.Close() + break + } + } + if err == io.EOF { + return nil + } + return err +} diff --git a/pkg/kudoctl/cmd/diagnostics/gziphelper_test.go b/pkg/kudoctl/cmd/diagnostics/gziphelper_test.go new file mode 100644 index 000000000..2d1e50734 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/gziphelper_test.go @@ -0,0 +1,54 @@ +package diagnostics + +import ( + "bytes" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type failingReader struct { + io.ReadCloser +} + +func (r *failingReader) Read(p []byte) (n int, err error) { + n, _ = r.ReadCloser.Read(p) + return n, errFakeTestError +} + +func Test_streamGzipper_write(t *testing.T) { + tests := []struct { + desc string + stream io.ReadCloser + expected string + wantErr bool + }{ + { + desc: "gzip OK", + stream: ioutil.NopCloser(strings.NewReader(testLog)), + expected: testLogGZipped, + wantErr: false, + }, + { + desc: "gzip fails, flush what's read", + stream: &failingReader{ioutil.NopCloser(strings.NewReader(testLog))}, + expected: testLogGZipped, + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.desc, func(t *testing.T) { + var b bytes.Buffer + z := &streamGzipper{ + w: &b, + } + err := z.write(tt.stream) + assert.True(t, err != nil == tt.wantErr) + assert.Equal(t, tt.expected, b.String()) + }) + } +} diff --git a/pkg/kudoctl/cmd/diagnostics/print.go b/pkg/kudoctl/cmd/diagnostics/print.go new file mode 100644 index 000000000..221a1be4d --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/print.go @@ -0,0 +1,150 @@ +package diagnostics + +import ( + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/spf13/afero" + "gopkg.in/yaml.v2" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" +) + +const ( + DiagDir = "diag" + KudoDir = "diag/kudo" +) + +type printMode string + +const ( + ObjectWithDir printMode = "ObjectsWithDir" + ObjectListWithDirs printMode = "ObjectListWithDirs" // print each object into its own nested directory based on its name and kind + RuntimeObject printMode = "RuntimeObject" // print as a file based on its kind only +) + +// nonFailingPrinter - print provided data into provided directory and accumulate errors instead of returning them. +// Creates a nested directory if an object type requires so. +type nonFailingPrinter struct { + fs afero.Fs + errors []string +} + +func (p *nonFailingPrinter) printObject(obj runtime.Object, parentDir string, mode printMode) { + switch mode { + case ObjectWithDir: + if err := printSingleObject(p.fs, obj, parentDir); err != nil { + p.errors = append(p.errors, err.Error()) + } + case ObjectListWithDirs: + err := meta.EachListItem(obj, func(object runtime.Object) error { + if err := printSingleObject(p.fs, object, parentDir); err != nil { + p.errors = append(p.errors, err.Error()) + } + return nil + }) + if err != nil { + p.errors = append(p.errors, err.Error()) + } + case RuntimeObject: + fallthrough + default: + if err := printSingleRuntimeObject(p.fs, obj, parentDir); err != nil { + p.errors = append(p.errors, err.Error()) + } + } +} + +func (p *nonFailingPrinter) printError(err error, parentDir, name string) { + b := []byte(err.Error()) + if err := doPrint(p.fs, byteWriter{b}.write, parentDir, fmt.Sprintf("%s.err", name)); err != nil { + p.errors = append(p.errors, err.Error()) + } +} + +func (p *nonFailingPrinter) printLog(log io.ReadCloser, parentDir, name string) { + if err := doPrint(p.fs, gzipStreamWriter{log}.write, parentDir, fmt.Sprintf("%s.log.gz", name)); err != nil { + p.errors = append(p.errors, err.Error()) + } +} + +func (p *nonFailingPrinter) printYaml(v interface{}, parentDir, name string) { + if err := printYaml(p.fs, v, parentDir, name); err != nil { + p.errors = append(p.errors, err.Error()) + } +} + +// printSingleObject - print a runtime.object assuming it exposes metadata by implementing metav1.object +func printSingleObject(fs afero.Fs, obj runtime.Object, parentDir string) error { + if !isKudoCR(obj) { + err := kudo.SetGVKFromScheme(obj, scheme.Scheme) + if err != nil { + return err + } + } + + o, ok := obj.(metav1.Object) + if !ok { + return fmt.Errorf("invalid print mode: can't get name for %s", strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)) + } + + relToParentDir := fmt.Sprintf("%s_%s", strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind), o.GetName()) + dir := filepath.Join(parentDir, relToParentDir) + name := fmt.Sprintf("%s.yaml", o.GetName()) + return doPrint(fs, objYamlWriter{obj}.write, dir, name) +} + +// printSingleRuntimeObject - print a runtime.Object in the supplied dir. +func printSingleRuntimeObject(fs afero.Fs, obj runtime.Object, dir string) error { + err := kudo.SetGVKFromScheme(obj, scheme.Scheme) + if err != nil { + return err + } + + name := fmt.Sprintf("%s.yaml", strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)) + return doPrint(fs, objYamlWriter{obj}.write, dir, name) +} + +func printYaml(fs afero.Fs, v interface{}, dir, name string) error { + b, err := yaml.Marshal(v) + if err != nil { + return fmt.Errorf("failed to marshal object to %s/%s.yaml: %v", dir, name, err) + } + + name = fmt.Sprintf("%s.yaml", name) + return doPrint(fs, byteWriter{b}.write, dir, name) +} + +func createFile(fs afero.Fs, dir, name string) (afero.File, error) { + err := fs.MkdirAll(dir, 0700) + if err != nil { + return nil, fmt.Errorf("failed to create directory %s: %v", dir, err) + } + + fileWithPath := filepath.Join(dir, name) + file, err := fs.Create(fileWithPath) + if err != nil { + return nil, fmt.Errorf("failed to create %s: %v", fileWithPath, err) + } + return file, nil +} +func doPrint(fs afero.Fs, writeFn func(io.Writer) error, dir, name string) error { + file, err := createFile(fs, dir, name) + if err != nil { + return err + } + defer file.Close() + return writeFn(file) +} + +func isKudoCR(obj runtime.Object) bool { + kind := obj.GetObjectKind().GroupVersionKind().Kind + return kind == "Instance" || kind == "Operator" || kind == "OperatorVersion" +} diff --git a/pkg/kudoctl/cmd/diagnostics/print_test.go b/pkg/kudoctl/cmd/diagnostics/print_test.go new file mode 100644 index 000000000..f39f04462 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/print_test.go @@ -0,0 +1,366 @@ +package diagnostics + +import ( + "fmt" + "io" + "io/ioutil" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1" + "github.com/kudobuilder/kudo/pkg/version" +) + +const ( + pod1Yaml = `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + name: my-fancy-pod-01 + namespace: my-namespace +spec: + containers: + - name: my-fancy-container-01 + resources: {} +status: {} +` + pod2Yaml = `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + name: my-fancy-pod-02 + namespace: my-namespace +spec: + containers: + - name: my-fancy-container-01 + resources: {} +status: {} +` + podListYaml = `apiVersion: v1 +items: +- metadata: + creationTimestamp: null + name: my-fancy-pod-01 + namespace: my-namespace + spec: + containers: + - name: my-fancy-container-01 + resources: {} + status: {} +- metadata: + creationTimestamp: null + name: my-fancy-pod-02 + namespace: my-namespace + spec: + containers: + - name: my-fancy-container-01 + resources: {} + status: {} +kind: PodList +metadata: {} +` + operatorYaml = `apiVersion: kudo.dev/v1beta1 +kind: Operator +metadata: + creationTimestamp: null + name: my-fancy-operator + namespace: my-namespace +spec: {} +status: {} +` + versionYaml = `gitversion: dev +gitcommit: dev +builddate: "1970-01-01T00:00:00Z" +goversion: go1.13.4 +compiler: gc +platform: linux/amd64 +` +) + +var ( + pod1 = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Namespace: fakeNamespace, Name: "my-fancy-pod-01"}, + Spec: v1.PodSpec{ + Containers: []v1.Container{{Name: "my-fancy-container-01"}}, + }, + } + pod2 = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Namespace: fakeNamespace, Name: "my-fancy-pod-02"}, + Spec: v1.PodSpec{ + Containers: []v1.Container{{Name: "my-fancy-container-01"}}, + }, + } + pod3 = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Namespace: fakeNamespace, Name: "my-fancy-pod-03"}, + Spec: v1.PodSpec{ + Containers: []v1.Container{{Name: "my-fancy-container-01"}}, + }, + } +) + +func TestPrinter_printObject(t *testing.T) { + tests := []struct { + desc string + obj runtime.Object + parentDir string + mode printMode + expFiles []string + expData []string + failOn string + }{ + { + desc: "kube object with dir", + obj: &pod1, + parentDir: "root", + mode: ObjectWithDir, + expData: []string{pod1Yaml}, + expFiles: []string{"root/pod_my-fancy-pod-01/my-fancy-pod-01.yaml"}, + }, + { + desc: "kudo object with dir", + obj: &v1beta1.Operator{ + TypeMeta: metav1.TypeMeta{ + Kind: "Operator", + APIVersion: "kudo.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{Namespace: fakeNamespace, Name: "my-fancy-operator"}, + }, + parentDir: "root", + mode: ObjectWithDir, + expData: []string{operatorYaml}, + expFiles: []string{"root/operator_my-fancy-operator/my-fancy-operator.yaml"}, + }, + { + desc: "kube object as runtime object", + obj: &pod1, + parentDir: "root", + mode: RuntimeObject, + expData: []string{pod1Yaml}, + expFiles: []string{"root/pod.yaml"}, + }, + { + desc: "list of objects as runtime object", + obj: &v1.PodList{ + Items: []v1.Pod{ + pod1, + pod2, + }, + }, + parentDir: "root", + mode: RuntimeObject, + expData: []string{podListYaml}, + expFiles: []string{"root/podlist.yaml"}, + }, + { + desc: "list of objects with dirs", + obj: &v1.PodList{ + Items: []v1.Pod{ + pod1, + pod2, + }, + }, + parentDir: "root", + mode: ObjectListWithDirs, + expData: []string{pod1Yaml, pod2Yaml}, + expFiles: []string{"root/pod_my-fancy-pod-01/my-fancy-pod-01.yaml", "root/pod_my-fancy-pod-02/my-fancy-pod-02.yaml"}, + }, + { + desc: "list of objects with dirs, one fails", + obj: &v1.PodList{ + Items: []v1.Pod{ + pod1, + pod2, + pod3, + }, + }, + parentDir: "root", + mode: ObjectListWithDirs, + expData: []string{pod1Yaml, pod2Yaml}, + expFiles: []string{"root/pod_my-fancy-pod-01/my-fancy-pod-01.yaml", "root/pod_my-fancy-pod-02/my-fancy-pod-02.yaml"}, + failOn: "root/pod_my-fancy-pod-03/my-fancy-pod-03.yaml", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.desc, func(t *testing.T) { + wantErr := tt.failOn != "" + var fs = afero.NewMemMapFs() + if wantErr { + fs = &failingFs{failOn: tt.failOn, Fs: fs} + } + p := &nonFailingPrinter{ + fs: fs, + } + p.printObject(tt.obj, tt.parentDir, tt.mode) + for i, fname := range tt.expFiles { + b, err := afero.ReadFile(fs, fname) + assert.Nil(t, err) + assert.Equal(t, tt.expData[i], string(b)) + } + assert.Equal(t, !wantErr, len(p.errors) == 0) + }) + } +} + +func TestPrinter_printError(t *testing.T) { + tests := []struct { + desc string + e error + parentDir string + name string + expFiles []string + expData []string + failOn string + }{ + { + desc: "print error OK", + e: errFakeTestError, + parentDir: "root", + name: "service", + expFiles: []string{"root/service.err"}, + expData: []string{errFakeTestError.Error()}, + }, + { + desc: "print error failure", + e: errFakeTestError, + parentDir: "root", + name: "service", + failOn: "root/service.err", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.desc, func(t *testing.T) { + wantErr := tt.failOn != "" + var fs = afero.NewMemMapFs() + if wantErr { + fs = &failingFs{failOn: tt.failOn, Fs: fs} + } + p := &nonFailingPrinter{ + fs: fs, + } + p.printError(tt.e, tt.parentDir, tt.name) + for i, fname := range tt.expFiles { + b, err := afero.ReadFile(fs, fname) + assert.Nil(t, err) + assert.Equal(t, tt.expData[i], string(b)) + } + assert.Equal(t, !wantErr, len(p.errors) == 0) + }) + } +} + +func TestPrinter_printLog(t *testing.T) { + tests := []struct { + desc string + log io.ReadCloser + parentDir string + podName string + containerName string + expFiles []string + expData []string + failOn string + }{ + { + desc: "print log OK", + log: ioutil.NopCloser(strings.NewReader(testLog)), + parentDir: "root", + podName: "my-fancy-pod-01", + containerName: "my-fancy-container-01", + expFiles: []string{"root/pod_my-fancy-pod-01/my-fancy-container-01.log.gz"}, + expData: []string{testLogGZipped}, + }, + { + desc: "print log failure", + log: ioutil.NopCloser(strings.NewReader(testLog)), + parentDir: "root", + podName: "my-fancy-pod-01", + containerName: "my-fancy-container-01", + failOn: "root/pod_my-fancy-pod-01/my-fancy-container-01.log.gz", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.desc, func(t *testing.T) { + wantErr := tt.failOn != "" + var fs = afero.NewMemMapFs() + if wantErr { + fs = &failingFs{failOn: tt.failOn, Fs: fs} + } + p := &nonFailingPrinter{ + fs: fs, + } + p.printLog(tt.log, filepath.Join(tt.parentDir, fmt.Sprintf("pod_%s", tt.podName)), tt.containerName) + for i, fname := range tt.expFiles { + b, err := afero.ReadFile(fs, fname) + assert.Nil(t, err) + assert.Equal(t, tt.expData[i], string(b)) + } + assert.Equal(t, !wantErr, len(p.errors) == 0) + }) + } +} + +func TestPrinter_printYaml(t *testing.T) { + tests := []struct { + desc string + v interface{} + parentDir string + name string + expFiles []string + expData []string + failOn string + }{ + { + desc: "print Yaml OK", + v: version.Info{ + GitVersion: "dev", + GitCommit: "dev", + BuildDate: "1970-01-01T00:00:00Z", + GoVersion: "go1.13.4", + Compiler: "gc", + Platform: "linux/amd64", + }, + parentDir: "root", + name: "version", + expFiles: []string{"root/version.yaml"}, + expData: []string{versionYaml}, + failOn: "", + }, + { + desc: "print Yaml OK", + v: version.Info{}, + parentDir: "root", + name: "version", + failOn: "root/version.yaml", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + wantErr := tt.failOn != "" + var fs = afero.NewMemMapFs() + if wantErr { + fs = &failingFs{failOn: tt.failOn, Fs: fs} + } + p := &nonFailingPrinter{ + fs: fs, + } + p.printYaml(tt.v, tt.parentDir, tt.name) + for i, fname := range tt.expFiles { + b, err := afero.ReadFile(fs, fname) + assert.Nil(t, err) + assert.Equal(t, tt.expData[i], string(b)) + } + assert.Equal(t, !wantErr, len(p.errors) == 0) + }) + } +} diff --git a/pkg/kudoctl/cmd/diagnostics/processing_context.go b/pkg/kudoctl/cmd/diagnostics/processing_context.go new file mode 100644 index 000000000..ab8fe146f --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/processing_context.go @@ -0,0 +1,57 @@ +package diagnostics + +import ( + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1" +) + +// processingContext - shared data for the resource collectors +// provides property accessors allowing to define a collector before the data it needs is available +// provides update callback functions. callbacks panic if called on a wrong type of runtime.object +type processingContext struct { + root string + opName string + opVersionName string + instanceName string + pods []v1.Pod +} + +func (ctx *processingContext) rootDirectory() string { + return ctx.root +} + +func (ctx *processingContext) operatorDirectory() string { + return fmt.Sprintf("%s/operator_%s", ctx.root, ctx.opName) +} + +func (ctx *processingContext) instanceDirectory() string { + return fmt.Sprintf("%s/instance_%s", ctx.operatorDirectory(), ctx.instanceName) +} + +func (ctx *processingContext) setOperatorNameFromOperatorVersion(obj runtime.Object) { + ctx.opName = obj.(*v1beta1.OperatorVersion).Spec.Operator.Name +} + +func (ctx *processingContext) setOperatorVersionNameFromInstance(obj runtime.Object) { + ctx.opVersionName = obj.(*v1beta1.Instance).Spec.OperatorVersion.Name +} + +func (ctx *processingContext) setPods(o runtime.Object) { + ctx.pods = o.(*v1.PodList).Items +} + +func (ctx *processingContext) operatorVersionName() string { + return ctx.opVersionName +} + +func (ctx *processingContext) operatorName() string { + return ctx.opName +} + +func (ctx *processingContext) podList() []v1.Pod { + return ctx.pods +} diff --git a/pkg/kudoctl/cmd/diagnostics/resource_funcs.go b/pkg/kudoctl/cmd/diagnostics/resource_funcs.go new file mode 100644 index 000000000..37eb0f28e --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/resource_funcs.go @@ -0,0 +1,144 @@ +package diagnostics + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "reflect" + + "github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1" + "github.com/kudobuilder/kudo/pkg/kudoctl/env" + "github.com/kudobuilder/kudo/pkg/kudoctl/kudoinit" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" + kudoutil "github.com/kudobuilder/kudo/pkg/util/kudo" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" +) + +// resourceFuncsConfig - a wrapper for Kube and Kudo clients and common invocation parameters +// for loading Kube and Kudo resources +type resourceFuncsConfig struct { + c *kudo.Client + ns string + instanceObj *v1beta1.Instance + opts metav1.ListOptions + logOpts corev1.PodLogOptions +} + +// newInstanceResources is a configuration for instance-related resources +func newInstanceResources(instanceName string, options *Options, c *kudo.Client, s *env.Settings) (*resourceFuncsConfig, error) { + instance, err := c.GetInstance(instanceName, s.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get instance %s/%s: %v", s.Namespace, instanceName, err) + } + if instance == nil { + return nil, fmt.Errorf("instance %s/%s not found", s.Namespace, instanceName) + } + return &resourceFuncsConfig{ + c: c, + ns: s.Namespace, + instanceObj: instance, + opts: metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", kudoutil.OperatorLabel, instance.Labels[kudoutil.OperatorLabel])}, + logOpts: corev1.PodLogOptions{SinceSeconds: options.LogSince}, + }, nil +} + +// newKudoResources is a configuration for Kudo controller related resources +// panics if used to load Kudo CRDs (e.g. instance etc.) +func newKudoResources(options *Options, c *kudo.Client) (*resourceFuncsConfig, error) { + opts := metav1.ListOptions{LabelSelector: fmt.Sprintf("app=%s", kudoinit.DefaultKudoLabel)} + ns, err := c.KubeClientset.CoreV1().Namespaces().List(opts) + if err != nil { + return nil, fmt.Errorf("failed to get kudo system namespace: %v", err) + } + if ns == nil || len(ns.Items) == 0 { + return nil, fmt.Errorf("kudo system namespace not found") + } + return &resourceFuncsConfig{ + c: c, + ns: ns.Items[0].Name, + opts: opts, + logOpts: corev1.PodLogOptions{SinceSeconds: options.LogSince}, + }, nil +} + +type stringGetter func() string + +func (r *resourceFuncsConfig) instance() (runtime.Object, error) { + return r.instanceObj, nil +} + +func (r *resourceFuncsConfig) operatorVersion(name stringGetter) func() (runtime.Object, error) { + return func() (runtime.Object, error) { + return r.c.GetOperatorVersion(name(), r.ns) + } +} + +func (r *resourceFuncsConfig) operator(name stringGetter) func() (runtime.Object, error) { + return func() (runtime.Object, error) { + return r.c.GetOperator(name(), r.ns) + } +} + +func (r *resourceFuncsConfig) deployments() (runtime.Object, error) { + obj, err := r.c.KubeClientset.AppsV1().Deployments(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) pods() (runtime.Object, error) { + obj, err := r.c.KubeClientset.CoreV1().Pods(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) services() (runtime.Object, error) { + obj, err := r.c.KubeClientset.CoreV1().Services(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) replicaSets() (runtime.Object, error) { + obj, err := r.c.KubeClientset.AppsV1().ReplicaSets(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) statefulSets() (runtime.Object, error) { + obj, err := r.c.KubeClientset.AppsV1().StatefulSets(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) serviceAccounts() (runtime.Object, error) { + obj, err := r.c.KubeClientset.CoreV1().ServiceAccounts(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) clusterRoleBindings() (runtime.Object, error) { + obj, err := r.c.KubeClientset.RbacV1().ClusterRoleBindings().List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) roleBindings() (runtime.Object, error) { + obj, err := r.c.KubeClientset.RbacV1().RoleBindings(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) clusterRoles() (runtime.Object, error) { + obj, err := r.c.KubeClientset.RbacV1().ClusterRoles().List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) roles() (runtime.Object, error) { + obj, err := r.c.KubeClientset.RbacV1().Roles(r.ns).List(r.opts) + return obj, err +} + +func (r *resourceFuncsConfig) log(podName, containerName string) (io.ReadCloser, error) { + req := r.c.KubeClientset.CoreV1().Pods(r.ns).GetLogs(podName, &corev1.PodLogOptions{SinceSeconds: r.logOpts.SinceSeconds, Container: containerName}) + // a hack for tests: fake client returns rest.Request{} for GetLogs and Stream panics with null-pointer + if reflect.DeepEqual(*req, rest.Request{}) { + return ioutil.NopCloser(&bytes.Buffer{}), nil + } + return req.Stream() +} diff --git a/pkg/kudoctl/cmd/diagnostics/runner.go b/pkg/kudoctl/cmd/diagnostics/runner.go new file mode 100644 index 000000000..1e15b38c8 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/runner.go @@ -0,0 +1,33 @@ +package diagnostics + +// collector - generic interface for diagnostic data collection +// implementors are expected to return only fatal errors and handle non-fatal ones themselves +type collector interface { + collect(printer *nonFailingPrinter) error +} + +// runner - sequential runner for Collectors reducing error checking boilerplate code +type runner struct { + collectors []collector +} + +func (r *runner) addCollector(c collector) { + r.collectors = append(r.collectors, c) +} + +func (r *runner) addObjDump(v interface{}, dir stringGetter, name string) { + r.addCollector(&objCollector{ + obj: v, + parentDir: dir, + name: name, + }) +} + +func (r *runner) run(printer *nonFailingPrinter) error { + for _, c := range r.collectors { + if err := c.collect(printer); err != nil { + return err + } + } + return nil +} diff --git a/pkg/kudoctl/cmd/diagnostics/runner_helper.go b/pkg/kudoctl/cmd/diagnostics/runner_helper.go new file mode 100644 index 000000000..55c08fe77 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/runner_helper.go @@ -0,0 +1,167 @@ +package diagnostics + +import ( + "github.com/kudobuilder/kudo/pkg/kudoctl/env" + "github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo" + "github.com/kudobuilder/kudo/pkg/version" +) + +func diagForInstance(instance string, options *Options, c *kudo.Client, info version.Info, s *env.Settings, p *nonFailingPrinter) error { + ir, err := newInstanceResources(instance, options, c, s) + if err != nil { + p.printError(err, DiagDir, "instance") + return err + } + + ctx := &processingContext{root: DiagDir, instanceName: instance} + + runner := runnerForInstance(ir, ctx) + runner.addObjDump(info, ctx.rootDirectory, "version") + runner.addObjDump(s, ctx.rootDirectory, "settings") + + if err := runner.run(p); err != nil { + return err + } + + return nil +} + +func diagForKudoManager(options *Options, c *kudo.Client, p *nonFailingPrinter) error { + kr, err := newKudoResources(options, c) + if err != nil { + return err + } + ctx := &processingContext{root: KudoDir} + + runner := runnerForKudoManager(kr, ctx) + + if err := runner.run(p); err != nil { + return err + } + + return nil +} + +func runnerForInstance(ir *resourceFuncsConfig, ctx *processingContext) *runner { + r := &runner{} + + instance := resourceCollectorGroup{[]resourceCollector{ + { + loadResourceFn: ir.instance, + name: "instance", + parentDir: ctx.operatorDirectory, + failOnError: true, + callback: ctx.setOperatorVersionNameFromInstance, + printMode: ObjectWithDir}, + { + loadResourceFn: ir.operatorVersion(ctx.operatorVersionName), + name: "operatorversion", + parentDir: ctx.operatorDirectory, + failOnError: true, + callback: ctx.setOperatorNameFromOperatorVersion, + printMode: ObjectWithDir}, + { + loadResourceFn: ir.operator(ctx.operatorName), + name: "operator", + parentDir: ctx.rootDirectory, + failOnError: true, + printMode: ObjectWithDir}, + }} + r.addCollector(instance) + + r.addCollector(&resourceCollector{ + loadResourceFn: ir.pods, + name: "pod", + parentDir: ctx.instanceDirectory, + callback: ctx.setPods, + printMode: ObjectListWithDirs}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.services, + name: "service", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.deployments, + name: "deployment", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.statefulSets, + name: "statefulset", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.replicaSets, + name: "replicaset", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.statefulSets, + name: "statefulset", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.serviceAccounts, + name: "serviceaccount", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.clusterRoleBindings, + name: "clusterrolebinding", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.roleBindings, + name: "rolebinding", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.clusterRoles, + name: "clusterrole", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: ir.roles, + name: "role", + parentDir: ctx.instanceDirectory, + printMode: RuntimeObject}) + r.addCollector(&logsCollector{ + loadLogFn: ir.log, + pods: ctx.podList, + parentDir: ctx.instanceDirectory, + }) + + return r +} + +func runnerForKudoManager(kr *resourceFuncsConfig, ctx *processingContext) *runner { + r := &runner{} + + r.addCollector(&resourceCollector{ + loadResourceFn: kr.pods, + name: "pod", + parentDir: ctx.rootDirectory, + callback: ctx.setPods, + printMode: ObjectListWithDirs}) + r.addCollector(&resourceCollector{ + loadResourceFn: kr.services, + name: "service", + parentDir: ctx.rootDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: kr.statefulSets, + name: "statefulset", + parentDir: ctx.rootDirectory, + printMode: RuntimeObject}) + r.addCollector(&resourceCollector{ + loadResourceFn: kr.serviceAccounts, + name: "serviceaccount", + parentDir: ctx.rootDirectory, + printMode: RuntimeObject}) + r.addCollector(&logsCollector{ + loadLogFn: kr.log, + pods: ctx.podList, + parentDir: ctx.rootDirectory}) + + return r +} diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/cluster_role.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/cluster_role.yaml new file mode 100644 index 000000000..bcedb660e --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/cluster_role.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + creationTimestamp: "2020-04-17T13:11:57Z" + labels: + kubernetes.io/bootstrapping: rbac-defaults + name: cluster-admin + resourceVersion: "44" + selfLink: /apis/rbac.authorization.k8s.io/v1/clusterroles/cluster-admin + uid: 00d4e229-e4ce-43f4-94e9-1c2930af13e3 +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- nonResourceURLs: + - '*' + verbs: + - '*' diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/cow_pod.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/cow_pod.yaml new file mode 100644 index 000000000..7b331f8cf --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/cow_pod.yaml @@ -0,0 +1,111 @@ +apiVersion: v1 +kind: Pod +metadata: + annotations: + kudo.dev/last-plan-execution-uid: 1dc8b21d-c3b5-4136-83d0-ae51b033dbb6 + kudo.dev/operator-version: 0.2.0 + kudo.dev/phase: main + kudo.dev/plan: deploy + kudo.dev/step: app + creationTimestamp: "2020-04-28T09:13:54Z" + generateName: cowsay-instance-deployment-6bb9f8dfd6- + labels: + app: nginx + heritage: kudo + kudo.dev/instance: cowsay-instance + kudo.dev/operator: cowsay + pod-template-hash: 6bb9f8dfd6 + name: cowsay-instance-deployment-6bb9f8dfd6-5cm72 + namespace: my-namespace + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: ReplicaSet + name: cowsay-instance-deployment-6bb9f8dfd6 + uid: 4c46f7a2-3571-4047-9a15-be5043c6b357 + resourceVersion: "1978664" + selfLink: /api/v1/namespaces/my-namespace/pods/cowsay-instance-deployment-6bb9f8dfd6-5cm72 + uid: b46ca902-4683-4c0e-ac9b-7b472704eaa1 +spec: + containers: + - image: nginx:1.7.9 + imagePullPolicy: IfNotPresent + name: nginx + ports: + - containerPort: 80 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /usr/share/nginx/html/ + name: www + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-wl85x + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + nodeName: node007 + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: default + serviceAccountName: default + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - configMap: + defaultMode: 420 + name: cowsayinstance.deploy.main.genfiles.genwww.indexhtml + name: www + - name: default-token-wl85x + secret: + defaultMode: 420 + secretName: default-token-wl85x +status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2020-04-28T09:13:54Z" + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: "2020-04-28T09:14:25Z" + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: "2020-04-28T09:14:25Z" + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: "2020-04-28T09:13:54Z" + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://f31fa7ee23310fa7947f67b15b8266435bb952b83529efb4e2caa4e37bb6a072 + image: nginx:1.7.9 + imageID: docker-pullable://nginx@sha256:e3456c851a152494c3e4ff5fcc26f240206abac0c9d794affb40e0714846c451 + lastState: {} + name: nginx + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2020-04-28T09:14:24Z" + hostIP: 192.168.0.107 + phase: Running + podIP: 172.17.0.9 + podIPs: + - ip: 172.17.0.9 + qosClass: BestEffort + startTime: "2020-04-28T09:13:54Z" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/kudo_default_serviceaccount.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_default_serviceaccount.yaml new file mode 100644 index 000000000..b59c55590 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_default_serviceaccount.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + creationTimestamp: "2020-04-23T13:36:40Z" + name: default + namespace: kudo-system + resourceVersion: "1070689" + selfLink: /api/v1/namespaces/kudo-system/serviceaccounts/default + uid: 565ae57e-212b-4c2b-a5c7-d35878d89af2 +secrets: +- name: default-token-h9q84 diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/kudo_ns.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_ns.yaml new file mode 100644 index 000000000..e6fc29cf1 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_ns.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"app":"kudo-manager"},"name":"kudo-system"},"spec":{},"status":{}} + creationTimestamp: "2020-04-23T13:36:40Z" + labels: + app: kudo-manager + name: kudo-system + resourceVersion: "1070680" + selfLink: /api/v1/namespaces/kudo-system + uid: b7daf244-5c12-47c5-a542-3aa107ef824c +spec: + finalizers: + - kubernetes +status: + phase: Active diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/kudo_pod.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_pod.yaml new file mode 100644 index 000000000..3d25bdd73 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_pod.yaml @@ -0,0 +1,116 @@ +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2020-04-23T13:36:40Z" + generateName: kudo-controller-manager- + labels: + app: kudo-manager + control-plane: controller-manager + controller-revision-hash: kudo-controller-manager-786d748586 + statefulset.kubernetes.io/pod-name: kudo-controller-manager-0 + name: kudo-controller-manager-0 + namespace: kudo-system + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: StatefulSet + name: kudo-controller-manager + uid: 625d4b4c-bffe-435c-a790-5a0a7a9f4b90 + resourceVersion: "1070814" + selfLink: /api/v1/namespaces/kudo-system/pods/kudo-controller-manager-0 + uid: 4db842c1-bd1f-4d80-8a68-1dd8d4fef4f5 +spec: + containers: + - command: + - /root/manager + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: SECRET_NAME + value: kudo-webhook-server-secret + - name: ENABLE_WEBHOOKS + value: "false" + image: kudobuilder/controller:v0.11.1 + imagePullPolicy: Always + name: manager + ports: + - containerPort: 443 + name: webhook-server + protocol: TCP + resources: + requests: + cpu: 100m + memory: 50Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kudo-manager-token-jk2t8 + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + hostname: kudo-controller-manager-0 + nodeName: node007 + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: kudo-manager + serviceAccountName: kudo-manager + subdomain: kudo-controller-manager-service + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - name: kudo-manager-token-jk2t8 + secret: + defaultMode: 420 + secretName: kudo-manager-token-jk2t8 +status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2020-04-23T13:36:40Z" + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: "2020-04-23T13:37:28Z" + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: "2020-04-23T13:37:28Z" + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: "2020-04-23T13:36:40Z" + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://5d2956670949ffce56df690653a20e4952b0c18e23a379f9fdf3bbc224b25e9c + image: kudobuilder/controller:v0.11.1 + imageID: docker-pullable://kudobuilder/controller@sha256:8bf5ba91dac87ca3af1246b16fc191b2f33917a55831c310e28067431a88967a + lastState: {} + name: manager + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2020-04-23T13:37:28Z" + hostIP: 192.168.0.107 + phase: Running + podIP: 172.17.0.4 + podIPs: + - ip: 172.17.0.4 + qosClass: Burstable + startTime: "2020-04-23T13:36:40Z" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/kudo_serviceaccounts.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_serviceaccounts.yaml new file mode 100644 index 000000000..91cfe74b7 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_serviceaccounts.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"app":"kudo-manager"},"name":"kudo-manager","namespace":"kudo-system"}} + creationTimestamp: "2020-04-23T13:36:40Z" + labels: + app: kudo-manager + name: kudo-manager + namespace: kudo-system + resourceVersion: "1070690" + selfLink: /api/v1/namespaces/kudo-system/serviceaccounts/kudo-manager + uid: 9e5950e3-5492-4436-8b4e-d99cdb17c166 + secrets: + - name: kudo-manager-token-jk2t8 +kind: ServiceAccountList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/kudo_services.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_services.yaml new file mode 100644 index 000000000..76a57328b --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_services.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: Service + metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"app":"kudo-manager","control-plane":"controller-manager"},"name":"kudo-controller-manager-service","namespace":"kudo-system"},"spec":{"ports":[{"name":"kudo","port":443,"targetPort":"webhook-server"}],"selector":{"app":"kudo-manager","control-plane":"controller-manager"}},"status":{"loadBalancer":{}}} + creationTimestamp: "2020-04-23T13:36:40Z" + labels: + app: kudo-manager + control-plane: controller-manager + name: kudo-controller-manager-service + namespace: kudo-system + resourceVersion: "1070688" + selfLink: /api/v1/namespaces/kudo-system/services/kudo-controller-manager-service + uid: cf82a3e2-42e3-466e-9474-ba27e7a0bfa7 + spec: + clusterIP: 10.109.220.9 + ports: + - name: kudo + port: 443 + protocol: TCP + targetPort: webhook-server + selector: + app: kudo-manager + control-plane: controller-manager + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +kind: ServiceList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/kudo_statefulsets.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_statefulsets.yaml new file mode 100644 index 000000000..ea5215113 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/kudo_statefulsets.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +items: +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"apps/v1","kind":"StatefulSet","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"app":"kudo-manager","control-plane":"controller-manager"},"name":"kudo-controller-manager","namespace":"kudo-system"},"spec":{"selector":{"matchLabels":{"app":"kudo-manager","control-plane":"controller-manager"}},"serviceName":"kudo-controller-manager-service","template":{"metadata":{"creationTimestamp":null,"labels":{"app":"kudo-manager","control-plane":"controller-manager"}},"spec":{"containers":[{"command":["/root/manager"],"env":[{"name":"POD_NAMESPACE","valueFrom":{"fieldRef":{"fieldPath":"metadata.namespace"}}},{"name":"SECRET_NAME","value":"kudo-webhook-server-secret"},{"name":"ENABLE_WEBHOOKS","value":"false"}],"image":"kudobuilder/controller:v0.11.1","imagePullPolicy":"Always","name":"manager","ports":[{"containerPort":443,"name":"webhook-server","protocol":"TCP"}],"resources":{"requests":{"cpu":"100m","memory":"50Mi"}}}],"serviceAccountName":"kudo-manager","terminationGracePeriodSeconds":10}},"updateStrategy":{}},"status":{"replicas":0}} + creationTimestamp: "2020-04-23T13:36:40Z" + generation: 1 + labels: + app: kudo-manager + control-plane: controller-manager + name: kudo-controller-manager + namespace: kudo-system + resourceVersion: "1070816" + selfLink: /apis/apps/v1/namespaces/kudo-system/statefulsets/kudo-controller-manager + uid: 625d4b4c-bffe-435c-a790-5a0a7a9f4b90 + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: kudo-manager + control-plane: controller-manager + serviceName: kudo-controller-manager-service + template: + metadata: + creationTimestamp: null + labels: + app: kudo-manager + control-plane: controller-manager + spec: + containers: + - command: + - /root/manager + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: SECRET_NAME + value: kudo-webhook-server-secret + - name: ENABLE_WEBHOOKS + value: "false" + image: kudobuilder/controller:v0.11.1 + imagePullPolicy: Always + name: manager + ports: + - containerPort: 443 + name: webhook-server + protocol: TCP + resources: + requests: + cpu: 100m + memory: 50Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: kudo-manager + serviceAccountName: kudo-manager + terminationGracePeriodSeconds: 10 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + currentRevision: kudo-controller-manager-786d748586 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updateRevision: kudo-controller-manager-786d748586 + updatedReplicas: 1 +kind: StatefulSetList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_instance.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_instance.yaml new file mode 100644 index 000000000..58538205a --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_instance.yaml @@ -0,0 +1,51 @@ +apiVersion: kudo.dev/v1beta1 +kind: Instance +metadata: + annotations: + kudo.dev/last-applied-instance-state: '{"operatorVersion":{"name":"zookeeper-0.3.0"},"planExecution":{}}' + creationTimestamp: "2020-05-18T20:19:10Z" + generation: 1 + labels: + kudo.dev/operator: zookeeper + name: zookeeper-instance + namespace: my-namespace + resourceVersion: "5833004" + selfLink: /apis/kudo.dev/v1beta1/namespaces/my-namespace/instances/zookeeper-instance + uid: 51b95bd7-03fa-4e25-969a-241ef0467712 +spec: + operatorVersion: + name: zookeeper-0.3.0 + planExecution: {} +status: + aggregatedStatus: + status: COMPLETE + planStatus: + deploy: + lastUpdatedTimestamp: "2020-05-18T20:22:02Z" + name: deploy + phases: + - name: zookeeper + status: COMPLETE + steps: + - name: deploy + status: COMPLETE + - name: validation + status: COMPLETE + steps: + - name: validation + status: COMPLETE + - name: cleanup + status: COMPLETE + status: COMPLETE + uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + validation: + name: validation + phases: + - name: connection + status: NEVER_RUN + steps: + - name: connection + status: NEVER_RUN + - name: cleanup + status: NEVER_RUN + status: NEVER_RUN diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_operator.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_operator.yaml new file mode 100644 index 000000000..75d55f95b --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_operator.yaml @@ -0,0 +1,20 @@ +apiVersion: kudo.dev/v1beta1 +kind: Operator +metadata: + creationTimestamp: "2020-05-18T20:19:10Z" + generation: 1 + name: zookeeper + namespace: my-namespace + resourceVersion: "5832427" + selfLink: /apis/kudo.dev/v1beta1/namespaces/my-namespace/operators/zookeeper + uid: 0d60dbf5-86c1-40e4-9a7b-88a030e90eaf +spec: + kubernetesVersion: 1.14.8 + kudoVersion: 0.10.0 + maintainers: + - email: avarkockova@mesosphere.com + name: Alena Varkockova + - email: runyontr@gmail.com + name: Tom Runyon + url: https://zookeeper.apache.org/ +status: {} diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_operatorversion.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_operatorversion.yaml new file mode 100644 index 000000000..b6db76b50 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_operatorversion.yaml @@ -0,0 +1,664 @@ +apiVersion: kudo.dev/v1beta1 +kind: OperatorVersion +metadata: + creationTimestamp: "2020-05-18T20:19:10Z" + generation: 1 + name: zookeeper-0.3.0 + namespace: my-namespace + resourceVersion: "5832428" + selfLink: /apis/kudo.dev/v1beta1/namespaces/my-namespace/operatorversions/zookeeper-0.3.0 + uid: 4dfbef59-705b-4633-b601-8c6e5e8058ad +spec: + appVersion: 3.4.14 + operator: + kind: Operator + name: zookeeper + parameters: + - default: "3" + description: Number of nodes spun up for Zookeeper + displayName: Node Count + name: NODE_COUNT + required: true + - default: 1024Mi + description: Amount of memory to provide to Zookeeper pods + name: MEMORY + required: true + - default: 2048Mi + description: Memory (limit) for the Zookeeper nodes pods. spec.containers[].resources.limits.memory + name: MEM_LIMIT + required: true + - default: 250m + description: Amount of cpu to provide to Zookeeper pods + name: CPUS + required: true + - default: 1000m + description: CPUs (limit) for the Zookeeper nodes pods. spec.containers[].resources.limits.cpu + name: CPUS_LIMIT + required: true + - description: The storage class to be used in volumeClaimTemplates. By default + its not required and the default storage class is used. + name: STORAGE_CLASS + required: false + trigger: not-allowed + - default: 5Gi + description: Disk size for the Zookeeper servers + name: DISK_SIZE + required: true + trigger: not-allowed + - default: "2181" + description: | + The port on which the Zookeeper process will listen for client requests. The default is 2181. + name: CLIENT_PORT + required: true + - default: "2888" + description: | + The port on which the Zookeeper process will listen for requests from other servers in the ensemble. + The default is 2888. + name: SERVER_PORT + required: true + - default: "3888" + description: | + The port on which the Zookeeper process will perform leader election. The default is 3888. + name: ELECTION_PORT + required: true + plans: + deploy: + phases: + - name: zookeeper + steps: + - name: deploy + tasks: + - infra + - app + strategy: parallel + - name: validation + steps: + - name: validation + tasks: + - validation + - name: cleanup + tasks: + - validation-cleanup + strategy: serial + strategy: serial + validation: + phases: + - name: connection + steps: + - name: connection + tasks: + - validation + - name: cleanup + tasks: + - validation-cleanup + strategy: serial + strategy: serial + tasks: + - kind: Apply + name: infra + spec: + resources: + - bootstrap.sh.yaml + - healthcheck.sh.yaml + - services.yaml + - pdb.yaml + - kind: Apply + name: app + spec: + resources: + - statefulset.yaml + - kind: Apply + name: validation + spec: + resources: + - validation.yaml + - kind: Delete + name: validation-cleanup + spec: + resources: + - validation.yaml + - kind: Dummy + name: not-allowed + spec: {} + templates: + bootstrap.sh.yaml: |- + apiVersion: v1 + data: + bootstrap.sh: | + #!/usr/bin/env bash + # Copyright 2017 The Kubernetes 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. + # + # + #Usage: start-zookeeper [OPTIONS] + # Starts a ZooKeeper server based on the supplied options. + # --servers The number of servers in the ensemble. The default + # value is 1. + + # --data_dir The directory where the ZooKeeper process will store its + # snapshots. The default is /var/lib/zookeeper/data. + + # --data_log_dir The directory where the ZooKeeper process will store its + # write ahead log. The default is + # /var/lib/zookeeper/data/log. + + # --conf_dir The directoyr where the ZooKeeper process will store its + # configuration. The default is /opt/zookeeper/conf. + + # --client_port The port on which the ZooKeeper process will listen for + # client requests. The default is 2181. + + # --election_port The port on which the ZooKeeper process will perform + # leader election. The default is 3888. + + # --server_port The port on which the ZooKeeper process will listen for + # requests from other servers in the ensemble. The + # default is 2888. + + # --tick_time The length of a ZooKeeper tick in ms. The default is + # 2000. + + # --init_limit The number of Ticks that an ensemble member is allowed + # to perform leader election. The default is 10. + + # --sync_limit The maximum session timeout that the ensemble will + # allows a client to request. The default is 5. + + # --heap The maximum amount of heap to use. The format is the + # same as that used for the Xmx and Xms parameters to the + # JVM. e.g. --heap=2G. The default is 2G. + + # --max_client_cnxns The maximum number of client connections that the + # ZooKeeper process will accept simultaneously. The + # default is 60. + + # --snap_retain_count The maximum number of snapshots the ZooKeeper process + # will retain if purge_interval is greater than 0. The + # default is 3. + + # --purge_interval The number of hours the ZooKeeper process will wait + # between purging its old snapshots. If set to 0 old + # snapshots will never be purged. The default is 0. + + # --max_session_timeout The maximum time in milliseconds for a client session + # timeout. The default value is 2 * tick time. + + # --min_session_timeout The minimum time in milliseconds for a client session + # timeout. The default value is 20 * tick time. + + # --log_level The log level for the zookeeeper server. Either FATAL, + # ERROR, WARN, INFO, DEBUG. The default is INFO. + + + USER=`whoami` + HOST=`hostname -s` + DOMAIN=`hostname -d` + LOG_LEVEL=INFO + DATA_DIR="/var/lib/zookeeper/data" + DATA_LOG_DIR="/var/lib/zookeeper/log" + LOG_DIR="/logs" + CONF_DIR="/conf" + CLIENT_PORT={{ .Params.CLIENT_PORT }} + SERVER_PORT={{ .Params.SERVER_PORT }} + ELECTION_PORT={{ .Params.ELECTION_PORT }} + TICK_TIME=2000 + INIT_LIMIT=10 + SYNC_LIMIT=5 + HEAP=2G + MAX_CLIENT_CNXNS=60 + SNAP_RETAIN_COUNT=3 + PURGE_INTERVAL=0 + SERVERS=1 + + function print_usage() { + echo "\ + Usage: start-zookeeper [OPTIONS] + Starts a ZooKeeper server based on the supplied options. + --servers The number of servers in the ensemble. The default + value is 1. + --data_dir The directory where the ZooKeeper process will store its + snapshots. The default is /var/lib/zookeeper/data. + --data_log_dir The directory where the ZooKeeper process will store its + write ahead log. The default is + /var/lib/zookeeper/data/log. + --conf_dir The directoyr where the ZooKeeper process will store its + configuration. The default is /opt/zookeeper/conf. + --client_port The port on which the ZooKeeper process will listen for + client requests. The default is 2181. + --election_port The port on which the ZooKeeper process will perform + leader election. The default is 3888. + --server_port The port on which the ZooKeeper process will listen for + requests from other servers in the ensemble. The + default is 2888. + --tick_time The length of a ZooKeeper tick in ms. The default is + 2000. + --init_limit The number of Ticks that an ensemble member is allowed + to perform leader election. The default is 10. + --sync_limit The maximum session timeout that the ensemble will + allows a client to request. The default is 5. + --heap The maximum amount of heap to use. The format is the + same as that used for the Xmx and Xms parameters to the + JVM. e.g. --heap=2G. The default is 2G. + --max_client_cnxns The maximum number of client connections that the + ZooKeeper process will accept simultaneously. The + default is 60. + --snap_retain_count The maximum number of snapshots the ZooKeeper process + will retain if purge_interval is greater than 0. The + default is 3. + --purge_interval The number of hours the ZooKeeper process will wait + between purging its old snapshots. If set to 0 old + snapshots will never be purged. The default is 0. + --max_session_timeout The maximum time in milliseconds for a client session + timeout. The default value is 2 * tick time. + --min_session_timeout The minimum time in milliseconds for a client session + timeout. The default value is 20 * tick time. + --log_level The log level for the zookeeeper server. Either FATAL, + ERROR, WARN, INFO, DEBUG. The default is INFO. + " + } + + function create_data_dirs() { + if [ ! -d $DATA_DIR ]; then + mkdir -p $DATA_DIR + chown -R $USER:$USER $DATA_DIR + fi + + if [ ! -d $DATA_LOG_DIR ]; then + mkdir -p $DATA_LOG_DIR + chown -R $USER:$USER $DATA_LOG_DIR + fi + + if [ ! -d $CONF_DIR ]; then + mkdir -p $CONF_DIR + chown -R $USER:$USER $CONF_DIR + fi + + if [ ! -d $LOG_DIR ]; then + mkdir -p $LOG_DIR + chown -R $USER:$USER $LOG_DIR + fi + if [ ! -f $ID_FILE ] && [ $SERVERS -gt 1 ]; then + echo $MY_ID >> $ID_FILE + fi + } + + function print_servers() { + for (( i=1; i<=$SERVERS; i++ )) + do + echo "server.$i=$NAME-$((i-1)).$DOMAIN:$SERVER_PORT:$ELECTION_PORT" + done + } + + function create_config() { + rm -f $CONFIG_FILE + echo "Zookeeper configuration..." + tee $CONFIG_FILE </dev/null + + clientPort=$CLIENT_PORT + dataDir=$DATA_DIR + dataLogDir=$DATA_LOG_DIR + tickTime=$TICK_TIME + initLimit=$INIT_LIMIT + syncLimit=$SYNC_LIMIT + maxClientCnxns=$MAX_CLIENT_CNXNS + minSessionTimeout=$MIN_SESSION_TIMEOUT + maxSessionTimeout=$MAX_SESSION_TIMEOUT + autopurge.snapRetainCount=$SNAP_RETAIN_COUNT + autopurge.purgeInteval=$PURGE_INTERVAL + + EOF + if [ $SERVERS -gt 1 ]; then + print_servers >> $CONFIG_FILE + fi + cat $CONFIG_FILE >&2 + } + + function create_jvm_props() { + rm -f $JAVA_ENV_FILE + echo "ZOO_LOG_DIR=$LOG_DIR" >> $JAVA_ENV_FILE + echo "JVMFLAGS=\"-Xmx$HEAP -Xms$HEAP\"" >> $JAVA_ENV_FILE + } + + function create_log_props() { + rm -f $LOGGER_PROPS_FILE + echo "Creating ZooKeeper log4j configuration" + tee $LOGGER_PROPS_FILE </dev/null + + zookeeper.root.logger=CONSOLE,ROLLINGFILE + zookeeper.console.threshold=$LOG_LEVEL + zookeeper.log.dir=$LOG_DIR + log4j.rootLogger=\${zookeeper.root.logger} + log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender + log4j.appender.CONSOLE.Threshold=\${zookeeper.console.threshold} + log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout + log4j.appender.CONSOLE.layout.ConversionPattern="%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n" + log4j.appender.ROLLINGFILE.File=\${zookeeper.log.dir}/zookeeper.log + log4j.appender.ROLLINGFILE.MaxFileSize=10MB + log4j.appender.ROLLINGFILE.MaxBackupIndex=10 + + EOF + + } + + optspec=":hv-:" + while getopts "$optspec" optchar; do + + case "${optchar}" in + -) + case "${OPTARG}" in + servers=*) + SERVERS=${OPTARG##*=} + ;; + data_dir=*) + DATA_DIR=${OPTARG##*=} + ;; + data_log_dir=*) + DATA_LOG_DIR=${OPTARG##*=} + ;; + log_dir=*) + LOG_DIR=${OPTARG##*=} + ;; + conf_dir=*) + CONF_DIR=${OPTARG##*=} + ;; + client_port=*) + CLIENT_PORT=${OPTARG##*=} + ;; + election_port=*) + ELECTION_PORT=${OPTARG##*=} + ;; + server_port=*) + SERVER_PORT=${OPTARG##*=} + ;; + tick_time=*) + TICK_TIME=${OPTARG##*=} + ;; + init_limit=*) + INIT_LIMIT=${OPTARG##*=} + ;; + sync_limit=*) + SYNC_LIMIT=${OPTARG##*=} + ;; + heap=*) + HEAP=${OPTARG##*=} + ;; + max_client_cnxns=*) + MAX_CLIENT_CNXNS=${OPTARG##*=} + ;; + snap_retain_count=*) + SNAP_RETAIN_COUNT=${OPTARG##*=} + ;; + purge_interval=*) + PURGE_INTERVAL=${OPTARG##*=} + ;; + max_session_timeout=*) + MAX_SESSION_TIMEOUT=${OPTARG##*=} + ;; + min_session_timeout=*) + MIN_SESSION_TIMEOUT=${OPTARG##*=} + ;; + log_level=*) + LOG_LEVEL=${OPTARG##*=} + ;; + *) + echo "Unknown option --${OPTARG}" >&2 + exit 1 + ;; + esac;; + h) + print_usage + exit + ;; + v) + echo "Parsing option: '-${optchar}'" >&2 + ;; + *) + if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then + echo "Non-option argument: '-${OPTARG}'" >&2 + fi + ;; + esac + done + export PATH=$PATH:$ZOOKEEPERPATH/bin + MIN_SESSION_TIMEOUT=${MIN_SESSION_TIMEOUT:- $((TICK_TIME*2))} + MAX_SESSION_TIMEOUT=${MAX_SESSION_TIMEOUT:- $((TICK_TIME*20))} + ID_FILE="$DATA_DIR/myid" + CONFIG_FILE="$CONF_DIR/zoo.cfg" + LOGGER_PROPS_FILE="$CONF_DIR/log4j.properties" + JAVA_ENV_FILE="$CONF_DIR/java.env" + if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then + NAME=${BASH_REMATCH[1]} + ORD=${BASH_REMATCH[2]} + else + echo "Failed to parse name and ordinal of Pod" + exit 1 + fi + + MY_ID=$((ORD+1)) + + create_data_dirs && create_config && create_jvm_props && create_log_props && exec zkServer.sh start-foreground + + + kind: ConfigMap + metadata: + name: {{ .Name }}-bootstrap + healthcheck.sh.yaml: "apiVersion: v1\ndata:\n healthcheck.sh: |\n #!/usr/bin/env + bash\n # Copyright 2017 The Kubernetes Authors.\n #\n # Licensed under + the Apache License, Version 2.0 (the \"License\");\n # you may not use this + file except in compliance with the License.\n # You may obtain a copy of + the License at\n #\n # http://www.apache.org/licenses/LICENSE-2.0\n + \ #\n # Unless required by applicable law or agreed to in writing, software\n + \ # distributed under the License is distributed on an \"AS IS\" BASIS,\n + \ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n + \ # See the License for the specific language governing permissions and\n + \ # limitations under the License.\n\n # zkOk.sh uses the ruok ZooKeeper + four letter work to determine if the instance\n # is health. The $? variable + will be set to 0 if server responds that it is\n # healthy, or 1 if the server + fails to respond.\n\n OK=$(echo ruok | nc 127.0.0.1 $1)\n if [ \"$OK\" + == \"imok\" ]; then\n \texit 0\n else\n \texit 1\n fi\n\nkind: ConfigMap\nmetadata:\n + \ name: {{ .Name }}-healthcheck" + pdb.yaml: | + apiVersion: policy/v1beta1 + kind: PodDisruptionBudget + metadata: + name: {{ .Name }}-pdb + namespace: {{ .Namespace }} + labels: + app: zookeeper + zookeeper: {{ .Name }} + spec: + selector: + matchLabels: + app: zookeeper + kudo.dev/instance: {{ .Name }} + maxUnavailable: 1 + services.yaml: |- + apiVersion: v1 + kind: Service + metadata: + name: {{ .Name }}-hs + namespace: {{ .Namespace }} + labels: + app: zookeeper + zookeeper: {{ .Name }} + spec: + ports: + - port: {{ .Params.SERVER_PORT }} + name: server + - port: {{ .Params.ELECTION_PORT }} + name: leader-election + clusterIP: None + selector: + app: zookeeper + instance: {{ .Name }} + --- + apiVersion: v1 + kind: Service + metadata: + name: {{ .Name }}-cs + namespace: {{ .Namespace }} + labels: + app: zookeeper + zookeeper: {{ .Name }} + spec: + ports: + - port: {{ .Params.CLIENT_PORT }} + name: client + selector: + app: zookeeper + instance: {{ .Name }} + statefulset.yaml: | + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .Name }}-{{ .OperatorName }} + namespace: {{ .Namespace }} + labels: + zookeeper: {{ .OperatorName }} + app: zookeeper + instance: {{ .Name }} + annotations: + reloader.kudo.dev/auto: "true" + spec: + selector: + matchLabels: + app: zookeeper + zookeeper: {{ .OperatorName }} + instance: {{ .Name }} + serviceName: {{ .Name }}-hs + replicas: {{ .Params.NODE_COUNT }} + updateStrategy: + type: RollingUpdate + podManagementPolicy: Parallel + template: + metadata: + labels: + app: zookeeper + zookeeper: {{ .OperatorName }} + instance: {{ .Name }} + spec: + containers: + - name: kubernetes-zookeeper + imagePullPolicy: Always + image: "zookeeper:3.4.14" + resources: + requests: + memory: {{ .Params.MEMORY }} + cpu: {{ .Params.CPUS }} + limits: + memory: {{ .Params.MEM_LIMIT }} + cpu: {{ .Params.CPUS_LIMIT }} + ports: + - containerPort: {{ .Params.CLIENT_PORT }} + name: client + - containerPort: {{ .Params.SERVER_PORT }} + name: server + - containerPort: {{ .Params.ELECTION_PORT }} + name: leader-election + command: + - sh + - -c + - "ZOOKEEPERPATH=`pwd` /etc/zookeeper/bootstrap.sh \ + --servers={{ .Params.NODE_COUNT }} \ + --data_dir=/var/lib/zookeeper/data \ + --data_log_dir=/logs \ + --conf_dir=/conf \ + --client_port={{ .Params.CLIENT_PORT }} \ + --election_port={{ .Params.ELECTION_PORT }} \ + --server_port={{ .Params.SERVER_PORT }} \ + --tick_time=2000 \ + --init_limit=10 \ + --sync_limit=5 \ + --heap=512M \ + --max_client_cnxns=60 \ + --snap_retain_count=3 \ + --purge_interval=12 \ + --max_session_timeout=40000 \ + --min_session_timeout=4000 \ + --log_level=INFO" + readinessProbe: + exec: + command: + - sh + - -c + - "/etc/healthcheck/healthcheck.sh {{ .Params.CLIENT_PORT }}" + initialDelaySeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - sh + - -c + - "/etc/healthcheck/healthcheck.sh {{ .Params.CLIENT_PORT }}" + initialDelaySeconds: 10 + timeoutSeconds: 5 + periodSeconds: 30 + volumeMounts: + - name: {{ .Name }}-datadir + mountPath: /var/lib/zookeeper + - name: {{ .Name }}-bootstrap + mountPath: /etc/zookeeper + - name: {{ .Name }}-healthcheck + mountPath: /etc/healthcheck + securityContext: + runAsUser: 1000 + fsGroup: 1000 + volumes: + - name: {{ .Name }}-bootstrap + configMap: + name: {{ .Name }}-bootstrap + defaultMode: 0777 + - name: {{ .Name }}-healthcheck + configMap: + name: {{ .Name }}-healthcheck + defaultMode: 0777 + volumeClaimTemplates: + - metadata: + name: {{ .Name }}-datadir + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: {{ .Params.DISK_SIZE }} + {{ if .Params.STORAGE_CLASS }} + storageClassName: {{ .Params.STORAGE_CLASS }} + {{ end }} + validation.yaml: | + apiVersion: batch/v1 + kind: Job + metadata: + name: {{ .Name }}-validation + spec: + template: + metadata: + name: "validation" + spec: + restartPolicy: Never + containers: + - name: kubernetes-zookeeper + imagePullPolicy: Always + image: "zookeeper:3.4.14" + env: + - name: CONN + value: {{ if gt (int .Params.NODE_COUNT) 0 }} {{ .Name }}-zookeeper-0.{{ .Name }}-hs:{{ .Params.CLIENT_PORT }}{{- $root := . -}}{{ range $i, $v := untilStep 1 (int .Params.NODE_COUNT) 1 }},{{ $root.Name }}-zookeeper-{{ $v }}.{{ $root.Name }}-hs:{{ $root.Params.CLIENT_PORT }}{{ end }}{{ end }} + resources: + requests: + memory: "64Mi" + cpu: "0.1" + command: + - bash + - -c + - "until bin/zkCli.sh -server $CONN ls /; do sleep 5; done" + version: 0.3.0 +status: {} diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_pods.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_pods.yaml new file mode 100644 index 000000000..d35af6da9 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_pods.yaml @@ -0,0 +1,530 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: Pod + metadata: + annotations: + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + creationTimestamp: "2020-05-18T20:19:11Z" + generateName: zookeeper-instance-zookeeper- + labels: + app: zookeeper + controller-revision-hash: zookeeper-instance-zookeeper-687d6785d6 + heritage: kudo + instance: zookeeper-instance + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + statefulset.kubernetes.io/pod-name: zookeeper-instance-zookeeper-0 + zookeeper: zookeeper + name: zookeeper-instance-zookeeper-0 + namespace: my-namespace + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: StatefulSet + name: zookeeper-instance-zookeeper + uid: 809fe0e3-c4b9-4540-98ce-b939a5dca6ba + resourceVersion: "5832852" + selfLink: /api/v1/namespaces/my-namespace/pods/zookeeper-instance-zookeeper-0 + uid: 66404584-a29a-414e-b48a-34d0aa275bff + spec: + containers: + - command: + - sh + - -c + - ZOOKEEPERPATH=`pwd` /etc/zookeeper/bootstrap.sh --servers=3 --data_dir=/var/lib/zookeeper/data + --data_log_dir=/logs --conf_dir=/conf --client_port=2181 --election_port=3888 + --server_port=2888 --tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M + --max_client_cnxns=60 --snap_retain_count=3 --purge_interval=12 --max_session_timeout=40000 + --min_session_timeout=4000 --log_level=INFO + image: zookeeper:3.4.14 + imagePullPolicy: Always + livenessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + name: kubernetes-zookeeper + ports: + - containerPort: 2181 + name: client + protocol: TCP + - containerPort: 2888 + name: server + protocol: TCP + - containerPort: 3888 + name: leader-election + protocol: TCP + readinessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: 250m + memory: 1Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/zookeeper + name: zookeeper-instance-datadir + - mountPath: /etc/zookeeper + name: zookeeper-instance-bootstrap + - mountPath: /etc/healthcheck + name: zookeeper-instance-healthcheck + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-wz4h5 + readOnly: true + - args: + - /pause + image: gcr.io/google_containers/pause-amd64:3.0 + imagePullPolicy: IfNotPresent + name: pause-debug + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-8nrb2 + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + nodeName: eprupetw1146 + priority: 0 + restartPolicy: Never + schedulerName: default-scheduler + securityContext: {} + serviceAccount: default + serviceAccountName: default + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - name: zookeeper-instance-datadir + persistentVolumeClaim: + claimName: zookeeper-instance-datadir-zookeeper-instance-zookeeper-0 + - configMap: + defaultMode: 511 + name: zookeeper-instance-bootstrap + name: zookeeper-instance-bootstrap + - configMap: + defaultMode: 511 + name: zookeeper-instance-healthcheck + name: zookeeper-instance-healthcheck + - name: default-token-wz4h5 + secret: + defaultMode: 420 + secretName: default-token-wz4h5 + status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:19:12Z" + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:21:10Z" + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:21:10Z" + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:19:12Z" + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://5e429e6c09ce4e23fe2cd954bb85be7e2a58518d6f88d1e1e4c998fa1d5a1e91 + image: zookeeper:3.4.14 + imageID: docker-pullable://zookeeper@sha256:159c6430a6dd305531c46b87c8dc928927c44c89b4b3eedcab0d4f233611761a + lastState: {} + name: kubernetes-zookeeper + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2020-05-18T20:20:58Z" + hostIP: 192.168.0.107 + phase: Running + podIP: 172.17.0.5 + podIPs: + - ip: 172.17.0.5 + qosClass: Burstable + startTime: "2020-05-18T20:19:12Z" +- apiVersion: v1 + kind: Pod + metadata: + annotations: + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + creationTimestamp: "2020-05-18T20:19:11Z" + generateName: zookeeper-instance-zookeeper- + labels: + app: zookeeper + controller-revision-hash: zookeeper-instance-zookeeper-687d6785d6 + heritage: kudo + instance: zookeeper-instance + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + statefulset.kubernetes.io/pod-name: zookeeper-instance-zookeeper-1 + zookeeper: zookeeper + name: zookeeper-instance-zookeeper-1 + namespace: my-namespace + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: StatefulSet + name: zookeeper-instance-zookeeper + uid: 809fe0e3-c4b9-4540-98ce-b939a5dca6ba + resourceVersion: "5832885" + selfLink: /api/v1/namespaces/my-namespace/pods/zookeeper-instance-zookeeper-1 + uid: ef9745d7-7fbe-404f-9074-78cdcaa34528 + spec: + containers: + - command: + - sh + - -c + - ZOOKEEPERPATH=`pwd` /etc/zookeeper/bootstrap.sh --servers=3 --data_dir=/var/lib/zookeeper/data + --data_log_dir=/logs --conf_dir=/conf --client_port=2181 --election_port=3888 + --server_port=2888 --tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M + --max_client_cnxns=60 --snap_retain_count=3 --purge_interval=12 --max_session_timeout=40000 + --min_session_timeout=4000 --log_level=INFO + image: zookeeper:3.4.14 + imagePullPolicy: Always + livenessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + name: kubernetes-zookeeper + ports: + - containerPort: 2181 + name: client + protocol: TCP + - containerPort: 2888 + name: server + protocol: TCP + - containerPort: 3888 + name: leader-election + protocol: TCP + readinessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: 250m + memory: 1Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/zookeeper + name: zookeeper-instance-datadir + - mountPath: /etc/zookeeper + name: zookeeper-instance-bootstrap + - mountPath: /etc/healthcheck + name: zookeeper-instance-healthcheck + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-wz4h5 + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + hostname: zookeeper-instance-zookeeper-1 + nodeName: node007 + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 1000 + runAsUser: 1000 + serviceAccount: default + serviceAccountName: default + subdomain: zookeeper-instance-hs + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - name: zookeeper-instance-datadir + persistentVolumeClaim: + claimName: zookeeper-instance-datadir-zookeeper-instance-zookeeper-1 + - configMap: + defaultMode: 511 + name: zookeeper-instance-bootstrap + name: zookeeper-instance-bootstrap + - configMap: + defaultMode: 511 + name: zookeeper-instance-healthcheck + name: zookeeper-instance-healthcheck + - name: default-token-wz4h5 + secret: + defaultMode: 420 + secretName: default-token-wz4h5 + status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:19:12Z" + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:21:20Z" + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:21:20Z" + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:19:12Z" + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://0ee34f3a1d2598044c2fbb47f0007d010a76c988219f1c4d548e1233773c4742 + image: zookeeper:3.4.14 + imageID: docker-pullable://zookeeper@sha256:159c6430a6dd305531c46b87c8dc928927c44c89b4b3eedcab0d4f233611761a + lastState: {} + name: kubernetes-zookeeper + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2020-05-18T20:21:02Z" + hostIP: 192.168.0.107 + phase: Running + podIP: 172.17.0.6 + podIPs: + - ip: 172.17.0.6 + qosClass: Burstable + startTime: "2020-05-18T20:19:12Z" +- apiVersion: v1 + kind: Pod + metadata: + annotations: + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + creationTimestamp: "2020-05-18T20:19:11Z" + generateName: zookeeper-instance-zookeeper- + labels: + app: zookeeper + controller-revision-hash: zookeeper-instance-zookeeper-687d6785d6 + heritage: kudo + instance: zookeeper-instance + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + statefulset.kubernetes.io/pod-name: zookeeper-instance-zookeeper-2 + zookeeper: zookeeper + name: zookeeper-instance-zookeeper-2 + namespace: my-namespace + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: StatefulSet + name: zookeeper-instance-zookeeper + uid: 809fe0e3-c4b9-4540-98ce-b939a5dca6ba + resourceVersion: "5832875" + selfLink: /api/v1/namespaces/my-namespace/pods/zookeeper-instance-zookeeper-2 + uid: ff1bd469-a89d-4185-9d98-6335c36d0152 + spec: + containers: + - command: + - sh + - -c + - ZOOKEEPERPATH=`pwd` /etc/zookeeper/bootstrap.sh --servers=3 --data_dir=/var/lib/zookeeper/data + --data_log_dir=/logs --conf_dir=/conf --client_port=2181 --election_port=3888 + --server_port=2888 --tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M + --max_client_cnxns=60 --snap_retain_count=3 --purge_interval=12 --max_session_timeout=40000 + --min_session_timeout=4000 --log_level=INFO + image: zookeeper:3.4.14 + imagePullPolicy: Always + livenessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + name: kubernetes-zookeeper + ports: + - containerPort: 2181 + name: client + protocol: TCP + - containerPort: 2888 + name: server + protocol: TCP + - containerPort: 3888 + name: leader-election + protocol: TCP + readinessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: 250m + memory: 1Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/zookeeper + name: zookeeper-instance-datadir + - mountPath: /etc/zookeeper + name: zookeeper-instance-bootstrap + - mountPath: /etc/healthcheck + name: zookeeper-instance-healthcheck + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-wz4h5 + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + hostname: zookeeper-instance-zookeeper-2 + nodeName: node007 + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 1000 + runAsUser: 1000 + serviceAccount: default + serviceAccountName: default + subdomain: zookeeper-instance-hs + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - name: zookeeper-instance-datadir + persistentVolumeClaim: + claimName: zookeeper-instance-datadir-zookeeper-instance-zookeeper-2 + - configMap: + defaultMode: 511 + name: zookeeper-instance-bootstrap + name: zookeeper-instance-bootstrap + - configMap: + defaultMode: 511 + name: zookeeper-instance-healthcheck + name: zookeeper-instance-healthcheck + - name: default-token-wz4h5 + secret: + defaultMode: 420 + secretName: default-token-wz4h5 + status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:19:15Z" + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:21:18Z" + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:21:18Z" + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: "2020-05-18T20:19:15Z" + status: "True" + type: PodScheduled + containerStatuses: + - containerID: docker://a11d63947ca231c2c75f1577ba2ff6e84bf82a9af2ee038a82809e11de014da9 + image: zookeeper:3.4.14 + imageID: docker-pullable://zookeeper@sha256:159c6430a6dd305531c46b87c8dc928927c44c89b4b3eedcab0d4f233611761a + lastState: {} + name: kubernetes-zookeeper + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2020-05-18T20:21:07Z" + hostIP: 192.168.0.107 + phase: Running + podIP: 172.17.0.7 + podIPs: + - ip: 172.17.0.7 + qosClass: Burstable + startTime: "2020-05-18T20:19:15Z" +kind: PodList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_pvcs.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_pvcs.yaml new file mode 100644 index 000000000..704cf703f --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_pvcs.yaml @@ -0,0 +1,120 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"4e1b1a70-839b-11ea-84a3-b00cd12cc2cf","leaseDurationSeconds":15,"acquireTime":"2020-04-23T13:37:29Z","renewTime":"2020-04-23T13:38:00Z","leaderTransitions":0}' + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.beta.kubernetes.io/storage-provisioner: k8s.io/minikube-hostpath + creationTimestamp: "2020-04-23T13:37:29Z" + finalizers: + - kubernetes.io/pvc-protection + labels: + app: zookeeper + heritage: kudo + instance: zk + kudo.dev/instance: zk + kudo.dev/operator: zookeeper + zookeeper: zookeeper + name: zk-datadir-zk-zookeeper-0 + namespace: default + resourceVersion: "1071019" + selfLink: /api/v1/namespaces/default/persistentvolumeclaims/zk-datadir-zk-zookeeper-0 + uid: db46bc7b-da33-4f01-8f37-e69f5e7867ac + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-db46bc7b-da33-4f01-8f37-e69f5e7867ac + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 5Gi + phase: Bound +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"4e1b1a70-839b-11ea-84a3-b00cd12cc2cf","leaseDurationSeconds":15,"acquireTime":"2020-04-23T13:37:29Z","renewTime":"2020-04-23T13:38:01Z","leaderTransitions":0}' + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.beta.kubernetes.io/storage-provisioner: k8s.io/minikube-hostpath + creationTimestamp: "2020-04-23T13:37:29Z" + finalizers: + - kubernetes.io/pvc-protection + labels: + app: zookeeper + heritage: kudo + instance: zk + kudo.dev/instance: zk + kudo.dev/operator: zookeeper + zookeeper: zookeeper + name: zk-datadir-zk-zookeeper-1 + namespace: default + resourceVersion: "1071020" + selfLink: /api/v1/namespaces/default/persistentvolumeclaims/zk-datadir-zk-zookeeper-1 + uid: 347fbfd2-e2f6-42f1-a1be-458aa34a32f4 + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-347fbfd2-e2f6-42f1-a1be-458aa34a32f4 + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 5Gi + phase: Bound +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"4e1b1a70-839b-11ea-84a3-b00cd12cc2cf","leaseDurationSeconds":15,"acquireTime":"2020-04-23T13:37:29Z","renewTime":"2020-04-23T13:38:01Z","leaderTransitions":0}' + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.beta.kubernetes.io/storage-provisioner: k8s.io/minikube-hostpath + creationTimestamp: "2020-04-23T13:37:29Z" + finalizers: + - kubernetes.io/pvc-protection + labels: + app: zookeeper + heritage: kudo + instance: zk + kudo.dev/instance: zk + kudo.dev/operator: zookeeper + zookeeper: zookeeper + name: zk-datadir-zk-zookeeper-2 + namespace: default + resourceVersion: "1071021" + selfLink: /api/v1/namespaces/default/persistentvolumeclaims/zk-datadir-zk-zookeeper-2 + uid: c0dbb58d-70af-4e86-979c-9bab2aa1578c + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-c0dbb58d-70af-4e86-979c-9bab2aa1578c + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 5Gi + phase: Bound +kind: PersistentVolumeClaimList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_pvs.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_pvs.yaml new file mode 100644 index 000000000..37f52ba38 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_pvs.yaml @@ -0,0 +1,105 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: PersistentVolume + metadata: + annotations: + hostPathProvisionerIdentity: 4e1b198e-839b-11ea-84a3-b00cd12cc2cf + pv.kubernetes.io/provisioned-by: k8s.io/minikube-hostpath + creationTimestamp: "2020-05-18T20:19:11Z" + finalizers: + - kubernetes.io/pv-protection + name: pvc-37800340-a87f-4607-896e-da091174def7 + resourceVersion: "5832498" + selfLink: /api/v1/persistentvolumes/pvc-37800340-a87f-4607-896e-da091174def7 + uid: 68b866d7-6284-4aab-bbd5-3e447a4844b7 + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 5Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: zookeeper-instance-datadir-zookeeper-instance-zookeeper-1 + namespace: my-namespace + resourceVersion: "5832449" + uid: 37800340-a87f-4607-896e-da091174def7 + hostPath: + path: /tmp/hostpath-provisioner/pvc-37800340-a87f-4607-896e-da091174def7 + type: "" + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Bound +- apiVersion: v1 + kind: PersistentVolume + metadata: + annotations: + hostPathProvisionerIdentity: 4e1b198e-839b-11ea-84a3-b00cd12cc2cf + pv.kubernetes.io/provisioned-by: k8s.io/minikube-hostpath + creationTimestamp: "2020-05-18T20:19:11Z" + finalizers: + - kubernetes.io/pv-protection + name: pvc-ac2ffdd6-362a-4204-82b9-1249b2dbf3be + resourceVersion: "5832494" + selfLink: /api/v1/persistentvolumes/pvc-ac2ffdd6-362a-4204-82b9-1249b2dbf3be + uid: f7a92631-d5de-47a6-9437-28d8ae5d2170 + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 5Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: zookeeper-instance-datadir-zookeeper-instance-zookeeper-0 + namespace: my-namespace + resourceVersion: "5832445" + uid: ac2ffdd6-362a-4204-82b9-1249b2dbf3be + hostPath: + path: /tmp/hostpath-provisioner/pvc-ac2ffdd6-362a-4204-82b9-1249b2dbf3be + type: "" + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Bound +- apiVersion: v1 + kind: PersistentVolume + metadata: + annotations: + hostPathProvisionerIdentity: 4e1b198e-839b-11ea-84a3-b00cd12cc2cf + pv.kubernetes.io/provisioned-by: k8s.io/minikube-hostpath + creationTimestamp: "2020-05-18T20:19:12Z" + finalizers: + - kubernetes.io/pv-protection + name: pvc-dd49319a-03bb-4135-8798-71ceb7bfcbc2 + resourceVersion: "5832502" + selfLink: /api/v1/persistentvolumes/pvc-dd49319a-03bb-4135-8798-71ceb7bfcbc2 + uid: 6c922bf3-b9be-4043-aff0-b593fbc151ab + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 5Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: zookeeper-instance-datadir-zookeeper-instance-zookeeper-2 + namespace: my-namespace + resourceVersion: "5832458" + uid: dd49319a-03bb-4135-8798-71ceb7bfcbc2 + hostPath: + path: /tmp/hostpath-provisioner/pvc-dd49319a-03bb-4135-8798-71ceb7bfcbc2 + type: "" + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Bound +kind: PersistentVolumeList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_service_accounts.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_service_accounts.yaml new file mode 100644 index 000000000..bbb71e443 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_service_accounts.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: ServiceAccount + metadata: + creationTimestamp: "2020-05-18T20:19:05Z" + name: default + namespace: my-namespace + resourceVersion: "5832417" + selfLink: /api/v1/namespaces/my-namespace/serviceaccounts/default + uid: 42549179-81c9-4e78-99b2-611f465328bc + secrets: + - name: default-token-wz4h5 +kind: ServiceAccountList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_services.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_services.yaml new file mode 100644 index 000000000..c82ef5be6 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_services.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +items: +- apiVersion: v1 + kind: Service + metadata: + annotations: + kudo.dev/last-applied-configuration: | + {"kind":"Service","apiVersion":"v1","metadata":{"name":"zookeeper-instance-cs","namespace":"my-namespace","creationTimestamp":null,"labels":{"app":"zookeeper","heritage":"kudo","kudo.dev/instance":"zookeeper-instance","kudo.dev/operator":"zookeeper","zookeeper":"zookeeper-instance"},"annotations":{"kudo.dev/last-plan-execution-uid":"7ce82b37-3638-47c7-8bcb-42b807c3c470","kudo.dev/operator-version":"0.3.0","kudo.dev/phase":"zookeeper","kudo.dev/plan":"deploy","kudo.dev/step":"deploy"},"ownerReferences":[{"apiVersion":"kudo.dev/v1beta1","kind":"Instance","name":"zookeeper-instance","uid":"51b95bd7-03fa-4e25-969a-241ef0467712","controller":true,"blockOwnerDeletion":true}]},"spec":{"ports":[{"name":"client","port":2181,"targetPort":0}],"selector":{"app":"zookeeper","instance":"zookeeper-instance"}},"status":{"loadBalancer":{}}} + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + creationTimestamp: "2020-05-18T20:19:10Z" + labels: + app: zookeeper + heritage: kudo + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + zookeeper: zookeeper-instance + name: zookeeper-instance-cs + namespace: my-namespace + ownerReferences: + - apiVersion: kudo.dev/v1beta1 + blockOwnerDeletion: true + controller: true + kind: Instance + name: zookeeper-instance + uid: 51b95bd7-03fa-4e25-969a-241ef0467712 + resourceVersion: "5832483" + selfLink: /api/v1/namespaces/my-namespace/services/zookeeper-instance-cs + uid: f400e978-53b1-479b-b176-f18cd5c96e5c + spec: + clusterIP: 10.98.117.69 + ports: + - name: client + port: 2181 + protocol: TCP + targetPort: 2181 + selector: + app: zookeeper + instance: zookeeper-instance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + annotations: + kudo.dev/last-applied-configuration: | + {"kind":"Service","apiVersion":"v1","metadata":{"name":"zookeeper-instance-hs","namespace":"my-namespace","creationTimestamp":null,"labels":{"app":"zookeeper","heritage":"kudo","kudo.dev/instance":"zookeeper-instance","kudo.dev/operator":"zookeeper","zookeeper":"zookeeper-instance"},"annotations":{"kudo.dev/last-plan-execution-uid":"7ce82b37-3638-47c7-8bcb-42b807c3c470","kudo.dev/operator-version":"0.3.0","kudo.dev/phase":"zookeeper","kudo.dev/plan":"deploy","kudo.dev/step":"deploy"},"ownerReferences":[{"apiVersion":"kudo.dev/v1beta1","kind":"Instance","name":"zookeeper-instance","uid":"51b95bd7-03fa-4e25-969a-241ef0467712","controller":true,"blockOwnerDeletion":true}]},"spec":{"ports":[{"name":"server","port":2888,"targetPort":0},{"name":"leader-election","port":3888,"targetPort":0}],"selector":{"app":"zookeeper","instance":"zookeeper-instance"},"clusterIP":"None"},"status":{"loadBalancer":{}}} + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + creationTimestamp: "2020-05-18T20:19:10Z" + labels: + app: zookeeper + heritage: kudo + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + zookeeper: zookeeper-instance + name: zookeeper-instance-hs + namespace: my-namespace + ownerReferences: + - apiVersion: kudo.dev/v1beta1 + blockOwnerDeletion: true + controller: true + kind: Instance + name: zookeeper-instance + uid: 51b95bd7-03fa-4e25-969a-241ef0467712 + resourceVersion: "5832476" + selfLink: /api/v1/namespaces/my-namespace/services/zookeeper-instance-hs + uid: 18210a16-6dc9-4e36-850e-8a527f192ac8 + spec: + clusterIP: None + ports: + - name: server + port: 2888 + protocol: TCP + targetPort: 2888 + - name: leader-election + port: 3888 + protocol: TCP + targetPort: 3888 + selector: + app: zookeeper + instance: zookeeper-instance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +kind: ServiceList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testdata/zk_statefulsets.yaml b/pkg/kudoctl/cmd/diagnostics/testdata/zk_statefulsets.yaml new file mode 100644 index 000000000..2c19d1e05 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testdata/zk_statefulsets.yaml @@ -0,0 +1,170 @@ +apiVersion: apps/v1 +items: +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + annotations: + kudo.dev/last-applied-configuration: | + {"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"zookeeper-instance-zookeeper","namespace":"my-namespace","creationTimestamp":null,"labels":{"app":"zookeeper","heritage":"kudo","instance":"zookeeper-instance","kudo.dev/instance":"zookeeper-instance","kudo.dev/operator":"zookeeper","zookeeper":"zookeeper"},"annotations":{"kudo.dev/last-plan-execution-uid":"7ce82b37-3638-47c7-8bcb-42b807c3c470","kudo.dev/operator-version":"0.3.0","kudo.dev/phase":"zookeeper","kudo.dev/plan":"deploy","kudo.dev/step":"deploy","reloader.kudo.dev/auto":"true"},"ownerReferences":[{"apiVersion":"kudo.dev/v1beta1","kind":"Instance","name":"zookeeper-instance","uid":"51b95bd7-03fa-4e25-969a-241ef0467712","controller":true,"blockOwnerDeletion":true}]},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"zookeeper","instance":"zookeeper-instance","zookeeper":"zookeeper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"zookeeper","heritage":"kudo","instance":"zookeeper-instance","kudo.dev/instance":"zookeeper-instance","kudo.dev/operator":"zookeeper","zookeeper":"zookeeper"},"annotations":{"kudo.dev/last-plan-execution-uid":"7ce82b37-3638-47c7-8bcb-42b807c3c470","kudo.dev/operator-version":"0.3.0","kudo.dev/phase":"zookeeper","kudo.dev/plan":"deploy","kudo.dev/step":"deploy"}},"spec":{"volumes":[{"name":"zookeeper-instance-bootstrap","configMap":{"name":"zookeeper-instance-bootstrap","defaultMode":511}},{"name":"zookeeper-instance-healthcheck","configMap":{"name":"zookeeper-instance-healthcheck","defaultMode":511}}],"containers":[{"name":"kubernetes-zookeeper","image":"zookeeper:3.4.14","command":["sh","-c","ZOOKEEPERPATH=`pwd` /etc/zookeeper/bootstrap.sh --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/logs --conf_dir=/conf --client_port=2181 --election_port=3888 --server_port=2888 --tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60 --snap_retain_count=3 --purge_interval=12 --max_session_timeout=40000 --min_session_timeout=4000 --log_level=INFO"],"ports":[{"name":"client","containerPort":2181},{"name":"server","containerPort":2888},{"name":"leader-election","containerPort":3888}],"resources":{"limits":{"cpu":"1","memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}},"volumeMounts":[{"name":"zookeeper-instance-datadir","mountPath":"/var/lib/zookeeper"},{"name":"zookeeper-instance-bootstrap","mountPath":"/etc/zookeeper"},{"name":"zookeeper-instance-healthcheck","mountPath":"/etc/healthcheck"}],"livenessProbe":{"exec":{"command":["sh","-c","/etc/healthcheck/healthcheck.sh 2181"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":30},"readinessProbe":{"exec":{"command":["sh","-c","/etc/healthcheck/healthcheck.sh 2181"]},"initialDelaySeconds":10,"timeoutSeconds":5},"imagePullPolicy":"Always"}],"securityContext":{"runAsUser":1000,"fsGroup":1000}}},"volumeClaimTemplates":[{"metadata":{"name":"zookeeper-instance-datadir","creationTimestamp":null,"labels":{"heritage":"kudo","kudo.dev/instance":"zookeeper-instance","kudo.dev/operator":"zookeeper"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}}},"status":{}}],"serviceName":"zookeeper-instance-hs","podManagementPolicy":"Parallel","updateStrategy":{"type":"RollingUpdate"}},"status":{"replicas":0}} + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + reloader.kudo.dev/auto: "true" + creationTimestamp: "2020-05-18T20:19:11Z" + generation: 1 + labels: + app: zookeeper + heritage: kudo + instance: zookeeper-instance + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + zookeeper: zookeeper + name: zookeeper-instance-zookeeper + namespace: my-namespace + ownerReferences: + - apiVersion: kudo.dev/v1beta1 + blockOwnerDeletion: true + controller: true + kind: Instance + name: zookeeper-instance + uid: 51b95bd7-03fa-4e25-969a-241ef0467712 + resourceVersion: "5832889" + selfLink: /apis/apps/v1/namespaces/my-namespace/statefulsets/zookeeper-instance-zookeeper + uid: 809fe0e3-c4b9-4540-98ce-b939a5dca6ba + spec: + podManagementPolicy: Parallel + replicas: 3 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: zookeeper + instance: zookeeper-instance + zookeeper: zookeeper + serviceName: zookeeper-instance-hs + template: + metadata: + annotations: + kudo.dev/last-plan-execution-uid: 7ce82b37-3638-47c7-8bcb-42b807c3c470 + kudo.dev/operator-version: 0.3.0 + kudo.dev/phase: zookeeper + kudo.dev/plan: deploy + kudo.dev/step: deploy + creationTimestamp: null + labels: + app: zookeeper + heritage: kudo + instance: zookeeper-instance + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + zookeeper: zookeeper + spec: + containers: + - command: + - sh + - -c + - ZOOKEEPERPATH=`pwd` /etc/zookeeper/bootstrap.sh --servers=3 --data_dir=/var/lib/zookeeper/data + --data_log_dir=/logs --conf_dir=/conf --client_port=2181 --election_port=3888 + --server_port=2888 --tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M + --max_client_cnxns=60 --snap_retain_count=3 --purge_interval=12 --max_session_timeout=40000 + --min_session_timeout=4000 --log_level=INFO + image: zookeeper:3.4.14 + imagePullPolicy: Always + livenessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + name: kubernetes-zookeeper + ports: + - containerPort: 2181 + name: client + protocol: TCP + - containerPort: 2888 + name: server + protocol: TCP + - containerPort: 3888 + name: leader-election + protocol: TCP + readinessProbe: + exec: + command: + - sh + - -c + - /etc/healthcheck/healthcheck.sh 2181 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: 250m + memory: 1Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/zookeeper + name: zookeeper-instance-datadir + - mountPath: /etc/zookeeper + name: zookeeper-instance-bootstrap + - mountPath: /etc/healthcheck + name: zookeeper-instance-healthcheck + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 1000 + runAsUser: 1000 + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 511 + name: zookeeper-instance-bootstrap + name: zookeeper-instance-bootstrap + - configMap: + defaultMode: 511 + name: zookeeper-instance-healthcheck + name: zookeeper-instance-healthcheck + updateStrategy: + type: RollingUpdate + volumeClaimTemplates: + - metadata: + creationTimestamp: null + labels: + heritage: kudo + kudo.dev/instance: zookeeper-instance + kudo.dev/operator: zookeeper + name: zookeeper-instance-datadir + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + volumeMode: Filesystem + status: + phase: Pending + status: + collisionCount: 0 + currentReplicas: 3 + currentRevision: zookeeper-instance-zookeeper-687d6785d6 + observedGeneration: 1 + readyReplicas: 3 + replicas: 3 + updateRevision: zookeeper-instance-zookeeper-687d6785d6 + updatedReplicas: 3 +kind: StatefulSetList +metadata: + resourceVersion: "" + selfLink: "" diff --git a/pkg/kudoctl/cmd/diagnostics/testhelper_test.go b/pkg/kudoctl/cmd/diagnostics/testhelper_test.go new file mode 100644 index 000000000..4ef9348e1 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/testhelper_test.go @@ -0,0 +1,29 @@ +package diagnostics + +import ( + "fmt" + + "github.com/spf13/afero" +) + +const ( + testLog = "2020/05/25 08:00:55 Ein Fichtenbaum steht einsam im Norden auf kahler Höh" + testLogGZipped = "\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff2202\xd070\xd572U0\xb0\xb020" + + "\xb025Up\xcd\xccSp\xcbL\xce(I\xcdKJ,\xcdU(.I\xcd(QH\xcd\xcc+N\xccU\xc8\xccU\xf0" + + "\xcb/JI\xcdSH,MS\xc8N\xcc\xc8I-R\xf08\xbc-\x03\x10\x00\x00\xff\xff'\b\x1b\xe7J\x00\x00\x00" +) + +var errFakeTestError = fmt.Errorf("fake test error") + +// failingFs is a wrapper of afero.Fs to simulate a specific file creation failure for printer +type failingFs struct { + afero.Fs + failOn string +} + +func (s *failingFs) Create(name string) (afero.File, error) { + if name == s.failOn { + return nil, errFakeTestError + } + return s.Fs.Create(name) +} diff --git a/pkg/kudoctl/cmd/diagnostics/writers.go b/pkg/kudoctl/cmd/diagnostics/writers.go new file mode 100644 index 000000000..7cf361a69 --- /dev/null +++ b/pkg/kudoctl/cmd/diagnostics/writers.go @@ -0,0 +1,34 @@ +package diagnostics + +import ( + "io" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" +) + +type objYamlWriter struct { + obj runtime.Object +} + +func (w objYamlWriter) write(file io.Writer) error { + printer := printers.YAMLPrinter{} + return printer.PrintObj(w.obj, file) +} + +type byteWriter struct { + b []byte +} + +func (w byteWriter) write(file io.Writer) error { + _, err := file.Write(w.b) + return err +} + +type gzipStreamWriter struct { + stream io.ReadCloser +} + +func (w gzipStreamWriter) write(file io.Writer) error { + return newGzipWriter(file).write(w.stream) +} diff --git a/pkg/kudoctl/cmd/root.go b/pkg/kudoctl/cmd/root.go index 78cb4a2f5..cd784b0f5 100644 --- a/pkg/kudoctl/cmd/root.go +++ b/pkg/kudoctl/cmd/root.go @@ -65,6 +65,7 @@ and serves as an API aggregation layer. cmd.AddCommand(newSearchCmd(fs, cmd.OutOrStdout())) cmd.AddCommand(newTestCmd()) cmd.AddCommand(newVersionCmd()) + cmd.AddCommand(newDiagnosticsCmd(fs)) initGlobalFlags(cmd, cmd.OutOrStdout()) diff --git a/pkg/kudoctl/kudoinit/types.go b/pkg/kudoctl/kudoinit/types.go index 5c69fd630..ac02da635 100644 --- a/pkg/kudoctl/kudoinit/types.go +++ b/pkg/kudoctl/kudoinit/types.go @@ -14,6 +14,7 @@ const ( DefaultNamespace = "kudo-system" DefaultServiceName = "kudo-controller-manager-service" DefaultSecretName = "kudo-webhook-server-secret" //nolint + DefaultKudoLabel = "kudo-manager" defaultGracePeriod = 10 defaultServiceAccount = "kudo-manager" ) @@ -43,6 +44,6 @@ type Step interface { } func GenerateLabels(labels map[string]string) map[string]string { - labels["app"] = "kudo-manager" + labels["app"] = DefaultKudoLabel return labels } diff --git a/pkg/kudoctl/util/kudo/kudo.go b/pkg/kudoctl/util/kudo/kudo.go index ba25409f8..91ae4c406 100644 --- a/pkg/kudoctl/util/kudo/kudo.go +++ b/pkg/kudoctl/util/kudo/kudo.go @@ -36,7 +36,7 @@ import ( // Client is a KUDO Client providing access to a kudo clientset and kubernetes clientsets type Client struct { kudoClientset versioned.Interface - kubeClientset kubernetes.Interface + KubeClientset kubernetes.Interface } // NewClient creates new KUDO Client @@ -82,7 +82,7 @@ func NewClient(kubeConfigPath string, requestTimeout int64, validateInstall bool } return &Client{ kudoClientset: kudoClientset, - kubeClientset: kubeClientset, + KubeClientset: kubeClientset, }, nil } @@ -90,7 +90,7 @@ func NewClient(kubeConfigPath string, requestTimeout int64, validateInstall bool func NewClientFromK8s(kudo versioned.Interface, kube kubernetes.Interface) *Client { result := Client{} result.kudoClientset = kudo - result.kubeClientset = kube + result.KubeClientset = kube return &result } @@ -145,8 +145,8 @@ func (c *Client) InstanceExistsInCluster(operatorName, namespace, version, insta // Populate the GVK from scheme, since it is cleared by design on typed objects. // https://github.com/kubernetes/client-go/issues/413 -func setGVKFromScheme(object runtime.Object) error { - gvks, unversioned, err := scheme.Scheme.ObjectKinds(object) +func SetGVKFromScheme(object runtime.Object, scheme *runtime.Scheme) error { + gvks, unversioned, err := scheme.ObjectKinds(object) if err != nil { return err } @@ -158,6 +158,9 @@ func setGVKFromScheme(object runtime.Object) error { } return nil } +func setGVKFromScheme(object runtime.Object) error { + return SetGVKFromScheme(object, scheme.Scheme) +} // GetInstance queries kubernetes api for instance of given name in given namespace // returns error for error conditions. Instance not found is not considered an error and will result in 'nil, nil' @@ -187,6 +190,20 @@ func (c *Client) GetOperatorVersion(name, namespace string) (*v1beta1.OperatorVe return ov, err } +// GetOperatorVersion queries kubernetes api for operator of given name in given namespace +// returns error for all other errors that not found, not found is treated as result being 'nil, nil' +func (c *Client) GetOperator(name, namespace string) (*v1beta1.Operator, error) { + o, err := c.kudoClientset.KudoV1beta1().Operators(namespace).Get(name, v1.GetOptions{}) + if apierrors.IsNotFound(err) { + return nil, nil + } + if err != nil { + return o, fmt.Errorf("failed to get operator %s/%s: %v", namespace, name, err) + } + err = setGVKFromScheme(o) + return o, err +} + // UpdateInstance updates operatorversion on instance func (c *Client) UpdateInstance(instanceName, namespace string, operatorVersion *string, parameters map[string]string, triggeredPlan *string, wait bool, waitTime time.Duration) error { var oldInstance *v1beta1.Instance @@ -426,7 +443,7 @@ func (c *Client) CreateNamespace(namespace, manifest string) error { } ns.Annotations["created-by"] = "kudo-cli" - _, err := c.kubeClientset.CoreV1().Namespaces().Create(ns) + _, err := c.KubeClientset.CoreV1().Namespaces().Create(ns) return err } diff --git a/pkg/kudoctl/util/kudo/kudo_test.go b/pkg/kudoctl/util/kudo/kudo_test.go index d3b3c1fa1..48593ec33 100644 --- a/pkg/kudoctl/util/kudo/kudo_test.go +++ b/pkg/kudoctl/util/kudo/kudo_test.go @@ -651,7 +651,7 @@ metadata: if test.shouldFail { t.Errorf("expected test %s to fail", test.name) } else { - namespace, err := k2o.kubeClientset. + namespace, err := k2o.KubeClientset. CoreV1(). Namespaces(). Get(test.namespace, metav1.GetOptions{})