Skip to content

Commit

Permalink
ROX-24273: PR comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ludydoo committed May 22, 2024
1 parent 5d86710 commit 37cea7d
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 119 deletions.
288 changes: 170 additions & 118 deletions fleetshard/pkg/central/reconciler/gen/main.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
// We do not install the tenant-resources using helm, so we don't have garbage collection when we delete the chart.
// The main obstacle when performing garbage collection manually is that some resources might be optional (not rendered in all cases)
// and we need to be able to identify them. The reconciler should be able to identify the resources that it should manage.

// The purpose of this file is to
// 1. Extract the apiVersions/kinds that are present in the tenant-resources chart
// 2. Append to the list of apiVersion/kinds managed by the tenant-resources chart
// Resources removed from the chart will not be removed from the list. This is intentional.
// When rolling out a new version of the chart, the reconciler should be aware of resources
// managed by previous versions of the chart.
// 3. The generated file is used by the reconciler to determine the resources that it should manage
// 4. If the generated file is out of date, CI should fail
// This program generates a list of GVKs used by the tenant-resources helm chart for enabling garbage collection.
//
// To enable garbage collection without this list, we would need to...
// - Manually maintain a list of GVKs present in the chart, which would be error-prone.
// - Or have the reconciler list all objects for all possible GVKs, which would be very expensive.
// - Switch to the native helm client or the helm operator, which would require a lot of changes.
//
// This program automatically generates that list.
//
// Process:
// - It extract the GVKs that are present in the tenant-resources chart manifests
// - It extract the GVKs that are already declared in the generated file
// - It combines the two lists
// - It writes the combined list to the generated file

package main

