Skip to content

Commit

Permalink
fix: Enforce NonAdminBackup spec field values (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
mateusoliveira43 authored Nov 21, 2024
1 parent 697ab8f commit 9fdcb88
Show file tree
Hide file tree
Showing 10 changed files with 622 additions and 44 deletions.
50 changes: 46 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ limitations under the License.
package main

import (
"context"
"crypto/tls"
"flag"
"fmt"
"os"

// TODO when to update oadp-operator version in go.mod?
"github.com/openshift/oadp-operator/api/v1alpha1"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
Expand All @@ -55,6 +60,8 @@ func init() {
// +kubebuilder:scaffold:scheme
}

// +kubebuilder:rbac:groups=oadp.openshift.io,resources=dataprotectionapplications,verbs=list

func main() {
var metricsAddr string
var enableLeaderElection bool
Expand Down Expand Up @@ -104,7 +111,15 @@ func main() {
os.Exit(1)
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
restConfig := ctrl.GetConfigOrDie()

enforcedBackupSpec, err := getEnforcedSpec(restConfig, oadpNamespace)
if err != nil {
setupLog.Error(err, "unable to get enforced spec")
os.Exit(1)
}

mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
Expand Down Expand Up @@ -133,9 +148,10 @@ func main() {
}

if err = (&controller.NonAdminBackupReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OADPNamespace: oadpNamespace,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OADPNamespace: oadpNamespace,
EnforcedBackupSpec: enforcedBackupSpec,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NonAdminBackup")
os.Exit(1)
Expand All @@ -157,3 +173,29 @@ func main() {
os.Exit(1)
}
}

func getEnforcedSpec(restConfig *rest.Config, oadpNamespace string) (*velerov1.BackupSpec, error) {
dpaClientScheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(dpaClientScheme))
dpaClient, err := client.New(restConfig, client.Options{
Scheme: dpaClientScheme,
})
if err != nil {
return nil, err
}
// TODO we could pass DPA name as env var and do a get call directly. Better?
dpaList := &v1alpha1.DataProtectionApplicationList{}
err = dpaClient.List(context.Background(), dpaList)
if err != nil {
return nil, err
}
enforcedBackupSpec := &velerov1.BackupSpec{}
for _, dpa := range dpaList.Items {
if dpa.Namespace == oadpNamespace {
if dpa.Spec.NonAdmin != nil && dpa.Spec.NonAdmin.EnforceBackupSpec != nil {
enforcedBackupSpec = dpa.Spec.NonAdmin.EnforceBackupSpec
}
}
}
return enforcedBackupSpec, nil
}
6 changes: 6 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ kind: ClusterRole
metadata:
name: non-admin-controller-role
rules:
- apiGroups:
- oadp.openshift.io
resources:
- dataprotectionapplications
verbs:
- list
- apiGroups:
- oadp.openshift.io
resources:
Expand Down
221 changes: 221 additions & 0 deletions docs/design/admin_control_over_spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# Admin users control over non admin objects' spec

## Abstract

Non Admin Controller (NAC) restricts the usage of OADP operator with NonAdminBackups and NonAdminRestores.
Admin users may want to further restrict this by restricting NonAdminBackup/NonAdminRestore spec fields values.

## Background

Non Admin Controller (NAC) adds the ability to admin users restrict the use of OADP operator for non admin users, by only allowing them to create backup/restores from their namespaces with NonAdminBackups/NonAdminRestores.
Admin users may want to further restrict non admin users operations, like forcing a specific time to live (TTL) for NonAdminBackups associated Velero Backups.
This design enables admin users to set custom default values for NonAdminBackup/NonAdminRestore spec fields, which can not be overridden by non-admin users.

## Goals

Enable admin users to
- set custom default values for NonAdminBackup spec.backupSpec fields, which can not be overridden
- TODO restore

Also
- Show custom default values validation errors in NAC object statuses and in NAC logs

## Non Goals

- Show NonAdminBackup spec.backupSpec fields/TODO NonAdminRestore custom default values to non admin users
- Prevent non admin users to create NonAdminBackup/NonAdminRestore with overridden defaults
- Allow admin users to set second level defaults (for example, NonAdminBackup `spec.backupSpec.labelSelector` can have a custom default value, but not just `spec.backupSpec.labelSelector.matchLabels`)
- Check if there are on-going NAC operations prior to recreating NAC Pod

## High-Level Design

A field will be added to OADP DPA object. With it, admin users will be able to select which NonAdminBackup `spec.backupSpec` fields have custom default (and enforced) values. NAC will respect the set values. If a NonAdminBackup is created with fields overriding any enforced values, it will fail validation prior to creating an associated Velero Backup. If admin user changes any enforced field value, NAC Pod is recreated to always be up to date with admin user enforcements.

TODO restore

> **Note:** if there are on-going NAC operations prior to recreating NAC Pod, reconcile progress might get lost for NAC objects.
## Detailed Design

Field `spec.nonAdmin.enforceBackupSpec`, of the same type as the Velero Backup Spec, will be added to OADP DPA object.

With it, admin users will be able to select which NonAdminBackup `spec.backupSpec` fields have custom default (and enforced) values.

To avoid mistakes, not all fields will be able to be enforced, like `IncludedNamespaces`, that could break NAC usage.

NAC will respect the set values by reading DPA field during startup.If admin user changes any enforced field value, NAC Pod is recreated (and only NAC Pod) to always be up to date with admin user enforcements.

If a NonAdminBackup is created with fields overriding any enforced values, it will fail validation prior to creating an associated Velero Backup. Validation error is shown in NonAdminBackup status and NAC logs.

If NonAdminBackup respects enforcement, the created associated Velero Backup will have the enforced spec field values.

Enforcement is done dynamically. If new field is added to Velero Backup Spec, it will be presented to user without code changes. If a field changes type/or default value, tests will warn us.

### Example workflows

In this example, admin user has configured NAC with the following OADP DPA options
```yaml
...
spec:
...
nonAdmin:
enable: true
enforceBackupSpecs:
snapshotVolumes: false
unsupportedOverrides:
tech-preview-ack: 'true'
```
That means, that the 2 following NonAdminBackup will be accepted by NAC validation
```yaml
...
spec:
backupSpec:
snapshotVolumes: false
```
```yaml
...
spec:
backupSpec:
ttl: 3h
```
> **Note:** the related Velero Backup for this NonAdminBackup will have `spec.snapshotVolumes` set to false

But this NonAdminBackup will not be accepted by NAC validation
```yaml
...
spec:
backupSpec:
snapshotVolumes: true
```
NonAdminBackup status and NAC log will have the following message:
> spec.backupSpec.snapshotVolumes field value is enforced by admin user, can not override it

## Alternatives Considered

Instead of using a DPA field, using a ConfigMap was considered. Since users would not have type assertion when creating those ConfigMap and parsing it would be harder in NAC side, it was discarded.

## Security Considerations

No security considerations.

## Compatibility

No compatibility issues.

## Implementation

Add `EnforceBackupSpec` struct to OADP DPA `NonAdmin` struct
```go
type NonAdmin struct {
// which bakup spec field values to enforce
// +optional
EnforceBackupSpec *velero.BackupSpec `json:"enforceBackupSpec,omitempty"`
}
```
TODO restore

Validate admin user did not enforce a field that can break NAC functionality
```go
if r.checkNonAdminEnabled() {
if r.dpa.Spec.NonAdmin.EnforceBackupSpec != nil {
if !reflect.ValueOf(r.dpa.Spec.NonAdmin.EnforceBackupSpec.IncludedNamespaces).IsZero() {
return false, errors.New("admin users can not set DPA spec.nonAdmin.enforceBackupSpecs.includedNamespaces field")
}
}
}
```

Store previous `EnforceBackupSpec` value, so when admin user changes it, Deployment is also changed to trigger a Pod recreation
```go
const (
enforcedBackupSpecKey = "enforced-backup-spec"
)

var (
previousEnforcedBackupSpec *velero.BackupSpec = nil
dpaBackupSpecResourceVersion = ""
)

func ensureRequiredSpecs(deploymentObject *appsv1.Deployment, dpa *oadpv1alpha1.DataProtectionApplication, image string, imagePullPolicy corev1.PullPolicy) error {
if len(dpaBackupSpecResourceVersion) == 0 || !reflect.DeepEqual(dpa.Spec.NonAdmin.EnforceBackupSpec, previousEnforcedBackupSpec) {
dpaBackupSpecResourceVersion = dpa.GetResourceVersion()
}
previousEnforcedBackupSpec = dpa.Spec.NonAdmin.EnforceBackupSpec
// TODO same thing for restore
enforcedSpecAnnotation := map[string]string{
enforcedBackupSpecKey: dpaBackupSpecResourceVersion,
}

templateObjectAnnotations := deploymentObject.Spec.Template.GetAnnotations()
if templateObjectAnnotations == nil {
deploymentObject.Spec.Template.SetAnnotations(enforcedSpecAnnotation)
} else {
templateObjectAnnotations[enforcedBackupSpecKey] = enforcedSpecAnnotation[enforcedBackupSpecKey]
// TODO same thing for restore
deploymentObject.Spec.Template.SetAnnotations(templateObjectAnnotations)
}
}
```

During NAC startup, read OADP DPA, to be able to apply admin user enforcement
```go
restConfig := ctrl.GetConfigOrDie()

dpaClientScheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(dpaClientScheme))
dpaClient, err := client.New(restConfig, client.Options{
Scheme: dpaClientScheme,
})
if err != nil {
setupLog.Error(err, "unable to create Kubernetes client")
os.Exit(1)
}
dpaList := &v1alpha1.DataProtectionApplicationList{}
err = dpaClient.List(context.Background(), dpaList)
if err != nil {
setupLog.Error(err, "unable to list DPAs")
os.Exit(1)
}
```

Modify ValidateSpec function to use `EnforceBackupSpec` and apply that to non admin users' NonAdminBackup request
```go
func ValidateBackupSpec(nonAdminBackup *nacv1alpha1.NonAdminBackup, enforcedBackupSpec *velerov1.BackupSpec) error {
enforcedSpec := reflect.ValueOf(enforcedBackupSpec).Elem()
for index := range enforcedSpec.NumField() {
enforcedField := enforcedSpec.Field(index)
enforcedFieldName := enforcedSpec.Type().Field(index).Name
currentField := reflect.ValueOf(nonAdminBackup.Spec.BackupSpec).Elem().FieldByName(enforcedFieldName)
if !enforcedField.IsZero() && !currentField.IsZero() && !reflect.DeepEqual(enforcedField.Interface(), currentField.Interface()) {
field, _ := reflect.TypeOf(nonAdminBackup.Spec.BackupSpec).Elem().FieldByName(enforcedFieldName)
tagName, _, _ := strings.Cut(field.Tag.Get("json"), ",")
return fmt.Errorf(
"NonAdminBackup spec.backupSpec.%v field value is enforced by admin user, can not override it",
tagName,
)
}
}
}
```

Before creating NonAdminBackup's related Velero Backup, apply any missing fields to it that admin user has enforced
```go
enforcedSpec := reflect.ValueOf(r.EnforcedBackupSpec).Elem()
for index := range enforcedSpec.NumField() {
enforcedField := enforcedSpec.Field(index)
enforcedFieldName := enforcedSpec.Type().Field(index).Name
currentField := reflect.ValueOf(backupSpec).Elem().FieldByName(enforcedFieldName)
if !enforcedField.IsZero() && currentField.IsZero() {
currentField.Set(enforcedField)
}
}
```

For more details, check https://github.com/openshift/oadp-operator/pull/1584 and https://github.com/migtools/oadp-non-admin/pull/110.

## Open Issues

- Show NonAdminBackup spec.backupSpec fields/TODO NonAdminRestore custom default values to non admin users https://github.com/migtools/oadp-non-admin/issues/111

15 changes: 8 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/migtools/oadp-non-admin

go 1.22.0
go 1.22.2

toolchain go1.22.6

Expand All @@ -9,11 +9,13 @@ require (
github.com/google/uuid v1.6.0
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
github.com/openshift/oadp-operator v1.0.2-0.20241119153315-6947e30c7ec5
github.com/stretchr/testify v1.9.0
github.com/vmware-tanzu/velero v1.12.0
github.com/vmware-tanzu/velero v1.14.0
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
sigs.k8s.io/controller-runtime v0.17.2
)

Expand Down Expand Up @@ -58,11 +60,11 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
Expand All @@ -74,7 +76,6 @@ require (
k8s.io/component-base v0.29.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
Expand Down
Loading

0 comments on commit 9fdcb88

Please sign in to comment.