diff --git a/Dockerfile b/Dockerfile index 0fe35bc..32a823c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,5 +6,8 @@ RUN wget https://github.com/gohugoio/hugo/releases/download/v0.45.1/hugo_0.45.1_ && ln -s /tmp/public/ /usr/share/nginx/html \ && cd /tmp \ && hugo \ - && cp ./nginx/default.conf /etc/nginx/conf.d/default.conf + && cp ./nginx/default.conf /etc/nginx/conf.d/default.conf \ + && apk update \ + && apk add nodejs npm \ + && npm install -g asyncapi-generator CMD ["/bin/sh", "/tmp/nginx/start.sh"] \ No newline at end of file diff --git a/Makefile b/Makefile index 76ab1ea..97fe833 100644 --- a/Makefile +++ b/Makefile @@ -51,13 +51,13 @@ build-all: clean-all build-site build-server build-docker ## Performs clean-all #--- Run targets --- run-server: ## Builds the in the server directory and runs it with default settings cd server && go generate && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ../dist/server *.go - MODE=LOCAL HUGODIR=$(CURRDIR)/webapp HUGOSTORE=$(CURRDIR)/webapp/content/apis SWAGGERSTORE=$(CURRDIR)/webapp/static/swaggerdocs EXTERNALIP=$(EXTIP) ./dist/server + MODE=LOCAL ASYNCDOCSTORE=$(CURRDIR)/webapp/static/asyncapidocs HUGODIR=$(CURRDIR)/webapp HUGOSTORE=$(CURRDIR)/webapp/content/apis SWAGGERSTORE=$(CURRDIR)/webapp/static/swaggerdocs EXTERNALIP=$(EXTIP) ./dist/server run-docker: ## Runs a docker container with default settings docker run -it --rm -p 80:80 -v $(HOME)/.kube:/root/.kube -v $(HOME)/.minikube:/home/$(USER)/.minikube -e MODE=LOCAL -e HUGODIR="/tmp" -e EXTERNALIP=$(EXTIP) -e HUGOCMD="sh -c \"cd /tmp && hugo\"" --name=apiscout $(DOCKERREPO)/apiscout:latest run-hugo: ## Runs the embedded Hugo server on port 1313 - cd webapp && hugo server -D --disableFastRender + cd webapp && hugo server --renderToDisk run-docs: ## Runs the embedded Hugo server on port 1313 for the documentation cd docs && hugo server -D --disableFastRender --themesDir ../webapp/themes diff --git a/README.md b/README.md index e046397..664acc7 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ apiscout looks for two annotations to be able to index a service: * `apiscout/index: 'true'` This annotation ensures that apiscout indexes the service * `apiscout/swaggerUrl: '/swaggerspec'` This is the URL from where apiscout will read the OpenAPI document +* `apiscout/asyncApiUrl: '/asyncapispec'` This is the URL from where apiscout will read the AsyncAPI document ## Environment variables for the docker container diff --git a/apiscout.yml b/apiscout.yml index 4f11fc8..6dc3fc5 100644 --- a/apiscout.yml +++ b/apiscout.yml @@ -31,7 +31,7 @@ spec: spec: containers: - name: apiscout - image: + image: env: - name: MODE value: "KUBE" diff --git a/samples/invoiceservice-go/Dockerfile b/samples/invoiceservice-go/Dockerfile index a28e4b8..769084f 100644 --- a/samples/invoiceservice-go/Dockerfile +++ b/samples/invoiceservice-go/Dockerfile @@ -4,5 +4,6 @@ ENV HTTPPORT=8080 \ PAYMENTSERVICE=bla ADD invoiceservice-go . ADD swagger.json . +ADD asyncapi.yaml . EXPOSE 8080 CMD ./invoiceservice-go \ No newline at end of file diff --git a/samples/invoiceservice-go/Makefile b/samples/invoiceservice-go/Makefile index 0736031..443ab4a 100644 --- a/samples/invoiceservice-go/Makefile +++ b/samples/invoiceservice-go/Makefile @@ -27,6 +27,7 @@ build-app: build-docker: cp Dockerfile dist/Dockerfile cp swagger.json dist/swagger.json + cp asyncapi.yaml dist/asyncapi.yaml eval $$(minikube docker-env) ;\ cd dist && docker build -t $(DOCKERHUBUSER)/invoiceservice-go:$(DOCKERTAG) . diff --git a/samples/invoiceservice-go/README.md b/samples/invoiceservice-go/README.md index 35f88e0..b6b4358 100644 --- a/samples/invoiceservice-go/README.md +++ b/samples/invoiceservice-go/README.md @@ -8,8 +8,9 @@ This sample Flogo application is used to demonstrate some key Flogo constructs, ├── Dockerfile <-- A Dockerfile to build a container based on an Alpine base image ├── main.go <-- The Go source code for the app ├── Makefile <-- A Makefile to help build and deploy the app -├── payment-go-svc.yml <-- The Kubernetes deployment file +├── invoice-go-svc.yml <-- The Kubernetes deployment file ├── README.md <-- This file +├── asyncapi.yaml <-- The AsyncAPI specification (async spec example) └── swagger.json <-- The OpenAPI specification for the app ``` @@ -32,6 +33,7 @@ To build and deploy the app to Kubernetes, run the make targets for _deps_, _bui After starting the app, it will register with two endpoints: * **/api/invoices/:id**: Get the invoice details for the invoice ID. * **/swagger**: Get the OpenAPI specification for this app +* **/asyncapispec**: Get the AsyncAPI specification for this app ## API Scout -As you deploy the app to Kubernetes, after a few seconds the API will be found by API Scout and indexed. The lines 36 to 38 in [invoice-go-svc.yml](./invoice-go-svc.yml) are the annotations that make sure the API is found. \ No newline at end of file +As you deploy the app to Kubernetes, after a few seconds the API will be found by API Scout and indexed. The lines 34 to 36 in [invoice-go-svc.yml](./invoice-go-svc.yml) are the annotations that make sure the API is found. \ No newline at end of file diff --git a/samples/invoiceservice-go/asyncapi.yaml b/samples/invoiceservice-go/asyncapi.yaml new file mode 100644 index 0000000..d84de30 --- /dev/null +++ b/samples/invoiceservice-go/asyncapi.yaml @@ -0,0 +1,161 @@ +asyncapi: "1.2.0" +info: + title: invoiceservice + version: "1.0.0" + x-logo: https://avatars0.githubusercontent.com/u/16401334?v=4&s=200 + description: | + This is a simple example of an _AsyncAPI_ document. + termsOfService: https://api.company.com/terms +baseTopic: 'hitch' + +servers: + - url: api.company.com:{port}/{app-id} + description: Allows you to connect using the MQTT protocol. + scheme: mqtt + variables: + app-id: + default: demo + description: You can find your `app-id` in our control panel, under the auth tab. + port: + enum: + - '5676' + - '5677' + default: '5676' + - url: api.company.com:{port}/{app-id} + description: Allows you to connect using the AMQP protocol. + scheme: amqp + variables: + app-id: + default: demo + description: You can find your `app-id` in our control panel, under the auth tab. + port: + enum: + - '5676' + - '5677' + default: '5676' + +topics: + accounts.1.0.action.user.signup: + publish: + $ref: "#/components/messages/userSignUp" + accounts.1.0.event.user.signup: + subscribe: + $ref: "#/components/messages/userSignedUp" + +components: + messages: + userSignUp: + deprecated: true + summary: Action to sign a user up. + description: | + Multiline description of what this action does. **It allows Markdown.** + tags: + - name: user + - name: signup + headers: + type: object + properties: + qos: + $ref: "#/components/schemas/MQTTQoSHeader" + retainFlag: + $ref: "#/components/schemas/MQTTRetainHeader" + payload: + type: object + properties: + user: + $ref: "#/components/schemas/userCreate" + signup: + $ref: "#/components/schemas/signup" + + + userSignedUp: + payload: + type: object + properties: + test: + type: array + items: + type: object + properties: + key1: + type: string + key2: + type: integer + user: + $ref: "#/components/schemas/user" + signup: + $ref: "#/components/schemas/signup" + schemas: + id: + title: id + description: Resource identifier + type: string + username: + title: username + description: User handle + type: string + datetime: + title: datetime + description: Date and Time of the message + type: string + format: date-time + MQTTQoSHeader: + title: qos + description: Quality of Service + type: integer + format: int32 + default: 1 + enum: + - 0 + - 2 + MQTTRetainHeader: + title: retainFlag + description: | + This flag determines if the message will be saved by the broker for the specified + topic as last known good value. New clients that subscribe to that topic will receive + the last retained message on that topic instantly after subscribing. More on retained messages + and best practices in one of the next posts. + type: boolean + default: false + user: + type: object + required: + - id + - username + properties: + id: + description: User Id + $ref: "#/components/schemas/id" + full_name: + description: User full name + type: string + username: + $ref: "#/components/schemas/username" + userCreate: + type: object + required: + - username + properties: + full_name: + description: User full name + type: string + username: + $ref: "#/components/schemas/username" + + signup: + type: object + required: + - method + - datetime + properties: + method: + description: Signup method + type: string + enum: + - email + - facebook + - twitter + - github + - google + datetime: + $ref: "#/components/schemas/datetime" \ No newline at end of file diff --git a/samples/invoiceservice-go/invoice-go-svc.yml b/samples/invoiceservice-go/invoice-go-svc.yml index afec4f1..4ccb18a 100644 --- a/samples/invoiceservice-go/invoice-go-svc.yml +++ b/samples/invoiceservice-go/invoice-go-svc.yml @@ -32,6 +32,7 @@ metadata: namespace: default annotations: apiscout/index: 'true' + apiscout/asyncApiUrl: '/asyncapispec' apiscout/swaggerUrl: '/swaggerspec' spec: ports: diff --git a/samples/invoiceservice-go/main.go b/samples/invoiceservice-go/main.go index 89ff222..60bb6fd 100644 --- a/samples/invoiceservice-go/main.go +++ b/samples/invoiceservice-go/main.go @@ -15,6 +15,7 @@ import ( "github.com/TIBCOSoftware/flogo-lib/engine" "github.com/TIBCOSoftware/flogo-lib/flogo" "github.com/TIBCOSoftware/flogo-lib/logger" + "github.com/ghodss/yaml" "github.com/retgits/flogo-components/activity/randomnumber" ) @@ -49,6 +50,7 @@ func appBuilder() *flogo.App { trg := app.NewTrigger(&rt.RestTrigger{}, map[string]interface{}{"port": port}) trg.NewFuncHandler(map[string]interface{}{"method": "GET", "path": "/api/invoices/:id"}, handler) trg.NewFuncHandler(map[string]interface{}{"method": "GET", "path": "/swaggerspec"}, SwaggerSpec) + trg.NewFuncHandler(map[string]interface{}{"method": "GET", "path": "/asyncapispec"}, YamlSpec) return app } @@ -133,3 +135,28 @@ func getEnvKey(key string, fallback string) string { } return fallback } + +// YamlSpec is the function that gets executedto retrieve the AsynSpec +func YamlSpec(ctx context.Context, inputs map[string]*data.Attribute) (map[string]*data.Attribute, error) { + // The return message is a map[string]*data.Attribute which we'll have to construct + response := make(map[string]interface{}) + ret := make(map[string]*data.Attribute) + + fileData, err := ioutil.ReadFile("asyncapi.yaml") + if err != nil { + ret["code"], _ = data.NewAttribute("code", data.TypeInteger, 500) + response["msg"] = err.Error() + } else { + ret["code"], _ = data.NewAttribute("code", data.TypeInteger, 200) + var data map[string]interface{} + if err := yaml.Unmarshal(fileData, &data); err != nil { + panic(err) + } + response = data + } + + ret["data"], _ = data.NewAttribute("data", data.TypeAny, response) + + return ret, nil + +} diff --git a/samples/streetlightsapp-go/.gitignore b/samples/streetlightsapp-go/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/samples/streetlightsapp-go/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/samples/streetlightsapp-go/Dockerfile b/samples/streetlightsapp-go/Dockerfile new file mode 100644 index 0000000..9612151 --- /dev/null +++ b/samples/streetlightsapp-go/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:latest +RUN apk update && apk add ca-certificates +ENV HTTPPORT=8090 +ADD streetlightsapp-go . +ADD streetlightsapi.yaml . +EXPOSE 8090 +CMD ./streetlightsapp-go \ No newline at end of file diff --git a/samples/streetlightsapp-go/Makefile b/samples/streetlightsapp-go/Makefile new file mode 100644 index 0000000..6e294ed --- /dev/null +++ b/samples/streetlightsapp-go/Makefile @@ -0,0 +1,39 @@ +.PHONY: deps + +#--- Variables --- +DOCKERHUBUSER=naresh +DOCKERTAG=latest +IPADDR=xxx.xxx.xxx.xxx + +#--- Get the dependencies --- +deps: + go get -u ./... + +#--- Clean up the dist folder --- +clean: + rm -rf dist + +#--- Clean up the Kubernetes deployment --- +clean-kube: + kubectl delete deployment streetlights-go-svc + kubectl delete svc streetlights-go-svc + +#--- Build the Flogo app --- +build-app: + go generate + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o dist/streetlightsapp-go + +#--- Build the docker container --- +build-docker: + cp Dockerfile dist/Dockerfile + cp streetlightsapi.yaml dist/streetlightsapi.yaml + eval $$(minikube docker-env) ;\ + cd dist && docker build -t $(DOCKERHUBUSER)/streetlightsapp-go:$(DOCKERTAG) . + +#--- Run the container --- +run-docker: + docker run --rm -it -p 9999:8090 $(DOCKERHUBUSER)/streetlightsapp-go:$(DOCKERTAG) + +#--- Run the app on Kubernetes --- +run-kube: + kubectl apply -f streetlights-go-svc.yml \ No newline at end of file diff --git a/samples/streetlightsapp-go/README.md b/samples/streetlightsapp-go/README.md new file mode 100644 index 0000000..0ae6eac --- /dev/null +++ b/samples/streetlightsapp-go/README.md @@ -0,0 +1,36 @@ +# Streetlights Service + +This sample application is used to demonstrate AsyncAPI app and is set to be indexed by API Scout. + +## Files +```bash +. +├── Dockerfile <-- A Dockerfile to build a container based on an Alpine base image +├── main.go <-- The Go source code for the app +├── Makefile <-- A Makefile to help build and deploy the app +├── streetlights-go-svc.yml <-- The Kubernetes deployment file +├── README.md <-- This file +└── streetlightsapi.yaml <-- The AsyncAPI specification for the app +``` + +## Make targets +The [Makefile](./Makefile) has a few targets: +* **deps**: Get all the Go dependencies for the app +* **clean**: Remove the `dist` folder for a new deployment +* **clean-kube**: Remove all the deployed artifacts from Kubernetes +* **build-app**: Build an executable (and store it in the dist folder) +* **build-docker**: Build a Docker container from the contents of the `dist` folder +* **run-docker**: Run the Docker image with default settings +* **run-kube**: Deploy the app to Kubernetes + +_For more detailed information on the commands that are executed you can check out the [Makefile](./Makefile)_ + +## Build and deploy the app +To build and deploy the app to Kubernetes, run the make targets for _deps_, _build-app_, _build-docker_ and _run-kube_ + +## API +After starting the app, it will register with below endpoint: +* **/asyncapispec**: Get the AsyncAPI specification for this app + +## API Scout +As you deploy the app to Kubernetes, after a few seconds the API will be found by API Scout and indexed. The lines 34 to 35 in [streetlights-go-svc.yml](./streetlights-go-svc.yml) are the annotations that make sure the API is found. \ No newline at end of file diff --git a/samples/streetlightsapp-go/main.go b/samples/streetlightsapp-go/main.go new file mode 100644 index 0000000..aea5350 --- /dev/null +++ b/samples/streetlightsapp-go/main.go @@ -0,0 +1,84 @@ +//go:generate go run $GOPATH/src/github.com/TIBCOSoftware/flogo-lib/flogo/gen/gen.go $GOPATH +package main + +import ( + "context" + "io/ioutil" + "os" + "strconv" + + rt "github.com/TIBCOSoftware/flogo-contrib/trigger/rest" + "github.com/TIBCOSoftware/flogo-lib/core/data" + "github.com/TIBCOSoftware/flogo-lib/engine" + "github.com/TIBCOSoftware/flogo-lib/flogo" + "github.com/TIBCOSoftware/flogo-lib/logger" + "github.com/ghodss/yaml" +) + +var ( + httpport = getEnvKey("HTTPPORT", "8090") +) + +func main() { + // Create a new Flogo app + app := appBuilder() + + e, err := flogo.NewEngine(app) + + if err != nil { + logger.Error(err) + return + } + + engine.RunEngine(e) +} + +func appBuilder() *flogo.App { + app := flogo.NewApp() + + // Convert the HTTPPort to an integer + port, err := strconv.Atoi(httpport) + if err != nil { + logger.Error(err) + } + + // Register the HTTP trigger + trg := app.NewTrigger(&rt.RestTrigger{}, map[string]interface{}{"port": port}) + trg.NewFuncHandler(map[string]interface{}{"method": "GET", "path": "/asyncapispec"}, YamlSpec) + + return app +} + +// getEnvKey tries to get the specified key from the OS environment and returns either the +// value or the fallback that was provided +func getEnvKey(key string, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +// YamlSpec is the function that gets executedto retrieve the SwaggerSpec +func YamlSpec(ctx context.Context, inputs map[string]*data.Attribute) (map[string]*data.Attribute, error) { + // The return message is a map[string]*data.Attribute which we'll have to construct + response := make(map[string]interface{}) + ret := make(map[string]*data.Attribute) + + fileData, err := ioutil.ReadFile("streetlightsapi.yaml") + if err != nil { + ret["code"], _ = data.NewAttribute("code", data.TypeInteger, 500) + response["msg"] = err.Error() + } else { + ret["code"], _ = data.NewAttribute("code", data.TypeInteger, 200) + var data map[string]interface{} + if err := yaml.Unmarshal(fileData, &data); err != nil { + panic(err) + } + response = data + } + + ret["data"], _ = data.NewAttribute("data", data.TypeAny, response) + + return ret, nil + +} diff --git a/samples/streetlightsapp-go/streetlights-go-svc.yml b/samples/streetlightsapp-go/streetlights-go-svc.yml new file mode 100644 index 0000000..32fe4e1 --- /dev/null +++ b/samples/streetlightsapp-go/streetlights-go-svc.yml @@ -0,0 +1,43 @@ +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + run: streetlights-go-svc + name: streetlights-go-svc + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + run: streetlights-go-svc + template: + metadata: + labels: + run: streetlights-go-svc + spec: + containers: + - name: streetlights-go-svc + image: naresh/streetlightsapp-go:latest + imagePullPolicy: Never + ports: + - containerPort: 8090 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + run: streetlights-go-svc + name: streetlights-go-svc + namespace: default + annotations: + apiscout/index: 'true' + apiscout/asyncApiUrl: '/asyncapispec' +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 8090 + selector: + run: streetlights-go-svc + type: LoadBalancer diff --git a/samples/streetlightsapp-go/streetlightsapi.yaml b/samples/streetlightsapp-go/streetlightsapi.yaml new file mode 100644 index 0000000..76fe811 --- /dev/null +++ b/samples/streetlightsapp-go/streetlightsapi.yaml @@ -0,0 +1,108 @@ +asyncapi: '1.1.0' +info: + title: Streetlights API + version: '1.0.0' + description: | + The Smartylighting Streetlights API allows you to remotely manage the city lights. + ### Check out its awesome features: + * Turn a specific streetlight on/off 🌃 + * Dim a specific streetlight 😎 + * Receive real-time information about environmental lighting conditions 📈 + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +baseTopic: smartylighting.streetlights.1.0 + +servers: + - url: api.streetlights.smartylighting.com:{port} + scheme: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' + +security: + - apiKey: [] + +topics: + event.{streetlightId}.lighting.measured: + parameters: + - name: streetlightId + description: The ID of the streetlight. + schema: + type: string + publish: + $ref: '#/components/messages/lightMeasured' + + action.{streetlightId}.turn.on: + subscribe: + $ref: '#/components/messages/turnOnOff' + + action.{streetlightId}.turn.off: + subscribe: + $ref: '#/components/messages/turnOnOff' + + action.{streetlightId}.dim: + subscribe: + $ref: '#/components/messages/dimLight' + +components: + messages: + lightMeasured: + summary: Inform about environmental lighting conditions for a particular streetlight. + payload: + $ref: "#/components/schemas/lightMeasuredPayload" + turnOnOff: + summary: Command a particular streetlight to turn the lights on or off. + payload: + $ref: "#/components/schemas/turnOnOffPayload" + dimLight: + summary: Command a particular streetlight to dim the lights. + payload: + $ref: "#/components/schemas/dimLightPayload" + + schemas: + lightMeasuredPayload: + type: object + properties: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: "#/components/schemas/sentAt" + turnOnOffPayload: + type: object + properties: + command: + type: string + enum: + - on + - off + description: Whether to turn on or off the light. + sentAt: + $ref: "#/components/schemas/sentAt" + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: "#/components/schemas/sentAt" + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + + securitySchemes: + apiKey: + type: apiKey + in: user + description: Provide your API key as the user and leave the password empty. \ No newline at end of file diff --git a/server/main.go b/server/main.go index c50ac2e..f9b24ee 100644 --- a/server/main.go +++ b/server/main.go @@ -35,6 +35,8 @@ var ( externalIP = util.GetEnvKey("EXTERNALIP", "") // The base directory for Hugo hugoDir = util.GetEnvKey("HUGODIR", "") + // The localtion where to store async api docs + asyncDocStore = util.GetEnvKey("ASYNCDOCSTORE", "/tmp/static/asyncapidocs") ) // main is the main entrypoint to start APIScout @@ -55,7 +57,7 @@ func main() { log.Printf("------------------------------------------------------------\n") // Create a new APIScout server instance - srv, err := server.New(swaggerStore, hugoStore, runMode, externalIP, hugoDir) + srv, err := server.New(swaggerStore, hugoStore, runMode, externalIP, hugoDir, asyncDocStore) if err != nil { panic(err.Error()) } diff --git a/server/server/server.go b/server/server/server.go index 8d423cd..0f45a03 100644 --- a/server/server/server.go +++ b/server/server/server.go @@ -26,18 +26,21 @@ type Server struct { ExternalIP string // The base directory for Hugo HugoDir string + // The localtion where to store async api docs + AsyncDocStore string } // New creates a new instance of the Server -func New(swaggerStore string, hugoStore string, runMode string, externalIP string, hugoDir string) (*Server, error) { +func New(swaggerStore string, hugoStore string, runMode string, externalIP string, hugoDir string, asyncDocStore string) (*Server, error) { // Return a new struct return &Server{ - ServiceMap: make(map[string]string), - SwaggerStore: swaggerStore, - HugoStore: hugoStore, - RunMode: runMode, - ExternalIP: externalIP, - HugoDir: hugoDir, + ServiceMap: make(map[string]string), + SwaggerStore: swaggerStore, + HugoStore: hugoStore, + RunMode: runMode, + ExternalIP: externalIP, + HugoDir: hugoDir, + AsyncDocStore: asyncDocStore, }, nil } diff --git a/server/server/service.go b/server/server/service.go index b08fb27..52efed1 100644 --- a/server/server/service.go +++ b/server/server/service.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/TIBCOSoftware/apiscout/server/util" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/watch" ) @@ -18,6 +18,8 @@ const ( annotation = "apiscout/index" // The annotation for apiscout to get the OpenAPI doc from swaggerURL = "apiscout/swaggerUrl" + // The annotation for apiscout to get the AsyncAPI doc from + asyncAPIURL = "apiscout/asyncApiUrl" ) // handleService takes the Kubernetes service object and the EventType as input to determine what @@ -91,13 +93,43 @@ func add(service *v1.Service, srv *Server) error { port = service.Spec.Ports[0].Port } - apidoc, err := util.GetAPIDoc(fmt.Sprintf("http://%s:%d%s", ip, port, service.Annotations[swaggerURL])) + apis := getApis(service) + + // create service name folder + serviceFold := filepath.Join(srv.HugoStore, strings.Replace(strings.ToLower(service.Name), " ", "-", -1)) + err := util.CreateFolder(serviceFold) if err != nil { - log.Printf("Error while retrieving API document from %s: %s", fmt.Sprintf("http://%s:%d%s", ip, port, service.Annotations[swaggerURL]), err.Error()) + log.Fatalln("Error while creating service name folder ", err) return err } - util.WriteSwaggerToDisk(service.Name, apidoc, fmt.Sprintf("%s:%d", ip, port), srv.SwaggerStore, srv.HugoStore) + for apiType, docURL := range apis { + + apidoc, err := util.GetAPIDoc(fmt.Sprintf("http://%s:%d%s", ip, port, docURL)) + + if err != nil { + log.Printf("Error while retrieving API document from %s: %s", fmt.Sprintf("http://%s:%d%s", ip, port, docURL), err.Error()) + return err + } + + if strings.Compare(apiType, "OPENAPI") == 0 { + err = util.WriteSwaggerToDisk(service.Name, apidoc, fmt.Sprintf("%s:%d", ip, port), srv.SwaggerStore, srv.HugoStore) + } else { + err = util.GenerateMarkdownFile(srv.AsyncDocStore, srv.HugoStore, apidoc, service.Name) + } + + if err != nil { + log.Fatalf(" Service %s addition failed for doc type %s ", service.Name, apiType) + return err + } + } + + // create service markup file + err = util.WriteServiceMarkdownFile(serviceFold, strings.Replace(strings.ToLower(service.Name), " ", "-", -1)) + if err != nil { + log.Fatal("Error while creating service markup file ", err) + return err + } srv.ServiceMap[service.Name] = "DONE" log.Printf("Service %s has been added to API Scout\n", service.Name) @@ -110,23 +142,55 @@ func add(service *v1.Service, srv *Server) error { func remove(service *v1.Service, srv *Server) error { log.Printf("Attempting to delete %s\n", service.Name) - // Remove JSON file - filename := filepath.Join(srv.SwaggerStore, fmt.Sprintf("%s.json", strings.Replace(strings.ToLower(service.Name), " ", "-", -1))) - err := os.Remove(filename) - if err != nil { - return err - } + var docPath string + + apis := getApis(service) + + for apiType := range apis { + + if strings.Compare(strings.ToUpper(apiType), "ASYNCAPI") == 0 { + docPath = srv.AsyncDocStore + } else { + docPath = srv.SwaggerStore + } + + // Remove JSON file + filename := filepath.Join(docPath, fmt.Sprintf("%s.json", strings.Replace(strings.ToLower(service.Name), " ", "-", -1))) + err := os.Remove(filename) + if err != nil { + return err + } + + // Remove Markdown file + filename = filepath.Join(srv.HugoStore, strings.Replace(strings.ToLower(service.Name), " ", "-", -1), fmt.Sprintf("%s-%s.md", strings.Replace(strings.ToLower(service.Name), " ", "-", -1), strings.ToLower(apiType))) + err = os.Remove(filename) + if err != nil { + return err + } - // Remove Markdown file - filename = filepath.Join(srv.HugoStore, fmt.Sprintf("%s.md", strings.Replace(strings.ToLower(service.Name), " ", "-", -1))) - err = os.Remove(filename) - if err != nil { - return err } + // Remove service markup file and folder + os.RemoveAll(filepath.Join(srv.HugoStore, strings.Replace(strings.ToLower(service.Name), " ", "-", -1))) + os.Remove(filepath.Join(srv.HugoStore, strings.Replace(strings.ToLower(service.Name), " ", "-", -1))) + // Remove service from service map delete(srv.ServiceMap, service.Name) log.Printf("Service %s has been removed from API Scout\n", service.Name) return nil } + +func getApis(service *v1.Service) map[string]string { + multiAPIMap := make(map[string]string) + + if len(service.Annotations[asyncAPIURL]) != 0 { + multiAPIMap["ASYNCAPI"] = service.Annotations[asyncAPIURL] + } + + if len(service.Annotations[swaggerURL]) != 0 { + multiAPIMap["OPENAPI"] = service.Annotations[swaggerURL] + } + + return multiAPIMap +} diff --git a/server/server/service_test.go b/server/server/service_test.go index 0549778..c121183 100644 --- a/server/server/service_test.go +++ b/server/server/service_test.go @@ -8,9 +8,8 @@ import ( "strings" "testing" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/watch" - - "k8s.io/api/core/v1" ) const swaggerJSONPayload = `{ @@ -163,7 +162,7 @@ func TestHandleService(t *testing.T) { os.MkdirAll(tempPath, 0777) - srv, err := New(tempPath, tempPath, runMode, externalIP, tempPath) + srv, err := New(tempPath, tempPath, runMode, externalIP, tempPath, "", "") if err != nil { panic(err.Error()) } diff --git a/server/util/apidoc.go b/server/util/apidoc.go index ef87e1f..525bb9d 100644 --- a/server/util/apidoc.go +++ b/server/util/apidoc.go @@ -16,8 +16,9 @@ import ( // A template for the Markdown file for Hugo const markdown = `--- -title: {{.title}} +title: {{.title}}-openapi weight: 1000 +#post: "openapi" --- {{.json}}` @@ -113,30 +114,13 @@ func WriteSwaggerToDisk(name string, apidoc string, svchost string, swaggerStore s := buf.String() // Determine where to save the file - filename = filepath.Join(hugoStore, fmt.Sprintf("%s.md", strings.Replace(strings.ToLower(name), " ", "-", -1))) + filename = filepath.Join(hugoStore, strings.Replace(strings.ToLower(name), " ", "-", -1), fmt.Sprintf("%s-openapi.md", strings.Replace(strings.ToLower(name), " ", "-", -1))) log.Printf("Preparing to write %s to disk", filename) os.Remove(filename) - // Create a file on disk - file, err = os.Create(filename) - if err != nil { - log.Printf("error while creating file: %s", err.Error()) - return fmt.Errorf("error while creating file: %s", err.Error()) - } - defer file.Close() - - // Open the file to write - file, err = os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - log.Printf("error while opening file: %s", err.Error()) - return fmt.Errorf("error while opening file: %s", err.Error()) - } - - // Write the Markdown doc to disk - _, err = file.Write([]byte(s)) + err = createFileWithContent(filename, s) if err != nil { - log.Printf("error while writing Markdown to disk: %s", err.Error()) - return fmt.Errorf("error while writing Markdown to disk: %s", err.Error()) + return err } return nil diff --git a/server/util/asyncapi.go b/server/util/asyncapi.go new file mode 100644 index 0000000..913688e --- /dev/null +++ b/server/util/asyncapi.go @@ -0,0 +1,93 @@ +package util + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" +) + +// A template for the Markdown file for Hugo +const asyncMarkdown = `--- +title: {{.title}}-asyncapi +weight: 1000 +#post: "asyncapi" +--- + +{{.content}}` + +// GenerateMarkdownFile generates markdown file for asyncapi docs +func GenerateMarkdownFile(srcFile, destFile, apiDoc, serviceName string) error { + + log.Print("Writing file from received data") + + asyncFile := filepath.Join(srcFile, fmt.Sprintf("%s.json", strings.Replace(strings.ToLower(serviceName), " ", "-", -1))) + log.Printf("Preparing to write %s to disk", asyncFile) + os.Remove(asyncFile) + + err := createFileWithContent(asyncFile, apiDoc) + if err != nil { + return err + } + + log.Print("Generating AsyncApi markdown file") + + cmd := exec.Command("ag", "-o", destFile, asyncFile, "markdown") + output, err := cmd.CombinedOutput() + log.Print(string(output)) + + if err != nil { + return err + } + + // read markdown file + data, err := ioutil.ReadFile(filepath.Join(destFile, "asyncapi.md")) + if err != nil { + return nil + } + os.Remove(filepath.Join(destFile, "asyncapi.md")) + + // Render the Markdown file based on the template + t := template.Must(template.New("top").Parse(asyncMarkdown)) + buf := &bytes.Buffer{} + + // Unmarshal the string into a proper document + var docContent map[string]interface{} + if err := json.Unmarshal([]byte(apiDoc), &docContent); err != nil { + log.Printf("error while unmarshaling apidoc: %s", err.Error()) + return fmt.Errorf("error while unmarshaling apidoc: %s", err.Error()) + } + + var title string + if val, ok := docContent["info"].(map[string]interface{})["title"]; ok { + title = val.(string) + } + + dataMap := make(map[string]interface{}) + dataMap["title"] = title + dataMap["content"] = string(data) + + if err := t.Execute(buf, dataMap); err != nil { + log.Printf("error while rendering Markdown file: %s", err.Error()) + return fmt.Errorf("error while rendering Markdown file: %s", err.Error()) + } + s := buf.String() + + // Determine where to save the file + mdFilename := filepath.Join(destFile, strings.Replace(strings.ToLower(serviceName), " ", "-", -1), fmt.Sprintf("%s-asyncapi.md", strings.Replace(strings.ToLower(serviceName), " ", "-", -1))) + log.Printf("Preparing to write %s to disk", mdFilename) + os.Remove(mdFilename) + + err = createFileWithContent(mdFilename, s) + if err != nil { + return err + } + + return nil +} diff --git a/server/util/hugo.go b/server/util/hugo.go index 212fe45..4d99d93 100644 --- a/server/util/hugo.go +++ b/server/util/hugo.go @@ -3,13 +3,15 @@ package util import ( "log" + "os" "os/exec" + "path/filepath" ) // GenerateDocs is a wrapper around the Hugo binary in the container and instructs the binary to generate the site func GenerateDocs(hugoDir string) error { log.Print("Regeneratig Hugo content") - + os.RemoveAll(filepath.Join(hugoDir, "public")) cmd := exec.Command("sh", "-c", "hugo") cmd.Dir = hugoDir output, err := cmd.CombinedOutput() diff --git a/server/util/os.go b/server/util/os.go index 98495a5..eb02427 100644 --- a/server/util/os.go +++ b/server/util/os.go @@ -1,7 +1,11 @@ // Package util implements utility methods package util -import "os" +import ( + "fmt" + "log" + "os" +) // GetEnvKey tries to get the specified key from the OS environment and returns either the // value or the fallback that was provided @@ -19,3 +23,42 @@ func HomeDir() string { } return os.Getenv("USERPROFILE") // windows } + +func createFileWithContent(filename, content string) error { + + // Create a file on disk + file, err := os.Create(filename) + if err != nil { + log.Printf("error while creating file: %s", err.Error()) + return fmt.Errorf("error while creating file: %s", err.Error()) + } + defer file.Close() + + // Open the file to write + file, err = os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + log.Printf("error while opening file: %s", err.Error()) + return fmt.Errorf("error while opening file: %s", err.Error()) + } + + // Write the Markdown doc to disk + _, err = file.Write([]byte(content)) + if err != nil { + log.Printf("error while writing Markdown to disk: %s", err.Error()) + return fmt.Errorf("error while writing Markdown to disk: %s", err.Error()) + } + + return nil +} + +// CreateFolder creates folder for given path +func CreateFolder(path string) error { + _, err := os.Stat(path) + if err != nil { + err = os.MkdirAll(path, 0777) + if err != nil { + return err + } + } + return nil +} diff --git a/server/util/servicedoc.go b/server/util/servicedoc.go new file mode 100644 index 0000000..3a0e228 --- /dev/null +++ b/server/util/servicedoc.go @@ -0,0 +1,48 @@ +package util + +import ( + "bytes" + "fmt" + "log" + "os" + "path/filepath" + "text/template" +) + +const serviceMarkdown = `--- +title: {{.title}} +weight: 5 +--- +` + +// WriteServiceMarkdownFile writes service index file for given servicename +func WriteServiceMarkdownFile(path, serviceName string) error { + + fileName := filepath.Join(path, "_index.md") + log.Printf("Preparing to write %s to disk", fileName) + + os.Remove(fileName) + + dataMap := make(map[string]interface{}) + dataMap["title"] = serviceName + + // Render the Markdown file based on the template + t := template.Must(template.New("top").Parse(serviceMarkdown)) + buf := &bytes.Buffer{} + + if err := t.Execute(buf, dataMap); err != nil { + log.Printf("error while rendering Markdown file: %s", err.Error()) + return fmt.Errorf("error while rendering Markdown file: %s", err.Error()) + } + s := buf.String() + + s = s + "\n {{% children %}} " + + // creating file + err := createFileWithContent(fileName, s) + if err != nil { + return err + } + + return nil +} diff --git a/webapp/layouts/shortcodes/oas3.html b/webapp/layouts/shortcodes/oas3.html index e0faeb4..976a857 100644 --- a/webapp/layouts/shortcodes/oas3.html +++ b/webapp/layouts/shortcodes/oas3.html @@ -41,6 +41,7 @@ // Build a system const ui = SwaggerUIBundle({ url: "{{ .Get "url" }}", + validatorUrl : false, dom_id: '#swagger-ui', deepLinking: true, presets: [