Modernizing SOAP Interfaces with Gloo Portal

If you still have some legacy SOAP (Simple Object Access Protocol) applications, it is possible to keep running them while modernizing your connectivity, giving you a more robust operating environment so that you can upgrade or replace them when you are ready. The purpose of this post is to show by example how you can now publish these SOAP interfaces using Gloo Edge Enterprise (an enhanced Envoy Proxy API gateway) with Gloo Portal 1.1 (a developer’s portal to share APIs).

Looking for information about SOAP interfaces may lead you to Google “soap introduction date,” in which case you may find an article identifying the Babylonians as the producers of the first soap around 2800 BCE. At first glance, that sounds about right since SOAP has seemingly been around for centuries now. Alas, the SOAP messaging protocols—and therefore SOAP interfaces—are of more recent origin. My own archeological research dates it back to about 2000 CE (which is roughly equivalent to 2800 BCE in computer years).

So those of us who are old enough have had plenty of time to build and deploy systems with SOAP interfaces, and you can still find them scattered across the enterprise computing landscape. Even though most organizations are not sponsoring new development using SOAP, there are scores of legacy SOAP assets still delivering value. The lives of these assets can be extended by fronting them with API gateway technology that updates their interfaces and security postures using modern standards.

Solo.io delivered on this promise with an XSLT 3.0 (Extensible Stylesheet Language Transformations) capability in the Gloo Edge Enterprise 1.8 release in June of this year. You can read more about that here and here.

Now with the Gloo Portal 1.1 release, you can not only access these systems using Gloo Edge, but also you can better manage the lifecycles of these APIs and even present them to consumers in a dynamically generated web portal.

All Kubernetes resources used here are available on GitHub.

Building an environment for modernizing or connecting your SOAP interfaces

In order to do this, you’ll need a Kubernetes cluster and associated tools, plus an instance of Gloo Edge Enterprise to complete this guide. In particular, we’ll be using the CLI utilities kubectl, glooctl, curl, and jq.

We used Google Kubernetes Engine (GKE) with Kubernetes v1.20.9 to test this guide, although any recent version with any Kubernetes provider should suffice. If you prefer to work locally instead of with a remote cluster, we’ve have good success with Kind. If you go the Kind route, use these instructions for your installation of Gloo Edge.

We use Gloo Edge Enterprise v1.9.0 as our API gateway. Use this guide if you need to install Gloo Edge Enterprise. If you don’t already have access to the enterprise bits of Gloo Edge, you can request a free trial here.

Gloo Portal allows you to catalog, manage and securely publish running APIs to onboard developers both inside and outside your organization. It supports both REST/OpenAPI and gRPC interfaces. In this post, we’ll show you how to publish SOAP/XML interfaces as well. And as with all Gloo products, these facilities are delivered with a cloud-native architecture that fits hand-in-glove with your Kubernetes and Istio deployments.

Gloo Portal is included with every Gloo Edge Enterprise subscription. But since not all customers use it, it is packaged as a separate installation from the core Edge API Gateway. We are using Portal v1.1.1 for this exercise. Follow this guide to install Portal with Gloo Edge as its deployment target.

Note that Gloo Portal also supports deployment to Gloo Mesh Gateway in addition to Gloo Edge.  However, in this blog post we’ll focus strictly on deployment to Gloo Edge.

We’ll begin by establishing the base application and testing it out with its native SOAP/XML interface. Later we’ll progress to wrapping it with a REST/JSON facade and publishing its APIs with Gloo Portal.

Establishing the SOAP interface

In this exercise, we’ll use the World Cities example application as the basis for our portal work. It’s a simple application that provides a single query endpoint to perform a fuzzy search for city names in a public database. It is also the basis for the Gloo Edge SOAP/XML documentation.

The World Cities application is already built and packaged in an OCI (Open Container Initiative) container. We’ll use a standard Kubernetes service deployment configuration to spin it up in our cluster.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/01-world-cities-svc.yaml

You should see this response:

deployment.apps/world-cities-soap-service created
service/world-cities-soap-service created

Let’s confirm that the resulting pod spun up as expected:

kubectl get pod -l app=world-cities-soap-service

We expect to see a running world-cities pod, like this:

NAME                                         READY   STATUS    RESTARTS   AGE
world-cities-soap-service-799cf66848-4gjrz   1/1     Running   0          90s

