Skip to content

Commit

Permalink
Merge pull request #77 from appuio/sync-default-org-attribute-from-co…
Browse files Browse the repository at this point in the history
…ntrol-api

Sync user default organization from the control-api
  • Loading branch information
bastjan authored Mar 5, 2024
2 parents 25d0fa7 + 297b247 commit 61d6295
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 21 deletions.
1 change: 1 addition & 0 deletions config/foreign_rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rules:
- appuio.io
resources:
- usageprofiles
- users
verbs:
- get
- list
Expand Down
2 changes: 2 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,6 @@ rules:
verbs:
- get
- list
- patch
- update
- watch
97 changes: 97 additions & 0 deletions controllers/userattributesync_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package controllers

import (
"context"
"encoding/json"

controlv1 "github.com/appuio/control-api/apis/v1"
userv1 "github.com/openshift/api/user/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"

"github.com/appuio/appuio-cloud-agent/controllers/clustersource"
)

// UserAttributeSyncReconciler reconciles a User object
type UserAttributeSyncReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder

ForeignClient client.Client
}

const DefaultOrganizationAnnotation = "appuio.io/default-organization"

//+kubebuilder:rbac:groups=user.openshift.io,resources=users,verbs=get;list;watch;update;patch

// Reconcile syncs the User with the upstream User resource from the foreign (Control-API) cluster.
// Currently the following attributes are synced:
// - .spec.preferences.defaultOrganizationRef -> .metadata.annotations["appuio.io/default-organization"]
func (r *UserAttributeSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("Reconciling User")

var upstream controlv1.User
if err := r.ForeignClient.Get(ctx, client.ObjectKey{Name: req.Name}, &upstream); err != nil {
if apierrors.IsNotFound(err) {
l.Info("Upstream user not found")
return ctrl.Result{}, nil
}
l.Error(err, "unable to get upstream User")
return ctrl.Result{}, err
}

var local userv1.User
if err := r.Get(ctx, client.ObjectKey{Name: req.Name}, &local); err != nil {
if apierrors.IsNotFound(err) {
l.Info("Local user not found")
return ctrl.Result{}, nil
}
l.Error(err, "unable to get local User")
return ctrl.Result{}, err
}

if local.Annotations != nil && local.Annotations[DefaultOrganizationAnnotation] == upstream.Spec.Preferences.DefaultOrganizationRef {
l.Info("User has correct default organization annotation")
return ctrl.Result{}, nil
}

patch := map[string]any{
"metadata": map[string]any{
"annotations": map[string]string{
DefaultOrganizationAnnotation: upstream.Spec.Preferences.DefaultOrganizationRef,
},
},
}
encPatch, err := json.Marshal(patch)
if err != nil {
l.Error(err, "unable to marshal patch")
return ctrl.Result{}, err
}

if err := r.Client.Patch(ctx, &local, client.RawPatch(types.StrategicMergePatchType, encPatch)); err != nil {
l.Error(err, "unable to patch User")
return ctrl.Result{}, err
}

// Record event so we don't trigger another reconcile loop but still know when the last sync happened.
r.Recorder.Eventf(&local, "Normal", "Reconciled", "Reconciled User")
l.Info("User reconciled")

return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *UserAttributeSyncReconciler) SetupWithManagerAndForeignCluster(mgr ctrl.Manager, foreign clustersource.ClusterSource) error {
return ctrl.NewControllerManagedBy(mgr).
For(&userv1.User{}).
WatchesRawSource(foreign.SourceFor(&controlv1.User{}), &handler.EnqueueRequestForObject{}).
Complete(r)
}
80 changes: 80 additions & 0 deletions controllers/userattributesync_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package controllers

import (
"context"
"testing"

controlv1 "github.com/appuio/control-api/apis/v1"
userv1 "github.com/openshift/api/user/v1"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
)

func Test_UserAttributeSyncReconciler_Reconcile(t *testing.T) {
upstream := controlv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "johndoe",
},
Spec: controlv1.UserSpec{
Preferences: controlv1.UserPreferences{
DefaultOrganizationRef: "thedoening",
},
},
}
onlyUpstream := controlv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "johnupstream",
},
Spec: controlv1.UserSpec{
Preferences: controlv1.UserPreferences{
DefaultOrganizationRef: "onlyupstream",
},
},
}
local := userv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "johndoe",
},
}
onlyLocal := userv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "onlylocal",
},
}

