Skip to content

Commit

Permalink
reducing complexity in pkg/k8s/rbac/gen.go
Browse files Browse the repository at this point in the history
* upgrade orderedmap to v2.2.0 to support generic
* gofmt to address go report card
* new functions for each step in role creation & add tests

remove internal pkgs, add GoDS

implement role gen, add docs, make and print version

rearrange main, use make in build workflow

[skip ci] fix gh badge, add pkg.go.dev

fix missing resources & failing tests, update CI

* new make target for running tests
* removed kubeval & added kubeconform.
  See: instrumenta/kubeval#268 (comment)
* use newer conftest install instructions
* fix version flag

make test & e2e separate, fix workflow

attempt to fix ci again

path issue

merge resources across matching API groups

* fixes regression. Necessary for the batch/v1 jobs resource to not be erased. Jobs are the only API resource
  that do not appear in alpha/beta groups where its companion
  resource, CronJob, does in fact exist in two groups (up until k8s v1.25).

 see: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-25

passing test, sort resource names

empty commit
  • Loading branch information
coopernetes committed Feb 21, 2023
1 parent 04b2261 commit bd74349
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 168 deletions.
19 changes: 11 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,29 @@ jobs:
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies & install
- name: Get dependencies, run go test & install
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
go install -v .
curl -fsSLO https://github.com/open-policy-agent/conftest/releases/download/v0.30.0/conftest_0.30.0_Linux_x86_64.tar.gz
tar -C /usr/local/bin -xzvf conftest_0.30.0_Linux_x86_64.tar.gz
wget -q https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz
tar xf kubeval-linux-amd64.tar.gz
sudo cp kubeval /usr/local/bin
make test SHELL='bash -x'
make install SHELL='bash -x'
export PATH="$(go env GOPATH)/bin:$PATH"
kube-role-gen -help
- name: Setup kind
uses: engineerd/setup-kind@v0.5.0
with:
version: "v0.12.0"
image: kindest/node:v1.23.4@sha256:0e34f0d0fd448aa2f2819cfd74e99fe5793a6e4938b328f657c8e3f81ee0dfb9
- name: Run Kubernetes tests
run: |
LATEST_VERSION=$(wget -O - "https://api.github.com/repos/open-policy-agent/conftest/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-)
wget "https://github.com/open-policy-agent/conftest/releases/download/v${LATEST_VERSION}/conftest_${LATEST_VERSION}_Linux_x86_64.tar.gz"
tar xzf conftest_${LATEST_VERSION}_Linux_x86_64.tar.gz
sudo mv conftest /usr/local/bin
go install github.com/yannh/kubeconform/cmd/kubeconform@latest
kubectl cluster-info
export PATH="$(go env GOPATH)/bin:$PATH"
tests/e2e_tests.sh
make e2e
54 changes: 54 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
SHELL := /bin/bash

# The name of the executable (default is current directory name)
TARGET := $(shell echo "$${PWD##*/}" )
MAIN := ./cmd/kube-role-gen
.DEFAULT_GOAL: $(TARGET)

# These will be provided to the target
VERSION := 0.0.6
BUILD := `git rev-parse HEAD`

# Use linker flags to provide version/build settings to the target
LDFLAGS=-ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)"

# go source files, ignore vendor directory
SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*")

.PHONY: all build test clean install uninstall fmt simplify check run

all: check install

$(TARGET): $(SRC)
@go build $(LDFLAGS) -o $(TARGET) $(MAIN)/main.go

build: $(TARGET)
@true

test:
go test ./...

e2e: test
tests/e2e_tests.sh

clean:
@rm -f $(TARGET)

install:
@go install $(LDFLAGS) $(MAIN)

uninstall: clean
@rm -f $$(which ${TARGET})

fmt:
@gofmt -l -w $(SRC)

simplify:
@gofmt -s -l -w $(SRC)

check:
@test -z $(shell gofmt -l $(MAIN)/main.go | tee /dev/stderr) || echo "[WARN] Fix formatting issues with 'make fmt'"
@go vet ./...

