From 91d1e7a30a3b60778aee37eb2b9d57f0aa950eae Mon Sep 17 00:00:00 2001 From: "Homayoon (Hue) Alimohammadi" Date: Thu, 25 Jul 2024 14:19:39 +0400 Subject: [PATCH 1/5] Show correct message on NodeStatus failure --- .gitignore | 4 +- src/k8s/cmd/k8s/k8s_config.go | 6 +- src/k8s/cmd/k8s/k8s_helm.go | 6 +- src/k8s/cmd/k8s/k8s_kubectl.go | 6 +- src/k8s/cmd/k8s/k8s_local_node_status.go | 9 +- src/k8s/cmd/k8s/k8s_status.go | 6 +- src/k8s/cmd/k8s/node_status.go | 36 ++++++++ src/k8s/cmd/k8s/node_status_test.go | 101 +++++++++++++++++++++++ src/k8s/pkg/k8sd/api/node.go | 2 +- 9 files changed, 146 insertions(+), 30 deletions(-) create mode 100644 src/k8s/cmd/k8s/node_status.go create mode 100644 src/k8s/cmd/k8s/node_status_test.go diff --git a/.gitignore b/.gitignore index a2f58b2f0..976bbd3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /parts/ /stage/ /prime/ +.vscode/ +env/ **.snap @@ -20,4 +22,4 @@ k8s_*.txt /docs/tools/.sphinx/warnings.txt /docs/tools/.sphinx/.wordlist.dic /docs/tools/.sphinx/.doctrees/ -/docs/tools/.sphinx/node_modules \ No newline at end of file +/docs/tools/.sphinx/node_modules diff --git a/src/k8s/cmd/k8s/k8s_config.go b/src/k8s/cmd/k8s/k8s_config.go index 5a16acbdc..24ae5d65b 100644 --- a/src/k8s/cmd/k8s/k8s_config.go +++ b/src/k8s/cmd/k8s/k8s_config.go @@ -32,11 +32,7 @@ func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - if _, err := client.NodeStatus(cmd.Context()); err != nil { - cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") - env.Exit(1) - return - } + _ = GetNodeStatus(client, cmd, env) ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) cobra.OnFinalize(cancel) diff --git a/src/k8s/cmd/k8s/k8s_helm.go b/src/k8s/cmd/k8s/k8s_helm.go index 7ae707a51..9f9cb6484 100644 --- a/src/k8s/cmd/k8s/k8s_helm.go +++ b/src/k8s/cmd/k8s/k8s_helm.go @@ -24,11 +24,7 @@ func newHelmCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - if status, err := client.NodeStatus(cmd.Context()); err != nil { - cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") - env.Exit(1) - return - } else if status.ClusterRole == apiv1.ClusterRoleWorker { + if status := GetNodeStatus(client, cmd, env); status.ClusterRole == apiv1.ClusterRoleWorker { cmd.PrintErrln("Error: k8s helm commands are not allowed on worker nodes.") env.Exit(1) return diff --git a/src/k8s/cmd/k8s/k8s_kubectl.go b/src/k8s/cmd/k8s/k8s_kubectl.go index 28aeeff0e..27dc974d8 100644 --- a/src/k8s/cmd/k8s/k8s_kubectl.go +++ b/src/k8s/cmd/k8s/k8s_kubectl.go @@ -24,11 +24,7 @@ func newKubectlCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - if status, err := client.NodeStatus(cmd.Context()); err != nil { - cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") - env.Exit(1) - return - } else if status.ClusterRole == apiv1.ClusterRoleWorker { + if status := GetNodeStatus(client, cmd, env); status.ClusterRole == apiv1.ClusterRoleWorker { cmd.PrintErrln("Error: k8s kubectl commands are not allowed on worker nodes.") env.Exit(1) return diff --git a/src/k8s/cmd/k8s/k8s_local_node_status.go b/src/k8s/cmd/k8s/k8s_local_node_status.go index 81422feb9..d9cac2f21 100644 --- a/src/k8s/cmd/k8s/k8s_local_node_status.go +++ b/src/k8s/cmd/k8s/k8s_local_node_status.go @@ -22,14 +22,7 @@ func newLocalNodeStatusCommand(env cmdutil.ExecutionEnvironment) *cobra.Command return } - status, err := client.NodeStatus(cmd.Context()) - if err != nil { - cmd.PrintErrf("Error: Failed to get the status of the local node.\n\nThe error was: %v\n", err) - env.Exit(1) - return - } - - outputFormatter.Print(status) + outputFormatter.Print(GetNodeStatus(client, cmd, env)) }, } cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") diff --git a/src/k8s/cmd/k8s/k8s_status.go b/src/k8s/cmd/k8s/k8s_status.go index e5ffdec5b..538ae3b27 100644 --- a/src/k8s/cmd/k8s/k8s_status.go +++ b/src/k8s/cmd/k8s/k8s_status.go @@ -35,11 +35,7 @@ func newStatusCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) cobra.OnFinalize(cancel) - if _, err := client.NodeStatus(cmd.Context()); err != nil { - cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") - env.Exit(1) - return - } + _ = GetNodeStatus(client, cmd, env) status, err := client.ClusterStatus(ctx, opts.waitReady) if err != nil { diff --git a/src/k8s/cmd/k8s/node_status.go b/src/k8s/cmd/k8s/node_status.go new file mode 100644 index 000000000..4da4f5e07 --- /dev/null +++ b/src/k8s/cmd/k8s/node_status.go @@ -0,0 +1,36 @@ +package k8s + +import ( + "errors" + "net/http" + + "github.com/spf13/cobra" + + apiv1 "github.com/canonical/k8s/api/v1" + cmdutil "github.com/canonical/k8s/cmd/util" + "github.com/canonical/k8s/pkg/client/k8sd" + + "github.com/canonical/lxd/shared/api" +) + +func GetNodeStatus(client k8sd.Client, cmd *cobra.Command, env cmdutil.ExecutionEnvironment) apiv1.NodeStatus { + status, err := client.NodeStatus(cmd.Context()) + if err == nil { + return status + } + + if errors.As(err, &api.StatusError{}) { + // the returned `ok` can be ignored since we're using errors.As() + // on the same type immediately before it + statusErr, _ := err.(api.StatusError) + if statusErr.Status() == http.StatusServiceUnavailable { + cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") + env.Exit(1) + return status + } + } + + cmd.PrintErrf("Error: Failed to retrieve the node status.\n\nThe error was: %v\n", err) + env.Exit(1) + return status +} diff --git a/src/k8s/cmd/k8s/node_status_test.go b/src/k8s/cmd/k8s/node_status_test.go new file mode 100644 index 000000000..0d211f4dd --- /dev/null +++ b/src/k8s/cmd/k8s/node_status_test.go @@ -0,0 +1,101 @@ +package k8s_test + +import ( + "bytes" + "context" + "errors" + "net/http" + "testing" + + apiv1 "github.com/canonical/k8s/api/v1" + "github.com/canonical/k8s/cmd/k8s" + cmdutil "github.com/canonical/k8s/cmd/util" + k8sdmock "github.com/canonical/k8s/pkg/client/k8sd/mock" + snapmock "github.com/canonical/k8s/pkg/snap/mock" + "github.com/canonical/lxd/shared/api" + + . "github.com/onsi/gomega" +) + +func TestGetNodeStatusUtil(t *testing.T) { + type testcase struct { + expectedCode int + name string + nodeStatusErr error + expectedInStdErr []string + nodeStatusResult apiv1.NodeStatus + } + + tests := []testcase{ + { + name: "NoError", + nodeStatusResult: apiv1.NodeStatus{ + Name: "name", Address: "addr", + ClusterRole: apiv1.ClusterRoleControlPlane, DatastoreRole: apiv1.DatastoreRoleVoter, + }, + }, + { + name: "DaemonNotInitialized", + nodeStatusErr: api.StatusErrorf(http.StatusServiceUnavailable, "Daemon not yet initialized"), + expectedCode: 1, + expectedInStdErr: []string{"The node is not part of a Kubernetes cluster. You can bootstrap a new cluster"}, + }, + { + name: "RandomError", + nodeStatusErr: errors.New("something went bad"), + expectedCode: 1, + expectedInStdErr: []string{"something went bad", "Failed to retrieve the node status"}, + }, + { + name: "ContextCanceled", + nodeStatusErr: context.Canceled, + expectedCode: 1, + expectedInStdErr: []string{context.Canceled.Error(), "Failed to retrieve the node status"}, + }, + { + name: "ContextDeadlineExceeded", + nodeStatusErr: context.DeadlineExceeded, + expectedCode: 1, + expectedInStdErr: []string{context.DeadlineExceeded.Error(), "Failed to retrieve the node status"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + var ( + returnCode int + stdout = &bytes.Buffer{} + stderr = &bytes.Buffer{} + mockClient = &k8sdmock.Mock{ + NodeStatusErr: tt.nodeStatusErr, + NodeStatusResult: tt.nodeStatusResult, + } + env = cmdutil.ExecutionEnvironment{ + Stdout: stdout, + Stderr: stderr, + Getuid: func() int { return 0 }, + Snap: &snapmock.Snap{ + Mock: snapmock.Mock{ + K8sdClient: mockClient, + }, + }, + Exit: func(rc int) { returnCode = rc }, + } + cmd = k8s.NewRootCmd(env) + ) + + status := k8s.GetNodeStatus(mockClient, cmd, env) + + g.Expect(returnCode).To(Equal(tt.expectedCode)) + for _, exp := range tt.expectedInStdErr { + g.Expect(stderr.String()).To(ContainSubstring(exp)) + } + + if tt.expectedCode == 0 { + g.Expect(status).To(Equal(tt.nodeStatusResult)) + } + }) + } +} diff --git a/src/k8s/pkg/k8sd/api/node.go b/src/k8s/pkg/k8sd/api/node.go index bc9664f5f..eb6ea1c03 100644 --- a/src/k8s/pkg/k8sd/api/node.go +++ b/src/k8s/pkg/k8sd/api/node.go @@ -14,7 +14,7 @@ func (e *Endpoints) getNodeStatus(s *state.State, r *http.Request) response.Resp status, err := impl.GetLocalNodeStatus(r.Context(), s, snap) if err != nil { - response.InternalError(err) + return response.InternalError(err) } result := apiv1.GetNodeStatusResponse{ From 93bb4d2227e13dfa34fa8ca28fca881bf7abdf50 Mon Sep 17 00:00:00 2001 From: "Homayoon (Hue) Alimohammadi" Date: Thu, 25 Jul 2024 14:29:31 +0400 Subject: [PATCH 2/5] Add comments to GetNodeStatus --- src/k8s/cmd/k8s/node_status.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/k8s/cmd/k8s/node_status.go b/src/k8s/cmd/k8s/node_status.go index 4da4f5e07..bb6fa9e5e 100644 --- a/src/k8s/cmd/k8s/node_status.go +++ b/src/k8s/cmd/k8s/node_status.go @@ -13,6 +13,10 @@ import ( "github.com/canonical/lxd/shared/api" ) +// GetNodeStatus retrieves the NodeStatus from k8sd client. +// If the daemon is not initialized, it exits with an error +// describing that the cluster should be bootstrapped. +// In case of any other errors it exits and shows the error. func GetNodeStatus(client k8sd.Client, cmd *cobra.Command, env cmdutil.ExecutionEnvironment) apiv1.NodeStatus { status, err := client.NodeStatus(cmd.Context()) if err == nil { From b52bdae27386ac3a0171df3aa7c15cdc75fac78e Mon Sep 17 00:00:00 2001 From: "Homayoon (Hue) Alimohammadi" Date: Thu, 25 Jul 2024 18:51:18 +0400 Subject: [PATCH 3/5] Address comments and issues --- src/k8s/cmd/k8s/k8s_config.go | 10 ++- src/k8s/cmd/k8s/k8s_helm.go | 10 ++- src/k8s/cmd/k8s/k8s_kubectl.go | 10 ++- src/k8s/cmd/k8s/k8s_local_node_status.go | 13 ++- src/k8s/cmd/k8s/k8s_status.go | 10 ++- src/k8s/cmd/k8s/node_status.go | 40 --------- src/k8s/cmd/k8s/node_status_test.go | 101 ----------------------- src/k8s/cmd/util/node_status.go | 37 +++++++++ src/k8s/cmd/util/node_status_test.go | 90 ++++++++++++++++++++ 9 files changed, 175 insertions(+), 146 deletions(-) delete mode 100644 src/k8s/cmd/k8s/node_status.go delete mode 100644 src/k8s/cmd/k8s/node_status_test.go create mode 100644 src/k8s/cmd/util/node_status.go create mode 100644 src/k8s/cmd/util/node_status_test.go diff --git a/src/k8s/cmd/k8s/k8s_config.go b/src/k8s/cmd/k8s/k8s_config.go index 24ae5d65b..bd0a2b190 100644 --- a/src/k8s/cmd/k8s/k8s_config.go +++ b/src/k8s/cmd/k8s/k8s_config.go @@ -32,7 +32,15 @@ func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - _ = GetNodeStatus(client, cmd, env) + if _, isBootstrapped, err := cmdutil.GetNodeStatus(cmd.Context(), client, env); !isBootstrapped { + cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") + env.Exit(1) + return + } else if err != nil { + cmd.PrintErrf("Error: Failed to retrieve the node status.\n\nThe error was: %v\n", err) + env.Exit(1) + return + } ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) cobra.OnFinalize(cancel) diff --git a/src/k8s/cmd/k8s/k8s_helm.go b/src/k8s/cmd/k8s/k8s_helm.go index 9f9cb6484..383749a58 100644 --- a/src/k8s/cmd/k8s/k8s_helm.go +++ b/src/k8s/cmd/k8s/k8s_helm.go @@ -24,7 +24,15 @@ func newHelmCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - if status := GetNodeStatus(client, cmd, env); status.ClusterRole == apiv1.ClusterRoleWorker { + if status, isBootstrapped, err := cmdutil.GetNodeStatus(cmd.Context(), client, env); !isBootstrapped { + cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") + env.Exit(1) + return + } else if err != nil { + cmd.PrintErrf("Error: Failed to retrieve the node status.\n\nThe error was: %v\n", err) + env.Exit(1) + return + } else if status.ClusterRole == apiv1.ClusterRoleWorker { cmd.PrintErrln("Error: k8s helm commands are not allowed on worker nodes.") env.Exit(1) return diff --git a/src/k8s/cmd/k8s/k8s_kubectl.go b/src/k8s/cmd/k8s/k8s_kubectl.go index 27dc974d8..93d191d7d 100644 --- a/src/k8s/cmd/k8s/k8s_kubectl.go +++ b/src/k8s/cmd/k8s/k8s_kubectl.go @@ -24,7 +24,15 @@ func newKubectlCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { return } - if status := GetNodeStatus(client, cmd, env); status.ClusterRole == apiv1.ClusterRoleWorker { + if status, isBootstrapped, err := cmdutil.GetNodeStatus(cmd.Context(), client, env); !isBootstrapped { + cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") + env.Exit(1) + return + } else if err != nil { + cmd.PrintErrf("Error: Failed to retrieve the node status.\n\nThe error was: %v\n", err) + env.Exit(1) + return + } else if status.ClusterRole == apiv1.ClusterRoleWorker { cmd.PrintErrln("Error: k8s kubectl commands are not allowed on worker nodes.") env.Exit(1) return diff --git a/src/k8s/cmd/k8s/k8s_local_node_status.go b/src/k8s/cmd/k8s/k8s_local_node_status.go index d9cac2f21..16c7ded13 100644 --- a/src/k8s/cmd/k8s/k8s_local_node_status.go +++ b/src/k8s/cmd/k8s/k8s_local_node_status.go @@ -22,7 +22,18 @@ func newLocalNodeStatusCommand(env cmdutil.ExecutionEnvironment) *cobra.Command return } - outputFormatter.Print(GetNodeStatus(client, cmd, env)) + status, isBootstrapped, err := cmdutil.GetNodeStatus(cmd.Context(), client, env) + if !isBootstrapped { + cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") + env.Exit(1) + return + } else if err != nil { + cmd.PrintErrf("Error: Failed to retrieve the local node status.\n\nThe error was: %v\n", err) + env.Exit(1) + return + } + + outputFormatter.Print(status) }, } cmd.Flags().StringVar(&opts.outputFormat, "output-format", "plain", "set the output format to one of plain, json or yaml") diff --git a/src/k8s/cmd/k8s/k8s_status.go b/src/k8s/cmd/k8s/k8s_status.go index 538ae3b27..a6f107f06 100644 --- a/src/k8s/cmd/k8s/k8s_status.go +++ b/src/k8s/cmd/k8s/k8s_status.go @@ -35,7 +35,15 @@ func newStatusCmd(env cmdutil.ExecutionEnvironment) *cobra.Command { ctx, cancel := context.WithTimeout(cmd.Context(), opts.timeout) cobra.OnFinalize(cancel) - _ = GetNodeStatus(client, cmd, env) + if _, isBootstrapped, err := cmdutil.GetNodeStatus(cmd.Context(), client, env); !isBootstrapped { + cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") + env.Exit(1) + return + } else if err != nil { + cmd.PrintErrf("Error: Failed to retrieve the node status.\n\nThe error was: %v\n", err) + env.Exit(1) + return + } status, err := client.ClusterStatus(ctx, opts.waitReady) if err != nil { diff --git a/src/k8s/cmd/k8s/node_status.go b/src/k8s/cmd/k8s/node_status.go deleted file mode 100644 index bb6fa9e5e..000000000 --- a/src/k8s/cmd/k8s/node_status.go +++ /dev/null @@ -1,40 +0,0 @@ -package k8s - -import ( - "errors" - "net/http" - - "github.com/spf13/cobra" - - apiv1 "github.com/canonical/k8s/api/v1" - cmdutil "github.com/canonical/k8s/cmd/util" - "github.com/canonical/k8s/pkg/client/k8sd" - - "github.com/canonical/lxd/shared/api" -) - -// GetNodeStatus retrieves the NodeStatus from k8sd client. -// If the daemon is not initialized, it exits with an error -// describing that the cluster should be bootstrapped. -// In case of any other errors it exits and shows the error. -func GetNodeStatus(client k8sd.Client, cmd *cobra.Command, env cmdutil.ExecutionEnvironment) apiv1.NodeStatus { - status, err := client.NodeStatus(cmd.Context()) - if err == nil { - return status - } - - if errors.As(err, &api.StatusError{}) { - // the returned `ok` can be ignored since we're using errors.As() - // on the same type immediately before it - statusErr, _ := err.(api.StatusError) - if statusErr.Status() == http.StatusServiceUnavailable { - cmd.PrintErrln("Error: The node is not part of a Kubernetes cluster. You can bootstrap a new cluster with:\n\n sudo k8s bootstrap") - env.Exit(1) - return status - } - } - - cmd.PrintErrf("Error: Failed to retrieve the node status.\n\nThe error was: %v\n", err) - env.Exit(1) - return status -} diff --git a/src/k8s/cmd/k8s/node_status_test.go b/src/k8s/cmd/k8s/node_status_test.go deleted file mode 100644 index 0d211f4dd..000000000 --- a/src/k8s/cmd/k8s/node_status_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package k8s_test - -import ( - "bytes" - "context" - "errors" - "net/http" - "testing" - - apiv1 "github.com/canonical/k8s/api/v1" - "github.com/canonical/k8s/cmd/k8s" - cmdutil "github.com/canonical/k8s/cmd/util" - k8sdmock "github.com/canonical/k8s/pkg/client/k8sd/mock" - snapmock "github.com/canonical/k8s/pkg/snap/mock" - "github.com/canonical/lxd/shared/api" - - . "github.com/onsi/gomega" -) - -func TestGetNodeStatusUtil(t *testing.T) { - type testcase struct { - expectedCode int - name string - nodeStatusErr error - expectedInStdErr []string - nodeStatusResult apiv1.NodeStatus - } - - tests := []testcase{ - { - name: "NoError", - nodeStatusResult: apiv1.NodeStatus{ - Name: "name", Address: "addr", - ClusterRole: apiv1.ClusterRoleControlPlane, DatastoreRole: apiv1.DatastoreRoleVoter, - }, - }, - { - name: "DaemonNotInitialized", - nodeStatusErr: api.StatusErrorf(http.StatusServiceUnavailable, "Daemon not yet initialized"), - expectedCode: 1, - expectedInStdErr: []string{"The node is not part of a Kubernetes cluster. You can bootstrap a new cluster"}, - }, - { - name: "RandomError", - nodeStatusErr: errors.New("something went bad"), - expectedCode: 1, - expectedInStdErr: []string{"something went bad", "Failed to retrieve the node status"}, - }, - { - name: "ContextCanceled", - nodeStatusErr: context.Canceled, - expectedCode: 1, - expectedInStdErr: []string{context.Canceled.Error(), "Failed to retrieve the node status"}, - }, - { - name: "ContextDeadlineExceeded", - nodeStatusErr: context.DeadlineExceeded, - expectedCode: 1, - expectedInStdErr: []string{context.DeadlineExceeded.Error(), "Failed to retrieve the node status"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - var ( - returnCode int - stdout = &bytes.Buffer{} - stderr = &bytes.Buffer{} - mockClient = &k8sdmock.Mock{ - NodeStatusErr: tt.nodeStatusErr, - NodeStatusResult: tt.nodeStatusResult, - } - env = cmdutil.ExecutionEnvironment{ - Stdout: stdout, - Stderr: stderr, - Getuid: func() int { return 0 }, - Snap: &snapmock.Snap{ - Mock: snapmock.Mock{ - K8sdClient: mockClient, - }, - }, - Exit: func(rc int) { returnCode = rc }, - } - cmd = k8s.NewRootCmd(env) - ) - - status := k8s.GetNodeStatus(mockClient, cmd, env) - - g.Expect(returnCode).To(Equal(tt.expectedCode)) - for _, exp := range tt.expectedInStdErr { - g.Expect(stderr.String()).To(ContainSubstring(exp)) - } - - if tt.expectedCode == 0 { - g.Expect(status).To(Equal(tt.nodeStatusResult)) - } - }) - } -} diff --git a/src/k8s/cmd/util/node_status.go b/src/k8s/cmd/util/node_status.go new file mode 100644 index 000000000..71d9cd896 --- /dev/null +++ b/src/k8s/cmd/util/node_status.go @@ -0,0 +1,37 @@ +package cmdutil + +import ( + "context" + "errors" + "net/http" + + apiv1 "github.com/canonical/k8s/api/v1" + "github.com/canonical/k8s/pkg/client/k8sd" + + "github.com/canonical/lxd/shared/api" +) + +// GetNodeStatus retrieves the NodeStatus from k8sd client. If the daemon is not initialized, it exits with an error +// describing that the cluster should be bootstrapped. In case of any other errors it exits and shows the error. +func GetNodeStatus(ctx context.Context, client k8sd.Client, env ExecutionEnvironment) (status apiv1.NodeStatus, isBootstrapped bool, err error) { + status, err = client.NodeStatus(ctx) + if err == nil { + return status, true, nil + } + + if errors.As(err, &api.StatusError{}) { + // the returned `ok` can be ignored since we're using errors.As() + // on the same type immediately before it + statusErr, _ := err.(api.StatusError) + + // if we get an `http.StatusServiceUnavailable` it will be (most likely) because + // the handler we're trying to reach is not `AllowedBeforeInit` and hence we can understand that + // the daemon is not yet initialized (this statement should be available + // in the `statusErr.Error()` explicitly but for the sake of decoupling we don't rely on that) + if statusErr.Status() == http.StatusServiceUnavailable { + return status, false, err + } + } + + return status, true, err +} diff --git a/src/k8s/cmd/util/node_status_test.go b/src/k8s/cmd/util/node_status_test.go new file mode 100644 index 000000000..c60de4d41 --- /dev/null +++ b/src/k8s/cmd/util/node_status_test.go @@ -0,0 +1,90 @@ +package cmdutil_test + +import ( + "context" + "errors" + "net/http" + "testing" + + apiv1 "github.com/canonical/k8s/api/v1" + cmdutil "github.com/canonical/k8s/cmd/util" + k8sdmock "github.com/canonical/k8s/pkg/client/k8sd/mock" + snapmock "github.com/canonical/k8s/pkg/snap/mock" + "github.com/canonical/lxd/shared/api" + + . "github.com/onsi/gomega" +) + +func TestGetNodeStatusUtil(t *testing.T) { + type testcase struct { + expIsBootstrapped bool + name string + nodeStatusErr error + nodeStatusResult apiv1.NodeStatus + } + + tests := []testcase{ + { + name: "NoError", + expIsBootstrapped: true, + nodeStatusResult: apiv1.NodeStatus{ + Name: "name", Address: "addr", + ClusterRole: apiv1.ClusterRoleControlPlane, DatastoreRole: apiv1.DatastoreRoleVoter, + }, + }, + { + name: "DaemonNotInitialized", + nodeStatusErr: api.StatusErrorf(http.StatusServiceUnavailable, "Daemon not yet initialized"), + expIsBootstrapped: false, + }, + { + name: "RandomError", + nodeStatusErr: errors.New("something went bad"), + expIsBootstrapped: true, + }, + { + name: "ContextCanceled", + nodeStatusErr: context.Canceled, + expIsBootstrapped: true, + }, + { + name: "ContextDeadlineExceeded", + nodeStatusErr: context.DeadlineExceeded, + expIsBootstrapped: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + var ( + mockClient = &k8sdmock.Mock{ + NodeStatusErr: test.nodeStatusErr, + NodeStatusResult: test.nodeStatusResult, + } + env = cmdutil.ExecutionEnvironment{ + Getuid: func() int { return 0 }, + Snap: &snapmock.Snap{ + Mock: snapmock.Mock{ + K8sdClient: mockClient, + }, + }, + } + ) + + status, isBootstrapped, err := cmdutil.GetNodeStatus(context.TODO(), mockClient, env) + + g.Expect(isBootstrapped).To(Equal(test.expIsBootstrapped)) + if test.nodeStatusErr == nil { + g.Expect(err).To(BeNil()) + } else { + g.Expect(err).To(MatchError(test.nodeStatusErr)) + } + + if err == nil { + g.Expect(status).To(Equal(test.nodeStatusResult)) + } + }) + } +} From 7d10ccb612ce18c4071aecbded89a6921f8fa7ec Mon Sep 17 00:00:00 2001 From: "Homayoon (Hue) Alimohammadi" Date: Thu, 25 Jul 2024 20:24:54 +0400 Subject: [PATCH 4/5] Update gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 976bbd3fa..8c7172b67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ /parts/ /stage/ /prime/ -.vscode/ -env/ **.snap From 3753b6d69abff06a820bb1f0488df0a6d2b61911 Mon Sep 17 00:00:00 2001 From: "Homayoon (Hue) Alimohammadi" Date: Thu, 25 Jul 2024 20:31:53 +0400 Subject: [PATCH 5/5] Fix comments --- src/k8s/cmd/util/node_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s/cmd/util/node_status.go b/src/k8s/cmd/util/node_status.go index 71d9cd896..e5e4892e0 100644 --- a/src/k8s/cmd/util/node_status.go +++ b/src/k8s/cmd/util/node_status.go @@ -11,7 +11,7 @@ import ( "github.com/canonical/lxd/shared/api" ) -// GetNodeStatus retrieves the NodeStatus from k8sd client. If the daemon is not initialized, it exits with an error +// GetNodeStatus retrieves the NodeStatus from k8sd server. If the daemon is not initialized, it exits with an error // describing that the cluster should be bootstrapped. In case of any other errors it exits and shows the error. func GetNodeStatus(ctx context.Context, client k8sd.Client, env ExecutionEnvironment) (status apiv1.NodeStatus, isBootstrapped bool, err error) { status, err = client.NodeStatus(ctx)