Add DR provisioning client initContainer 54/135254/4 2.1.14
authorJack Lucas <jflos@sonoris.net>
Thu, 29 Jun 2023 18:04:59 +0000 (14:04 -0400)
committerJack Lucas <jflos@sonoris.net>
Wed, 12 Jul 2023 15:30:16 +0000 (11:30 -0400)
Add initContainer code to provision DR feeds
and subscriptions, replacing dbc-client.

Issue-ID: DMAAP-1893
Signed-off-by: Jack Lucas <jflos@sonoris.net>
Change-Id: I43d8eaf199ff8327fbcc01f756f6506f3768cb95

datarouter-prov-client/README.md [new file with mode: 0644]
datarouter-prov-client/misc/drprov-client.sh [new file with mode: 0755]
datarouter-prov-client/pom.xml [new file with mode: 0644]
datarouter-prov-client/src/main/resources/Dockerfile [new file with mode: 0644]
pom.xml

diff --git a/datarouter-prov-client/README.md b/datarouter-prov-client/README.md
new file mode 100644 (file)
index 0000000..b7e35b2
--- /dev/null
@@ -0,0 +1,73 @@
+# Data Router provisioning client
+
+## Overview
+The Data Router (DR) provisioning client runs as a Kubernetes initContainer for ONAP DCAE applications that use DR feeds to
+transfer data between applications.   The logic for the client is contained in a script that makes requests to the DR provisioning
+node using the DR provisioning API.  (See the [API documentation](https://docs.onap.org/projects/onap-dmaap-datarouter/en/london/apis/data-router-api.html#dmaap-data-router-api) for details.)
+
+The DR provisioning client (drprov-client) replaces the DMaaP Bus Controller client (dbc-client).  The dbc-client used the DMaaP Bus Controller to
+provision data router feeds and subscribers and DMaaP Message Router topics and clients.  The Message Router provisioning functionality
+is no longer needed, and Bus Controller will be deprecated and removed from the ONAP tree.
+
+The provisioning logic is in the [drprov-client.sh script](./misc/drprov-client.sh).  This script is set as the entrypoint for the initContainer.
+
+The drprov-client performs two high-level tasks:
+
+- Makes requests to the DR provisioning API to create feeds and subscriptions and captures the API's responses.
+- Uses the API's response to update a component's configuration file by replacing placeholders in the file (in the form of environment variable names) with values from the API responses.
+
+The drprov-client script queries the DR provisioning API to determine if a feed already exists (based on the feed name and feed version) and does not attempt to create the feed again.  Instead, it retrieves the feed information for the existing feed and supplies that information to a component.
+
+Similarly, the drprov-client script queries the DR provisioning API to determine if a subscription already exists (based on the username, password, and delivery URL for the subscription).  If one exists, the script does not create a new subscription.
+
+## Usage
+DR has been used only by components in the DCAE project.
+  The [DCAE common
+Helm templates](https://git.onap.org/oom/tree/kubernetes/dcaegen2-services/common/dcaegen2-services-common/templates) create the files need by the drprov-client to make API requests to the DR provisioning server.  They also insert the drprov-client initContainer into the Kubernetes deployment spec for an application that uses DR feeds.  The DCAE common templates rely on the [common DMaaP provisioning template](https://git.onap.org/oom/tree/kubernetes/common/common/templates/_dmaapProvisioning.tpl).
+
+ The developer of a DCAE component that uses DR simply adds a section to the component's `values.yaml` file.  For details on how feeds and subscriptions are defined in a component's `values.yaml` file, see the [inline documentation for the common DMaaP provisioning template](https://git.onap.org/oom/tree/kubernetes/common/common/templates/_dmaapProvisioning.tpl).
+
+## Changes from dbc-client
+The DMaaP bus controller provided a unified API that supported provisioning for both DR and the DMaaP Message Router.   The bus controller was
+a Java program with its own database, and it shielded its clients from some of the details of the DR provisioning interface.   The drprov-client interacts
+directly with the DR provisioning API, and there are some changes in how it behaves and how components that use DR are configured via their values.yaml files.
+
+### What happened to publishers?
+DR does not have a concept of a publisher that's distinct from a feed.  Every feed has at least one "endpoint", a username/password pair that an
+application will supply via HTTP Basic Authentication when publishing a file.  The bus controller always created an endpoint (with random username
+and password) when a client requested creation of a feed.  The bus controller then allowed clients to request creation of a "publisher".  The bus
+controller implemented this by using the DR API's updating capability (via an HTTP PUT request) to add an endpoint to the feed.   It would have been
+possible to replicate this functionality in a script, but it would have added complexity.  The one current use case employing DR feeds does not require this capability.   Note that it's still possible
+to have multiple applications (or multiple instances of the same application) publishing to a DR feed.  They just have to use the same username and
+password that was provided when the feed was first created.
+
+This change implies two changes for the values.yaml files of DR clients:
+  - A client that publishes to DR creates a feed that includes an endpoint.  It no longer creates a separate publisher.
+  - A client that subscribes to a feed does not need to supply a definition for the feed.  The subscription definition
+   includes the feed name and feed version, and that's enough information to allow the drprov-client to associate the
+   subscription with the feed.
+
+A client that subscribes to a feed needs to include the publisher in its readiness check, so that the feed is set up before the subscriber provisioning runs.  (This is already the case for the one existing DR subscriber in ONAP.)
+
+### Location
+The bus controller also had a notion of "location" which has no corresponding notion in DR, so references to "dcaeLocation" in the provisioning data are no longer needed.
+
+### Owner
+The bus controller interface included a field called "owner" for a feed.  This may have had some function within the bus controller.  It had a minor effect on the bus controller's invocation of the DR provisioning API (setting the `X-DMAAP-DR-ON-BEHALF-OF` header in the API requests associated with the feed).  It adds no real value.  Maintaining it would add complexity to the drprov-client script and would complicate debugging DR provisioning issues.
+
+### Classification
+The bus controller interface included a field called "asprClassification" for a feed.  The field is required by DR and indicates the sensitivity of the information transmitted in a feed.  This has not been used in ONAP, but it is a required field. "aspr" is an internal acronym at the company where DR was originally developed. The field has been renamed "classification".  "unclassified" is a reasonable value to use in ONAP.
+
+### One initContainer instead of two
+Using the dbc-client involved running two initContainers.  The first made API requests to the bus controller API and stored the responses in a volume on the pod.  The second used the stored data to update the component's configuration using
+information returned by the API calls.  The drprov-client runs in a single initContainer that makes the API calls and updates the component's configuration.
+
+## Limitations
+
+The drprov-client targets a limited range of uses for DR within ONAP.  It is not a general purpose provisioning solution for DR.  The following is a summary of some of the important limitations:
+
+  - A pod running a subscriber to a feed must wait for the feed's publisher to become ready, to ensure that the feed has been created.
+  - While multiple applications (or multiple instances of the same application) can publish to the same feed, they must use the same username and password.
+  - Feeds and subscriptions are not known to Kubernetes or Helm, so that deleting a publisher or a subscriber via Kubernetes or Helm does not delete the corresponding feeds or subscriptions in DR.  The drprov-client handles the case of a pod being restarted by reusing an existing feed or subscription.
+
+DR was originally designed to move large files to multiple destinations over a wide geographic area.  While it clearly works for large file transfers within a Kubernetes cluster, it isn't optimal.  Currently (as of the Montreal release of ONAP), DR is used by two components ([the DCAE datafile collector](https://git.onap.org/dcaegen2/collectors/datafile/tree/), which publishes performance management data to a DR feed), and [the DCAE pm-mapper](https://git.onap.org/dcaegen2/services/pm-mapper/tree/), which subscribes to the same feed.  If new use cases requiring large file transfer emerge in the future, it would be wise to look at transfer mechanisms designed specifically for the Kubernetes environment.
diff --git a/datarouter-prov-client/misc/drprov-client.sh b/datarouter-prov-client/misc/drprov-client.sh
new file mode 100755 (executable)
index 0000000..7101795
--- /dev/null
@@ -0,0 +1,286 @@
+#!/bin/sh
+
+PROVURL=${PROVURL:-"http://dmaap-dr-prov:8080"}
+DRCONFIGDIR=${DRCONFIGDIR:-"/opt/app/config"}
+ONBEHALFHDR="X-DMAAP-DR-ON-BEHALF-OF: drprovclient"
+FEEDTYPE="Content-Type: application/vnd.dmaap-dr.feed"
+SUBTYPE="Content-Type: application/vnd.dmaap-dr.subscription"
+APPCONFIGINPUT=${APPCONFIGINPUT:-"/config-input"}
+APPCONFIG=${APPCONFIG:-"/config"}
+
+function logit() {
+    # Direct log entries to stderr because to
+    # allow logging inside functions that use
+    # stdout to return values
+    echo $(date -u -Ins)\|"$@" >&2
+}
+
+function getFeedByNameVer() {
+# Get feed info using name and version
+#   $1 -- Feed name (arbitrary string without embedded '"')
+#   $2 -- Feed version (arbitrary string without embedded '"')
+#   Returns feed data and exits with 0 if
+#   feed is found.
+#   Returns empty string and exits with 1 in
+#   any other case.
+
+    # Construct urlencoded query
+    local NAME="$(printf %s "$1" | tr -d '"' | jq -R -r @uri)"
+    local VER="$(printf %s "$2" | tr -d '"' | jq -R -r @uri)"
+    local QUERYURL="${PROVURL}"/?"name=${NAME}"\&version="${VER}"
+    local FEEDDATA
+
+    # Make the query
+    # Not checking exact cause for error,
+    # just looking for success or not.
+    local RV=1
+    if FEEDDATA=$(curl --fail -s -H "${ONBEHALFHDR}" "${QUERYURL}")
+    then
+           echo ${FEEDDATA}
+        RV=0
+    fi
+
+    return ${RV}
+}
+
+function subscriptionExists() {
+#  See if there a subscription to the feed
+#  that has the specified username, password,
+#  and delivery URL.
+#      $1 -- subscribe URL for the feed
+#      $2 -- username for the subscription
+#      $3 -- password for the subscription
+#      $4 -- delivery URL for the subscription
+# Sets a return value of 0 if a matching
+# subscription is found and echoes the
+# corresponding subscription URL.
+# Others sets a return value of 1 and
+# echoes an empty string.
+
+local RV=1
+local SUBRESP
+local SUBDATA
+local SUBLIST
+
+# Query the feed's subscribe URL to get a
+# list of the URLs for existing subscriptions
+if SUBRESP=$(curl -s --fail -H "${ONBEHALFHDR}" "$1")
+then
+    # Loop through the list of existing subscriptions
+    while read -r SUBURL   # read from $SUBRESP (see redirect on "done")
+    do
+        # Retrieve subscription data from the subscription's URL
+        if SUBDATA=$(curl -s --fail -H "${ONBEHALFHDR}" "${SUBURL}")
+        then
+            local SUBUSER=$(echo ${SUBDATA} | jq -r .delivery.user)
+            local SUBPASS=$(echo ${SUBDATA} | jq -r .delivery.password)
+            local SUBDELURL=$(echo ${SUBDATA} | jq -r .delivery.url)
+            if [ "$2" = "${SUBUSER}" -a "$3" = "${SUBPASS}" -a "$4" = "${SUBDELURL}" ]
+            then
+                RV=0  #TRUE
+                break
+            fi
+        else
+            # This will happen, for instance, if the name in
+            # in the "X-DMAAP-DR-ON-BEHALF-OF" header doesn't
+            # match the owner of the feed.  (Not likely in
+            # the ONAP use case, but possible.)  Could also be
+            # the result of connectivity issues, bad URL,...
+            logit "WARNING: Could not retrieve ${SUBURL}"
+        fi
+    done < <(echo ${SUBRESP} | jq -r .[])
+ else
+    logit "ERROR: failed to fetch subscription list from $1"
+fi
+
+echo ${SUBURL}
+return ${RV}
+}
+
+function createFeedFromFile() {
+# Create a feed using information from a JSON file
+# Note that creating a feed also creates the publisher
+#   $1 -- Path to JSON file
+#   Returns feed data from the DR provisioning node
+#   and exits with 0 if the feed is created.
+#   Returns empty string and exits with 1 in
+#   any other case.
+
+    local FEEDDATA
+    local RV=1
+
+    if test -f "$1"
+    then
+        # Substitute any environment variables in the subscription file
+        local FEEDREQUEST=$(envsubst < "$1")
+        if FEEDDATA=$(curl --fail -s --data-ascii "${FEEDREQUEST}" -H "${ONBEHALFHDR}" -H "$FEEDTYPE" ${PROVURL}/)
+        then
+            echo ${FEEDDATA}
+            RV=0
+        fi
+    fi
+
+return ${RV}
+}
+
+function createSubscriptionFromFile() {
+# Create a subscription to a feed from a JSON file
+# if a subscription with the same username, password
+# and delivery URL doesn't already exist.
+# We don't want multiple subscriptions if for some
+# reason a subscriber's pod is redeployed.
+# $1 -- JSON file defining the subscription
+#
+    local SUBURL
+    local SUBDATA
+    local EXISTINGSUB
+
+    local RV=1
+
+    if test -f "$1"
+    then
+        # Extract feed name and version from the JSON file
+        local FEEDNAME=$(jq '.feed.name' "$1")
+        local FEEDVER=$(jq '.feed.version' "$1")
+
+        # Extract subscription parameters from the JSON file
+        # (needed for checking if there's an existing subscription)
+        local SUBUSER=$(jq -r '.delivery.user' "$1")
+        local SUBPASS=$(jq -r '.delivery.password' "$1")
+        local SUBDELURL=$(jq -r '.delivery.url' "$1")
+
+        # Look up the feed and get the subscribe URL
+        if SUBURL=$(getFeedByNameVer "${FEEDNAME}" "${FEEDVER}" | jq -r .links.subscribe)
+        then
+            # Check whether a matching subscription already exists
+            if EXISTINGSUB=$(subscriptionExists ${SUBURL} ${SUBUSER} ${SUBPASS} ${SUBDELURL})
+            then
+                logit "Using existing subscription: ${EXISTINGSUB}.  No new subscription created."
+                RV=0
+            else
+                # Substitute any environment variables in the subscription file
+                local SUBREQUEST=$(envsubst < "$1")
+                # Create the subscription
+                if SUBDATA=$(curl --fail -s --data-ascii "${SUBREQUEST}" -H "${ONBEHALFHDR}" -H "${SUBTYPE}" ${SUBURL})
+                then
+                    logit "Created new subscription: $(echo ${SUBDATA} | jq -r '.links.self')"
+                    RV=0
+                fi
+            fi
+        fi
+    fi
+    return ${RV}
+}
+
+function createOrGetFeed() {
+# Retrieve feed data from the DR provisioning node for
+# the feed described in a JSON file.  If the feed
+# does not exist, create the file.
+#  $1 -- Path to JSON file
+#  Returns feed data from the DR provisioning node
+#  if the feed exists or if it has been successfully
+#  created, and exits with 0.
+#  Returns empty string and exits with 1 in
+#  any other case.
+
+    local FEEDDATA
+
+    local RV=1
+
+    if test -f "$1"
+    then
+        # Extract feed name and version from file
+        local NAME=$(cat "$1" | jq .name)
+        local VER=$(cat "$1" | jq .version)
+
+        # Check whether feed already exists
+        # (DR does not allow two feeds with same name and version)
+        if FEEDDATA=$(getFeedByNameVer "${NAME}" "${VER}")
+        then
+            logit "Using existing feed: $(echo ${FEEDDATA} | jq -r '.links.self'). No new feed created."
+            RV=0
+        else
+            # Create feed
+            if FEEDDATA=$(createFeedFromFile "$1")
+            then
+                logit "Created new feed:  $(echo ${FEEDDATA} | jq -r '.links.self')" >&2
+                RV=0
+            fi
+        fi
+    fi
+
+    echo ${FEEDDATA}
+    return $RV
+}
+
+function provisionFeeds() {
+# Create a feed for each JSON file in the
+# directory specified in $1, unless the
+# a feed with the same name and version
+# already exists, in which case use the
+# information for the existing feed.
+# $1 -- Path to directory containing JSON
+#       files defining DR feeds
+
+    local FEEDDATA
+    if test -d "$1"
+    then
+        for FEEDFILE in $(ls "$1"/*.json)
+        do
+            logit "Creating feed from ${FEEDFILE}"
+            if FEEDDATA=$(createOrGetFeed ${FEEDFILE})
+            then
+                # Set environment variables with result data
+                # Note that FEEDNUM is taken from the number that's embedded
+                # in the file defining the feed.
+                FEEDNUM=$(echo "${FEEDFILE}" | sed 's/^.*\/.*-\([0-9]\+\).json/\1/')
+                export DR_FEED_PUBURL_${FEEDNUM}="$(echo ${FEEDDATA} | jq '.links.publish')"
+                export DR_FEED_LOGURL_${FEEDNUM}="$(echo ${FEEDDATA} | jq '.links.log')"
+            fi
+        done
+    fi
+}
+
+function provisionSubscriptions() {
+# Create a subscription for each JSON file in the
+# directory specified in $1
+# $1 -- Path to directory containing JSON
+#       files definining DR subscriptions
+# Note that when provisioning a subscription to a feed,
+# the DR API doesn't return any additional information
+# that the subscriber needs.  Hence no information is
+# extracted from the DR API response, and no environment
+# variables are exported (unlike the provisionFeeds function.)
+
+    if test -d "$1"
+    then
+        for SUBFILE in $(ls "$1"/*.json)
+        do
+            logit "Creating subscription from ${SUBFILE}"
+            createSubscriptionFromFile ${SUBFILE}
+        done
+    fi
+}
+
+function updateConfigurations() {
+# Run envsubst against each file in $1 (the application
+# configuration input directory) to create a corresponding
+# file in $2 (the application configuration directory) with
+# environment variables replaced by the values set in the
+# provisioning steps.  The file(s) in $2 will be used by
+# the application to get (among other things) the information
+# needed to work with DR feeds.
+# $1 -- path to application configuration input directory
+# $2 -- path to application configuration directory
+    cd "$1"
+    for CONFFILE in $(ls -1)
+    do
+        logit "Substituting environment vars in ${CONFFILE}"
+        envsubst <${CONFFILE} > "$2"/${CONFFILE}
+    done
+}
+set -ue
+
+provisionFeeds ${DRCONFIGDIR}/feeds
+provisionSubscriptions ${DRCONFIGDIR}/dr_subs
+updateConfigurations ${APPCONFIGINPUT} ${APPCONFIG}
diff --git a/datarouter-prov-client/pom.xml b/datarouter-prov-client/pom.xml
new file mode 100644 (file)
index 0000000..4188fbf
--- /dev/null
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<!--
+  ============LICENSE_START==========================================
+   Copyright (c) J. F. Lucas. All rights reserved.
+  ===================================================================
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  ============LICENSE_END============================================
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>datarouter-prov-client</artifactId>
+  <name>datarouter-prov-client</name>
+  <parent>
+    <groupId>org.onap.dmaap.datarouter</groupId>
+    <artifactId>parent</artifactId>
+    <version>${revision}</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+  <description>Init container for provisioning Data Router feeds, publishers, and subscribers.</description>
+  <properties>
+    <docker.location>${basedir}/target/${project.artifactId}</docker.location>
+    <dmaapdrprovclient.image.name>${docker.image.root}${project.artifactId}</dmaapdrprovclient.image.name>
+    <sitePath>/content/sites/site/org/onap/dmaap/drprov-client/${project.version}</sitePath>
+  </properties>
+  <build>
+    <finalName>datarouter-prov-client</finalName>
+    <!-- Copy files to docker-stage to be included in image -->
+    <resources>
+      <resource>
+        <targetPath>${basedir}/target/docker-stage</targetPath>
+        <directory>${basedir}/src/main/resources</directory>
+        <includes>
+          <include>Dockerfile</include>
+        </includes>
+      </resource>
+      <resource>
+        <targetPath>${basedir}/target/docker-stage/opt/app/drprov-client/bin</targetPath>
+        <directory>${basedir}/misc</directory>
+        <includes>
+          <include>drprov-client.sh</include>
+        </includes>
+      </resource>
+    </resources>
+  </build>
+  <profiles>
+    <profile>
+      <id>docker</id>
+      <properties>
+        <skipDockerBuild>${skip.docker.build}</skipDockerBuild>
+        <skipTests>true</skipTests>
+      </properties>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.gmaven</groupId>
+            <artifactId>gmaven-plugin</artifactId>
+          </plugin>
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <extensions>true</extensions>
+            <configuration>
+              <images>
+                <image>
+                  <name>${dmaapdrprovclient.image.name}</name>
+                  <build>
+                    <cleanup>try</cleanup>
+                    <noCache>true</noCache>
+                    <optimise>true</optimise>
+                    <contextDir>${basedir}/target/docker-stage</contextDir>
+                    <dockerFile>Dockerfile</dockerFile>
+                    <tags>
+                      <tag>${dockertag1}</tag>
+                      <tag>${dockertag2}</tag>
+                    </tags>
+                  </build>
+                </image>
+              </images>
+            </configuration>
+            <executions>
+              <execution>
+                <id>generate-images</id>
+                <phase>install</phase>
+                <goals>
+                  <goal>build</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>push-images</id>
+                <phase>deploy</phase>
+                <goals>
+                  <goal>push</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/datarouter-prov-client/src/main/resources/Dockerfile b/datarouter-prov-client/src/main/resources/Dockerfile
new file mode 100644 (file)
index 0000000..601d88d
--- /dev/null
@@ -0,0 +1,36 @@
+#  ============LICENSE_START====================================================
+#  Copyright (C) 2023 J. F. Lucas.  All rights reserved.
+#  ===========================================================================
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#  ============LICENSE_END====================================================
+#
+FROM alpine:3.18.0
+
+MAINTAINER DMAAP Team
+
+COPY /opt /opt
+
+WORKDIR /opt/app/drprov-client
+
+# Install curl, jq, and gettext
+RUN apk add --no-cache curl jq gettext
+
+RUN chmod +x /opt/app/drprov-client/bin/*
+
+RUN addgroup -S -g 1001 onap \
+    && adduser -S -u 1000 onap -G onap \
+    && chown -R onap:onap /opt/
+
+USER onap
+
+ENTRYPOINT ["sh", "/opt/app/drprov-client/bin/drprov-client.sh" ]
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index d0cc6b8..c461c9b 100755 (executable)
--- a/pom.xml
+++ b/pom.xml
@@ -4,6 +4,7 @@
   * ===========================================================================
   * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
   * Modifications Copyright (C) 2018 Nokia. All rights reserved.
+  * Copyright (d) 2023 J. F. Lucas.  All rights reserved.
   * ===========================================================================
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   * ============LICENSE_END====================================================
-  *
-  * ECOMP is a trademark and service mark of AT&T Intellectual Property.
-  *
--->
+  -->
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.dmaap.datarouter</groupId>
@@ -86,6 +84,7 @@
     </properties>
     <modules>
         <module>datarouter-prov</module>
+        <module>datarouter-prov-client</module>
         <module>datarouter-node</module>
         <module>datarouter-subscriber</module>
         <module>datarouter-docker-compose</module>
                     <groupId>io.fabric8</groupId>
                     <artifactId>docker-maven-plugin</artifactId>
                     <version>${io.fabric8.version}</version>
+                    <extensions>true</extensions>
                     <configuration>
                         <skipBuild>${docker.skip.build}</skipBuild>
                         <verbose>${docker.verbose}</verbose>