Gloo Edge automatically discovers this service deployment and generates a Kubernetes custom resource called an Upstream, to which the gateway can route requests. Let’s confirm that this Upstream was created successfully using the expected naming convention. Note that for this operation, we’re using the glooctl CLI utility, which you installed along with Gloo Edge.

glooctl get upstream --name default-world-cities-soap-service-8080

Note that glooctl shows both our Upstream and the fact that its status is Accepted.

+----------------------------------------+------------+----------+--------------------------------+
|                UPSTREAM                |    TYPE    |  STATUS  |            DETAILS             |
+----------------------------------------+------------+----------+--------------------------------+
| default-world-cities-soap-service-8080 | Kubernetes | Accepted | svc name:                      |
|                                        |            |          | world-cities-soap-service      |
|                                        |            |          | svc namespace: default         |
|                                        |            |          | port:          8080            |
|                                        |            |          |                                |
+----------------------------------------+------------+----------+--------------------------------+

Establishing an initial VirtualService

VirtualServices are a key concept in Gloo Edge that define routing rules for requests coming into your cluster. They determine the ultimate Upstream destination for the request, in addition to applying policies to manage security, rate limiting, transformations, and the like.

We will initially establish a VirtualService that performs no SOAP/XML conversions. In other words, we’ll interact with the SOAP service in its native language.

The VirtualService to accomplish this is quite simple, as you can see from its YAML configuration below. It accepts traffic from any domain, with any path prefix, and routes it to the Upstream discovered for the world-cities service that we installed earlier.

apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: world-city-service-vs
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - '*'
    routes:
      - matchers:
        - prefix: /
        routeAction:
          single:
            upstream:
              # Upstream generated by gloo edge discovery
              name: default-world-cities-soap-service-8080
              namespace: gloo-system
        options:
          autoHostRewrite: true

Let’s apply the above configuration to create the VirtualService.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/02-world-cities-vs.yaml

You should see this response:

virtualservice.gateway.solo.io/world-city-service-vs created

You can easily confirm that the VirtualService was accepted using the glooctl utility.

glooctl get vs world-city-service-vs

Expected response:

+-----------------------+--------------+---------+------+----------+-----------------+----------------------------------------------------+
|    VIRTUAL SERVICE    | DISPLAY NAME | DOMAINS | SSL  |  STATUS  | LISTENERPLUGINS |                       ROUTES                       |
+-----------------------+--------------+---------+------+----------+-----------------+----------------------------------------------------+
| world-city-service-vs |              | *       | none | Accepted |                 | / ->                                               |
|                       |              |         |      |          |                 | gloo-system.default-world-cities-soap-service-8080 |
|                       |              |         |      |          |                 | (upstream)                                         |
+-----------------------+--------------+---------+------+----------+-----------------+----------------------------------------------------+

Configuring DNS rules

If you used the single kubectl command above to create the portal artifacts, then you likely do not have the necessary network configuration in place. In order to visit the portal being served at our domain petstore.example.com, we’ll need to make sure a DNS rule exists that will resolve that domain to that address.

Throughout this exercise, we are using hostnames to access the services we are publishing using Gloo Edge and Portal. Depending on your environment, you may need to adjust your network configuration if you want to follow along precisely. There are many ways to set up DNS rules for the domains defined in the VirtualServices, Environments and Portals that we are creating.

We are using GKE to test this exercise, and GKE binds an external IP address to the Envoy proxy deployed by Gloo Edge. You can locate that address using the glooctl utility like this:

INGRESS_HOST=$(glooctl proxy address | cut -f 1 -d ':')

Note that if you’re using a different platform like Amazon EKS, it may produce a hostname instead of an address. In that case, you’ll need to use a different approach like supplying a Host header with your curl commands. This example from the Gloo Portal docs might help.

But for the purposes of our setup with GKE using a simple workstation client, we’ll modify our local /etc/hosts file with an entry to manually resolve the Environment and Portal domains that we’ll be using.

Let’s add entries for the worldcities.example.com and api.worldcities.example.com hostname, substituting INGRESS_HOST with your IP address, as shown below.

cat <<EOF | sudo tee -a /etc/hosts

