Skip to content

Commit

Permalink
Merge pull request #44 from gianlucam76/restore
Browse files Browse the repository at this point in the history
Add ability to store resources.
  • Loading branch information
gianlucam76 authored Feb 2, 2024
2 parents 672d23c + 18a9d0e commit e2f4937
Show file tree
Hide file tree
Showing 37 changed files with 227 additions and 63 deletions.
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ To add an example, simply create a new file in the example directory with a desc

1️⃣ **Schedule**: Specify the frequency at which the Cleaner should scan the cluster and identify stale resources. Utilize the Cron syntax to define recurring schedules.

2️⃣ **DryRun**: Enable safe testing of the Cleaner's filtering logic without affecting actual resource configurations. Resources matching the criteria will be identified, but no changes will be applied.
2️⃣ **DryRun**: Enable safe testing of the Cleaner's filtering logic without affecting actual resource configurations. Resources matching the criteria will be identified, but no changes will be applied. Set Action to __Scan__.

3️⃣ **Label Filtering**: Select resources based on user-defined labels, filtering out unwanted or outdated components. Refine the selection based on label key, operation (equal, different, etc.), and value.

Expand Down Expand Up @@ -175,7 +175,6 @@ metadata:
name: completed-pods
spec:
schedule: "* 0 * * *"
dryRun: false
resourcePolicySet:
resourceSelectors:
- kind: Pod
Expand Down Expand Up @@ -283,7 +282,6 @@ metadata:
name: pods-from-job
spec:
schedule: "* 0 * * *"
dryRun: false
resourcePolicySet:
resourceSelectors:
- kind: Pod
Expand Down Expand Up @@ -420,7 +418,7 @@ YAML can be found [here](https://github.com/gianlucam76/k8s-cleaner/blob/main/un
## DryRun
To preview which resources match the __Cleaner__'s criteria, set the __DryRun__ flag to true. The Cleaner will still execute its logic but will not actually delete or update any resources. To identify matching resources, search the controller logs for the message "resource is a match for cleaner".
To preview which resources match the __Cleaner__'s criteria, set the __Action__ field to _Scan_. The Cleaner will still execute its logic but will not actually delete or update any resources. To identify matching resources, search the controller logs for the message "resource is a match for cleaner".
```yaml
apiVersion: apps.projectsveltos.io/v1alpha1
Expand All @@ -429,7 +427,6 @@ metadata:
name: cleaner-sample1
spec:
schedule: "* 0 * * *" # Runs every day at midnight
dryRun: true # Set to true to preview matching resources
resourcePolicySet:
resourceSelectors:
- namespace: test
Expand All @@ -443,10 +440,10 @@ spec:
- key: environment
operation: Different
value: prouction # Match deployments with the "environment" label different from "production"
action: Delete
action: Scan
```
By setting DryRun to true, you can safely test the Cleaner's filtering logic without affecting your actual deployment configurations. Once you're confident in the filtering criteria, you can set DryRun back to false to enable automatic resource deletion.
By setting __Action__ to _Scan_, you can safely test the Cleaner's filtering logic without affecting your actual deployment configurations. Once you're confident in the filtering criteria, you can set _Action_ to delete or modify.
## Schedule
Expand Down Expand Up @@ -618,6 +615,19 @@ spec:
namespace: test
```
### Store Resource YAML
Sometimes it is convenient to store resources before Cleaner deletes/modifies those.
Cleaner has an optional field __StoreResourcePath__. When set, Cleaner will dump all matching resources before any modification
(delete or updated) was done.
Matching resources will be stored
```
/<__StoreResourcePath__ value>/<Cleaner name>/<resourceNamespace>/<resource Kind>/<resource Name>.yaml
```

## Validate Your Cleaner Configuration

To verify the correctness of your __Cleaner__ configuration, follow the comprehensive instructions provided in the documentation: [here](https://github.com/gianlucam76/k8s-cleaner/blob/main/internal/controller/executor/validate_transform/README.md) and [here](https://github.com/gianlucam76/k8s-cleaner/blob/main/internal/controller/executor/validate_transform/README.md).
Expand Down
11 changes: 5 additions & 6 deletions api/v1alpha1/cleaner_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,6 @@ type CleanerSpec struct {
// +optional
Transform string `json:"transform,omitempty"`

// DryRun if set to true, will have controller delete/update no resource.
// All matching resources will be listed in logs
// +kubebuilder:default:=false
// +optional
DryRun bool `json:"dryRun,omitempty"`

// Schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.
Schedule string `json:"schedule"`

Expand All @@ -159,6 +153,11 @@ type CleanerSpec struct {
// +patchStrategy=merge,retainKeys
// +optional
Notifications []Notification `json:"notifications,omitempty"`

// StoreResources will store full resources in this directory.
// Must be a volume where Cleaner can dump all matching resources.
// +optional
StoreResourcePath string `json:"storeResourcePath,omitempty"`
}

// CleanerStatus defines the observed state of Cleaner
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/report_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ type ResourceInfo struct {
// Resource identify a Kubernetes resource
Resource corev1.ObjectReference `json:"resource,omitempty"`

// FullResource contains full resources before
// before Cleaner took an action on it
// +optional
FullResource []byte `json:"fullResource,omitempty"`

// Message is an optional field.
// +optional
Message string `json:"message,omitempty"`
Expand Down
9 changes: 8 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

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

9 changes: 4 additions & 5 deletions config/crd/bases/apps.projectsveltos.io_cleaners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ spec:
- Transform
- Scan
type: string
dryRun:
default: false
description: DryRun if set to true, will have controller delete/update
no resource. All matching resources will be listed in logs
type: boolean
notifications:
description: Notification is a list of source of events to evaluate.
items:
Expand Down Expand Up @@ -191,6 +186,10 @@ spec:
will be counted as failed ones.
format: int64
type: integer
storeResourcePath:
description: StoreResources will store full resources in this directory.
Must be a volume where Cleaner can dump all matching resources.
type: string
transform:
description: Transform contains a function "transform" in lua language.
When Action is set to *Transform*, this function will be invoked
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/apps.projectsveltos.io_reports.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ spec:
description: Resources identify a set of Kubernetes resource
items:
properties:
fullResource:
description: FullResource contains full resources before before
Cleaner took an action on it
format: byte
type: string
message:
description: Message is an optional field.
type: string
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/cleaner_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (r *CleanerReconciler) reconcileNormal(ctx context.Context, cleanerScope *s
func (r *CleanerReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager,
numOfWorker int, logger logr.Logger) error {

executor.InitializeClient(ctx, logger, mgr.GetConfig(), mgr.GetClient(), numOfWorker)
executor.InitializeClient(ctx, logger, mgr.GetConfig(), mgr.GetClient(), mgr.GetScheme(), numOfWorker)

return ctrl.NewControllerManagedBy(mgr).
For(&appsv1alpha1.Cleaner{}).
Expand Down
5 changes: 4 additions & 1 deletion internal/controller/executor/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -72,6 +73,7 @@ type Manager struct {
log logr.Logger
client.Client
config *rest.Config
scheme *runtime.Scheme

mu *sync.Mutex

Expand All @@ -92,7 +94,7 @@ type Manager struct {

// InitializeClient initializes a client
func InitializeClient(ctx context.Context, l logr.Logger, config *rest.Config,
c client.Client, numOfWorker int) {
c client.Client, scheme *runtime.Scheme, numOfWorker int) {

if managerInstance == nil {
getClientLock.Lock()
Expand Down Expand Up @@ -124,6 +126,7 @@ func (m *Manager) startWorkloadWorkers(ctx context.Context, numOfWorker int, log
m.results = make(map[string]error)
k8sClient = m.Client
config = m.config
scheme = m.scheme

for i := 0; i < numOfWorker; i++ {
go processRequests(ctx, i, logger.WithValues("worker", fmt.Sprintf("%d", i)))
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/executor/executor_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ var _ = BeforeSuite(func() {
logger, err := zap.NewDevelopment()
Expect(err).To(BeNil())

executor.InitializeClient(context.TODO(), zapr.NewLogger(logger), config, k8sClient, 10)
executor.InitializeClient(context.TODO(), zapr.NewLogger(logger), config, k8sClient, scheme, 10)

By("bootstrapping completed")
})
Expand Down
5 changes: 0 additions & 5 deletions internal/controller/executor/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"fmt"
"io"
"os"
"strings"
"time"

"github.com/bwmarrin/discordgo"
Expand Down Expand Up @@ -98,10 +97,6 @@ func generateReportSpec(resources []ResourceResult, cleaner *appsv1alpha1.Cleane
reportSpec := appsv1alpha1.ReportSpec{}
reportSpec.Action = cleaner.Spec.Action
message := fmt.Sprintf(". time: %v", time.Now())
if cleaner.Spec.DryRun {
message += fmt.Sprintf(" . Cleaner is set in DryRun mode so no resource was actually %s",
strings.ToLower(string(cleaner.Spec.Action)))
}

reportSpec.ResourceInfo = make([]appsv1alpha1.ResourceInfo, len(resources))
for i := range resources {
Expand Down
147 changes: 147 additions & 0 deletions internal/controller/executor/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
Copyright 2023. projectsveltos.io. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package executor

import (
"fmt"
appsv1alpha1 "gianlucam76/k8s-cleaner/api/v1alpha1"
"os"
"path"
"path/filepath"

"github.com/go-logr/logr"
"gopkg.in/yaml.v2"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

logs "github.com/projectsveltos/libsveltos/lib/logsettings"
)

const (
permission0600 = 0600
permission0644 = 0644
permission0755 = 0755
)

func storeResources(processedResources []ResourceResult, scheme *runtime.Scheme, cleaner *appsv1alpha1.Cleaner,
logger logr.Logger) error {

if cleaner.Spec.StoreResourcePath == "" {
return nil
}

folder, err := getFolder(cleaner.Spec.StoreResourcePath, cleaner.Name, logger)
if err != nil {
return err
}

for i := range processedResources {
err = dumpObject(processedResources[i].Resource, scheme, *folder, logger)
if err != nil {
logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to store object %s %s/%s: %v",
processedResources[i].Resource.GetKind(), processedResources[i].Resource.GetNamespace(),
processedResources[i].Resource.GetName(), err))
// Error is ignored as Cleaner tries to store other resources
}
}

return nil
}

func getFolder(storage, cleanerName string, logger logr.Logger) (*string, error) {
l := logger.WithValues("cleaner", cleanerName)
l.V(logs.LogDebug).Info("getting directory containing collections for instance")

if _, err := os.Stat(storage); os.IsNotExist(err) {
logger.V(logs.LogInfo).Info(fmt.Sprintf("directory %s not found", storage))
return nil, err
}

artifactFolder := filepath.Join(storage, cleanerName)

return &artifactFolder, nil
}

// dumpObject is a helper function to generically dump resource definition
// given the resource reference and file path for dumping location.
func dumpObject(resource *unstructured.Unstructured, scheme *runtime.Scheme, logPath string, logger logr.Logger) error {
// Do not store resource version
resource.SetResourceVersion("")
err := addTypeInformationToObject(scheme, resource)
if err != nil {
return err
}

logger = logger.WithValues("kind", resource.GetObjectKind())
logger = logger.WithValues("resource", fmt.Sprintf("%s %s",
resource.GetNamespace(), resource.GetName()))

if !resource.GetDeletionTimestamp().IsZero() {
logger.V(logs.LogDebug).Info("resource is marked for deletion. Do not collect it.")
}

resourceYAML, err := yaml.Marshal(resource.UnstructuredContent())
if err != nil {
return err
}

metaObj, err := apimeta.Accessor(resource)
if err != nil {
return err
}

kind := resource.GetObjectKind().GroupVersionKind().Kind
namespace := metaObj.GetNamespace()
name := metaObj.GetName()

resourceFilePath := path.Join(logPath, namespace, kind, name+".yaml")
err = os.MkdirAll(filepath.Dir(resourceFilePath), permission0755)
if err != nil {
return err
}

f, err := os.OpenFile(resourceFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, permission0644)
if err != nil {
return err
}
defer f.Close()

logger.V(logs.LogDebug).Info(fmt.Sprintf("storing resource in %s", resourceFilePath))
return os.WriteFile(f.Name(), resourceYAML, permission0600)
}

func addTypeInformationToObject(scheme *runtime.Scheme, obj client.Object) error {
gvks, _, err := scheme.ObjectKinds(obj)
if err != nil {
return err
}

for _, gvk := range gvks {
if gvk.Kind == "" {
continue
}
if gvk.Version == "" || gvk.Version == runtime.APIVersionInternal {
continue
}
obj.GetObjectKind().SetGroupVersionKind(gvk)
break
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ metadata:
spec:
schedule: "* 0 * * *"
action: Delete
dryRun: false
resourcePolicySet:
resourceSelectors:
- kind: Pod
Expand Down
Loading

0 comments on commit e2f4937

Please sign in to comment.