Skip to content

Commit

Permalink
MGMT-16112: Add OpenAPI handler
Browse files Browse the repository at this point in the history
This patch adds a new `/o2ims-infrastructureInventory/v1/openapi`
endpoint that returns the OpenAPI specification of the service.

Currently this specification only covers the metadata and the deployment
manager endpoints. Other endpoints will be added later.

Related: https://issues.redhat.com/browse/MGMT-16112
Signed-off-by: Juan Hernandez <[email protected]>
  • Loading branch information
jhernand committed Dec 15, 2023
1 parent 8c36791 commit 0c1b212
Show file tree
Hide file tree
Showing 9 changed files with 642 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ lint:
golangci-lint --version
golangci-lint run --verbose --print-resources-usage --modules-download-mode=vendor --timeout=5m0s

.PHONY: lint-openapi
lint-openapi:
@echo "Run lint OpenAPI"
spectral lint --ruleset internal/openapi/lint.yaml internal/openapi/spec.yaml

.PHONY: deps-update
deps-update:
@echo "Update dependencies"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/json-iterator/go v1.1.12
github.com/onsi/ginkgo/v2 v2.13.0
github.com/onsi/gomega v1.29.0
github.com/thoas/go-funk v0.9.3
go.uber.org/mock v0.3.0
k8s.io/api v0.28.4
k8s.io/apimachinery v0.28.4
Expand Down Expand Up @@ -39,7 +40,6 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/thoas/go-funk v0.9.3 // indirect
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/term v0.14.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions hack/install_test_deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ if ! [ -x "$(command -v golangci-lint)" ]; then
rm tarball
fi


if ! [ -x "$(command -v spectral)" ]; then
echo "Downloading spectral"

curl -Lo spectral https://github.com/stoplightio/spectral/releases/download/v6.11.0/spectral-linux-x64
echo 0e151d3dc5729750805428f79a152fa01dd4c203f1d9685ef19f4fd4696fcd5f spectral | sha256sum -c
chmod +x spectral
mv spectral $(go env GOPATH)/bin
fi
16 changes: 16 additions & 0 deletions internal/cmd/server/start_metadata_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/openshift-kni/oran-o2ims/internal"
"github.com/openshift-kni/oran-o2ims/internal/exit"
"github.com/openshift-kni/oran-o2ims/internal/network"
"github.com/openshift-kni/oran-o2ims/internal/openapi"
"github.com/openshift-kni/oran-o2ims/internal/service"
)

Expand Down Expand Up @@ -99,6 +100,21 @@ func (c *MetadataServerCommand) run(cmd *cobra.Command, argv []string) error {
service.SendError(w, http.StatusMethodNotAllowed, "Method not allowed")
})

// Create the handler that serves the OpenAPI metadata:
openapiHandler, err := openapi.NewHandler().
SetLogger(logger).
Build()
if err != nil {
logger.Error(
"Failed to create OpenAPI handler",
slog.String("error", err.Error()),
)
return exit.Error(1)
}
router.PathPrefix(
"/o2ims-infrastructureInventory/v1/openapi",
).Handler(openapiHandler)

// Create the handler that servers the information about the versions of the API:
versionsHandler, err := service.NewVersionsHandler().
SetLogger(logger).
Expand Down
103 changes: 103 additions & 0 deletions internal/openapi/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2023 Red Hat Inc.
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.
*/

package openapi

import (
"embed"
"encoding/json"
"errors"
"log/slog"
"net/http"

"gopkg.in/yaml.v3"
)

//go:embed spec.yaml
var dataFS embed.FS

// HandlerBuilder contains the data and logic needed to create a new handler for the OpenAPI
// metadata. Don't create instances of this type directly, use the NewHandler function instead.
type HandlerBuilder struct {
logger *slog.Logger
}

// Handler knows how to respond to requests for the OpenAPI metadata. Don't create instances of
// this type directly, use the NewHandler function instead.
type Handler struct {
logger *slog.Logger
spec []byte
}

// NewHandler creates a builder that can then be used to configure and create a handler for the
// OpenAPI metadata.
func NewHandler() *HandlerBuilder {
return &HandlerBuilder{}
}

// SetLogger sets the logger that the handler will use to write to the log. This is mandatory.
func (b *HandlerBuilder) SetLogger(value *slog.Logger) *HandlerBuilder {
b.logger = value
return b
}