# Added for Solo.io Gloo Portal Exercises
${INGRESS_HOST} worldcities.example.com
${INGRESS_HOST} api.worldcities.example.com
EOF

Testing the SOAP application endpoint

With a Gloo Edge VirtualService in place, we can now exercise the SOAP/XML endpoint before converting it to use a more modern REST/JSON interface.

curl -X POST http://api.worldcities.example.com/ -H "SOAPAction:findCity" -H "content-type:application/xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.xmlsoap.org/soap/">
  <Header />
  <Body>
    <Query>
      <CityQuery>south bo</CityQuery>
    </Query>
  </Body>
</Envelope>'

Because we are not transforming the response, we should see a proper XML response to our SOAP/XML query:

<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
  <Header xmlns="http://schemas.xmlsoap.org/soap/envelope/"></Header>
  <Body xmlns="http://schemas.xmlsoap.org/soap/envelope/">
    <Content>
      <Match>
        <City>south boston</City>
        <Country>United States</Country>
        <SubCountry>Massachusetts</SubCountry>
        <GeoNameId>4951305</GeoNameId>
      </Match>
      <Match>
        <City>south peabody</City>
        <Country>United States</Country>
        <SubCountry>Massachusetts</SubCountry>
        <GeoNameId>4951473</GeoNameId>
      </Match>
      <Match>
        <City>south bradenton</City>
        <Country>United States</Country>
        <SubCountry>Florida</SubCountry>
        <GeoNameId>4173392</GeoNameId>
      </Match>
      <Match>
        <City>south burlington</City>
        <Country>United States</Country>
        <SubCountry>Vermont</SubCountry>
        <GeoNameId>5241248</GeoNameId>
      </Match>
    </Content>
  </Body>
</Envelope>

Producing an OpenAPI interface

Gloo Portal supports API management using two popular interface standards today, OpenAPI and gRPC. (Stay tuned for GraphQL, which will be coming soon.) For this exercise, we will convert our SOAP/XML interface to REST/JSON by employing OpenAPI.

You can produce OpenAPI specifications from scratch, but it is often easier to use Swagger tools to get started. That’s what we did in this case, using free Swagger tools to create an initial OpenAPI specification based on sample calls to our interface as described here. We then tweaked it by hand a bit to produce this JSON specification document.

