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 all 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
8 changes: 7 additions & 1 deletion internal/checks/kube/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type KubernetesChecker struct {
writer api.ResultWriter
coderVersion *semver.Version
log slog.Logger
rbacRequirements []*RBACRequirement
rbacRequirements map[*ResourceRequirement]ResourceVerbs
}

type Option func(k *KubernetesChecker)
Expand Down Expand Up @@ -87,6 +87,12 @@ func (k *KubernetesChecker) Run(ctx context.Context) error {
return xerrors.Errorf("check version: %w", err)
}

for _, res := range k.CheckResources(ctx) {
if err := k.writer.WriteResult(res); err != nil {
return xerrors.Errorf("check api resources: %w", err)
}
}

for _, res := range k.CheckRBAC(ctx) {
if err := k.writer.WriteResult(res); err != nil {
return xerrors.Errorf("check RBAC: %w", err)
Expand Down
63 changes: 12 additions & 51 deletions internal/checks/kube/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,73 +16,34 @@ import (
authorizationclientv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
)

type RBACRequirement struct {
APIGroup string
Resource string
Verbs []string
}

type VersionedRBACRequirements struct {
VersionConstraints *semver.Constraints
RBACRequirements []*RBACRequirement
}

var verbsCreateDeleteList = []string{"create", "delete", "list"}

func NewRBACRequirement(apiGroup, resource string, verbs ...string) *RBACRequirement {
return &RBACRequirement{
APIGroup: apiGroup,
Resource: resource,
Verbs: verbs,
}
}

var allVersionedRBACRequirements = []VersionedRBACRequirements{
{
VersionConstraints: api.MustConstraint(">= 1.20"),
RBACRequirements: []*RBACRequirement{
NewRBACRequirement("", "pods", verbsCreateDeleteList...),
NewRBACRequirement("", "roles", verbsCreateDeleteList...),
NewRBACRequirement("", "rolebindings", verbsCreateDeleteList...),
NewRBACRequirement("", "secrets", verbsCreateDeleteList...),
NewRBACRequirement("", "serviceaccounts", verbsCreateDeleteList...),
NewRBACRequirement("", "services", verbsCreateDeleteList...),
NewRBACRequirement("apps", "deployments", verbsCreateDeleteList...),
NewRBACRequirement("apps", "replicasets", verbsCreateDeleteList...),
NewRBACRequirement("apps", "statefulsets", verbsCreateDeleteList...),
NewRBACRequirement("extensions", "ingresses", verbsCreateDeleteList...),
},
},
}

func (k *KubernetesChecker) CheckRBAC(ctx context.Context) []*api.CheckResult {
const checkName = "kubernetes-rbac"
authClient := k.client.AuthorizationV1()
results := make([]*api.CheckResult, 0)

for _, req := range k.rbacRequirements {
for req, reqVerbs := range k.rbacRequirements {
resName := fmt.Sprintf("%s-%s", checkName, req.Resource)
if err := k.checkOneRBAC(ctx, authClient, req); err != nil {
if err := k.checkOneRBAC(ctx, authClient, req, reqVerbs); err != nil {
summary := fmt.Sprintf("missing permissions on resource %s: %s", req.Resource, err)
results = append(results, api.ErrorResult(resName, summary, err))
continue
}

summary := fmt.Sprintf("%s: can %s", req.Resource, strings.Join(req.Verbs, ", "))
summary := fmt.Sprintf("%s: can %s", req.Resource, strings.Join(reqVerbs, ", "))
results = append(results, api.PassResult(resName, summary))
}

return results
}

func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authorizationclientv1.AuthorizationV1Interface, req *RBACRequirement) error {
have := make([]string, 0, len(req.Verbs))
for _, verb := range req.Verbs {
func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authorizationclientv1.AuthorizationV1Interface, req *ResourceRequirement, reqVerbs ResourceVerbs) error {
have := make([]string, 0, len(reqVerbs))
for _, verb := range reqVerbs {
sar := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: k.namespace,
Group: req.APIGroup,
Group: req.Group,
Resource: req.Resource,
Verb: verb,
},
Expand All @@ -102,17 +63,17 @@ func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authori
}
}

if len(have) != len(req.Verbs) {
return xerrors.Errorf(fmt.Sprintf("need: %+v have: %+v", req.Verbs, have))
if len(have) != len(reqVerbs) {
return xerrors.Errorf(fmt.Sprintf("need: %+v have: %+v", reqVerbs, have))
}

return nil
}

func findClosestVersionRequirements(v *semver.Version) []*RBACRequirement {
for _, vreqs := range allVersionedRBACRequirements {
func findClosestVersionRequirements(v *semver.Version) map[*ResourceRequirement]ResourceVerbs {
for _, vreqs := range allRequirements {
if vreqs.VersionConstraints.Check(v) {
return vreqs.RBACRequirements
return vreqs.ResourceRequirements
}
}
return nil
Expand Down
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
}
53 changes: 53 additions & 0 deletions internal/checks/kube/resources_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kube

import (
"github.com/Masterminds/semver/v3"

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

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("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,
},
},
}

// ResourceRequirement describes a set of requirements on a specific version of a resource:
// whether it exists with that specific version, and what verbs the current user is permitted to perform
// on the resource.
type ResourceRequirement struct {
Group string
Resource string
Version string
}

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
}

var verbsCreateDeleteList ResourceVerbs = []string{"create", "delete", "list"}

// NewResourceRequirement is just a convenience function for creating ResourceRequirements.
func NewResourceRequirement(apiGroup, version, resource string) *ResourceRequirement {
return &ResourceRequirement{
Group: apiGroup,
Resource: resource,
Version: version,
}
}
Loading