generated from vshn/go-bootstrap
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #77 from appuio/sync-default-org-attribute-from-co…
…ntrol-api Sync user default organization from the control-api
- Loading branch information
Showing
6 changed files
with
222 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ rules: | |
- appuio.io | ||
resources: | ||
- usageprofiles | ||
- users | ||
verbs: | ||
- get | ||
- list | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -105,4 +105,6 @@ rules: | |
verbs: | ||
- get | ||
- list | ||
- patch | ||
- update | ||
- watch |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters