NONRTRIC-944: ServiceManager - Add kongclearup to Docker image

Issue-ID: NONRTRIC-944
Change-Id: I9c959d6930070acde22d59d3df3248ba125f8f68
Signed-off-by: DenisGNoonan <denis.noonan@est.tech>
diff --git a/.gitignore b/.gitignore
index 0dc4f08..8d0a84f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,8 +7,9 @@
 .pydevproject
 go.work*
 servicemanager.tar.gz
-servicemanager/servicemanager
 servicemanager/specs
+servicemanager/servicemanager
+servicemanager/internal/kongclearup
 coverage.txt
 # Ignore all .env files
 .env*
@@ -16,4 +17,3 @@
 !.env.example
 # Ignore all files in folders named "mocks"
 **/mocks/*
-servicemanager/specs/
diff --git a/servicemanager/Dockerfile b/servicemanager/Dockerfile
index ddb23ed..85ef603 100644
--- a/servicemanager/Dockerfile
+++ b/servicemanager/Dockerfile
@@ -29,6 +29,9 @@
 COPY . .
 RUN go build -o /servicemanager
 
+WORKDIR "/app/internal"
+RUN go build kongclearup.go
+
 ##
 ## Deploy
 ##
@@ -41,5 +44,6 @@
 
 ## Copy from "build" stage
 COPY --from=build /servicemanager .
+COPY --from=build /app/internal/kongclearup .
 
 ENTRYPOINT ["/app/servicemanager"]
diff --git a/servicemanager/README.md b/servicemanager/README.md
index a43d9de..4db3c8d 100644
--- a/servicemanager/README.md
+++ b/servicemanager/README.md
@@ -102,6 +102,21 @@
 docker image build . -t servicemanager
 ```
 
+## Kongclearup
+
+Please note that a special executable has been provided for deleting Kong routes and services that have been created ServiceManager in Kong. This executable is called `kongclearup` and is found in the working directory of the ServiceManger Docker image, at `/app/kongclearup`. When we create a Kong route or service, we add Kong tags with information as follows.
+  * apfId
+  * aefId
+  * apiId
+  * apiVersion
+  * resourceName
+
+When we delete Kong routes and services using `kongclearup`, we check for the existance of these tags, specifically, apfId, apiId and aefId. Only if these tags exist and have values do we proceed to delete the Kong service or route.
+
+The executable `kongclearup` uses the volume-mounted .env file to load the configuration giving the location of Kong.
+
+Please refer to `sme/servicemanager/internal/kongclearup.go`.
+
 ## Stand-alone Deployment on Kubernetes
 
 For a stand-alone deployment, please see the `deploy` folder for configurations to deploy to R1-SME-Manager to Kubernetes. We need the following steps.
@@ -119,7 +134,7 @@
       - src/
       - manifests/
 
-We store the Kubernetes manifests files in the manifests in the subfolder. We store the shell scripts in the src folder. 
+We store the Kubernetes manifests files in the manifests in the subfolder. We store the shell scripts in the src folder.
 
 In `deploy-to-k8s.sh`, we copy .env.example and use sed to replace the template values with values for testing/production. You will need to update this part of the script with your own values. There is an example sed replacement in function `substitute_manifest()` in `deploy-to-k8s.sh`. Here, you can substitute your own Docker images for Capifcore and Service Manager for local development.
 
diff --git a/servicemanager/internal/kongclear/kongclear.go b/servicemanager/internal/kongclear/kongclear.go
index 068cd7d..6ec3f08 100644
--- a/servicemanager/internal/kongclear/kongclear.go
+++ b/servicemanager/internal/kongclear/kongclear.go
@@ -1,194 +1,237 @@
-// -

-//   ========================LICENSE_START=================================

-//   O-RAN-SC

-//   %%

-//   Copyright (C) 2024: OpenInfra Foundation Europe

-//   %%

-//   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===================================

-//

-

-package kongclear

-

-import (

-	"encoding/json"

-	"fmt"

-	"net/http"

-

-	log "github.com/sirupsen/logrus"

-	resty "github.com/go-resty/resty/v2"

-)

-

-// Service represents the structure for Kong service creation

-type KongService struct {

-	ID   string `json:"id"`

-	Name string `json:"name"`

-	URL  string `json:"url"`

-}

-

-type KongServiceResponse struct {

-	ID string `json:"id"`

-}

-

-type ServiceResponse struct {

-	Data []KongService `json:"data"`

-}

-

-type KongRoute struct {

-	ID        string   `json:"id"`

-	Name      string   `json:"name"`

-	Paths     []string `json:"paths"`

-	Service   Service  `json:"service"`

-	StripPath bool     `json:"strip_path"`

-}

-

-type RouteResponse struct {

-	Data []KongRoute `json:"data"`

-}

-

-type Service struct {

-	ID string `json:"id"`

-}

-

-func KongClear(myEnv map[string]string, myPorts map[string]int) error {

-	log.Info("attempting to delete all Kong routes and services")

-

-	kongAdminApiUrl := fmt.Sprintf("%s://%s:%d/", myEnv["KONG_PROTOCOL"], myEnv["KONG_IPV4"], myPorts["KONG_CONTROL_PLANE_PORT"])

-

-	err := deleteAllRoutes(kongAdminApiUrl)

-	if err != nil {

-		log.Fatalf("error deleting routes %v", err)

-		return err

-	}

-

-	err = deleteAllServices(kongAdminApiUrl)

-	if err != nil {

-		log.Fatalf("error deleting services %v", err)

-		return err

-	}

-

-	log.Info("all Kong routes and services deleted successfully")

-	return err

-}

-

-

-func deleteAllRoutes(kongAdminApiUrl string) error {

-	routes, err := listRoutes(kongAdminApiUrl)

-	if err != nil {

-		return err

-	}

-

-	for _, route := range routes {

-		if err := deleteRoute(kongAdminApiUrl, route.ID); err != nil {

-			return err

-		}

-	}

-

-	return nil

-}

-

-func deleteAllServices(kongAdminApiUrl string) error {

-	services, err := listServices(kongAdminApiUrl)

-	if err != nil {

-		return err

-	}

-

-	for _, service := range services {

-		if err := deleteService(kongAdminApiUrl, service.ID); err != nil {

-			return err

-		}

-	}

-

-	return nil

-}

-

-func listServices(kongAdminApiUrl string) ([]KongService, error) {

-	client := resty.New()

-	resp, err := client.R().Get(kongAdminApiUrl + "services")

-

-	if err != nil {

-		return nil, err

-	}

-

-	if resp.StatusCode() != http.StatusOK {

-		err := fmt.Errorf("failed to list services, status code %d", resp.StatusCode())

-		return nil, err

-	}

-

-	var serviceResponse ServiceResponse

-	err = json.Unmarshal(resp.Body(), &serviceResponse)

-	if err != nil {

-		return nil, err

-	}

-

-	log.Infof("kong services %v", serviceResponse.Data)

-	return serviceResponse.Data, nil

-}

-

-func listRoutes(kongAdminApiUrl string) ([]KongRoute, error) {

-	client := resty.New()

-	resp, err := client.R().

-		Get(kongAdminApiUrl + "routes")

-

-	if err != nil {

-		return nil, err

-	}

-

-	if resp.StatusCode() != http.StatusOK {

-		err := fmt.Errorf("failed to list routes, status code %d", resp.StatusCode())

-		return nil, err

-	}

-

-	var routeResponse RouteResponse

-	err = json.Unmarshal(resp.Body(), &routeResponse)

-	if err != nil {

-		return nil, err

-	}

-

-	log.Infof("kong routes %v", routeResponse.Data)

-	return routeResponse.Data, nil

-}

-

-func deleteService(kongAdminApiUrl string, serviceID string) error {

-	log.Tracef("entering deleteService service ID %v", serviceID)

-	client := resty.New()

-	resp, err := client.R().Delete(kongAdminApiUrl + "services/" + serviceID)

-

-	if err != nil {

-		return err

-	}

-

-	if resp.StatusCode() != http.StatusNoContent {

-		err := fmt.Errorf("failed to delete service %s, status code %d", serviceID, resp.StatusCode())

-		return err

-	}

-

-	return nil

-}

-

-func deleteRoute(kongAdminApiUrl string, routeID string) error {

-	log.Infof("kong route id %v", routeID)

-	client := resty.New()

-	resp, err := client.R().Delete(kongAdminApiUrl + "routes/" + routeID)

-

-	if err != nil {

-		return err

-	}

-

-	if resp.StatusCode() != http.StatusNoContent {

-		err := fmt.Errorf("failed to delete route %s, status code %d", routeID, resp.StatusCode())

-		return err

-	}

-

-	return nil

-}
\ No newline at end of file
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2024: OpenInfra Foundation Europe
+//   %%
+//   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===================================
+//
+
+package kongclear
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+
+	resty "github.com/go-resty/resty/v2"
+	log "github.com/sirupsen/logrus"
+)
+
+// Service represents the structure for Kong service creation
+type KongService struct {
+	ID   string   `json:"id"`
+	Name string   `json:"name"`
+	URL  string   `json:"url"`
+	Tags []string `json:"tags"`
+}
+
+type KongServiceResponse struct {
+	ID string `json:"id"`
+}
+
+type ServiceResponse struct {
+	Data []KongService `json:"data"`
+}
+
+type KongRoute struct {
+	ID        string   `json:"id"`
+	Name      string   `json:"name"`
+	Paths     []string `json:"paths"`
+	Service   Service  `json:"service"`
+	StripPath bool     `json:"strip_path"`
+	Tags      []string `json:"tags"`
+}
+
+type RouteResponse struct {
+	Data []KongRoute `json:"data"`
+}
+
+type Service struct {
+	ID string `json:"id"`
+}
+
+func KongClear(myEnv map[string]string, myPorts map[string]int) error {
+	log.Info("delete only ServiceManager Kong routes and services")
+
+	kongAdminApiUrl := fmt.Sprintf("%s://%s:%d/", myEnv["KONG_PROTOCOL"], myEnv["KONG_IPV4"], myPorts["KONG_CONTROL_PLANE_PORT"])
+
+	err := deleteRoutes(kongAdminApiUrl)
+	if err != nil {
+		log.Fatalf("error deleting routes %v", err)
+		return err
+	}
+
+	err = deleteServices(kongAdminApiUrl)
+	if err != nil {
+		log.Fatalf("error deleting services %v", err)
+		return err
+	}
+
+	log.Info("finished deleting only ServiceManger Kong routes and services")
+	return err
+}
+
+func deleteRoutes(kongAdminApiUrl string) error {
+	routes, err := listRoutes(kongAdminApiUrl)
+	if err != nil {
+		return err
+	}
+
+	for _, route := range routes {
+		if areServiceManagerTags(route.Tags) {
+			if err := deleteRoute(kongAdminApiUrl, route.ID); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func deleteServices(kongAdminApiUrl string) error {
+	services, err := listServices(kongAdminApiUrl)
+	if err != nil {
+		return err
+	}
+
+	for _, service := range services {
+		if areServiceManagerTags(service.Tags) {
+			if err := deleteService(kongAdminApiUrl, service.ID); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func listRoutes(kongAdminApiUrl string) ([]KongRoute, error) {
+	client := resty.New()
+	resp, err := client.R().
+		Get(kongAdminApiUrl + "routes")
+
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode() != http.StatusOK {
+		err := fmt.Errorf("failed to list routes, status code %d", resp.StatusCode())
+		return nil, err
+	}
+
+	var routeResponse RouteResponse
+	err = json.Unmarshal(resp.Body(), &routeResponse)
+	if err != nil {
+		return nil, err
+	}
+
+	log.Infof("kong routes %v", routeResponse.Data)
+	return routeResponse.Data, nil
+}
+
+func listServices(kongAdminApiUrl string) ([]KongService, error) {
+	client := resty.New()
+	resp, err := client.R().Get(kongAdminApiUrl + "services")
+
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode() != http.StatusOK {
+		err := fmt.Errorf("failed to list services, status code %d", resp.StatusCode())
+		return nil, err
+	}
+
+	var serviceResponse ServiceResponse
+	err = json.Unmarshal(resp.Body(), &serviceResponse)
+	if err != nil {
+		return nil, err
+	}
+
+	log.Infof("kong services %v", serviceResponse.Data)
+	return serviceResponse.Data, nil
+}
+
+func areServiceManagerTags(tags []string) bool {
+	tagMap := make(map[string]string)
+
+	for _, tag := range tags {
+		log.Debugf("found tag %s", tag)
+		tagSlice := strings.Split(tag, ":")
+		log.Debugf("tag slice %v", tagSlice)
+		if (len(tagSlice) > 0) && (tagSlice[0] != "") {
+			if (len(tagSlice) > 1) {
+				tagMap[tagSlice[0]] = tagSlice[1]
+			} else {
+				tagMap[tagSlice[0]] = ""
+			}
+		}
+	}
+
+	if tagMap["apfId"] == "" {
+		log.Debug("did NOT find apfId")
+		return false
+	}
+	log.Debugf("found valid apfId %s", tagMap["apfId"])
+
+	if tagMap["aefId"] == "" {
+		log.Debug("did NOT find aefId")
+		return false
+	}
+	log.Debugf("found valid aefId %s", tagMap["aefId"])
+
+	if tagMap["apiId"] == "" {
+		log.Debug("did NOT find apiId")
+		return false
+	}
+	log.Debugf("found valid apiId %s", tagMap["apiId"])
+
+	return true
+}
+
+func deleteRoute(kongAdminApiUrl string, routeID string) error {
+	log.Infof("delete kong route id %s", routeID)
+	client := resty.New()
+	resp, err := client.R().Delete(kongAdminApiUrl + "routes/" + routeID)
+
+	if err != nil {
+		return err
+	}
+
+	if resp.StatusCode() != http.StatusNoContent {
+		err := fmt.Errorf("failed to delete route %s, status code %d", routeID, resp.StatusCode())
+		return err
+	}
+
+	return nil
+}
+
+func deleteService(kongAdminApiUrl string, serviceID string) error {
+	log.Infof("delete kong service id %s", serviceID)
+	client := resty.New()
+	resp, err := client.R().Delete(kongAdminApiUrl + "services/" + serviceID)
+
+	if err != nil {
+		return err
+	}
+
+	if resp.StatusCode() != http.StatusNoContent {
+		err := fmt.Errorf("failed to delete service %s, status code %d", serviceID, resp.StatusCode())
+		return err
+	}
+
+	return nil
+}
diff --git a/servicemanager/internal/publishservice/publishservice.go b/servicemanager/internal/publishservice/publishservice.go
index e678860..84d2bab 100644
--- a/servicemanager/internal/publishservice/publishservice.go
+++ b/servicemanager/internal/publishservice/publishservice.go
@@ -82,7 +82,7 @@
 
 	newServiceAPIDescription.PrepareNewService()
 
-	statusCode, err := newServiceAPIDescription.RegisterKong(ps.KongDomain, ps.KongProtocol, ps.KongIPv4, ps.KongDataPlanePort, ps.KongControlPlanePort)
+	statusCode, err := newServiceAPIDescription.RegisterKong(ps.KongDomain, ps.KongProtocol, ps.KongIPv4, ps.KongDataPlanePort, ps.KongControlPlanePort, apfId)
 	if (err != nil) || (statusCode != http.StatusCreated) {
 		// We can return with http.StatusForbidden if there is a http.StatusConflict detected by Kong
 		msg := err.Error()
diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go
index fe681f9..451f54b 100644
--- a/servicemanager/internal/publishserviceapi/typeupdate.go
+++ b/servicemanager/internal/publishserviceapi/typeupdate.go
@@ -38,7 +38,13 @@
 	sd.ApiId = &apiName
 }
 
-func (sd *ServiceAPIDescription) RegisterKong(kongDomain string, kongProtocol string, kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongControlPlanePort common29122.Port) (int, error) {
+func (sd *ServiceAPIDescription) RegisterKong(kongDomain string,
+		kongProtocol string,
+		kongIPv4 common29122.Ipv4Addr,
+		kongDataPlanePort common29122.Port,
+		kongControlPlanePort common29122.Port,
+		apfId string) (int, error) {
+
 	log.Trace("entering RegisterKong")
 	var (
 		statusCode int
@@ -46,7 +52,7 @@
 	)
 	kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
 
-	statusCode, err = sd.createKongRoutes(kongControlPlaneURL)
+	statusCode, err = sd.createKongRoutes(kongControlPlaneURL, apfId)
 	if (err != nil) || (statusCode != http.StatusCreated) {
 		return statusCode, err
 	}
@@ -57,7 +63,7 @@
 	return statusCode, nil
 }
 
-func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string) (int, error) {
+func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string, apfId string) (int, error) {
 	log.Trace("entering createKongRoutes")
 	var (
 		statusCode int
@@ -72,7 +78,7 @@
 		for _, version := range profile.Versions {
 			log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion)
 			for _, resource := range *version.Resources {
-				statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, resource, profile.AefId, version.ApiVersion)
+				statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, resource, apfId, profile.AefId, version.ApiVersion)
 				if (err != nil) || (statusCode != http.StatusCreated) {
 					return statusCode, err
 				}
@@ -82,32 +88,36 @@
 	return statusCode, nil
 }
 
-func (sd *ServiceAPIDescription) createKongRoute(kongControlPlaneURL string, client *resty.Client, resource Resource, aefId string, apiVersion string) (int, error) {
+func (sd *ServiceAPIDescription) createKongRoute(
+		kongControlPlaneURL string,
+		client *resty.Client,
+		resource Resource,
+		apfId string,
+		aefId string,
+		apiVersion string ) (int, error) {
 	log.Trace("entering createKongRoute")
-	uri := resource.Uri
 
-	if apiVersion != "" {
-		if apiVersion[0] != '/' {
-			apiVersion = "/" + apiVersion
-		}
-		if apiVersion[len(apiVersion)-1] != '/' && resource.Uri[0] != '/' {
-			apiVersion = apiVersion + "/"
-		}
-		uri = apiVersion + resource.Uri
-	}
+	resourceName := resource.ResourceName
+	apiId := *sd.ApiId
 
-	log.Debugf("createKongRoute, uri %s", uri)
+	tags := buildTags(apfId, aefId, apiId, apiVersion, resourceName)
+	log.Debugf("createKongRoute, tags %s", tags)
 
-	serviceName := *sd.ApiId + "_" + resource.ResourceName
+	serviceName := apiId + "_" + resourceName
+	routeName := serviceName
+
 	log.Debugf("createKongRoute, serviceName %s", serviceName)
+	log.Debugf("createKongRoute, routeName %s", routeName)
 	log.Debugf("createKongRoute, aefId %s", aefId)
 
-	statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, aefId)
+	uri := buildUriWithVersion(apiVersion, resource.Uri)
+	log.Debugf("createKongRoute, uri %s", uri)
+
+	statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, tags)
 	if (err != nil) || (statusCode != http.StatusCreated) {
 		return statusCode, err
 	}
 
-	routeName := serviceName
 	kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes"
 
 	// Define the route information for Kong
@@ -115,7 +125,7 @@
 		"name":       routeName,
 		"paths":      []string{uri},
 		"methods":    resource.Operations,
-		"tags":       []string{aefId},
+		"tags":       tags,
 		"strip_path": true,
 	}
 
@@ -144,7 +154,39 @@
 	return resp.StatusCode(), nil
 }
 
-func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, aefId string) (int, error) {
+func buildUriWithVersion(apiVersion string, uri string) string {
+	if apiVersion != "" {
+		if apiVersion[0] != '/' {
+			apiVersion = "/" + apiVersion
+		}
+		if apiVersion[len(apiVersion)-1] != '/' && uri[0] != '/' {
+			apiVersion = apiVersion + "/"
+		}
+		uri = apiVersion + uri
+	}
+	return uri
+}
+
+func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string  {
+	tagsMap := map[string]string{
+		"apfId": apfId,
+		"aefId": aefId,
+		"apiId": apiId,
+		"apiVersion": apiVersion,
+		"resourceName": resourceName,
+	}
+
+	// Convert the map to a slice of strings
+	var tagsSlice []string
+	for key, value := range tagsMap {
+		str := fmt.Sprintf("%s: %s", key, value)
+		tagsSlice = append(tagsSlice, str)
+	}
+
+	return tagsSlice
+}
+
+func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, tags []string) (int, error) {
 	log.Tracef("entering createKongService")
 	log.Tracef("createKongService, kongServiceName %s", kongServiceName)
 
@@ -167,7 +209,7 @@
 		"port":     firstAEFProfilePort,
 		"protocol": kongControlPlaneURLParsed.Scheme,
 		"path":     kongServiceUri,
-		"tags":     []string{aefId},
+		"tags":     tags,
 	}
 
 	// Kong admin API endpoint for creating a service