mirror of https://github.com/status-im/consul.git
[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:
parent
3bf999e46b
commit
54c974748e
|
@ -5,12 +5,19 @@ package apigateways
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
const (
|
||||
ControllerName = "consul.io/api-gateway"
|
||||
GatewayKind = "api-gateway"
|
||||
)
|
||||
|
||||
func Controller() *controller.Controller {
|
||||
|
@ -22,13 +29,73 @@ func Controller() *controller.Controller {
|
|||
|
||||
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.
|
||||
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.Trace("reconciling api gateway")
|
||||
|
||||
//TODO NET-7378
|
||||
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
|
||||
_, 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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -11,8 +11,10 @@ import (
|
|||
|
||||
"github.com/hashicorp/consul/internal/controller"
|
||||
"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/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/cache"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
|
@ -73,12 +75,21 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c
|
|||
return nil
|
||||
}
|
||||
|
||||
// If the workload is not for a xGateway, let the sidecarproxy reconciler handle it
|
||||
if gatewayKind := workload.Metadata["gateway-kind"]; gatewayKind == "" {
|
||||
rt.Logger.Trace("workload is not a gateway; skipping reconciliation", "workload", workloadID, "workloadData", workload.Data)
|
||||
switch workload.Metadata["gateway-type"] {
|
||||
case meshgateways.GatewayKind:
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
rt.Logger.Error("error reading proxy state template", "error", err)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -30,4 +30,5 @@ type (
|
|||
DecodedProxyStateTemplate = resource.DecodedResource[*pbmesh.ProxyStateTemplate]
|
||||
DecodedMeshGateway = resource.DecodedResource[*pbmesh.MeshGateway]
|
||||
DecodedComputedExportedServices = resource.DecodedResource[*pbmulticluster.ComputedExportedServices]
|
||||
DecodedAPIGateway = resource.DecodedResource[*pbmesh.APIGateway]
|
||||
)
|
||||
|
|
|
@ -19,7 +19,8 @@ func Register(r resource.Registry) {
|
|||
RegisterDestinationPolicy(r)
|
||||
RegisterComputedRoutes(r)
|
||||
RegisterMeshGateway(r)
|
||||
RegisterAPIGateway(r)
|
||||
RegisterMeshConfiguration(r)
|
||||
// todo (v2): uncomment once we implement it.
|
||||
//RegisterDestinationsConfiguration(r)
|
||||
// RegisterDestinationsConfiguration(r)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue