Skip to content

Commit

Permalink
ROX-24273: garbage collection for tenant-resources chart
Browse files Browse the repository at this point in the history
  • Loading branch information
ludydoo committed May 21, 2024
1 parent 0328362 commit 1cb030c
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 28 deletions.
3 changes: 3 additions & 0 deletions fleetshard/pkg/central/reconciler/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package reconciler

//go:generate go run gen/main.go
180 changes: 180 additions & 0 deletions fleetshard/pkg/central/reconciler/gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// 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

package main

import (
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
)

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

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

func getChartDir() string {
return path.Join(path.Dir(getCurrentFile()), "../../charts/data/tenant-resources/templates")
}

func getReconcilerDir() string {
return path.Join(path.Dir(getCurrentFile()), "..")
}

func generate() error {
chartDir := getChartDir()
seen := map[[3]string]bool{}
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
}
return nil
}); err != nil {
return err
}

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()

// read existing file
fileBytes, err := os.ReadFile(outFile)
if err != nil {
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)
}
sort.Slice(sorted, func(i, j int) bool {
left := sorted[i]
right := sorted[j]

if left[0] != right[0] {
return left[0] < right[0]
}
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.")
}
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
}
130 changes: 102 additions & 28 deletions fleetshard/pkg/central/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ const (
additionalAuthProviderConfigKey = "additional-auth-provider"

tenantImagePullSecretName = "stackrox" // pragma: allowlist secret

helmChartLabelKey = "helm.sh/chart"
helmChartNameLabel = "helm.sh/chart-name"
)

type verifyAuthProviderExistsFunc func(ctx context.Context, central private.ManagedCentral, client ctrlClient.Client) (bool, error)
Expand Down Expand Up @@ -1549,6 +1552,14 @@ func (r *CentralReconciler) disablePauseReconcileIfPresent(ctx context.Context,
}

