[NET-7280] Add APIGW support to the gatewayproxy controller (#20484)

* Add APIGW support to the gatewayproxy controller

* update copywrite headers
This commit is contained in:
John Maguire 2024-02-06 11:03:37 -05:00 committed by GitHub
parent 3bf999e46b
commit 54c974748e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 419 additions and 6 deletions

View File

@ -5,12 +5,19 @@ package apigateways
import ( import (
"context" "context"
"github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/apigateways/fetcher"
"github.com/hashicorp/consul/internal/resource"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"google.golang.org/protobuf/types/known/anypb"
) )
const ( const (
ControllerName = "consul.io/api-gateway" ControllerName = "consul.io/api-gateway"
GatewayKind = "api-gateway"
) )
func Controller() *controller.Controller { func Controller() *controller.Controller {
@ -22,13 +29,73 @@ func Controller() *controller.Controller {
type reconciler struct{} type reconciler struct{}
// Reconcile is responsible for creating a Service w/ a MeshGateway owner, // Reconcile is responsible for creating a Service w/ a APIGateway owner,
// in addition to other things discussed in the RFC. // in addition to other things discussed in the RFC.
func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error { func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error {
rt.Logger = rt.Logger.With("resource-id", req.ID) rt.Logger = rt.Logger.With("resource-id", req.ID)
rt.Logger.Trace("reconciling api gateway") rt.Logger.Trace("reconciling api gateway")
dataFetcher := fetcher.New(rt.Client)
decodedAPIGateway, err := dataFetcher.FetchAPIGateway(ctx, req.ID)
if err != nil {
rt.Logger.Trace("error reading the apigateway", "apigatewayID", req.ID, "error", err)
return err
} else if decodedAPIGateway == nil {
rt.Logger.Trace("apigateway not found", "apigatewayID", req.ID)
return nil
}
apigw := decodedAPIGateway.Data
ports := make([]*pbcatalog.ServicePort, 0, len(apigw.Listeners))
for _, listener := range apigw.Listeners {
ports = append(ports, &pbcatalog.ServicePort{
Protocol: listenerProtocolToCatalogProtocol(listener.Protocol),
TargetPort: listener.Name,
VirtualPort: listener.Port,
})
}
service := &pbcatalog.Service{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{req.ID.Name},
},
Ports: ports,
}
serviceData, err := anypb.New(service)
if err != nil {
return err
}
// TODO NET-7378 // TODO NET-7378
_, err = rt.Client.Write(ctx, &pbresource.WriteRequest{
Resource: &pbresource.Resource{
Data: serviceData,
Id: resource.ReplaceType(pbcatalog.ServiceType, req.ID),
Metadata: map[string]string{"gateway-kind": GatewayKind},
Owner: req.ID,
},
})
if err != nil {
return err
}
return nil return nil
} }
func listenerProtocolToCatalogProtocol(listenerProtocol string) pbcatalog.Protocol {
switch listenerProtocol {
case "http":
return pbcatalog.Protocol_PROTOCOL_HTTP
case "tcp":
return pbcatalog.Protocol_PROTOCOL_TCP
case "grpc":
return pbcatalog.Protocol_PROTOCOL_GRPC
default:
panic("this is a programmer error, the only available protocols are tcp/http/grpc")
}
}

View File

@ -0,0 +1,156 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package apigateways
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/mesh/internal/types"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/resourcetest"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/sdk/testutil"
)
type apigatewayControllerSuite struct {
suite.Suite
ctx context.Context
client pbresource.ResourceServiceClient
resourceClient *resourcetest.Client
rt controller.Runtime
apiGateway *pbresource.Resource
tenancies []*pbresource.Tenancy
}
func (suite *apigatewayControllerSuite) SetupTest() {
suite.ctx = testutil.TestContext(suite.T())
suite.tenancies = resourcetest.TestTenancies()
suite.client = svctest.NewResourceServiceBuilder().
WithRegisterFns(types.Register, catalog.RegisterTypes).
WithTenancies(suite.tenancies...).
Run(suite.T())
suite.resourceClient = resourcetest.NewClient(suite.client)
suite.rt = controller.Runtime{
Client: suite.client,
Logger: testutil.Logger(suite.T()),
}
}
func (suite *apigatewayControllerSuite) TestReconciler_Reconcile() {
suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
r := reconciler{}
ctx := context.Background()
testutil.RunStep(suite.T(), "api-gateway exists", func(t *testing.T) {
id := &pbresource.ID{
Name: "api-gateway",
Type: &pbresource.Type{
Group: "mesh",
GroupVersion: "v2beta1",
Kind: "APIGateway",
},
Tenancy: tenancy,
}
expectedWrittenService := &pbcatalog.Service{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{"api-gateway"},
},
Ports: []*pbcatalog.ServicePort{
{
VirtualPort: 9090,
TargetPort: "http-listener",
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
},
{
VirtualPort: 8080,
TargetPort: "tcp-listener",
Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
},
},
}
req := controller.Request{ID: id}
err := r.Reconcile(ctx, suite.rt, req)
require.NoError(t, err)
dec, err := resource.GetDecodedResource[*pbcatalog.Service](ctx, suite.client, resource.ReplaceType(pbcatalog.ServiceType, req.ID))
require.NoError(t, err)
require.Equal(t, dec.Data.Ports, expectedWrittenService.Ports)
require.Equal(t, dec.Data.Workloads, expectedWrittenService.Workloads)
})
testutil.RunStep(suite.T(), "api-gateway does not exist", func(t *testing.T) {
id := &pbresource.ID{
Name: "does-not-exist",
Type: &pbresource.Type{
Group: "mesh",
GroupVersion: "v2beta1",
Kind: "APIGateway",
},
Tenancy: tenancy,
}
req := controller.Request{ID: id}
err := r.Reconcile(ctx, suite.rt, req)
require.NoError(t, err)
dec, err := resource.GetDecodedResource[*pbcatalog.Service](ctx, suite.client, resource.ReplaceType(pbcatalog.ServiceType, req.ID))
require.NoError(t, err)
require.Nil(t, dec)
})
})
}
func TestAPIGatewayReconciler(t *testing.T) {
suite.Run(t, new(apigatewayControllerSuite))
}
func (suite *apigatewayControllerSuite) appendTenancyInfo(tenancy *pbresource.Tenancy) string {
return fmt.Sprintf("%s_Namespace_%s_Partition", tenancy.Namespace, tenancy.Partition)
}
func (suite *apigatewayControllerSuite) setupSuiteWithTenancy(tenancy *pbresource.Tenancy) {
suite.apiGateway = resourcetest.Resource(pbmesh.APIGatewayType, "api-gateway").
WithData(suite.T(), &pbmesh.APIGateway{
GatewayClassName: "consul",
Listeners: []*pbmesh.APIGatewayListener{
{
Name: "http-listener",
Port: 9090,
Protocol: "http",
},
{
Name: "tcp-listener",
Port: 8080,
Protocol: "tcp",
},
},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
}
func (suite *apigatewayControllerSuite) runTestCaseWithTenancies(t func(*pbresource.Tenancy)) {
for _, tenancy := range suite.tenancies {
suite.Run(suite.appendTenancyInfo(tenancy), func() {
suite.setupSuiteWithTenancy(tenancy)
t(tenancy)
})
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package fetcher
import (
"context"
"fmt"
"github.com/hashicorp/consul/internal/mesh/internal/types"
"github.com/hashicorp/consul/internal/resource"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"google.golang.org/protobuf/proto"
)
type Fetcher struct {
client pbresource.ResourceServiceClient
}
func New(client pbresource.ResourceServiceClient) *Fetcher {
return &Fetcher{
client: client,
}
}
// method on fetcher to fetch an apigateway
func (f *Fetcher) FetchAPIGateway(ctx context.Context, id *pbresource.ID) (*types.DecodedAPIGateway, error) {
assertResourceType(pbmesh.APIGatewayType, id.Type)
dec, err := resource.GetDecodedResource[*pbmesh.APIGateway](ctx, f.client, id)
if err != nil {
return nil, err
}
return dec, nil
}
func assertResourceType(expected, actual *pbresource.Type) {
if !proto.Equal(expected, actual) {
// this is always a programmer error so safe to panic
panic(fmt.Sprintf("expected a query for a type of %q, you provided a type of %q", expected, actual))
}
}

View File

@ -0,0 +1,113 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package fetcher
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/mesh/internal/types"
"github.com/hashicorp/consul/internal/resource/resourcetest"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/sdk/testutil"
)
type dataFetcherSuite struct {
suite.Suite
ctx context.Context
client pbresource.ResourceServiceClient
resourceClient *resourcetest.Client
rt controller.Runtime
apiGateway *pbresource.Resource
tenancies []*pbresource.Tenancy
}
func (suite *dataFetcherSuite) SetupTest() {
suite.ctx = testutil.TestContext(suite.T())
suite.tenancies = resourcetest.TestTenancies()
suite.client = svctest.NewResourceServiceBuilder().
WithRegisterFns(types.Register, catalog.RegisterTypes).
WithTenancies(suite.tenancies...).
Run(suite.T())
suite.resourceClient = resourcetest.NewClient(suite.client)
suite.rt = controller.Runtime{
Client: suite.client,
Logger: testutil.Logger(suite.T()),
}
}
func (suite *dataFetcherSuite) setupWithTenancy(tenancy *pbresource.Tenancy) {
suite.apiGateway = resourcetest.Resource(pbmesh.APIGatewayType, "apigw").
WithTenancy(tenancy).
WithData(suite.T(), &pbmesh.APIGateway{
GatewayClassName: "consul",
Listeners: []*pbmesh.APIGatewayListener{},
}).
Write(suite.T(), suite.client)
}
func (suite *dataFetcherSuite) TestFetcher_FetchAPIGateway() {
suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
f := Fetcher{
client: suite.client,
}
testutil.RunStep(suite.T(), "gateway does not exist", func(t *testing.T) {
nonExistantID := resourcetest.Resource(pbmesh.APIGatewayType, "not-found").WithTenancy(tenancy).ID()
svc, err := f.FetchAPIGateway(suite.ctx, nonExistantID)
require.NoError(t, err)
require.Nil(t, svc)
})
testutil.RunStep(suite.T(), "gateway exists", func(t *testing.T) {
svc, err := f.FetchAPIGateway(suite.ctx, suite.apiGateway.Id)
require.NoError(t, err)
require.NotNil(t, svc)
})
testutil.RunStep(suite.T(), "incorrect type is passed", func(t *testing.T) {
incorrectID := resourcetest.Resource(pbmesh.ProxyStateTemplateType, "api-1").ID()
defer func() {
err := recover()
require.NotNil(t, err)
}()
f.FetchAPIGateway(suite.ctx, incorrectID)
})
})
}
func TestDataFetcher(t *testing.T) {
suite.Run(t, new(dataFetcherSuite))
}
func (suite *dataFetcherSuite) appendTenancyInfo(tenancy *pbresource.Tenancy) string {
return fmt.Sprintf("%s_Namespace_%s_Partition", tenancy.Namespace, tenancy.Partition)
}
func (suite *dataFetcherSuite) cleanUpNodes() {
suite.resourceClient.MustDelete(suite.T(), suite.apiGateway.Id)
}
func (suite *dataFetcherSuite) runTestCaseWithTenancies(t func(*pbresource.Tenancy)) {
for _, tenancy := range suite.tenancies {
suite.Run(suite.appendTenancyInfo(tenancy), func() {
suite.setupWithTenancy(tenancy)
suite.T().Cleanup(func() {
suite.cleanUpNodes()
})
t(tenancy)
})
}
}

View File

@ -11,8 +11,10 @@ import (
"github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/controller/dependency" "github.com/hashicorp/consul/internal/controller/dependency"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/apigateways"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/gatewayproxy/builder" "github.com/hashicorp/consul/internal/mesh/internal/controllers/gatewayproxy/builder"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/gatewayproxy/fetcher" "github.com/hashicorp/consul/internal/mesh/internal/controllers/gatewayproxy/fetcher"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/meshgateways"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/sidecarproxy" "github.com/hashicorp/consul/internal/mesh/internal/controllers/sidecarproxy"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/sidecarproxy/cache" "github.com/hashicorp/consul/internal/mesh/internal/controllers/sidecarproxy/cache"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
@ -73,12 +75,21 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c
return nil return nil
} }
// If the workload is not for a xGateway, let the sidecarproxy reconciler handle it switch workload.Metadata["gateway-type"] {
if gatewayKind := workload.Metadata["gateway-kind"]; gatewayKind == "" { case meshgateways.GatewayKind:
rt.Logger.Trace("workload is not a gateway; skipping reconciliation", "workload", workloadID, "workloadData", workload.Data) rt.Logger.Trace("workload is a mesh-gateway; reconciling", "workload", workloadID, "workloadData", workload.Data)
return r.reconcileMeshGatewayProxyState(ctx, dataFetcher, workload, rt, req)
case apigateways.GatewayKind:
rt.Logger.Trace("workload is a api-gateway; reconciling", "workload", workloadID, "workloadData", workload.Data)
// TODO: NET-735 -- implement api-gateway reconciliation
return nil
default:
rt.Logger.Trace("workload is not a gateway; skipping reconciliation", "workload", workloadID)
return nil return nil
} }
}
func (r *reconciler) reconcileMeshGatewayProxyState(ctx context.Context, dataFetcher *fetcher.Fetcher, workload *resource.DecodedResource[*pbcatalog.Workload], rt controller.Runtime, req controller.Request) error {
proxyStateTemplate, err := dataFetcher.FetchProxyStateTemplate(ctx, req.ID) proxyStateTemplate, err := dataFetcher.FetchProxyStateTemplate(ctx, req.ID)
if err != nil { if err != nil {
rt.Logger.Error("error reading proxy state template", "error", err) rt.Logger.Error("error reading proxy state template", "error", err)

View File

@ -0,0 +1,20 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package types
import (
"github.com/hashicorp/consul/internal/resource"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
)
func RegisterAPIGateway(r resource.Registry) {
r.Register(resource.Registration{
Type: pbmesh.APIGatewayType,
Proto: &pbmesh.APIGateway{},
Scope: resource.ScopeNamespace,
ACLs: nil, // TODO NET-7289
Mutate: nil, // TODO NET-7617
Validate: nil, // TODO NET-7618
})
}

View File

@ -30,4 +30,5 @@ type (
DecodedProxyStateTemplate = resource.DecodedResource[*pbmesh.ProxyStateTemplate] DecodedProxyStateTemplate = resource.DecodedResource[*pbmesh.ProxyStateTemplate]
DecodedMeshGateway = resource.DecodedResource[*pbmesh.MeshGateway] DecodedMeshGateway = resource.DecodedResource[*pbmesh.MeshGateway]
DecodedComputedExportedServices = resource.DecodedResource[*pbmulticluster.ComputedExportedServices] DecodedComputedExportedServices = resource.DecodedResource[*pbmulticluster.ComputedExportedServices]
DecodedAPIGateway = resource.DecodedResource[*pbmesh.APIGateway]
) )

View File

@ -19,6 +19,7 @@ func Register(r resource.Registry) {
RegisterDestinationPolicy(r) RegisterDestinationPolicy(r)
RegisterComputedRoutes(r) RegisterComputedRoutes(r)
RegisterMeshGateway(r) RegisterMeshGateway(r)
RegisterAPIGateway(r)
RegisterMeshConfiguration(r) RegisterMeshConfiguration(r)
// todo (v2): uncomment once we implement it. // todo (v2): uncomment once we implement it.
// RegisterDestinationsConfiguration(r) // RegisterDestinationsConfiguration(r)