Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show correct message on NodeStatus failure #564

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 client. If the daemon is not initialized, it exits with an error
bschimke95 marked this conversation as resolved.
Show resolved Hide resolved
// 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)
bschimke95 marked this conversation as resolved.
Show resolved Hide resolved
}

result := apiv1.GetNodeStatusResponse{
Expand Down
Loading