run: install
@$(TARGET)
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# kube-role-gen
[![Go](https://github.com/coopernetes/kube-role-gen/workflows/Go/badge.svg)](https://github.com/coopernetes/kube-role-gen/actions?query=workflow%3AGo)
[![build](https://github.com/coopernetes/kube-role-gen/workflows/build/test/badge.svg)](https://github.com/coopernetes/kube-role-gen/actions?query=workflow%3AGo)
[![Go Report Card](https://goreportcard.com/badge/github.com/coopernetes/kube-role-gen)](https://goreportcard.com/report/github.com/coopernetes/kube-role-gen)
[![Go Reference](https://pkg.go.dev/badge/github.com/coopernetes/kube-role-gen.svg)](https://pkg.go.dev/github.com/coopernetes/kube-role-gen)

_Create a complete Kubernetes RBAC role_

Expand Down
48 changes: 34 additions & 14 deletions cmd/rolegen/main.go → cmd/kube-role-gen/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package rolegen
// The main package for the kube-role-gen executable
package main

import (
"bytes"
"flag"
"fmt"
"os"
"log"
"bytes"
"github.com/coopernetes/kube-role-gen/pkg/k8s"
jsonSer "k8s.io/apimachinery/pkg/runtime/serializer/json"
client "github.com/coopernetes/kube-role-gen/internal/k8s"
gen "github.com/coopernetes/kube-role-gen/pkg/k8s/rbac"
"log"
"os"
)

func Run() {
const Version = "v0.0.6"

func main() {
name := flag.String("name", "foo-clusterrole", "Override the name of the ClusterRole "+
"resource that is generated")
verbose := flag.Bool("v", false, "Enable verbose logging.")
Expand All @@ -20,24 +22,42 @@ func Run() {
kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file. "+
"If set, this will override the default behavior and "+
"ignore KUBECONFIG environment variable and/or $HOME/.kube/config file location.")
printVersion := flag.Bool("version", false, "Print version info")
flag.Parse()
d, err := client.SetupDiscoveryClient(*kubeconfig)
if *printVersion {
fmt.Println(Version)
os.Exit(0)
}

d, err := k8s.SetupDiscoveryClient(*kubeconfig)
if err != nil {
log.Printf("Error during client setup: %s", err.Error())
os.Exit(1)
}
_, list, err := d.ServerGroupsAndResources()
if err != nil {
fmt.Errorf("Unable to setup client!")
log.Printf("Error during resource discovery: %s", err.Error())
os.Exit(1)
}
cr := gen.GetOrderedResources(*d, *name, *verbose)
options := jsonSer.SerializerOptions{
Yaml: !*json,
Pretty: *jsonPretty,
cr := k8s.CreateGranularRole(list, *name, *verbose)
if err != nil {
log.Printf("Error during role creation, %s", err.Error())
os.Exit(1)
}
options := serializerOptions(*json, *jsonPretty)
serializer := jsonSer.NewSerializerWithOptions(jsonSer.DefaultMetaFactory, nil, nil, options)
var writer = bytes.NewBufferString("")
e := serializer.Encode(cr, writer)
if e != nil {
log.Printf("Error encountered during YAML encoding, %s", e.Error())
log.Printf("Error encountered during encoding, %s", e.Error())
os.Exit(1)
}
fmt.Println(writer.String())
}

func serializerOptions(json bool, pretty bool) jsonSer.SerializerOptions {
if json {
return jsonSer.SerializerOptions{Yaml: false, Pretty: pretty}
}
return jsonSer.SerializerOptions{Yaml: true, Pretty: false}
}
8 changes: 0 additions & 8 deletions cmd/rolegen/main_test.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/coopernetes/kube-role-gen
go 1.19

require (
github.com/elliotchance/orderedmap v1.5.0
github.com/elliotchance/orderedmap/v2 v2.2.0
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elliotchance/orderedmap v1.5.0 h1:1IsExUsjv5XNBD3ZdC7jkAAqLWOOKdbPTmkHx63OsBg=
github.com/elliotchance/orderedmap v1.5.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -107,7 +107,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
Expand Down
11 changes: 0 additions & 11 deletions internal/util/set.go

This file was deleted.

9 changes: 0 additions & 9 deletions main.go

This file was deleted.

12 changes: 10 additions & 2 deletions internal/k8s/client.go → pkg/k8s/discovery.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package k8s

import (
"log"
"os"
"k8s.io/client-go/discovery"
"k8s.io/client-go/tools/clientcmd"
"log"
"os"
)

// SetupDiscoveryClient will create a new DiscoveryClient. When the kubeconfig arg is unset, the
// client setup uses the usual default behaviour to load either from KUBECONFIG environment variable
// or the default location (usually $HOME/.kube/config). This is provided via client-go package via
// clientcmd.NewDefaultClientConfigLoadingRules.
//
// If kubeconfig string is non-empty, the client will attempt to load the configuration using this value
// by setting the ExplicitPath field on clientcmd.ClientConfigLoadingRules to override the default
// loading rules.
func SetupDiscoveryClient(kubeconfig string) (*discovery.DiscoveryClient, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if kubeconfig != "" {
Expand Down
129 changes: 129 additions & 0 deletions pkg/k8s/rbac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Package k8s provides functions to create Kubernetes RBAC roles objects based
// on discovered API resources. It also provides utility functions to setup a
// discovery client for a provided kubeconfig and obtain the list of discovered
// resources for use in this package.
package k8s

import (
"github.com/elliotchance/orderedmap/v2"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/strings/slices"
"log"
"sort"
"strings"
)

// CreateGranularRole creates a ClusterRole where each rules entry contains only the specific combination of API group
// and supported verbs for each resource. Resources with matching verbs are grouped together in a single PolicyRule.
// This differs from other implementations such as `kubectl create clusterrole` which will group together resources
// with verbs that are not applicable or supported.
//
// All PolicyRules in the ClusterRole this function returns represents a "matrix" of all resources available on the API
// and contains only the list of the supported verbs that resource handles.
func CreateGranularRole(apiResourceList []*metav1.APIResourceList, name string, verbose bool) *rbacv1.ClusterRole {
oMap := orderedmap.NewOrderedMap[string, map[string][]string]()
for _, resourceList := range apiResourceList {
if verbose {
log.Printf("Group %s contains %d resources", resourceList.GroupVersion, len(resourceList.APIResources))
}
groupName := extractGroupFromVersion(resourceList.GroupVersion)
if slices.Contains(oMap.Keys(), groupName) {
left, _ := oMap.Get(groupName)
right := convertToVerbMap(resourceList.APIResources, verbose)
oMap.Set(groupName, mergeVerbMaps(left, right))
} else {
oMap.Set(groupName, convertToVerbMap(resourceList.APIResources, verbose))
}
}
policyRules := policyRuleByOrderedMap(*oMap)
return &rbacv1.ClusterRole{
TypeMeta: metav1.TypeMeta{
Kind: "ClusterRole",
APIVersion: "rbac.authorization.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Rules: policyRules,
}
}

func extractGroupFromVersion(groupVersion string) string {
if groupVersion == "v1" {
return ""
}
return strings.Split(groupVersion, "/")[0]
}

func convertToVerbMap(resList []metav1.APIResource, verbose bool) map[string][]string {
verbMap := make(map[string][]string)
for _, res := range resList {
if verbose {
log.Printf("Resource: %s - Verbs: %s",
res.Name,
res.Verbs.String())
}
verbs := make([]string, len(res.Verbs))
copy(verbs, res.Verbs)
sort.Strings(verbs)
verbKey := strings.Join(verbs, ",")
if val, ok := verbMap[verbKey]; ok {
verbMap[verbKey] = append(val, res.Name)
} else {
verbMap[verbKey] = []string{res.Name}
}
}
for k := range verbMap {
sort.Strings(verbMap[k])
}
return verbMap
}

func mergeVerbMaps(left map[string][]string, right map[string][]string) map[string][]string {
merged := make(map[string][]string)
for k := range left {
merged[k] = left[k]
}
for k := range right {
if val, ok := merged[k]; ok {
set := make(map[string]bool)
for _, v := range right[k] {
set[v] = true
}
for _, v := range val {
set[v] = true
}
merged[k] = mapToSet(set)
} else {
merged[k] = right[k]
}
}
return merged
}

func policyRuleByOrderedMap(oMap orderedmap.OrderedMap[string, map[string][]string]) []rbacv1.PolicyRule {
policyRules := make([]rbacv1.PolicyRule, 0)
for _, group := range oMap.Keys() {
verbMap, _ := oMap.Get(group)
for verbStr := range verbMap {
verbs := strings.Split(verbStr, ",")
groupName := group
pr := &rbacv1.PolicyRule{
APIGroups: []string{groupName},
Verbs: verbs,
Resources: verbMap[verbStr],
}
policyRules = append(policyRules, *pr)
}
}
return policyRules
}

func mapToSet(m map[string]bool) []string {
s := make([]string, 0)
for k := range m {
s = append(s, k)
}
return s
}

0 comments on commit bd74349

Please sign in to comment.