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

Commit 8afac90

Browse files
authored
feat: check for resources (#9)
* chore: rename and refactor RBACRequirements to ResourceRequirements, add Version field * feat: check required api resources and associated unit tests
1 parent e8be680 commit 8afac90

File tree

5 files changed

+347
-53
lines changed

5 files changed

+347
-53
lines changed

internal/checks/kube/kubernetes.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type KubernetesChecker struct {
2222
writer api.ResultWriter
2323
coderVersion *semver.Version
2424
log slog.Logger
25-
rbacRequirements []*RBACRequirement
25+
rbacRequirements map[*ResourceRequirement]ResourceVerbs
2626
}
2727

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

90+
for _, res := range k.CheckResources(ctx) {
91+
if err := k.writer.WriteResult(res); err != nil {
92+
return xerrors.Errorf("check api resources: %w", err)
93+
}
94+
}
95+
9096
for _, res := range k.CheckRBAC(ctx) {
9197
if err := k.writer.WriteResult(res); err != nil {
9298
return xerrors.Errorf("check RBAC: %w", err)

internal/checks/kube/rbac.go

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,73 +16,34 @@ import (
1616
authorizationclientv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
1717
)
1818

19-
type RBACRequirement struct {
20-
APIGroup string
21-
Resource string
22-
Verbs []string
23-
}
24-
25-
type VersionedRBACRequirements struct {
26-
VersionConstraints *semver.Constraints
27-
RBACRequirements []*RBACRequirement
28-
}
29-
30-
var verbsCreateDeleteList = []string{"create", "delete", "list"}
31-
32-
func NewRBACRequirement(apiGroup, resource string, verbs ...string) *RBACRequirement {
33-
return &RBACRequirement{
34-
APIGroup: apiGroup,
35-
Resource: resource,
36-
Verbs: verbs,
37-
}
38-
}
39-
40-
var allVersionedRBACRequirements = []VersionedRBACRequirements{
41-
{
42-
VersionConstraints: api.MustConstraint(">= 1.20"),
43-
RBACRequirements: []*RBACRequirement{
44-
NewRBACRequirement("", "pods", verbsCreateDeleteList...),
45-
NewRBACRequirement("", "roles", verbsCreateDeleteList...),
46-
NewRBACRequirement("", "rolebindings", verbsCreateDeleteList...),
47-
NewRBACRequirement("", "secrets", verbsCreateDeleteList...),
48-
NewRBACRequirement("", "serviceaccounts", verbsCreateDeleteList...),
49-
NewRBACRequirement("", "services", verbsCreateDeleteList...),
50-
NewRBACRequirement("apps", "deployments", verbsCreateDeleteList...),
51-
NewRBACRequirement("apps", "replicasets", verbsCreateDeleteList...),
52-
NewRBACRequirement("apps", "statefulsets", verbsCreateDeleteList...),
53-
NewRBACRequirement("extensions", "ingresses", verbsCreateDeleteList...),
54-
},
55-
},
56-
}
57-
5819
func (k *KubernetesChecker) CheckRBAC(ctx context.Context) []*api.CheckResult {
5920
const checkName = "kubernetes-rbac"
6021
authClient := k.client.AuthorizationV1()
6122
results := make([]*api.CheckResult, 0)
6223

63-
for _, req := range k.rbacRequirements {
24+
for req, reqVerbs := range k.rbacRequirements {
6425
resName := fmt.Sprintf("%s-%s", checkName, req.Resource)
65-
if err := k.checkOneRBAC(ctx, authClient, req); err != nil {
26+
if err := k.checkOneRBAC(ctx, authClient, req, reqVerbs); err != nil {
6627
summary := fmt.Sprintf("missing permissions on resource %s: %s", req.Resource, err)
6728
results = append(results, api.ErrorResult(resName, summary, err))
6829
continue
6930
}
7031

71-
summary := fmt.Sprintf("%s: can %s", req.Resource, strings.Join(req.Verbs, ", "))
32+
summary := fmt.Sprintf("%s: can %s", req.Resource, strings.Join(reqVerbs, ", "))
7233
results = append(results, api.PassResult(resName, summary))
7334
}
7435

7536
return results
7637
}
7738

78-
func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authorizationclientv1.AuthorizationV1Interface, req *RBACRequirement) error {
79-
have := make([]string, 0, len(req.Verbs))
80-
for _, verb := range req.Verbs {
39+
func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authorizationclientv1.AuthorizationV1Interface, req *ResourceRequirement, reqVerbs ResourceVerbs) error {
40+
have := make([]string, 0, len(reqVerbs))
41+
for _, verb := range reqVerbs {
8142
sar := &authorizationv1.SelfSubjectAccessReview{
8243
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
8344
ResourceAttributes: &authorizationv1.ResourceAttributes{
8445
Namespace: k.namespace,
85-
Group: req.APIGroup,
46+
Group: req.Group,
8647
Resource: req.Resource,
8748
Verb: verb,
8849
},
@@ -102,17 +63,17 @@ func (k *KubernetesChecker) checkOneRBAC(ctx context.Context, authClient authori
10263
}
10364
}
10465

105-
if len(have) != len(req.Verbs) {
106-
return xerrors.Errorf(fmt.Sprintf("need: %+v have: %+v", req.Verbs, have))
66+
if len(have) != len(reqVerbs) {
67+
return xerrors.Errorf(fmt.Sprintf("need: %+v have: %+v", reqVerbs, have))
10768
}
10869

10970
return nil
11071
}
11172

112-
func findClosestVersionRequirements(v *semver.Version) []*RBACRequirement {
113-
for _, vreqs := range allVersionedRBACRequirements {
73+
func findClosestVersionRequirements(v *semver.Version) map[*ResourceRequirement]ResourceVerbs {
74+
for _, vreqs := range allRequirements {
11475
if vreqs.VersionConstraints.Check(v) {
115-
return vreqs.RBACRequirements
76+
return vreqs.ResourceRequirements
11677
}
11778
}
11879
return nil

internal/checks/kube/resources.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,60 @@ package kube
22

33
import (
44
"context"
5+
"fmt"
6+
7+
"golang.org/x/xerrors"
8+
"k8s.io/apimachinery/pkg/runtime/schema"
59

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

9-
func (k *KubernetesChecker) CheckResources(ctx context.Context) []*api.CheckResult {
13+
func (k *KubernetesChecker) CheckResources(_ context.Context) []*api.CheckResult {
14+
const checkName = "kubernetes-resources"
1015
results := make([]*api.CheckResult, 0)
16+
dc := k.client.Discovery()
17+
lists, err := dc.ServerPreferredResources()
18+
if err != nil {
19+
results = append(results, api.ErrorResult(checkName, "unable to fetch api resources from server", err))
20+
return results
21+
}
22+
23+
resourcesAvailable := make(map[ResourceRequirement]bool)
24+
for _, list := range lists {
25+
if len(list.APIResources) == 0 {
26+
continue
27+
}
28+
29+
gv, err := schema.ParseGroupVersion(list.GroupVersion)
30+
if err != nil {
31+
continue
32+
}
33+
34+
for _, resource := range list.APIResources {
35+
if len(resource.Verbs) == 0 {
36+
continue
37+
}
38+
39+
r := ResourceRequirement{
40+
Group: gv.Group,
41+
Version: gv.String(),
42+
Resource: resource.Name,
43+
}
44+
resourcesAvailable[r] = true
45+
}
46+
}
47+
48+
versionReqs := findClosestVersionRequirements(k.coderVersion)
49+
for versionReq := range versionReqs {
50+
if !resourcesAvailable[*versionReq] {
51+
msg := fmt.Sprintf("missing required resource:%q group:%q version:%q", versionReq.Resource, versionReq.Group, versionReq.Version)
52+
errResult := api.ErrorResult(checkName, msg, xerrors.New(msg))
53+
results = append(results, errResult)
54+
continue
55+
}
56+
msg := fmt.Sprintf("found required resource:%q group:%q version:%q", versionReq.Resource, versionReq.Group, versionReq.Version)
57+
results = append(results, api.PassResult(checkName, msg))
58+
}
59+
1160
return results
1261
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package kube
2+
3+
import (
4+
"github.com/Masterminds/semver/v3"
5+
6+
"github.com/cdr/coder-doctor/internal/api"
7+
)
8+
9+
var allRequirements = []VersionedResourceRequirements{
10+
{
11+
VersionConstraints: api.MustConstraint(">= 1.20"),
12+
ResourceRequirements: map[*ResourceRequirement]ResourceVerbs{
13+
NewResourceRequirement("", "v1", "pods"): verbsCreateDeleteList,
14+
NewResourceRequirement("", "v1", "secrets"): verbsCreateDeleteList,
15+
NewResourceRequirement("", "v1", "serviceaccounts"): verbsCreateDeleteList,
16+
NewResourceRequirement("", "v1", "services"): verbsCreateDeleteList,
17+
NewResourceRequirement("apps", "apps/v1", "deployments"): verbsCreateDeleteList,
18+
NewResourceRequirement("apps", "apps/v1", "replicasets"): verbsCreateDeleteList,
19+
NewResourceRequirement("apps", "apps/v1", "statefulsets"): verbsCreateDeleteList,
20+
NewResourceRequirement("networking.k8s.io", "networking.k8s.io/v1", "ingresses"): verbsCreateDeleteList,
21+
NewResourceRequirement("rbac.authorization.k8s.io", "rbac.authorization.k8s.io/v1", "roles"): verbsCreateDeleteList,
22+
NewResourceRequirement("rbac.authorization.k8s.io", "rbac.authorization.k8s.io/v1", "rolebindings"): verbsCreateDeleteList,
23+
},
24+
},
25+
}
26+
27+
// ResourceRequirement describes a set of requirements on a specific version of a resource:
28+
// whether it exists with that specific version, and what verbs the current user is permitted to perform
29+
// on the resource.
30+
type ResourceRequirement struct {
31+
Group string
32+
Resource string
33+
Version string
34+
}
35+
36+
type ResourceVerbs []string
37+
38+
// VersionedResourceRequirements is a set of ResourceRequirements for a specific version of Coder.
39+
type VersionedResourceRequirements struct {
40+
VersionConstraints *semver.Constraints
41+
ResourceRequirements map[*ResourceRequirement]ResourceVerbs
42+
}
43+
44+
var verbsCreateDeleteList ResourceVerbs = []string{"create", "delete", "list"}
45+
46+
// NewResourceRequirement is just a convenience function for creating ResourceRequirements.
47+
func NewResourceRequirement(apiGroup, version, resource string) *ResourceRequirement {
48+
return &ResourceRequirement{
49+
Group: apiGroup,
50+
Resource: resource,
51+
Version: version,
52+
}
53+
}

0 commit comments

Comments
 (0)