{
    "openapi": "3.0.1",
    "info": {
      "title": "worldCities",
      "description": "World Cities API",
      "version": "0.1"
    },
    "servers": [
      {
        "url": "http://api.worldcities.example.com"
      }
    ],
    "paths": {
      "/": {
        "post": {
          "description": "Query world cities by string pattern",
          "operationId": "/",
          "parameters": [
            {
              "name": "SOAPAction",
              "in": "header",
              "required": false,
              "style": "simple",
              "explode": false,
              "schema": {
                "type": "string"
              },
              "example": "findCity"
            }
          ],
          "requestBody": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/body"
                },
                "examples": {
                  "0": {
                    "value": "{\"cityQuery\": \"south bo\"}"
                  }
                }
              }
            }
          },
          "responses": {
            "200": {
              "description": "south-bo response",
              "content": {
                "application/json": {
                  "schema": {
                    "$ref": "#/components/schemas/inline_response_200"
                  },
                  "examples": {
                    "0": {
                      "value": "\n  { \"matches\" : \n    [ \n      { \"city\" : \"south boston\",\n        \"country\" : \"United States\",\n        \"subCountry\" : \"Massachusetts\",\n        \"geoNameId\" : \"4951305\" },\n      \n      { \"city\" : \"south peabody\",\n        \"country\" : \"United States\",\n        \"subCountry\" : \"Massachusetts\",\n        \"geoNameId\" : \"4951473\" },\n      \n      { \"city\" : \"south bradenton\",\n        \"country\" : \"United States\",\n        \"subCountry\" : \"Florida\",\n        \"geoNameId\" : \"4173392\" },\n      \n      { \"city\" : \"south burlington\",\n        \"country\" : \"United States\",\n        \"subCountry\" : \"Vermont\",\n        \"geoNameId\" : \"5241248\" } ] }"
                    }
                  }
                }
              }
            }
          },
          "servers": [
            {
              "url": "http://api.worldcities.example.com"
            }
          ]
        },
        "servers": [
          {
            "url": "http://api.worldcities.example.com"
          }
        ]
      }
    },
    "components": {
      "schemas": {
        "body": {
          "type": "object",
          "properties": {
            "cityQuery": {
              "type": "string"
            }
          }
        },
        "inline_response_200": {
          "type": "object",
          "properties": {
            "matches": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "geoNameId": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string"
                  },
                  "subCountry": {
                    "type": "string"
                  },
                  "city": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    }
  }

There are a couple of interesting interface changes represented in this document:

  • The SOAPAction will still be passed in via request header;
  • The cityQuery string will be passed in a JSON-formatted request body; and
  • The response will be returned in JSON format as well.

Using XSLT to convert XML to JSON

Before we apply the new OpenAPI interface specification and build a portal, let’s first take a look at the XSLT transformations that will convert the XML input and output of our service into JSON. This is also covered in the Gloo Edge transformation docs.

There are three key elements of the request transformation:

  1. xslt: This is the payload transformation. It converts a simple request body like {"cityQuery": "south bo"} into the full XML envelope required by the SOAP service, which we used in an earlier section. Note that a lot of the difficult work is delegated to the XSLT function json-to-xml. We use it in the request XSLT to transform the core of the JSON input to XML, and vice versa later with the response.
  2. nonXmlTransform: This is set to true since we are transforming JSON to XML. Natively, XSLT can only transform XML data. However, our input to the transformation is JSON, so by specifying this flag, we signal to our XSLT transformation filter that we are supplying non-XML (JSON) data as the input.
  3. setContentType: Since we are transforming the content type of the data from application/json to text/xml, we can set the new content-type header here.
- requestTransformation:
    xsltTransformation:
      xslt: |
        <?xml version="1.0" encoding="UTF-8"?>
          <xsl:stylesheet
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
          xmlns:math="http://www.w3.org/2005/xpath-functions/math"
          xmlns:xs="http://www.w3.org/2001/XMLSchema"
          exclude-result-prefixes="xs math" version="3.0">
            <xsl:output indent="yes" omit-xml-declaration="yes" />
            <xsl:strip-space elements="*"/>
            <xsl:template match="/" xmlns="http://schemas.xmlsoap.org/soap/envelope/">
              <Envelope >
                <Header/>
                <Body>
                  <Query>
                    <xsl:apply-templates select="json-to-xml(.)/*"/>
                  </Query>
                </Body>
              </Envelope>
             </xsl:template>
             <xsl:template match="map" xpath-default-namespace="http://www.w3.org/2005/xpath-functions"
             xmlns:web="http://www.qas.com/OnDemand-2011-03">
               <CityQuery><xsl:value-of select="string[@key='cityQuery']" /></CityQuery>
             </xsl:template>
           </xsl:stylesheet>
      nonXmlTransform: true
      setContentType: text/xml

The response transformation below is similar to what we’ve just shown, but in reverse.

  1. The xml-to-json XSLT function translates the XML response from the server to the JSON that we see in the response payload.
  2. The setContentType attribute identifies that we’ll be serving JSON in the response instead of the XML returned from the upstream service.
responseTransformation:
  xsltTransformation:
    xslt: |
      <?xml version="1.0" encoding="UTF-8"?>
        <xsl:stylesheet
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xpath-default-namespace="http://schemas.xmlsoap.org/soap/envelope/"
        version="3.0">
          <xsl:output method="text" omit-xml-declaration="yes" />
          <xsl:variable name="myMap">
            <map xmlns="http://www.w3.org/2005/xpath-functions"> 
              <array key="matches" > 
                <xsl:for-each select="/Envelope/Body/Content/Match"> 
                  <map> 
                    <string key="city"><xsl:value-of select="City"/></string> 
                    <string key="country"><xsl:value-of select="Country" /></string> 
                    <string key="subCountry"><xsl:value-of select="SubCountry" /></string> 
                    <string key="geoNameId"><xsl:value-of select="GeoNameId" /></string> 
                  </map> 
                </xsl:for-each> 
               </array> 
             </map> 
            </xsl:variable> 
            <xsl:template match="/"> 
              <xsl:apply-templates select="xml-to-json($myMap, map{'indent': true()})" /> 
            </xsl:template> 
          </xsl:stylesheet> 
        setContentType: application/json

You can inspect the full VirtualService with both the XSLT request and response transforms in GitHub. Let’s apply these changes to our VirtualService and test.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/04-world-cities-xslt-vs.yaml

You should see a response like this to indicate the VirtualService has been modified.

virtualservice.gateway.solo.io/world-city-service-vs configured

We will use curl with a new JSON request body to confirm that our change was successful.

curl http://api.worldcities.example.com/ -d '{"cityQuery": "south bo"}' -H "SOAPAction:findCity" -H "content-type:application/json" | jq

You can see that we are now receiving a JSON payload instead of XML in response to our request.

{
  "matches": [
    {
      "city": "south boston",
      "country": "United States",
      "subCountry": "Massachusetts",
      "geoNameId": "4951305"
    },
    {
      "city": "south peabody",
      "country": "United States",
      "subCountry": "Massachusetts",
      "geoNameId": "4951473"
    },
    {
      "city": "south bradenton",
      "country": "United States",
      "subCountry": "Florida",
      "geoNameId": "4173392"
    },
    {
      "city": "south burlington",
      "country": "United States",
      "subCountry": "Vermont",
      "geoNameId": "5241248"
    }
  ]
}

Establishing the initial portal API

At this point, we are using Gloo Edge with an XSLT transform to access our SOAP service in a REST/JSON style. But how do we bring it under management by Gloo Portal? That’s what we’ll cover in this section.

There are three Gloo Portal components we’ll configure here:

  1. An APIDoc to wrap the OpenAPI interface that we created earlier for the World Cities application;
  2. An APIProduct to apply policies to the service endpoint in the OpenAPI specification and package it for distribution; and
  3. An Environment to declare which APIProducts will be deployed to which compute environments (e.g., dev, QA, production).

The APIDoc is quite simple; basically just defining a schema that points to a URL containing the OpenAPI interface already built.

apiVersion: portal.gloo.solo.io/v1beta1
kind: APIDoc
metadata:
  name: world-cities-schema
  namespace: default
spec:
  ## specify the type of schema provided in this APIDoc.
  openApi:
    content:
      # we use a fetchUrl here to tell the Gloo Portal
      # to fetch the schema contents directly from the petstore service.
      # 
      # configmaps and inline strings are also supported.
      fetchUrl: https://raw.githubusercontent.com/jameshbarton/solo-resources/main/world-cities-soap/lib/world-cities-openapi.json

The initial APIProduct is quite simple as well. It makes all of the APIDoc endpoints—only one in this case—available to the portal and routes all requests to the world-cities Upstream service. Its most prominent features are the XSLT transformations we explored earlier to translate the request and response payloads to and from XML and JSON. These XSLTs are identical to the ones we tested earlier using only Gloo Edge. You can inspect the full APIProduct we’ll be using here.

The Environment custom resource that we’ll be creating is straightforward. We’re defining a single “development” environment hosted at api.worldcities.example.com. For now, its only function is to designate which APIProducts it will be publishing.

apiVersion: portal.gloo.solo.io/v1beta1
kind: Environment
metadata:
  name: dev-world-cities
  namespace: default
spec:
  domains:
  - api.worldcities.example.com
  displayInfo:
    description: This environment is meant for developers to deploy and test their world cities API.
    displayName: World Cities Development
  apiProducts:
  - namespaces:
    - default
    names:
    - world-cities-product

Let’s apply all three of these portal components together.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/05-apidoc-prod-envt-world-cities.yaml

You should see a response like this:

apidoc.portal.gloo.solo.io/world-cities-schema created
apiproduct.portal.gloo.solo.io/world-cities-product created
environment.portal.gloo.solo.io/dev-world-cities created

Once these portal custom resources are in place, the product has enough information to publish the API for consumption. We have not yet published the Portal CR, so the web UI sandbox is not yet available. But we can test the underlying API.

What happens is that once the APIDoc, APIProduct, and Environment are in place, Gloo Portal publishes a Gloo Edge virtual service that allows traffic on the routes specified in those components. We can observe that VirtualService using kubectl:

kubectl get virtualservice -A

You should see two VirtualServices returned:

NAMESPACE     NAME                    AGE
default       dev-world-cities        89s
gloo-system   world-city-service-vs   64m

The world-city-service-vs was produced earlier in this exercise when we built the entire VirtualService by hand to test the upstream SOAP service using just Gloo Edge. Now Gloo Portal has published the dev-world-cities service automatically to the Gloo Edge platform. (Note that the name of the portal-produced service, dev-world-cities, matches the name of the Environment for which it was produced.)

We will first delete the original hand-built service due to potential conflicts with the new Portal-produced service, and then test the new one.

kubectl delete vs world-city-service-vs -n gloo-system

You should see a result like this:

virtualservice.gateway.solo.io "world-city-service-vs" deleted

We will now test the portal-produced VirtualService:

curl -X POST http://api.worldcities.example.com/ -d '{"cityQuery": "south bo"}' -H "SOAPAction:findCity" -H "content-type:application/json" -i

You should see that the new service works as expected:

HTTP/1.1 200 OK
content-type: application/json
date: Tue, 12 Oct 2021 21:32:31 GMT
x-envoy-upstream-service-time: 18
content-length: 624
server: envoy

  { "matches" :
    [
      { "city" : "south boston",
        "country" : "United States",
        "subCountry" : "Massachusetts",
        "geoNameId" : "4951305" },

      { "city" : "south peabody",
        "country" : "United States",
        "subCountry" : "Massachusetts",
        "geoNameId" : "4951473" },

      { "city" : "south bradenton",
        "country" : "United States",
        "subCountry" : "Florida",
        "geoNameId" : "4173392" },

      { "city" : "south burlington",
        "country" : "United States",
        "subCountry" : "Vermont",
        "geoNameId" : "5241248" } ] }

Removing a final SOAP interface vestige

You may have noticed that one final vestige of the original SOAP interface remains. Specifically, there is a required SOAPAction header that you can see in the earlier curl commands.

What we’ll do in this section is add an early-stage transformation to inject that header rather than require it to be specified in the external request. So there will be two sets of transformations applied to each request:

  1. Inject the SOAPAction header; and
  2. Apply the XSLT header to translate the request and response payloads from JSON to XML and vice versa.

In addition to adding this transformation, we will also apply a change to our OpenAPI specification and its associated APIDoc to eliminate the need for this header in the application interface. The OpenAPI change simply eliminates the SOAPAction parameter from the interface specification. You can see the eliminated stanza here. We’ll also modify the APIDoc for our interface to point to the simplified JSON OpenAPI specification.

The extra transformation to inject the SOAPAction header automatically is available here. We will modify the APIProduct to add that stanza.

stagedTransformations:
  early:
    requestTransforms:
      - requestTransformation:
          transformationTemplate:
            headers:
              SOAPAction:
                text: 'findCity'

We’ll use kubectl to apply both of these changes.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/07-apidoc-prod-no-soapaction.yaml

You should see this response.

apidoc.portal.gloo.solo.io/world-cities-schema configured
apiproduct.portal.gloo.solo.io/world-cities-product configured

Finally, issue this modified curl command without the SOAPAction header and observe that the response is identical to what you saw earlier.

curl -X POST http://api.worldcities.example.com/ -d '{"cityQuery": "south bo"}' -H "content-type:application/json" -i

At this point, we have a working portal API that shows a REST/JSON interface to the outside world based on a proper OpenAPI specification, while maintaining the upstream service in its original SOAP form.

Producing a portal interface

While we now have an API generated by Gloo Portal, we do not yet have a web interface where developers outside the original team can more easily consume the published interface. That’s what we will tackle in this section.

The core of the interface specification is the Portal custom resource. It contains basic textual titles and descriptions, plus an optional set of images that are vital if visual branding is important to your project, and also a list of Environment CRs available from this Portal. You can optionally specify links to user documentation that will be displayed with the portal, along with more sophisticated CSS customizations for users where more precise control over branding is required.

We will specify a basic Portal interface for our world-cities application.

apiVersion: portal.gloo.solo.io/v1beta1
kind: Portal
metadata:
  name: world-cities-portal
  namespace: default
spec:
  displayName: World Cities Portal
  description: The Gloo Portal for the World Cities API
  banner:
    fetchUrl: https://i.imgur.com/7cQk0ac.png
  favicon:
    fetchUrl: https://i.imgur.com/7cQk0ac.png
  primaryLogo:
    fetchUrl: https://i.imgur.com/7cQk0ac.png
  customStyling: {}
  staticPages: []

  domains:
  - worldcities.example.com

  # This will include all API products of the environment in the portal
  publishedEnvironments:
  - name: dev-world-cities
    namespace: default

Let’s use kubectl to apply this Portal CR:

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/08-portal-world-cities.yaml

Expect to see this response:

portal.portal.gloo.solo.io/world-cities-portal created

We’ll also install a bare-bones User and Group to our environment so that we can access the portal. In your real environments, you’ll likely want to delegate authentication decisions like this to a proper IdP. That’s beyond the scope of this post, but you can learn more about it here.

This kubectl command will install a single User with username dev1 and password Pa$$w0rd. It will add the dev1 user to a Group called developers and will assign our Portal to the group. You can see the details of this configuration here.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/09-group-user-passwd.yaml

You should see that the Group, User, and a Secret to hold the user’s password are all created.

group.portal.gloo.solo.io/developers created
user.portal.gloo.solo.io/dev1 created
secret/dev1-password created

Fixing CORS errors with Gloo Portal and Gloo Edge

Now it should just be a simple matter of navigating to the portal’s web address and logging in. Then we should be able to exercise our RESTified SOAP interface. Right?

Try it out by navigating a web browser to http://worldcities.example.com/ and logging in using our username/password of dev1/Pa$$w0rd. Select the “World Cities Product” API and expand the lone POST endpoint.

Now try to “Execute” that endpoint.

You can see that invoking that endpoint failed, and that a few possible reasons are listed. We used Chrome developer tools to help isolate the cause. They show us in the snapshot below that there is indeed a CORS (Cross-Origin Resource Sharing) error when the web browser tries to invoke the endpoint that we curled earlier at api.worldcities.example.com.

The issue is that the injected SOAPAction header is something that is specific to our application. It isn’t one of the headers that the portal configures by default to allow it to pass from the web interface to the API. That’s why we see the CORS error.

No worries, though. Gloo Portal and Gloo Edge allow us to specify additional headers that we will allow to flow between the web interface and the underlying API. We will make corresponding changes to both our APIProduct and Environment to add SOAPAction to the list of allowed headers. This is how the change appears in the Environment custom resource:

  gatewayConfig:
    options:
      cors:
        allowCredentials: true
        allowHeaders:
        - api-key
        - Authorization
        - SOAPAction
        allowOrigin:
        - http://worldcities.example.com
        - https://worldcities.example.com

Let’s use kubectl to apply the CORS changes to both the APIProduct and Environment.

kubectl apply -f https://raw.githubusercontent.com/solo-io/solo-blog/main/soap-portal/10-apiprod-envt-cors.yaml

You should see a response like this:

apiproduct.portal.gloo.solo.io/world-cities-product configured
environment.portal.gloo.solo.io/dev-world-cities configured

Now if you establish a new session with the portal, you should see that our new interface to our SOAP service works exactly as expected.

If you have made it this far with us, then congratulations! You have successfully taken a SOAP service endpoint under management, converted it to a REST interface, published it to outside developer groups, and then consumed that interface using the generated Gloo Portal web interface. And you’ve done all that with YAML and XSLT configuration that lives completely outside the managed application. Impressive work!

Beyond SOAP applications

In this blog post, you’ve seen that you can keep running your SOAP applications while modernizing your connectivity, giving you a more robust operating environment so that you can upgrade or replace them when you are ready. In this case, we walked step-by-step through the process of building out a World Cities portal that transforms a SOAP/XML service into a proper REST interface modeled with the OpenAPI standard. And we did all of that with external configuration that did not require us to touch the underlying SOAP service. All of the code used in this post is available on GitHub.

We are just scratching the surface of features we could deploy to this portal. For example, in a perfect world we might want to apply rate limits to our interface and require its users to generate API keys so that we can better track and control their usage. Creating a Usage Plan is an easy way to add these capabilities. We might want to take other types of services under management, like those with a gRPC interface. We might want to explore Gloo Portal’s services to help with monetizing our APIs. We might want to explore managing our Gloo Portal artifacts with an administrative web interface in addition to the YAML-based configuration we used in this exercise. Finally, we might want to integrate with a proper IdP that supports the OIDC standard. All of these capabilities and more are available with Gloo Portal.

For more information, check out the following resources: