Skip to content

Commit

Permalink
Show correct message on NodeStatus failure (#564)
Browse files Browse the repository at this point in the history
* Show correct message on NodeStatus failure
  • Loading branch information
HomayoonAlimohammadi authored Jul 26, 2024
1 parent c7cb7c9 commit 6e610cd
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ k8s_*.txt
/docs/tools/.sphinx/warnings.txt
/docs/tools/.sphinx/.wordlist.dic
/docs/tools/.sphinx/.doctrees/
/docs/tools/.sphinx/node_modules
/docs/tools/.sphinx/node_modules
6 changes: 5 additions & 1 deletion src/k8s/cmd/k8s/k8s_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ func newKubeConfigCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
return
}

if _, err := client.NodeStatus(cmd.Context()); err != nil {
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)
Expand Down
6 changes: 5 additions & 1 deletion src/k8s/cmd/k8s/k8s_helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ func newHelmCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
return
}

if status, err := client.NodeStatus(cmd.Context()); err != nil {
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)
Expand Down
6 changes: 5 additions & 1 deletion src/k8s/cmd/k8s/k8s_kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ func newKubectlCmd(env cmdutil.ExecutionEnvironment) *cobra.Command {
return
}

if status, err := client.NodeStatus(cmd.Context()); err != nil {
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)
Expand Down
10 changes: 7 additions & 3 deletions src/k8s/cmd/k8s/k8s_local_node_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ 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)
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
}
Expand Down
6 changes: 5 additions & 1 deletion src/k8s/cmd/k8s/k8s_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ 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 {
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)
Expand Down
37 changes: 37 additions & 0 deletions src/k8s/cmd/util/node_status.go
Original file line number Diff line number Diff line change
@@ -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 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)
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
}
90 changes: 90 additions & 0 deletions src/k8s/cmd/util/node_status_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
})
}
}
2 changes: 1 addition & 1 deletion src/k8s/pkg/k8sd/api/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 6e610cd

Please sign in to comment.