// Build uses the data stored in the builder to create and configure a new handler.
func (b *HandlerBuilder) Build() (result *Handler, err error) {
// Check parameters:
if b.logger == nil {
err = errors.New("logger is mandatory")
return
}

// Load the specification:
spec, err := b.loadSpec()
if err != nil {
return
}

// Create and populate the object:
result = &Handler{
logger: b.logger,
spec: spec,
}
return
}

func (b *HandlerBuilder) loadSpec() (result []byte, err error) {
// Read the main file:
data, err := dataFS.ReadFile("spec.yaml")
if err != nil {
return
}
var spec any
err = yaml.Unmarshal(data, &spec)
if err != nil {
return
}
result, err = json.Marshal(spec)
return
}

// ServeHTTP is the implementation of the object HTTP handler interface.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write(h.spec)
if err != nil {
h.logger.Error(
"Failed to send data",
slog.String("error", err.Error()),
)
}
}
104 changes: 104 additions & 0 deletions internal/openapi/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
Copyright (c) 2023 Red Hat, Inc.
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.
*/

package openapi

import (
"encoding/json"
"mime"
"net/http"
"net/http/httptest"

. "github.com/onsi/ginkgo/v2/dsl/core"
. "github.com/onsi/ginkgo/v2/dsl/decorators"
. "github.com/onsi/gomega"

. "github.com/openshift-kni/oran-o2ims/internal/testing"
)

var _ = Describe("Handler", func() {
It("Returns a valid JSON content type and document", func() {
// Create the handler:
handler, err := NewHandler().
SetLogger(logger).
Build()
Expect(err).ToNot(HaveOccurred())
Expect(handler).ToNot(BeNil())

// Send the request:
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/", nil)
handler.ServeHTTP(recorder, request)

// Verify the content type:
Expect(recorder.Code).To(Equal(http.StatusOK))
contentType := recorder.Header().Get("Content-Type")
Expect(contentType).ToNot(BeEmpty())
mediaType, _, err := mime.ParseMediaType(contentType)
Expect(err).ToNot(HaveOccurred())
Expect(mediaType).To(Equal("application/json"))

// Verify the content format:
var spec any
err = json.Unmarshal(recorder.Body.Bytes(), &spec)
Expect(err).ToNot(HaveOccurred())
})

Describe("Content", Ordered, func() {
var spec any

BeforeAll(func() {
// Create the handler:
handler, err := NewHandler().
SetLogger(logger).
Build()
Expect(err).ToNot(HaveOccurred())
Expect(handler).ToNot(BeNil())

// Send the request:
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/", nil)
handler.ServeHTTP(recorder, request)

// Parse the response:
Expect(recorder.Code).To(Equal(http.StatusOK))
err = json.Unmarshal(recorder.Body.Bytes(), &spec)
Expect(err).ToNot(HaveOccurred())
})

It("Contains the basic fields", func() {
Expect(spec).To(MatchJQ(`.openapi`, "3.0.0"))
Expect(spec).To(MatchJQ(`.info.title`, "O2 IMS"))
Expect(spec).To(MatchJQ(`.info.version`, "1.0.0"))
})

It("Contains at least one path", func() {
Expect(spec).To(MatchJQ(`(.paths | length) > 0`, true))
})

It("Contains at least one schema", func() {
Expect(spec).To(MatchJQ(`(.components.schemas | length) > 0`, true))
})

It("All paths start with the expected prefix", func() {
Expect(spec).To(MatchJQ(`[.paths | keys[] | select(startswith("/o2ims-infrastructureInventory/") | not)] | length`, 0))
})

It("Contains the expected schemas", func() {
Expect(spec).To(MatchJQ(`any(.components.schemas | keys[]; . == "APIVersion")`, true))
Expect(spec).To(MatchJQ(`any(.components.schemas | keys[]; . == "APIVersions")`, true))
Expect(spec).To(MatchJQ(`any(.components.schemas | keys[]; . == "DeploymentManager")`, true))
})
})
})
20 changes: 20 additions & 0 deletions internal/openapi/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#
# Copyright (c) 2023 Red Hat, Inc.
#
# 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.
#

# This file contains the `spectral` configuration used to check the `spec.yaml` file.

extends: [[spectral:oas, all]]

rules:
info-contact: off
Loading

0 comments on commit 0c1b212

Please sign in to comment.