import (
"fmt"
"io/fs"
"k8s.io/apimachinery/pkg/runtime/schema"
"os"
"path"
"path/filepath"
Expand All @@ -25,156 +28,205 @@ import (
"strings"
)

var (
reconcilerDir = path.Join(path.Dir(getCurrentFile()), "..")
outFile = fmt.Sprintf("%s/zzz_managed_resources.go", reconcilerDir)
tenantResourcesChartDir = path.Join(path.Dir(getCurrentFile()), "../../charts/data/tenant-resources/templates")
gvkRegex = regexp.MustCompile(`schema.GroupVersionKind{Group: "(.*)", Version: "(.*)", Kind: "(.*)"},`)
)

const (
apiVersionPrefix = "apiVersion: "
kindPrefix = "kind: "
)

func main() {
fmt.Println("generating managed resources list for the tenant-resources chart")
if err := generate(); err != nil {
panic(err)
}
}

type resourceMap map[schema.GroupVersionKind]bool

func generate() error {

// Holds a map of GVKs that will be in the output file
// The value `bool` indicates that the resource is present in the tenant-resources helm chart
// If false, this means that the resource is only present in the generated file (for GVKs removed from the chart)
// But the GVK is still needed for garbage collection.
seen := resourceMap{}

// Finding GVKs used in the tenant-resources chart
if err := findGVKsInChart(tenantResourcesChartDir, seen); err != nil {
return err
}

// Finding GVKs already declared in the generated file
if err := findGVKsInGeneratedFile(seen); err != nil {
return err
}

// Re-generating the file
if err := generateGVKsList(seen); err != nil {
return err
}

return nil
}

func getCurrentFile() string {
_, file, _, _ := runtime.Caller(0)
return file
}

func getChartDir() string {
return path.Join(path.Dir(getCurrentFile()), "../../charts/data/tenant-resources/templates")
func generateGVKsList(seen resourceMap) error {
// Making sure resources are ordered (for deterministic output)
sorted := sortResourceKeys(seen)

builder := strings.Builder{}
builder.WriteString("// Code generated by fleetshard/pkg/central/reconciler/gen/main.go. DO NOT EDIT.\n")
builder.WriteString("package reconciler\n\n")
builder.WriteString("import (\n")
builder.WriteString("\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n")
builder.WriteString(")\n\n")

builder.WriteString("// tenantChartResourceGVKs is a list of GroupVersionKind that... \n")
builder.WriteString("// - are present in the tenant-resources helm chart\n")
builder.WriteString("// - were present in a previous version of the chart. A comment will indicate that manual removal from the list is required.\n")

builder.WriteString("var tenantChartResourceGVKs = []schema.GroupVersionKind{\n")
for _, k := range sorted {
builder.WriteString(fmt.Sprintf("\tschema.GroupVersionKind{Group: %q, Version: %q, Kind: %q},", k.Group, k.Version, k.Kind))
stillInChart := seen[k]
if !stillInChart {
builder.WriteString(" // This resource was present in a previous version of the chart. Manual removal is required.")
}
builder.WriteString("\n")
}
builder.WriteString("}\n")

genFile, err := os.Create(outFile)
if err != nil {
return err
}
defer genFile.Close()

genFile.WriteString(builder.String())
return nil
}

func getReconcilerDir() string {
return path.Join(path.Dir(getCurrentFile()), "..")
func sortResourceKeys(seen resourceMap) []schema.GroupVersionKind {
sorted := make([]schema.GroupVersionKind, 0, len(seen))
for k := range seen {
sorted = append(sorted, k)
}
sort.Slice(sorted, func(i, j int) bool {
left := sorted[i]
right := sorted[j]
return left.String() < right.String()
})
return sorted
}

func generate() error {
chartDir := getChartDir()
seen := map[[3]string]bool{}
func findGVKsInGeneratedFile(seen resourceMap) error {
if _, err := os.Stat(outFile); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}

file, err := os.Open(outFile)
if err != nil {
return err
}
defer file.Close()

fileBytes, err := os.ReadFile(outFile)
if err != nil {
return err
}

lines := strings.Split(string(fileBytes), "\n")

for _, line := range lines {
matches := gvkRegex.FindStringSubmatch(line)
if len(matches) != 4 {
continue
}
gvk := schema.GroupVersionKind{Group: matches[1], Version: matches[2], Kind: matches[3]}
isAlreadyPresent := seen[gvk]
seen[gvk] = isAlreadyPresent
}
return nil
}

func findGVKsInChart(chartDir string, seen resourceMap) error {
if err := filepath.WalkDir(chartDir, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}

ext := filepath.Ext(path)
if ext != ".yaml" && ext != ".yml" {
return nil
}

fileBytes, err := os.ReadFile(path)
if err != nil {
return err
}

fileStr := string(fileBytes)
lines := strings.Split(fileStr, "\n")
for i := 0; i < len(lines)-1; i++ {
line1 := lines[i]
line2 := lines[i+1]
if line1 > line2 {
line1, line2 = line2, line1
}

if !strings.HasPrefix(line1, "apiVersion: ") {
continue
}
if !strings.HasPrefix(line2, "kind: ") {
continue
}
apiVersion := strings.TrimSpace(strings.TrimPrefix(line1, "apiVersion: "))
kind := strings.TrimSpace(strings.TrimPrefix(line2, "kind: "))
apiVersionParts := strings.Split(apiVersion, "/")
if len(apiVersionParts) > 2 {
return fmt.Errorf("invalid apiVersion %s", apiVersion)
}
var group string
var version string

if len(apiVersionParts) == 2 {
group = apiVersionParts[0]
version = apiVersionParts[1]
} else {
version = apiVersionParts[0]
}

seen[[3]string{kind, group, version}] = true
if err := findGVKsInFile(fileBytes, seen); err != nil {
return err
}

return nil
}); err != nil {
return err
}
return nil
}

outFile := fmt.Sprintf("%s/zzz_managed_resources.go", getReconcilerDir())

// retrieve existing declared resources
if _, err := os.Stat(outFile); err == nil {
// file exists
file, err := os.Open(outFile)
if err != nil {
return err
}
defer file.Close()
func splitFileIntoResources(fileBytes []byte) []string {
fileStr := string(fileBytes)
return strings.Split(fileStr, "---")
}

// read existing file
fileBytes, err := os.ReadFile(outFile)
if err != nil {
func findGVKsInFile(fileBytes []byte, seen resourceMap) error {
resources := splitFileIntoResources(fileBytes)
for _, resource := range resources {
gvk, ok, err := findResourceGVK(resource)
if err != nil || !ok {
return err
}

fileStr := string(fileBytes)
lines := strings.Split(fileStr, "\n")
rgx := regexp.MustCompile(`schema.GroupVersionKind{Group: "(.*)", Version: "(.*)", Kind: "(.*)"},`)
for _, line := range lines {
matches := rgx.FindStringSubmatch(line)
if len(matches) != 4 {
continue
}
kind := matches[3]
group := matches[1]
version := matches[2]
fmt.Printf("found existing resource: %s/%s/%s\n", kind, group, version)
key := [3]string{kind, group, version}
seen[key] = seen[key]
}
}

sorted := make([][3]string, 0, len(seen))
for k := range seen {
sorted = append(sorted, k)
seen[gvk] = true
}
sort.Slice(sorted, func(i, j int) bool {
left := sorted[i]
right := sorted[j]
return nil
}

if left[0] != right[0] {
return left[0] < right[0]
func findResourceGVK(resource string) (schema.GroupVersionKind, bool, error) {
lines := strings.Split(resource, "\n")
apiVersion := ""
kind := ""
for i := 0; i < len(lines); i++ {
if strings.HasPrefix(lines[i], apiVersionPrefix) {
apiVersion = strings.TrimSpace(strings.TrimPrefix(lines[i], apiVersionPrefix))
continue
}
if left[1] != right[1] {
return left[1] < right[1]
}
return left[2] < right[2]
})

builder := strings.Builder{}
builder.WriteString("// Code generated by fleetshard/pkg/central/reconciler/gen/main.go. DO NOT EDIT.\n")
builder.WriteString("package reconciler\n\n")
builder.WriteString("import (\n")
builder.WriteString("\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n")
builder.WriteString(")\n\n")

builder.WriteString("var tenantChartResourceGVKs = []schema.GroupVersionKind{\n")
for _, k := range sorted {
builder.WriteString(fmt.Sprintf("\tschema.GroupVersionKind{Group: %q, Version: %q, Kind: %q},", k[1], k[2], k[0]))
stillInChart := seen[k]
if !stillInChart {
builder.WriteString(" // This resource was present in a previous version of the chart. Manual removal is required.")
if strings.HasPrefix(lines[i], kindPrefix) {
kind = strings.TrimSpace(strings.TrimPrefix(lines[i], kindPrefix))
continue
}
builder.WriteString("\n")
}
builder.WriteString("}\n")
if len(apiVersion) == 0 || len(kind) == 0 {
return schema.GroupVersionKind{}, false, nil
}

genFile, err := os.Create(outFile)
groupVersion, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return err
return schema.GroupVersionKind{}, false, err
}
defer genFile.Close()

genFile.WriteString(builder.String())

return nil
return groupVersion.WithKind(kind), true, nil
}
5 changes: 4 additions & 1 deletion fleetshard/pkg/central/reconciler/zzz_managed_resources.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 37cea7d

Please sign in to comment.