Skip to content
This repository was archived by the owner on Nov 14, 2024. It is now read-only.

feat: check for resources #9

Merged
merged 5 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
feat: check required api resources
  • Loading branch information
johnstcn committed Aug 26, 2021
commit dc3d8fdfb08ad01bb739feb28a905f757dfc3b5d
51 changes: 50 additions & 1 deletion internal/checks/kube/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,60 @@ package kube

import (
"context"
"fmt"

"golang.org/x/xerrors"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/cdr/coder-doctor/internal/api"
)

func (k *KubernetesChecker) CheckResources(ctx context.Context) []*api.CheckResult {
func (k *KubernetesChecker) CheckResources(_ context.Context) []*api.CheckResult {
const checkName = "kubernetes-resources"
results := make([]*api.CheckResult, 0)
dc := k.client.Discovery()
lists, err := dc.ServerPreferredResources()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Discovery APIs unfortunately don't accept a context, which means they can't be cancelled -- it's why the version checks re-implement the Discovery stuff

I filed a bug but 🤷‍♂️ I don't think anything can be done about it, really kubernetes/client-go#1001

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's unfortunate :( At least here, the worst case is the user gets impatient and hits ^C.

if err != nil {
results = append(results, api.ErrorResult(checkName, "unable to fetch api resources from server", err))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be SKIP rather than FAIL? after all, it's an indeterminate result - we don't know that the API resources are missing, and we don't know that they're available, either

Copy link
Member Author

@johnstcn johnstcn Aug 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not being able to check for API resources implies a lack of permissions or some other issue, which I'd argue is a FAIL.

Edit: nah you're right it's a skip. I've to add more resources, so will do this in another PR.

return results
}

resourcesAvailable := make(map[ResourceRequirement]bool)
for _, list := range lists {
if len(list.APIResources) == 0 {
continue
}

gv, err := schema.ParseGroupVersion(list.GroupVersion)
if err != nil {
continue
}

for _, resource := range list.APIResources {
if len(resource.Verbs) == 0 {
continue
}

r := ResourceRequirement{
Group: gv.Group,
Version: gv.String(),
Resource: resource.Name,
}
resourcesAvailable[r] = true
}
}

versionReqs := findClosestVersionRequirements(k.coderVersion)
for versionReq := range versionReqs {
if !resourcesAvailable[*versionReq] {
msg := fmt.Sprintf("missing required resource:%q group:%q version:%q", versionReq.Resource, versionReq.Group, versionReq.Version)
errResult := api.ErrorResult(checkName, msg, xerrors.New(msg))
results = append(results, errResult)
continue
}
msg := fmt.Sprintf("found required resource:%q group:%q version:%q", versionReq.Resource, versionReq.Group, versionReq.Version)
results = append(results, api.PassResult(checkName, msg))
}

return results
}
24 changes: 12 additions & 12 deletions internal/checks/kube/resources_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ var allRequirements = []VersionedResourceRequirements{
{
VersionConstraints: api.MustConstraint(">= 1.20"),
ResourceRequirements: map[*ResourceRequirement]ResourceVerbs{
NewResourceRequirement("", "v1", "pods"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "secrets"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "serviceaccounts"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "services"): verbsCreateDeleteList,
NewResourceRequirement("", "rbac.authorization.k8s.io/v1", "roles"): verbsCreateDeleteList,
NewResourceRequirement("", "rbac.authorization.k8s.io/v1", "rolebindings"): verbsCreateDeleteList,
NewResourceRequirement("apps", "apps/v1", "deployments"): verbsCreateDeleteList,
NewResourceRequirement("apps", "apps/v1", "replicasets"): verbsCreateDeleteList,
NewResourceRequirement("apps", "apps/v1", "statefulsets"): verbsCreateDeleteList,
NewResourceRequirement("extensions", "ingresses", "networking.k8s.io/v1"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "pods"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "secrets"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "serviceaccounts"): verbsCreateDeleteList,
NewResourceRequirement("", "v1", "services"): verbsCreateDeleteList,
NewResourceRequirement("apps", "apps/v1", "deployments"): verbsCreateDeleteList,
NewResourceRequirement("apps", "apps/v1", "replicasets"): verbsCreateDeleteList,
NewResourceRequirement("apps", "apps/v1", "statefulsets"): verbsCreateDeleteList,
NewResourceRequirement("networking.k8s.io", "networking.k8s.io/v1", "ingresses"): verbsCreateDeleteList,
NewResourceRequirement("rbac.authorization.k8s.io", "rbac.authorization.k8s.io/v1", "roles"): verbsCreateDeleteList,
NewResourceRequirement("rbac.authorization.k8s.io", "rbac.authorization.k8s.io/v1", "rolebindings"): verbsCreateDeleteList,
},
},
}
Expand All @@ -37,8 +37,8 @@ type ResourceVerbs []string

// VersionedResourceRequirements is a set of ResourceRequirements for a specific version of Coder.
type VersionedResourceRequirements struct {
VersionConstraints *semver.Constraints
ResourceRequirements map[*ResourceRequirement]ResourceVerbs
VersionConstraints *semver.Constraints
ResourceRequirements map[*ResourceRequirement]ResourceVerbs
}

var verbsCreateDeleteList ResourceVerbs = []string{"create", "delete", "list"}
Expand Down
184 changes: 174 additions & 10 deletions internal/checks/kube/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package kube

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -17,13 +19,24 @@ func Test_KubernetesChecker_CheckResources(t *testing.T) {
t.Parallel()

tests := []struct {
Name string
Response *metav1.APIResourceList
F func(*testing.T, []*api.CheckResult)
Name string
GroupListResponse *metav1.APIGroupList
F func(*testing.T, []*api.CheckResult)
}{
{
Name: "no resources available",
Response: emptyAPIResourceList,
Name: "all resources available",
GroupListResponse: fullAPIGroupList,
F: func(t *testing.T, results []*api.CheckResult) {
assert.False(t, "results should not be empty", len(results) == 0)
for _, result := range results {
assert.Equal(t, result.Name+" should have no error", nil, result.Details["error"])
assert.Equal(t, result.Name+" should pass", api.StatePassed, result.State)
}
},
},
{
Name: "no resources available",
GroupListResponse: emptyAPIGroupList,
F: func(t *testing.T, results []*api.CheckResult) {
assert.False(t, "results should not be empty", len(results) == 0)
for _, result := range results {
Expand All @@ -41,7 +54,33 @@ func Test_KubernetesChecker_CheckResources(t *testing.T) {
t.Run(test.Name, func(t *testing.T) {
t.Parallel()

server := newTestHTTPServer(t, http.StatusOK, test.Response)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch req.URL.Path {
case "/apis":
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(test.GroupListResponse)
assert.Success(t, "failed to encode response", err)
case "/api/v1":
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(v1ResourceList)
assert.Success(t, "failed to encode response", err)
case "/apis/apps/v1":
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(appsV1ResourceList)
assert.Success(t, "failed to encode response", err)
case "/apis/networking.k8s.io/v1":
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(networkingV1ResourceList)
assert.Success(t, "failed to encode response", err)
case "/apis/rbac.authorization.k8s.io/v1":
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(rbacV1ResourceList)
assert.Success(t, "failed to encode response", err)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()

client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL})
Expand All @@ -54,8 +93,133 @@ func Test_KubernetesChecker_CheckResources(t *testing.T) {
}
}

var emptyAPIResourceList *metav1.APIResourceList = &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{},
GroupVersion: "",
APIResources: []metav1.APIResource{},
var emptyAPIGroupList *metav1.APIGroupList = &metav1.APIGroupList{
TypeMeta: metav1.TypeMeta{
Kind: "APIGroupList",
APIVersion: "v1",
},
Groups: []metav1.APIGroup{},
}

var fullAPIGroupList *metav1.APIGroupList = &metav1.APIGroupList{
TypeMeta: metav1.TypeMeta{
Kind: "APIGroupList",
APIVersion: "v1",
},
Groups: []metav1.APIGroup{
{
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "v1",
Version: "v1",
},
},
},
{
Name: "apps",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "apps/v1",
Version: "v1",
},
},
},
{
Name: "networking.k8s.io",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "networking.k8s.io/v1",
Version: "v1",
},
},
},
{
Name: "rbac.authorization.k8s.io",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "rbac.authorization.k8s.io/v1",
Version: "v1",
},
},
},
},
}

var v1ResourceList = &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
},
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{
Name: "pods",
Verbs: []string{"get"},
},
{
Name: "secrets",
Verbs: []string{"get"},
},
{
Name: "serviceaccounts",
Verbs: []string{"get"},
},
{
Name: "services",
Verbs: []string{"get"},
},
},
}

var appsV1ResourceList = &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "apps/v1",
APIResources: []metav1.APIResource{
{
Name: "deployments",
Verbs: []string{"get"},
},
{
Name: "replicasets",
Verbs: []string{"get"},
},
{
Name: "statefulsets",
Verbs: []string{"get"},
},
},
}

var networkingV1ResourceList = &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "networking.k8s.io/v1",
APIResources: []metav1.APIResource{
{
Name: "ingresses",
Verbs: []string{"get"},
},
},
}

var rbacV1ResourceList = &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "rbac.authorization.k8s.io/v1",
APIResources: []metav1.APIResource{
{
Name: "roles",
Verbs: []string{"get"},
},
{
Name: "rolebindings",
Verbs: []string{"get"},
},
},
}