From 042ae1eb228a5ee4c44e69a4a2ef375ba410f1af Mon Sep 17 00:00:00 2001 From: Juan Hernandez Date: Mon, 6 Nov 2023 19:17:05 +0100 Subject: [PATCH] Add metadata server This patch adds a new `metadata-server` that responds to the API versions and cloud information endpoints. For example, for the API versions: ```shell $ curl http://localhost:8080/O2ims_infrastructureInventory/api_versions { "uriPrefix": "/O2ims_infrastructureInventory/v1", "apiVersions": [ { "version": "1.0.0" } ] } $ curl http://localhost:8080/O2ims_infrastructureInventory/v1/api_versions { "uriPrefix": "/O2ims_infrastructureInventory/v1", "apiVersions": [ { "version": "1.0.0" } ] } ``` And for the cloud information: ```shell $ curl http://localhost:8080/O2ims_infrastructureInventory/v1 { "oCloudId": "6575154c-72fc-4ed8-9a87-a81885ab38bb", "globalCloudId": "6575154c-72fc-4ed8-9a87-a81885ab38bb", "name": "OpenShift O-Cloud", "description": "OpenShift O-Cloud", "serviceUri": "https://localhost:8080", "extensions": {} } ``` Related: https://issues.redhat.com/browse/MGMT-16115 Signed-off-by: Juan Hernandez --- .vscode/launch.json | 13 ++ go.mod | 1 + go.sum | 2 + internal/cmd/server/flags.go | 20 ++ .../server/start_deployment_manager_server.go | 3 +- internal/cmd/server/start_metadata_server.go | 171 ++++++++++++++++++ internal/cmd/start_cmd.go | 1 + internal/service/cloud_info_handler.go | 91 ++++++++++ internal/service/object_adapter.go | 26 +-- internal/service/versions_handler.go | 138 ++++++++++++++ 10 files changed, 443 insertions(+), 23 deletions(-) create mode 100644 internal/cmd/server/flags.go create mode 100644 internal/cmd/server/start_metadata_server.go create mode 100644 internal/service/cloud_info_handler.go create mode 100644 internal/service/versions_handler.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 1eafbb5fa..aaaf4fc3a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,19 @@ "version" ] }, + { + "name": "start metadata-server", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": [ + "start", + "metadata-server", + "--log-level=debug", + "--cloud-id=6575154c-72fc-4ed8-9a87-a81885ab38bb" + ] + }, { "name": "start deployment-manager-server", "type": "go", diff --git a/go.mod b/go.mod index 47567133e..fc1ee930f 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( ) require ( + github.com/coreos/go-semver v0.3.1 github.com/go-logr/logr v1.2.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 diff --git a/go.sum b/go.sum index 31706a24a..ebf769498 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/cmd/server/flags.go b/internal/cmd/server/flags.go new file mode 100644 index 000000000..b1ec58529 --- /dev/null +++ b/internal/cmd/server/flags.go @@ -0,0 +1,20 @@ +/* +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 server + +// Names of command line flags: +const ( + cloudIDFlagName = "cloud-id" +) diff --git a/internal/cmd/server/start_deployment_manager_server.go b/internal/cmd/server/start_deployment_manager_server.go index f369f0599..e3fd1ae0a 100644 --- a/internal/cmd/server/start_deployment_manager_server.go +++ b/internal/cmd/server/start_deployment_manager_server.go @@ -238,7 +238,7 @@ func (c *DeploymentManagerServerCommand) run(cmd *cobra.Command, argv []string) objectAdapter, err := service.NewObjectAdapter(). SetLogger(logger). SetHandler(objectHandler). - SetID("deploymentManagerID"). + SetIDVariable("deploymentManagerID"). Build() if err != nil { logger.Error( @@ -269,5 +269,4 @@ func (c *DeploymentManagerServerCommand) run(cmd *cobra.Command, argv []string) const ( backendTokenFlagName = "backend-token" backendURLFlagName = "backend-url" - cloudIDFlagName = "cloud-id" ) diff --git a/internal/cmd/server/start_metadata_server.go b/internal/cmd/server/start_metadata_server.go new file mode 100644 index 000000000..a40a7f889 --- /dev/null +++ b/internal/cmd/server/start_metadata_server.go @@ -0,0 +1,171 @@ +/* +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 server + +import ( + "log/slog" + "net/http" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/openshift-kni/oran-o2ims/internal" + "github.com/openshift-kni/oran-o2ims/internal/exit" + "github.com/openshift-kni/oran-o2ims/internal/service" +) + +// MetadataServer creates and returns the `start metadata-server` command. +func MetadataServer() *cobra.Command { + c := NewMetadataServer() + result := &cobra.Command{ + Use: "metadata-server", + Short: "Starts the metadata server", + Args: cobra.NoArgs, + RunE: c.run, + } + flags := result.Flags() + _ = flags.String( + cloudIDFlagName, + "", + "O-Cloud identifier.", + ) + return result +} + +// MetadataServerCommand contains the data and logic needed to run the `start +// deployment-manager-server` command. +type MetadataServerCommand struct { +} + +// NewMetadataServer creates a new runner that knows how to execute the `start +// deployment-manager-server` command. +func NewMetadataServer() *MetadataServerCommand { + return &MetadataServerCommand{} +} + +// run executes the `start deployment-manager-server` command. +func (c *MetadataServerCommand) run(cmd *cobra.Command, argv []string) error { + // Get the context: + ctx := cmd.Context() + + // Get the dependencies from the context: + logger := internal.LoggerFromContext(ctx) + + // Get the flags: + flags := cmd.Flags() + + // Get the cloud identifier: + cloudID, err := flags.GetString(cloudIDFlagName) + if err != nil { + logger.Error( + "Failed to get cloud identifier flag", + "flag", cloudIDFlagName, + "error", err.Error(), + ) + return exit.Error(1) + } + if cloudID == "" { + logger.Error( + "Cloud identifier is empty", + "flag", cloudIDFlagName, + ) + return exit.Error(1) + } + logger.Info( + "Cloud identifier", + "value", cloudID, + ) + + // Create the router: + router := mux.NewRouter() + router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + service.SendError(w, http.StatusNotFound, "Not found") + }) + router.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + service.SendError(w, http.StatusMethodNotAllowed, "Method not allowed") + }) + + // Create the handler that servers the information about the versions of the API: + versionsHandler, err := service.NewVersionsHandler(). + SetLogger(logger). + Build() + if err != nil { + logger.Error( + "Failed to create versions handler", + slog.String("error", err.Error()), + ) + return exit.Error(1) + } + versionsAdapter, err := service.NewObjectAdapter(). + SetLogger(logger). + SetIDVariable("version"). + SetHandler(versionsHandler). + Build() + if err != nil { + logger.Error( + "Failed to create versions adapter", + slog.String("error", err.Error()), + ) + return exit.Error(1) + } + router.Handle( + "/O2ims_infrastructureInventory/api_versions", + versionsAdapter, + ).Methods(http.MethodGet) + router.Handle( + "/O2ims_infrastructureInventory/{version}/api_versions", + versionsAdapter, + ).Methods(http.MethodGet) + + // Create the handler that serves the information about the cloud: + cloudInfoHandler, err := service.NewCloudInfoHandler(). + SetLogger(logger). + SetCloudID(cloudID). + Build() + if err != nil { + logger.Error( + "Failed to create cloud info handler", + slog.String("error", err.Error()), + ) + return exit.Error(1) + } + cloudInfoAdapter, err := service.NewObjectAdapter(). + SetLogger(logger). + SetHandler(cloudInfoHandler). + Build() + if err != nil { + logger.Error( + "Failed to create cloud info adapter", + slog.String("error", err.Error()), + ) + return exit.Error(1) + } + router.Handle( + "/O2ims_infrastructureInventory/v1", + cloudInfoAdapter, + ).Methods(http.MethodGet) + + // Start the server: + err = http.ListenAndServe(":8080", router) + if err != nil { + logger.Error( + "server finished with error", + "error", err, + ) + return exit.Error(1) + } + + return nil +} diff --git a/internal/cmd/start_cmd.go b/internal/cmd/start_cmd.go index c3656f330..52e39a259 100644 --- a/internal/cmd/start_cmd.go +++ b/internal/cmd/start_cmd.go @@ -28,5 +28,6 @@ func Start() *cobra.Command { Args: cobra.NoArgs, } result.AddCommand(server.DeploymentManagerServer()) + result.AddCommand(server.MetadataServer()) return result } diff --git a/internal/service/cloud_info_handler.go b/internal/service/cloud_info_handler.go new file mode 100644 index 000000000..9f3281beb --- /dev/null +++ b/internal/service/cloud_info_handler.go @@ -0,0 +1,91 @@ +/* +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 service + +import ( + "context" + "errors" + "log/slog" + + "github.com/openshift-kni/oran-o2ims/internal/data" +) + +// CloudInfoHandlerBuilder contains the data and logic needed to create a new handler for the application +// root. Don't create instances of this type directly, use the NewCloudInfoHandler function instead. +type CloudInfoHandlerBuilder struct { + logger *slog.Logger + cloudID string +} + +// RootHander knows how to respond to requests for the application root. Don't create instances of +// this type directly, use the NewCloudInfoHandler function instead. +type CloudInfoHandler struct { + logger *slog.Logger + cloudID string +} + +// NewCloudInfoHandler creates a builder that can then be used to configure and create a handler for the +// root of the application. +func NewCloudInfoHandler() *CloudInfoHandlerBuilder { + return &CloudInfoHandlerBuilder{} +} + +// SetLogger sets the logger that the handler will use to write to the log. This is mandatory. +func (b *CloudInfoHandlerBuilder) SetLogger(value *slog.Logger) *CloudInfoHandlerBuilder { + b.logger = value + return b +} + +// SetCloudID sets the identifier of the O-Cloud of this handler. This is mandatory. +func (b *CloudInfoHandlerBuilder) SetCloudID(value string) *CloudInfoHandlerBuilder { + b.cloudID = value + return b +} + +// Build uses the data stored in the builder to create and configure a new handler. +func (b *CloudInfoHandlerBuilder) Build() (result *CloudInfoHandler, err error) { + // Check parameters: + if b.logger == nil { + err = errors.New("logger is mandatory") + return + } + if b.cloudID == "" { + err = errors.New("cloud identifier is mandatory") + return + } + + // Create and populate the object: + result = &CloudInfoHandler{ + logger: b.logger, + cloudID: b.cloudID, + } + return +} + +// Get is part of the implementation of the object handler interface. +func (h *CloudInfoHandler) Get(ctx context.Context, request *ObjectRequest) (response *ObjectResponse, + err error) { + response = &ObjectResponse{ + Object: data.Object{ + "oCloudId": h.cloudID, + "globalCloudId": h.cloudID, + "name": "OpenShift O-Cloud", + "description": "OpenShift O-Cloud", + "serviceUri": "https://localhost:8080", + "extensions": data.Object{}, + }, + } + return +} diff --git a/internal/service/object_adapter.go b/internal/service/object_adapter.go index 85b32ffd8..21c3d81af 100644 --- a/internal/service/object_adapter.go +++ b/internal/service/object_adapter.go @@ -54,9 +54,9 @@ func (b *ObjectAdapterBuilder) SetHandler(value ObjectHandler) *ObjectAdapterBui return b } -// SetID sets the name of the path variable that contains the identifier of the object. This is -// mandatory. -func (b *ObjectAdapterBuilder) SetID(value string) *ObjectAdapterBuilder { +// SetIDVariable sets the name of the path variable that contains the identifier of the object. This is +// optional. If not specified then no identifier will be passed to the handler. +func (b *ObjectAdapterBuilder) SetIDVariable(value string) *ObjectAdapterBuilder { b.id = value return b } @@ -72,10 +72,6 @@ func (b *ObjectAdapterBuilder) Build() (result *ObjectAdapter, err error) { err = errors.New("handler is mandatory") return } - if b.id == "" { - err = errors.New("name of path variable containing identifier is mandatory") - return - } // Prepare the JSON iterator API: jsonConfig := jsoniter.Config{ @@ -105,19 +101,7 @@ func (a *ObjectAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get the identifier: - id, ok := mux.Vars(r)[a.id] - if !ok { - a.logger.Error( - "Failed to find path variable", - "var", a.id, - ) - SendError( - w, - http.StatusInternalServerError, - "Failed to find path variable", - ) - return - } + id := mux.Vars(r)[a.id] // Create the request: request := &ObjectRequest{ @@ -134,7 +118,7 @@ func (a *ObjectAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) { SendError( w, http.StatusInternalServerError, - "Failed to get items", + "Internal error", ) return } diff --git a/internal/service/versions_handler.go b/internal/service/versions_handler.go new file mode 100644 index 000000000..fb9c507d7 --- /dev/null +++ b/internal/service/versions_handler.go @@ -0,0 +1,138 @@ +/* +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 service + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strconv" + "strings" + + "github.com/coreos/go-semver/semver" + + "github.com/openshift-kni/oran-o2ims/internal/data" +) + +// VersionsHandlerBuilder contains the data and logic needed to create a new handler that servers +// the list of versions of the API. Don't create instances of this type directly, use the +// NewVersionsHandler function instead. +type VersionsHandlerBuilder struct { + logger *slog.Logger +} + +// RootHander knows how to respond to requests for the the list of versions of the API. Don't +// create instances of this type directly, use the NewVersionsHandler function instead. +type VersionsHandler struct { + logger *slog.Logger +} + +// NewVersionsHandler creates a builder that can then be used to configure and create a handler for the +// list of versions of the API. +func NewVersionsHandler() *VersionsHandlerBuilder { + return &VersionsHandlerBuilder{} +} + +// SetLogger sets the logger that the handler will use to write to the log. This is mandatory. +func (b *VersionsHandlerBuilder) SetLogger(value *slog.Logger) *VersionsHandlerBuilder { + b.logger = value + return b +} + +// Build uses the data stored in the builder to create and configure a new handler. +func (b *VersionsHandlerBuilder) Build() (result *VersionsHandler, err error) { + // Check parameters: + if b.logger == nil { + err = errors.New("logger is mandatory") + return + } + + // Create and populate the object: + result = &VersionsHandler{ + logger: b.logger, + } + return +} + +// Get is part of the implementation of the object handler interface. +func (h *VersionsHandler) Get(ctx context.Context, request *ObjectRequest) (response *ObjectResponse, + err error) { + // If a specifc major version was included in the URL then we need to select and return + // only the ones that match that: + var selectedVersions []data.Object + if request.ID != "" { + selectedVersions = make([]data.Object, 0, 1) + if !strings.HasPrefix(request.ID, "v") { + err = fmt.Errorf( + "version identifier '%s' isn't valid, it should start with 'v'", + request.ID, + ) + return + } + var majorNumber int + majorNumber, err = strconv.Atoi(request.ID[1:]) + if err != nil { + return + } + for _, currentVersion := range allVersions { + versionValue, ok := currentVersion["version"] + if !ok { + h.logger.Error( + "Version doesn't have a version number, will ignore it", + slog.Any("version", currentVersion), + ) + continue + } + versionText, ok := versionValue.(string) + if !ok { + h.logger.Error( + "Version number isn't a string, will ignore it", + slog.Any("version", versionValue), + ) + continue + } + versionNumber, err := semver.NewVersion(versionText) + if err != nil { + h.logger.Error( + "Version number isn't a valid semantic version, will ignore it", + slog.String("version", versionText), + slog.String("error", err.Error()), + ) + continue + } + if versionNumber.Major == int64(majorNumber) { + selectedVersions = append(selectedVersions, currentVersion) + } + } + } else { + selectedVersions = allVersions + } + + // Return the result: + response = &ObjectResponse{ + Object: data.Object{ + "uriPrefix": "/O2ims_infrastructureInventory/v1", + "apiVersions": selectedVersions, + }, + } + return +} + +var allVersions = []data.Object{ + { + "version": "1.0.0", + }, +}