func (r *CentralReconciler) ensureChartResourcesExist(ctx context.Context, remoteCentral private.ManagedCentral) error {
getObjectKey := func(obj *unstructured.Unstructured) string {
return fmt.Sprintf("%s/%s/%s",
obj.GetAPIVersion(),
obj.GetKind(),
obj.GetName(),
)
}

vals, err := r.chartValues(remoteCentral)
if err != nil {
return fmt.Errorf("obtaining values for resources chart: %w", err)
Expand All @@ -1558,55 +1569,118 @@ func (r *CentralReconciler) ensureChartResourcesExist(ctx context.Context, remot
if err != nil {
return fmt.Errorf("rendering resources chart: %w", err)
}

helmChartLabelValue := r.getTenantResourcesChartHelmLabelValue()

// objectsThatShouldExist stores the keys of the objects we want to exist
var objectsThatShouldExist = map[string]struct{}{}

for _, obj := range objs {
objectsThatShouldExist[getObjectKey(obj)] = struct{}{}

if obj.GetNamespace() == "" {
obj.SetNamespace(remoteCentral.Metadata.Namespace)
}
if obj.GetLabels() == nil {
obj.SetLabels(map[string]string{})
}
labels := obj.GetLabels()
labels[managedByLabelKey] = labelManagedByFleetshardValue
labels[helmChartLabelKey] = helmChartLabelValue
labels[helmChartNameLabel] = r.resourcesChart.Name()
obj.SetLabels(labels)
err := charts.InstallOrUpdateChart(ctx, obj, r.client)
if err != nil {
return fmt.Errorf("failed to update central tenant object %w", err)
}
}

// Perform garbage collection
for _, gvk := range tenantChartResourceGVKs {
gvk := gvk
var existingObjects unstructured.UnstructuredList
existingObjects.SetGroupVersionKind(gvk)

if err := r.client.List(ctx, &existingObjects,
ctrlClient.InNamespace(remoteCentral.Metadata.Namespace),
ctrlClient.MatchingLabels{helmChartNameLabel: r.resourcesChart.Name()},
); err != nil {
return fmt.Errorf("failed to list tenant resources chart objects %v: %w", gvk, err)
}

for _, existingObject := range existingObjects.Items {
existingObject := &existingObject
if _, shouldExist := objectsThatShouldExist[getObjectKey(existingObject)]; shouldExist {
continue
}

// Re-check that the helm label is present & namespace matches.
// Failsafe against some potential k8s-client bug when listing objects with a label selector
if existingObject.GetLabels() == nil || existingObject.GetLabels()[helmChartNameLabel] != r.resourcesChart.Name() || existingObject.GetNamespace() != remoteCentral.Metadata.Namespace {
continue
}

if existingObject.GetDeletionTimestamp() != nil {
continue
}

// The object exists but it should not. Delete it.
if err := r.client.Delete(ctx, existingObject); err != nil {
if !apiErrors.IsNotFound(err) {
return fmt.Errorf("failed to delete central tenant object %w", err)
}
}
}
}

return nil
}

func (r *CentralReconciler) getTenantResourcesChartHelmLabelValue() string {
// the objects rendered by the helm chart will have a label in the format
// helm.sh/chart: <chart-name>-<chart-version>
return fmt.Sprintf("%s-%s", r.resourcesChart.Name(), r.resourcesChart.Metadata.Version)
}

func (r *CentralReconciler) ensureChartResourcesDeleted(ctx context.Context, remoteCentral *private.ManagedCentral) (bool, error) {
vals, err := r.chartValues(*remoteCentral)
if err != nil {
return false, fmt.Errorf("obtaining values for resources chart: %w", err)
}

objs, err := charts.RenderToObjects(helmReleaseName, remoteCentral.Metadata.Namespace, r.resourcesChart, vals)
if err != nil {
return false, fmt.Errorf("rendering resources chart: %w", err)
}
allObjectsDeleted := true

waitForDelete := false
for _, obj := range objs {
key := ctrlClient.ObjectKey{Namespace: obj.GetNamespace(), Name: obj.GetName()}
if key.Namespace == "" {
key.Namespace = remoteCentral.Metadata.Namespace
for _, gvk := range tenantChartResourceGVKs {
gvk := gvk
var existingObjects unstructured.UnstructuredList
existingObjects.SetGroupVersionKind(gvk)

if err := r.client.List(ctx, &existingObjects,
ctrlClient.InNamespace(remoteCentral.Metadata.Namespace),
ctrlClient.MatchingLabels{helmChartNameLabel: r.resourcesChart.Name()},
); err != nil {
return false, fmt.Errorf("failed to list tenant resources chart objects %v: %w", gvk, err)
}
var out unstructured.Unstructured
out.SetGroupVersionKind(obj.GroupVersionKind())
err := r.client.Get(ctx, key, &out)
if err != nil {
if apiErrors.IsNotFound(err) {

for _, existingObject := range existingObjects.Items {
existingObject := &existingObject

// Re-check that the helm label is present & namespace matches.
// Failsafe against some potential k8s-client bug when listing objects with a label selector
if existingObject.GetLabels() == nil || existingObject.GetLabels()[helmChartNameLabel] != r.resourcesChart.Name() || existingObject.GetNamespace() != remoteCentral.Metadata.Namespace {
continue
}
return false, fmt.Errorf("retrieving object %s/%s of type %v: %w", key.Namespace, key.Name, obj.GroupVersionKind(), err)
}
if out.GetDeletionTimestamp() != nil {
waitForDelete = true
continue
}
err = r.client.Delete(ctx, &out)
if err != nil && !apiErrors.IsNotFound(err) {
return false, fmt.Errorf("retrieving object %s/%s of type %v: %w", key.Namespace, key.Name, obj.GroupVersionKind(), err)

if existingObject.GetDeletionTimestamp() != nil {
allObjectsDeleted = false
continue
}

if err := r.client.Delete(ctx, existingObject); err != nil {
if !apiErrors.IsNotFound(err) {
return false, fmt.Errorf("failed to delete central tenant object %w", err)
}
}
}
}
return !waitForDelete, nil

return allObjectsDeleted, nil
}

func (r *CentralReconciler) ensureRoutesExist(ctx context.Context, remoteCentral private.ManagedCentral) error {
Expand Down
1 change: 1 addition & 0 deletions fleetshard/pkg/central/reconciler/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2622,6 +2622,7 @@ func TestReconciler_ensureChartResourcesExist_labelsAndAnnotations(t *testing.T)
"rhacs.redhat.com/tenant": "cb45idheg5ip6dq1jo4g",
"app.kubernetes.io/component": "egress-proxy",
"app.kubernetes.io/name": "central-tenant-resources",
"helm.sh/chart-name": "central-tenant-resources",
"helm.sh/chart": "central-tenant-resources-0.0.0",
}, egressProxyDeployment.ObjectMeta.Labels)

Expand Down
13 changes: 13 additions & 0 deletions 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 1cb030c

Please sign in to comment.