api + basic logic + hydra client crud + validation + happy path integration test
This commit is contained in:
@ -17,28 +17,77 @@ package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
|
||||
"github.com/ory/hydra-maester/hydra"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
clientIDKey = "client_id"
|
||||
clientSecretKey = "client_secret"
|
||||
)
|
||||
|
||||
type HydraClientInterface interface {
|
||||
GetOAuth2Client(id string) (*hydra.OAuth2ClientJSON, bool, error)
|
||||
PostOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error)
|
||||
PutOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error)
|
||||
DeleteOAuth2Client(id string) error
|
||||
}
|
||||
|
||||
// OAuth2ClientReconciler reconciles a OAuth2Client object
|
||||
type OAuth2ClientReconciler struct {
|
||||
HydraClient HydraClientInterface
|
||||
Log logr.Logger
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=hydra.ory.sh,resources=oauth2clients,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=hydra.ory.sh,resources=oauth2clients/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
func (r *OAuth2ClientReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
_ = context.Background()
|
||||
ctx := context.Background()
|
||||
_ = r.Log.WithValues("oauth2client", req.NamespacedName)
|
||||
|
||||
// your logic here
|
||||
var client hydrav1alpha1.OAuth2Client
|
||||
if err := r.Get(ctx, req.NamespacedName, &client); err != nil {
|
||||
if apierrs.IsNotFound(err) {
|
||||
if err := r.unregisterOAuth2Client(ctx, req.NamespacedName); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if client.Generation != client.Status.ObservedGeneration {
|
||||
|
||||
var registered = false
|
||||
var err error
|
||||
|
||||
if client.Status.ClientID != nil {
|
||||
|
||||
_, registered, err = r.HydraClient.GetOAuth2Client(*client.Status.ClientID)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if !registered {
|
||||
return ctrl.Result{}, r.registerOAuth2Client(ctx, &client)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, r.updateRegisteredOAuth2Client(&client)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
@ -48,3 +97,49 @@ func (r *OAuth2ClientReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
For(&hydrav1alpha1.OAuth2Client{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *OAuth2ClientReconciler) registerOAuth2Client(ctx context.Context, client *hydrav1alpha1.OAuth2Client) error {
|
||||
created, err := r.HydraClient.PostOAuth2Client(client.ToOAuth2ClientJSON())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientSecret := apiv1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: client.Name,
|
||||
Namespace: client.Namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
clientSecretKey: []byte(*created.Secret),
|
||||
clientIDKey: []byte(*created.ClientID),
|
||||
},
|
||||
}
|
||||
|
||||
err = r.Create(ctx, &clientSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.Status.Secret = &clientSecret.Name
|
||||
client.Status.ClientID = created.ClientID
|
||||
client.Status.ObservedGeneration = client.Generation
|
||||
return r.Status().Update(ctx, client)
|
||||
}
|
||||
|
||||
func (r *OAuth2ClientReconciler) unregisterOAuth2Client(ctx context.Context, namespacedName types.NamespacedName) error {
|
||||
var sec apiv1.Secret
|
||||
if err := r.Get(ctx, namespacedName, &sec); err != nil {
|
||||
if apierrs.IsNotFound(err) {
|
||||
r.Log.Info(fmt.Sprintf("unable to find secret corresponding with client %s/%s. Manual deletion recommended", namespacedName.Name, namespacedName.Namespace))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return r.HydraClient.DeleteOAuth2Client(string(sec.Data[clientIDKey]))
|
||||
}
|
||||
|
||||
func (r *OAuth2ClientReconciler) updateRegisteredOAuth2Client(client *hydrav1alpha1.OAuth2Client) error {
|
||||
_, err := r.HydraClient.PutOAuth2Client(client.ToOAuth2ClientJSON())
|
||||
return err
|
||||
}
|
||||
|
197
controllers/oauth2client_controller_integration_test.go
Normal file
197
controllers/oauth2client_controller_integration_test.go
Normal file
@ -0,0 +1,197 @@
|
||||
package controllers_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
|
||||
"github.com/ory/hydra-maester/controllers"
|
||||
"github.com/ory/hydra-maester/hydra"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
const timeout = time.Second * 5
|
||||
|
||||
var _ = Describe("OAuth2Client Controller", func() {
|
||||
Context("in a happy-path scenario", func() {
|
||||
|
||||
var tstName = "test"
|
||||
var tstNamespace = "default"
|
||||
var tstScopes = "a b c"
|
||||
var tstClientID = "testClientID"
|
||||
var tstSecret = "testSecret"
|
||||
|
||||
var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: tstName, Namespace: tstNamespace}}
|
||||
It("should call create OAuth2 client in Hydra and a Secret", func() {
|
||||
|
||||
s := scheme.Scheme
|
||||
err := hydrav1alpha1.AddToScheme(s)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a
|
||||
// channel when it is finished.
|
||||
mgr, err := manager.New(cfg, manager.Options{Scheme: s})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
c := mgr.GetClient()
|
||||
|
||||
mch := (&mockHydraClient{}).
|
||||
withSecret(tstSecret).
|
||||
withClientID(tstClientID)
|
||||
|
||||
recFn, requests := SetupTestReconcile(getAPIReconciler(mgr, mch))
|
||||
//_, requests := SetupTestReconcile(getApiReconciler(mgr))
|
||||
|
||||
Expect(add(mgr, recFn)).To(Succeed())
|
||||
|
||||
//Start the manager and the controller
|
||||
stopMgr, mgrStopped := StartTestManager(mgr)
|
||||
|
||||
//Ensure manager is stopped properly
|
||||
defer func() {
|
||||
close(stopMgr)
|
||||
mgrStopped.Wait()
|
||||
}()
|
||||
|
||||
instance := testInstance(tstName, tstNamespace, tstScopes)
|
||||
err = c.Create(context.TODO(), instance)
|
||||
// The instance object may not be a valid object because it might be missing some required fields.
|
||||
// Please modify the instance object by adding required fields and then remove the following if statement.
|
||||
if apierrors.IsInvalid(err) {
|
||||
Fail(fmt.Sprintf("failed to create object, got an invalid object error: %v", err))
|
||||
return
|
||||
}
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer c.Delete(context.TODO(), instance)
|
||||
Eventually(requests, timeout).Should(Receive(Equal(expectedRequest)))
|
||||
|
||||
//Verify the created CR instance status
|
||||
var retrieved hydrav1alpha1.OAuth2Client
|
||||
ok := client.ObjectKey{Name: tstName, Namespace: tstNamespace}
|
||||
err = c.Get(context.TODO(), ok, &retrieved)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(*retrieved.Status.ClientID).To(Equal(tstClientID))
|
||||
Expect(*retrieved.Status.Secret).To(Equal(tstName)) //Secret contents is not visible in the CR instance!
|
||||
|
||||
//Verify the created Secret
|
||||
var createdSecret = apiv1.Secret{}
|
||||
k8sClient.Get(context.TODO(), ok, &createdSecret)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(createdSecret.Data["client_secret"]).To(Equal([]byte(tstSecret)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// add adds a new Controller to mgr with r as the reconcile.Reconciler
|
||||
func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
// Create a new controller
|
||||
c, err := controller.New("api-gateway-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to Api
|
||||
err = c.Watch(&source.Kind{Type: &hydrav1alpha1.OAuth2Client{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(user): Modify this to be the types you create
|
||||
// Uncomment watch a Deployment created by Guestbook - change this for objects you create
|
||||
//err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{
|
||||
// IsController: true,
|
||||
// OwnerType: &webappv1.Guestbook{},
|
||||
//})
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAPIReconciler(mgr ctrl.Manager, mock *mockHydraClient) reconcile.Reconciler {
|
||||
return &controllers.OAuth2ClientReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: ctrl.Log.WithName("controllers").WithName("OAuth2Client"),
|
||||
HydraClient: mock,
|
||||
}
|
||||
}
|
||||
|
||||
func testInstance(name, namespace, scopes string) *hydrav1alpha1.OAuth2Client {
|
||||
|
||||
return &hydrav1alpha1.OAuth2Client{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: hydrav1alpha1.OAuth2ClientSpec{
|
||||
GrantTypes: []hydrav1alpha1.GrantType{"client_credentials"},
|
||||
ResponseTypes: []hydrav1alpha1.ResponseType{"token"},
|
||||
Scope: scopes,
|
||||
}}
|
||||
}
|
||||
|
||||
//TODO: Replace with full-fledged mocking framework (mockery/go-mock)
|
||||
type mockHydraClient struct {
|
||||
resSecret string
|
||||
resClientID string
|
||||
postedData *hydra.OAuth2ClientJSON
|
||||
}
|
||||
|
||||
func (m *mockHydraClient) withSecret(secret string) *mockHydraClient {
|
||||
m.resSecret = secret
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mockHydraClient) withClientID(clientID string) *mockHydraClient {
|
||||
m.resClientID = clientID
|
||||
return m
|
||||
}
|
||||
|
||||
//Returns the data previously "stored" by PostOAuth2Client
|
||||
func (m *mockHydraClient) GetOAuth2Client(id string) (*hydra.OAuth2ClientJSON, bool, error) {
|
||||
res := &hydra.OAuth2ClientJSON{
|
||||
ClientID: &m.resClientID,
|
||||
Secret: &m.resSecret,
|
||||
Name: m.postedData.Name,
|
||||
GrantTypes: m.postedData.GrantTypes,
|
||||
ResponseTypes: m.postedData.ResponseTypes,
|
||||
Scope: m.postedData.Scope,
|
||||
}
|
||||
return res, true, nil
|
||||
}
|
||||
|
||||
func (m *mockHydraClient) PostOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error) {
|
||||
m.postedData = o
|
||||
res := &hydra.OAuth2ClientJSON{
|
||||
ClientID: &m.resClientID,
|
||||
Secret: &m.resSecret,
|
||||
Name: o.Name,
|
||||
GrantTypes: o.GrantTypes,
|
||||
ResponseTypes: o.ResponseTypes,
|
||||
Scope: o.Scope,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *mockHydraClient) DeleteOAuth2Client(id string) error {
|
||||
panic("Should not be invoked!")
|
||||
}
|
||||
|
||||
func (m *mockHydraClient) PutOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error) {
|
||||
panic("Should not be invoked!")
|
||||
}
|
@ -13,22 +13,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
package controllers_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
@ -55,13 +57,11 @@ var _ = BeforeSuite(func(done Done) {
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
}
|
||||
|
||||
cfg, err := testEnv.Start()
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
err = hydrav1alpha1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
@ -76,3 +76,27 @@ var _ = AfterSuite(func() {
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
// SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and
|
||||
// writes the request to requests after Reconcile is finished.
|
||||
func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) {
|
||||
requests := make(chan reconcile.Request)
|
||||
fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
|
||||
result, err := inner.Reconcile(req)
|
||||
requests <- req
|
||||
return result, err
|
||||
})
|
||||
return fn, requests
|
||||
}
|
||||
|
||||
// StartTestManager adds recFn
|
||||
func StartTestManager(mgr manager.Manager) (chan struct{}, *sync.WaitGroup) {
|
||||
stop := make(chan struct{})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
Expect(mgr.Start(stop)).NotTo(HaveOccurred())
|
||||
}()
|
||||
return stop, wg
|
||||
}
|
||||
|
Reference in New Issue
Block a user