From 55ce734bb95a6a3471c25dbb0e46254b67735a08 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 6 Feb 2024 15:18:30 -0500 Subject: [PATCH] [NET-7158] CRUD hooks for api gateway v2 (#3519) * Add hooks for CRUD side effects for apigateway controller * Added tests for controller --- .../resources/api-gateway-controller.go | 36 +++- .../resources/api-gateway-controller_test.go | 179 ++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 control-plane/controllers/resources/api-gateway-controller_test.go diff --git a/control-plane/controllers/resources/api-gateway-controller.go b/control-plane/controllers/resources/api-gateway-controller.go index 08c116499f..8646fb1fce 100644 --- a/control-plane/controllers/resources/api-gateway-controller.go +++ b/control-plane/controllers/resources/api-gateway-controller.go @@ -7,6 +7,7 @@ import ( "context" "github.com/go-logr/logr" + k8serr "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -27,7 +28,30 @@ type APIGatewayController struct { // +kubebuilder:rbac:groups=mesh.consul.hashicorp.com,resources=tcproute/status,verbs=get;update;patch func (r *APIGatewayController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Logger(req.NamespacedName).Info("Reconciling APIGateway") + logger := r.Logger(req.NamespacedName) + logger.Info("Reconciling APIGateway") + + resource := &meshv2beta1.APIGateway{} + if err := r.Get(ctx, req.NamespacedName, resource); k8serr.IsNotFound(err) { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if err != nil { + logger.Error(err, "retrieving resource") + return ctrl.Result{}, err + } + + // Call hooks + if !resource.DeletionTimestamp.IsZero() { + logger.Info("deletion event") + + if err := r.onDelete(ctx, req, resource); err != nil { + return ctrl.Result{}, err + } + } else { + if err := r.onCreateUpdate(ctx, req, resource); err != nil { + return ctrl.Result{}, err + } + } + return r.Controller.ReconcileResource(ctx, r, req, &meshv2beta1.APIGateway{}) } @@ -42,3 +66,13 @@ func (r *APIGatewayController) UpdateStatus(ctx context.Context, obj client.Obje func (r *APIGatewayController) SetupWithManager(mgr ctrl.Manager) error { return setupWithManager(mgr, &meshv2beta1.APIGateway{}, r) } + +func (r *APIGatewayController) onCreateUpdate(ctx context.Context, req ctrl.Request, resource *meshv2beta1.APIGateway) error { + // TODO: NET-7449, NET-7450, and NET-7451 + return nil +} + +func (r *APIGatewayController) onDelete(ctx context.Context, req ctrl.Request, resource *meshv2beta1.APIGateway) error { + // TODO: NET-7449, NET-7450, and NET-7451 + return nil +} diff --git a/control-plane/controllers/resources/api-gateway-controller_test.go b/control-plane/controllers/resources/api-gateway-controller_test.go new file mode 100644 index 0000000000..ef531086dc --- /dev/null +++ b/control-plane/controllers/resources/api-gateway-controller_test.go @@ -0,0 +1,179 @@ +package resources + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/consul-k8s/control-plane/api/mesh/v2beta1" + "github.com/hashicorp/consul-k8s/control-plane/connect-inject/constants" + "github.com/hashicorp/consul-k8s/control-plane/helper/test" + pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" + "github.com/hashicorp/consul/proto-public/pbresource" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + + logrtest "github.com/go-logr/logr/testr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestAPIGatewayController_ReconcileResourceExists(t *testing.T) { + t.Parallel() + ctx := context.Background() + + s := runtime.NewScheme() + s.AddKnownTypes(schema.GroupVersion{ + Group: "mesh.consul.hashicorp.com", + Version: pbmesh.Version, + }, &v2beta1.APIGateway{}, &v2beta1.APIGatewayList{}) + + apiGW := &v2beta1.APIGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "api-gateway", + Namespace: metav1.NamespaceDefault, + }, + Spec: pbmesh.APIGateway{ + GatewayClassName: "consul", + Listeners: []*pbmesh.APIGatewayListener{ + { + Name: "http-listener", + Port: 9090, + Protocol: "http", + }, + }, + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(apiGW).Build() + + testClient := test.TestServerWithMockConnMgrWatcher(t, func(c *testutil.TestServerConfig) { + c.Experiments = []string{"resource-apis"} + }) + + gwCtrl := APIGatewayController{ + Client: fakeClient, + Log: logrtest.New(t), + Scheme: s, + Controller: &ConsulResourceController{ + ConsulClientConfig: testClient.Cfg, + ConsulServerConnMgr: testClient.Watcher, + }, + } + + // ensure the resource is not in consul yet + { + req := &pbresource.ReadRequest{Id: apiGW.ResourceID(constants.DefaultConsulNS, constants.DefaultConsulPartition)} + _, err := testClient.ResourceClient.Read(ctx, req) + require.Error(t, err) + } + + // now reconcile the resource + { + namespacedName := types.NamespacedName{ + Namespace: metav1.NamespaceDefault, + Name: apiGW.KubernetesName(), + } + + // First get it, so we have the latest revision number. + err := fakeClient.Get(ctx, namespacedName, apiGW) + require.NoError(t, err) + + resp, err := gwCtrl.Reconcile(ctx, ctrl.Request{ + NamespacedName: namespacedName, + }) + + require.NoError(t, err) + require.False(t, resp.Requeue) + } + + // now check that the object in Consul is as expected. + { + expectedResource := &pbmesh.APIGateway{ + GatewayClassName: "consul", + Listeners: []*pbmesh.APIGatewayListener{ + { + Name: "http-listener", + Port: 9090, + Protocol: "http", + }, + }, + } + req := &pbresource.ReadRequest{Id: apiGW.ResourceID(constants.DefaultConsulNS, constants.DefaultConsulPartition)} + res, err := testClient.ResourceClient.Read(ctx, req) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, apiGW.GetName(), res.GetResource().GetId().GetName()) + + data := res.GetResource().Data + actual := &pbmesh.APIGateway{} + require.NoError(t, data.UnmarshalTo(actual)) + + opts := append([]cmp.Option{protocmp.IgnoreFields(&pbresource.Resource{}, "status", "generation", "version")}, test.CmpProtoIgnoreOrder()...) + diff := cmp.Diff(expectedResource, actual, opts...) + require.Equal(t, "", diff, "APIGateway does not match") + } +} + +func TestAPIGatewayController_ReconcileAPIGWDoesNotExistInK8s(t *testing.T) { + t.Parallel() + ctx := context.Background() + + s := runtime.NewScheme() + s.AddKnownTypes(schema.GroupVersion{ + Group: "mesh.consul.hashicorp.com", + Version: pbmesh.Version, + }, &v2beta1.APIGateway{}, &v2beta1.APIGatewayList{}) + + fakeClient := fake.NewClientBuilder().WithScheme(s).Build() + + testClient := test.TestServerWithMockConnMgrWatcher(t, func(c *testutil.TestServerConfig) { + c.Experiments = []string{"resource-apis"} + }) + + gwCtrl := APIGatewayController{ + Client: fakeClient, + Log: logrtest.New(t), + Scheme: s, + Controller: &ConsulResourceController{ + ConsulClientConfig: testClient.Cfg, + ConsulServerConnMgr: testClient.Watcher, + }, + } + + // now reconcile the resource + { + namespacedName := types.NamespacedName{ + Namespace: metav1.NamespaceDefault, + Name: "api-gateway", + } + + resp, err := gwCtrl.Reconcile(ctx, ctrl.Request{ + NamespacedName: namespacedName, + }) + + require.NoError(t, err) + require.False(t, resp.Requeue) + require.Equal(t, ctrl.Result{}, resp) + } + + // ensure the resource is not in consul + { + req := &pbresource.ReadRequest{Id: &pbresource.ID{ + Name: "api-gateway", + Type: pbmesh.APIGatewayType, + Tenancy: &pbresource.Tenancy{ + Namespace: constants.DefaultConsulNS, + Partition: constants.DefaultConsulPartition, + }, + }} + + _, err := testClient.ResourceClient.Read(ctx, req) + require.Error(t, err) + } +}