client, scheme, recorder := prepareClient(t, &local, &onlyLocal)
foreignClient, _, _ := prepareClient(t, &upstream, &onlyUpstream)

subject := UserAttributeSyncReconciler{
Client: client,
Scheme: scheme,
Recorder: recorder,
ForeignClient: foreignClient,
}

t.Run("normal", func(t *testing.T) {
_, err := subject.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: upstream.Name}})
require.NoError(t, err)
var synced userv1.User
require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: upstream.Name}, &synced))
require.Equal(t, "thedoening", synced.Annotations[DefaultOrganizationAnnotation])
require.Equal(t, "Normal Reconciled Reconciled User", <-recorder.Events)

require.Len(t, recorder.Events, 0)
_, err = subject.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: upstream.Name}})
require.NoError(t, err)
require.Len(t, recorder.Events, 0)
})

t.Run("only local", func(t *testing.T) {
_, err := subject.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: onlyLocal.Name}})
require.NoError(t, err)
})

t.Run("only upstream", func(t *testing.T) {
_, err := subject.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: onlyUpstream.Name}})
require.NoError(t, err)
})
}
2 changes: 2 additions & 0 deletions controllers/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

controlv1 "github.com/appuio/control-api/apis/v1"
userv1 "github.com/openshift/api/user/v1"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -41,6 +42,7 @@ func prepareClient(t *testing.T, initObjs ...client.Object) (client.WithWatch, *
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, cloudagentv1.AddToScheme(scheme))
require.NoError(t, controlv1.AddToScheme(scheme))
require.NoError(t, userv1.AddToScheme(scheme))

client := fake.NewClientBuilder().
WithScheme(scheme).
Expand Down
61 changes: 40 additions & 21 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func main() {
flag.IntVar(&qps, "qps", 20, "QPS to use for the controller-runtime client")
flag.IntVar(&burst, "burst", 100, "Burst to use for the controller-runtime client")

var disableUserAttributeSync, disableUsageProfiles bool
flag.BoolVar(&disableUserAttributeSync, "disable-user-attribute-sync", false, "Disable the UserAttributeSync controller")
flag.BoolVar(&disableUsageProfiles, "disable-usage-profiles", false, "Disable the UsageProfile controllers")

opts := zap.Options{}
opts.BindFlags(flag.CommandLine)
flag.Parse()
Expand Down Expand Up @@ -146,31 +150,46 @@ func main() {
registerRatioController(mgr, conf, conf.OrganizationLabel)
registerOrganizationRBACController(mgr, conf.OrganizationLabel, conf.DefaultOrganizationClusterRoles)

if err := (&controllers.ZoneUsageProfileSyncReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("usage-profile-sync-controller"),
if !disableUserAttributeSync {
if err := (&controllers.UserAttributeSyncReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("user-attribute-sync-controller"),

ForeignClient: controlAPICluster.GetClient(),
}).SetupWithManagerAndForeignCluster(mgr, controlAPICluster); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ratio")
os.Exit(1)
ForeignClient: controlAPICluster.GetClient(),
}).SetupWithManagerAndForeignCluster(mgr, controlAPICluster); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "UserAttributeSync")
os.Exit(1)
}
}
if err := (&controllers.ZoneUsageProfileApplyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("usage-profile-apply-controller"),
Cache: mgr.GetCache(),

OrganizationLabel: conf.OrganizationLabel,
Transformers: []transformers.Transformer{
transformers.NewResourceQuotaTransformer("resourcequota.appuio.io"),
},
if !disableUsageProfiles {
if err := (&controllers.ZoneUsageProfileSyncReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("usage-profile-sync-controller"),

SelectedProfile: selectedUsageProfile,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ratio")
os.Exit(1)
ForeignClient: controlAPICluster.GetClient(),
}).SetupWithManagerAndForeignCluster(mgr, controlAPICluster); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ratio")
os.Exit(1)
}
if err := (&controllers.ZoneUsageProfileApplyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("usage-profile-apply-controller"),
Cache: mgr.GetCache(),

OrganizationLabel: conf.OrganizationLabel,
Transformers: []transformers.Transformer{
transformers.NewResourceQuotaTransformer("resourcequota.appuio.io"),
},

SelectedProfile: selectedUsageProfile,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ratio")
os.Exit(1)
}
}

psk := &skipper.PrivilegedUserSkipper{
Expand Down

0 comments on commit 61d6295

Please sign in to comment.