Skip to content

Commit

Permalink
Adding EnableWebsockets property to the gateway (radius-project#7643)
Browse files Browse the repository at this point in the history
# Description
Based on the issue created, web sockets are not supported in our Gateway
resource as of now. We added **EnableWebsockets** property to the
Gateway Route Resource so that it can be set by Radius users. By
default, it is set to false.

## Type of change
- This pull request fixes a bug in Radius and has an approved issue
(issue link required).
- This pull request adds or changes features of Radius and has an
approved issue (issue link required).
Fixes: radius-project#7590

---------

Signed-off-by: ytimocin <ytimocin@microsoft.com>
  • Loading branch information
ytimocin committed Jun 3, 2024
1 parent fe9355c commit d1a55c9
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 51 deletions.

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions pkg/corerp/api/v20231001preview/gateway_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ func (src *GatewayResource) ConvertTo() (v1.DataModelInterface, error) {
if src.Properties.Routes != nil {
for _, r := range src.Properties.Routes {
s := datamodel.GatewayRoute{
Destination: to.String(r.Destination),
Path: to.String(r.Path),
ReplacePrefix: to.String(r.ReplacePrefix),
Destination: to.String(r.Destination),
Path: to.String(r.Path),
ReplacePrefix: to.String(r.ReplacePrefix),
EnableWebsockets: to.Bool(r.EnableWebsockets),
}
routes = append(routes, s)
}
Expand Down Expand Up @@ -110,9 +111,10 @@ func (dst *GatewayResource) ConvertFrom(src v1.DataModelInterface) error {
if g.Properties.Routes != nil {
for _, r := range g.Properties.Routes {
s := &GatewayRoute{
Destination: to.Ptr(r.Destination),
Path: to.Ptr(r.Path),
ReplacePrefix: to.Ptr(r.ReplacePrefix),
Destination: to.Ptr(r.Destination),
Path: to.Ptr(r.Path),
ReplacePrefix: to.Ptr(r.ReplacePrefix),
EnableWebsockets: to.Ptr(r.EnableWebsockets),
}
routes = append(routes, s)
}
Expand Down
65 changes: 63 additions & 2 deletions pkg/corerp/api/v20231001preview/gateway_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func TestGatewayConvertVersionedToDataModel(t *testing.T) {
require.Equal(t, "mydestination", gw.Properties.Routes[0].Destination)
require.Equal(t, "mypath", gw.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", gw.Properties.Routes[0].ReplacePrefix)
require.False(t, gw.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", gw.Properties.URL)
require.Equal(t, []rpv1.OutputResource(nil), gw.Properties.Status.OutputResources)
require.Equal(t, "2023-10-01-preview", gw.InternalMetadata.UpdatedAPIVersion)
Expand All @@ -76,6 +77,7 @@ func TestGatewayConvertDataModelToVersioned(t *testing.T) {
require.Equal(t, "myapp.mydomain.com", *versioned.Properties.Hostname.FullyQualifiedHostname)
require.Equal(t, "myprefix", *versioned.Properties.Hostname.Prefix)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.False(t, *versioned.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "mypath", *versioned.Properties.Routes[0].Path)
require.Equal(t, "http://myprefix.myapp.mydomain.com", *versioned.Properties.URL)
require.Equal(t, resourcetypeutil.MustPopulateResourceStatus(&ResourceStatus{}), versioned.Properties.Status)
Expand Down Expand Up @@ -103,6 +105,7 @@ func TestGatewaySSLPassthroughConvertVersionedToDataModel(t *testing.T) {
require.Equal(t, "mydestination", gw.Properties.Routes[0].Destination)
require.Equal(t, "mypath", gw.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", gw.Properties.Routes[0].ReplacePrefix)
require.False(t, gw.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", gw.Properties.URL)
require.Equal(t, []rpv1.OutputResource(nil), gw.Properties.Status.OutputResources)
require.Equal(t, "2023-10-01-preview", gw.InternalMetadata.UpdatedAPIVersion)
Expand All @@ -128,9 +131,65 @@ func TestGatewaySSLPassthroughConvertDataModelToVersioned(t *testing.T) {
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", *versioned.Properties.Application)
require.Equal(t, "myapp.mydomain.com", *versioned.Properties.Hostname.FullyQualifiedHostname)
require.Equal(t, "myprefix", *versioned.Properties.Hostname.Prefix)
require.Equal(t, "mypath", *versioned.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.False(t, *versioned.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", *versioned.Properties.URL)
require.Equal(t, resourcetypeutil.MustPopulateResourceStatus(&ResourceStatus{}), versioned.Properties.Status)
require.Equal(t, true, *versioned.Properties.TLS.SSLPassthrough)
}

func TestGatewayEnableWebsocketsConvertVersionedToDataModel(t *testing.T) {
// arrange
rawPayload := testutil.ReadFixture("gatewayresourcedatamodel-with-enablewebsockets.json")
r := &GatewayResource{}
err := json.Unmarshal(rawPayload, r)
require.NoError(t, err)

// act
dm, err := r.ConvertTo()

// assert
require.NoError(t, err)
gw := dm.(*datamodel.Gateway)
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/gateways/gateway0", gw.ID)
require.Equal(t, "gateway0", gw.Name)
require.Equal(t, "Applications.Core/gateways", gw.Type)
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", gw.Properties.Application)
require.Equal(t, "myapp.mydomain.com", gw.Properties.Hostname.FullyQualifiedHostname)
require.Equal(t, "myprefix", gw.Properties.Hostname.Prefix)
require.Equal(t, "mydestination", gw.Properties.Routes[0].Destination)
require.Equal(t, "mypath", gw.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", gw.Properties.Routes[0].ReplacePrefix)
require.True(t, gw.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", gw.Properties.URL)
require.Equal(t, []rpv1.OutputResource(nil), gw.Properties.Status.OutputResources)
require.Equal(t, "2023-10-01-preview", gw.InternalMetadata.UpdatedAPIVersion)
require.Equal(t, true, gw.Properties.TLS.SSLPassthrough)
}

func TestGatewayEnableWebsocketsConvertDataModelToVersioned(t *testing.T) {
// arrange
rawPayload := testutil.ReadFixture("gatewayresourcedatamodel-with-enablewebsockets.json")
r := &datamodel.Gateway{}
err := json.Unmarshal(rawPayload, r)
require.NoError(t, err)

// act
versioned := &GatewayResource{}
err = versioned.ConvertFrom(r)

// assert
require.NoError(t, err)
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/gateways/gateway0", *versioned.ID)
require.Equal(t, "gateway0", *versioned.Name)
require.Equal(t, "Applications.Core/gateways", *versioned.Type)
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", *versioned.Properties.Application)
require.Equal(t, "myapp.mydomain.com", *versioned.Properties.Hostname.FullyQualifiedHostname)
require.Equal(t, "myprefix", *versioned.Properties.Hostname.Prefix)
require.Equal(t, "mypath", *versioned.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.True(t, *versioned.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", *versioned.Properties.URL)
require.Equal(t, resourcetypeutil.MustPopulateResourceStatus(&ResourceStatus{}), versioned.Properties.Status)
require.Equal(t, true, *versioned.Properties.TLS.SSLPassthrough)
Expand Down Expand Up @@ -158,6 +217,7 @@ func TestGatewayTLSTerminationConvertVersionedToDataModel(t *testing.T) {
require.Equal(t, "mydestination", gw.Properties.Routes[0].Destination)
require.Equal(t, "mypath", gw.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", gw.Properties.Routes[0].ReplacePrefix)
require.False(t, gw.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", gw.Properties.URL)
require.Equal(t, []rpv1.OutputResource(nil), gw.Properties.Status.OutputResources)
require.Equal(t, "2023-10-01-preview", gw.InternalMetadata.UpdatedAPIVersion)
Expand All @@ -184,9 +244,9 @@ func TestGatewayTLSTerminationConvertDataModelToVersioned(t *testing.T) {
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", *versioned.Properties.Application)
require.Equal(t, "myapp.mydomain.com", *versioned.Properties.Hostname.FullyQualifiedHostname)
require.Equal(t, "myprefix", *versioned.Properties.Hostname.Prefix)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.Equal(t, "mypath", *versioned.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.False(t, *versioned.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", *versioned.Properties.URL)
require.Equal(t, resourcetypeutil.MustPopulateResourceStatus(&ResourceStatus{}), versioned.Properties.Status)
require.Equal(t, "secretname", *versioned.Properties.TLS.CertificateFrom)
Expand Down Expand Up @@ -215,6 +275,7 @@ func TestGatewayTLSTerminationConvertVersionedToDataModel_NoMinProtocolVersion(t
require.Equal(t, "mydestination", gw.Properties.Routes[0].Destination)
require.Equal(t, "mypath", gw.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", gw.Properties.Routes[0].ReplacePrefix)
require.False(t, gw.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", gw.Properties.URL)
require.Equal(t, []rpv1.OutputResource(nil), gw.Properties.Status.OutputResources)
require.Equal(t, "2023-10-01-preview", gw.InternalMetadata.UpdatedAPIVersion)
Expand All @@ -241,9 +302,9 @@ func TestGatewayTLSTerminationConvertDataModelToVersioned_NoMinProtocolVersion(t
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0", *versioned.Properties.Application)
require.Equal(t, "myapp.mydomain.com", *versioned.Properties.Hostname.FullyQualifiedHostname)
require.Equal(t, "myprefix", *versioned.Properties.Hostname.Prefix)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.Equal(t, "mypath", *versioned.Properties.Routes[0].Path)
require.Equal(t, "myreplaceprefix", *versioned.Properties.Routes[0].ReplacePrefix)
require.False(t, *versioned.Properties.Routes[0].EnableWebsockets)
require.Equal(t, "http://myprefix.myapp.mydomain.com", *versioned.Properties.URL)
require.Equal(t, resourcetypeutil.MustPopulateResourceStatus(&ResourceStatus{}), versioned.Properties.Status)
require.Equal(t, "secretname", *versioned.Properties.TLS.CertificateFrom)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/gateways/gateway0",
"name": "gateway0",
"type": "Applications.Core/gateways",
"systemData": {
"createdBy": "fakeid@live.com",
"createdByType": "User",
"createdAt": "2021-09-24T19:09:54.2403864Z",
"lastModifiedBy": "fakeid@live.com",
"lastModifiedByType": "User",
"lastModifiedAt": "2021-09-24T20:09:54.2403864Z"
},
"tags": {
"env": "dev"
},
"properties": {
"status": {
"outputResources": [
{
"id": "/planes/test/local/providers/Test.Namespace/testResources/test-resource"
}
]
},
"application": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup/providers/Applications.Core/applications/app0",
"hostname": {
"fullyQualifiedHostname": "myapp.mydomain.com",
"prefix": "myprefix"
},
"routes": [
{
"destination": "mydestination",
"path": "mypath",
"replacePrefix": "myreplaceprefix",
"enableWebsockets": true
}
],
"tls": {
"sslPassthrough": true
},
"url": "http://myprefix.myapp.mydomain.com"
}
}
3 changes: 3 additions & 0 deletions pkg/corerp/api/v20231001preview/zz_generated_models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pkg/corerp/api/v20231001preview/zz_generated_models_serde.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions pkg/corerp/datamodel/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ type GatewayProperties struct {

// GatewayRoute represents the route attached to Gateway.
type GatewayRoute struct {
Destination string `json:"destination,omitempty"`
Path string `json:"path,omitempty"`
ReplacePrefix string `json:"replacePrefix,omitempty"`
Destination string `json:"destination,omitempty"`
Path string `json:"path,omitempty"`
ReplacePrefix string `json:"replacePrefix,omitempty"`
EnableWebsockets bool `json:"enableWebsockets,omitempty"`
}

// GatewayPropertiesHostname - Declare hostname information for the Gateway.
Expand Down
30 changes: 17 additions & 13 deletions pkg/corerp/renderers/gateway/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import (
resources_kubernetes "github.com/radius-project/radius/pkg/ucp/resources/kubernetes"
)

const secretStoreNotFound = "secretStore resource %s not found"
const invalidSecretStoreResource = "certificateFrom must reference a secretStore resource"

type Renderer struct {
}

Expand Down Expand Up @@ -134,46 +137,46 @@ func MakeRootHTTPProxy(ctx context.Context, options renderers.RenderOptions, gat
secretStoreResourceId := gateway.Properties.TLS.CertificateFrom
secretStoreResource, ok := dependencies[secretStoreResourceId]
if !ok {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("secretStore resource %s not found", secretStoreResourceId))
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf(secretStoreNotFound, secretStoreResourceId))
}

referencedResource := dependencies[secretStoreResourceId].Resource
if !strings.EqualFold(referencedResource.ResourceTypeName(), datamodel.SecretStoreResourceType) {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest("certificateFrom must reference a secretStore resource")
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(invalidSecretStoreResource)
}

// Validate the secretStore resource: it must be of type certificate and have tls.crt and tls.key
secretStore, ok := referencedResource.(*datamodel.SecretStore)
if !ok {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest("certificateFrom must reference a secretStore resource")
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(invalidSecretStoreResource)
}

if secretStore.Properties.Type != datamodel.SecretTypeCert {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest("certificateFrom must reference a secretStore resource with type certificate")
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(invalidSecretStoreResource + " with type certificate")
}

if secretStore.Properties.Data["tls.crt"] == nil {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest("certificateFrom must reference a secretStore resource with tls.crt")
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(invalidSecretStoreResource + " with tls.crt")
}

if secretStore.Properties.Data["tls.key"] == nil {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest("certificateFrom must reference a secretStore resource with tls.key")
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(invalidSecretStoreResource + " with tls.key")
}

// Get the name and namespace of the Kubernetes secret resource from the secretStore OutputResources
if secretStoreResource.OutputResources == nil {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("secretStore resource %s not found", secretStoreResourceId))
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf(secretStoreNotFound, secretStoreResourceId))
}

secretResourceID, ok := secretStoreResource.OutputResources[rpv1.LocalIDSecret]
if !ok {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("secretStore resource %s not found", secretStoreResourceId))
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf(secretStoreNotFound, secretStoreResourceId))
}

secretName := secretResourceID.Name()
secretNamespace := secretResourceID.FindScope(resources_kubernetes.ScopeNamespaces)
if secretNamespace == "" {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("secretStore resource %s not found", secretStoreResourceId))
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf(secretStoreNotFound, secretStoreResourceId))
}

contourTLSConfig = &contourv1.TLS{
Expand All @@ -193,16 +196,19 @@ func MakeRootHTTPProxy(ctx context.Context, options renderers.RenderOptions, gat
if sslPassthrough && (route.Path != "" || route.ReplacePrefix != "") {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest("cannot support `path` or `replacePrefix` in routes with sslPassthrough set to true")
}

routeName, err := getRouteName(&route)
if err != nil {
return rpv1.OutputResource{}, err
}

routeResourceName := kubernetes.NormalizeResourceName(routeName)
prefix := route.Path

if sslPassthrough {
prefix = "/"
}

includes = append(includes, contourv1.Include{
Name: routeResourceName,
Conditions: []contourv1.MatchCondition{
Expand Down Expand Up @@ -270,6 +276,7 @@ func MakeRootHTTPProxy(ctx context.Context, options renderers.RenderOptions, gat
Includes: includes,
},
}

if sslPassthrough {
rootHTTPProxy.Spec.TCPProxy = tcpProxy
}
Expand All @@ -292,14 +299,12 @@ func MakeRoutesHTTPProxies(ctx context.Context, options renderers.RenderOptions,
return []rpv1.OutputResource{}, err
}
port = urlPort

} else {
routeProperties := dependencies[route.Destination]
routePort, ok := routeProperties.ComputedValues["port"].(float64)
if ok {
port = int32(routePort)
}

}

routeName, err := getRouteName(&route)
Expand Down Expand Up @@ -335,13 +340,11 @@ func MakeRoutesHTTPProxies(ctx context.Context, options renderers.RenderOptions,
} else {
object.Spec.Routes[i].PathRewritePolicy.ReplacePrefix = append(object.Spec.Routes[i].PathRewritePolicy.ReplacePrefix, pathRewritePolicy.ReplacePrefix[0])
}

break outer
}
}
}
}

continue
}

Expand All @@ -366,6 +369,7 @@ func MakeRoutesHTTPProxies(ctx context.Context, options renderers.RenderOptions,
},
},
PathRewritePolicy: pathRewritePolicy,
EnableWebsockets: route.EnableWebsockets,
},
},
},
Expand Down
Loading

0 comments on commit d1a55c9

Please sign in to comment.