Implement HTTP Server based on OpenAPI spec (#6835)

* Implement HTTP Server based on OpenAPI spec

Signed-off-by: Parthvi Vala <pvala@redhat.com>
Co-authored-by: Armel Soro <asoro@redhat.com>
Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Starter server when odo dev starts

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add --api-server and --api-server-port flags to start API Server; write the port to stat file; TODO: make this feature experimental

Signed-off-by: Parthvi Vala <pvala@redhat.com>
Co-authored-by: Armel Soro <asoro@redhat.com>
Co-authored-by: Philippe Martin <phmartin@redhat.com>
Signed-off-by: Parthvi Vala <pvala@redhat.com>

Make the flag experimental

Signed-off-by: Parthvi Vala <pvala@redhat.com>

Make apiserver and apiserverport flag local

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Use container image to run openapi-generator-tool instead of a local CLI

Co-authored-by: Armel Soro <asoro@redhat.com>
Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add integration test

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Use label podman

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Regenerate the api files with openapitool v6.6.0 and changes from review

Signed-off-by: Parthvi Vala <pvala@redhat.com>
Co-authored-by: Armel Soro <asoro@redhat.com>

---------

Signed-off-by: Parthvi Vala <pvala@redhat.com>
Co-authored-by: Armel Soro <asoro@redhat.com>
Co-authored-by: Philippe Martin <phmartin@redhat.com>
This commit is contained in:
Parthvi Vala
2023-06-19 19:59:36 +05:30
committed by GitHub
parent 4f46fe92ed
commit 0b012f30e5
32 changed files with 1369 additions and 17 deletions

View File

@@ -17,6 +17,8 @@ run:
# Allowed values: readonly|vendor|mod
# By default, it isn't set.
modules-download-mode: vendor
skip-dirs:
- pkg/apiserver-gen
linters:
# Note that some linters not listed below are enabled by default.

View File

@@ -151,10 +151,6 @@ cross: ## compile for multiple platforms
generate-cli-structure:
go run cmd/cli-doc/cli-doc.go structure
.PHONY: generate-cli-reference
generate-cli-reference:
go run cmd/cli-doc/cli-doc.go reference > docs/cli-reference.adoc
# run make cross before this!
.PHONY: prepare-release
prepare-release: cross ## create gzipped binaries in ./dist/release/ for uploading to GitHub release page
@@ -232,3 +228,21 @@ test-e2e:
.PHONY: test-doc-automation
test-doc-automation:
$(RUN_GINKGO) $(GINKGO_FLAGS_ONE) --junit-report="test-doc-automation.xml" tests/documentation/...
# Generate OpenAPISpec library based on ododevapispec.yaml inside pkg/apiserver-gen; this will only generate interfaces
# Actual implementation must be done inside pkg/apiserver-impl
# Apart from generating the files, this target also formats the generated files
# and removes openapi.yaml to avoid any confusion regarding ododevapispec.yaml file and which file to use.
.PHONY: generate-apiserver
generate-apiserver: ## Generate OpenAPISpec library based on ododevapispec.yaml inside pkg/apiserver-gen
podman run --rm \
-v ${PWD}:/local \
docker.io/openapitools/openapi-generator-cli:v6.6.0 \
generate \
-i /local/ododevapispec.yaml \
-g go-server \
-o /local/pkg/apiserver-gen \
--additional-properties=outputAsLibrary=true,onlyInterfaces=true,hideGenerationTimestamp=true && \
echo "Formatting generated files:" && go fmt ./pkg/apiserver-gen/... && \
echo "Removing pkg/apiserver-gen/api/openapi.yaml" && rm ./pkg/apiserver-gen/api/openapi.yaml

2
go.mod
View File

@@ -22,6 +22,7 @@ require (
github.com/go-openapi/spec v0.20.8
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9
github.com/gorilla/mux v1.8.0
github.com/jedib0t/go-pretty/v6 v6.4.3
github.com/kubernetes-sigs/service-catalog v0.3.1
github.com/mattn/go-colorable v0.1.13
@@ -126,7 +127,6 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect

213
ododevapispec.yaml Normal file
View File

@@ -0,0 +1,213 @@
openapi: '3.0.2'
info:
title: odo dev
version: '0.1'
description: API interface for 'odo dev'
servers:
- url: /api/v1
paths:
/instance:
get:
description: Get information about the this 'odo dev' instance.
responses:
'200':
description: Information about the this 'odo dev' instance.
content:
application/json:
schema:
type: object
properties:
componentDirectory:
type: string
description: Directory on which this 'odo dev' instance is running
pid:
type: integer
description: PID of the this 'odo dev' instance.
example:
componentDirectory: "/Users/user/Documents/myproject"
pid: 42
delete:
description: "Stop this 'odo dev' instance"
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralSuccess'
example:
message: "'odo dev' instance with pid: 42 is shuting down."
description: "'odo dev' instance will shutdown."
/component:
get:
description: Get the Information about the component controlled by this 'odo dev' instance.
responses:
'200':
description: Information about the component.
content:
application/json:
schema:
type: object
properties:
component:
type: object
description: Description of the component. This is the same as output of 'odo describe component -o json'
example:
{
"devfilePath": "/home/tomas/Code/odo-examples/java-maven/devfile.yaml",
"devfileData": {
"devfile": {
"schemaVersion": "2.1.0",
"metadata": {
"name": "demo",
"version": "1.1.1",
"displayName": "Maven Java",
"description": "Upstream Maven and OpenJDK 11",
"tags": [
"Java",
"Maven"
],
"icon": "https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/java-maven.jpg",
"projectType": "Maven",
"language": "Java"
},
"components": [
{
"name": "tools",
"container": {
"image": "quay.io/eclipse/che-java11-maven:next",
"env": [
{
"name": "DEBUG_PORT",
"value": "5858"
}
],
"volumeMounts": [
{
"name": "m2",
"path": "/home/user/.m2"
}
],
"memoryLimit": "512Mi",
"mountSources": true,
"dedicatedPod": false,
"endpoints": [
{
"name": "http-maven",
"targetPort": 8080,
"secure": false
}
]
}
},
{
"name": "m2",
"volume": {
"ephemeral": false
}
}
],
"starterProjects": [
{
"name": "springbootproject",
"git": {
"remotes": {
"origin": "https://github.com/odo-devfiles/springboot-ex.git"
}
}
}
],
"commands": [
{
"id": "mvn-package",
"exec": {
"group": {
"kind": "build",
"isDefault": true
},
"commandLine": "mvn -Dmaven.repo.local=/home/user/.m2/repository package",
"component": "tools",
"workingDir": "${PROJECT_SOURCE}",
"hotReloadCapable": false
}
},
{
"id": "run",
"exec": {
"group": {
"kind": "run",
"isDefault": true
},
"commandLine": "java -jar target/*.jar",
"component": "tools",
"workingDir": "${PROJECT_SOURCE}",
"hotReloadCapable": false
}
},
{
"id": "debug",
"exec": {
"group": {
"kind": "debug",
"isDefault": true
},
"commandLine": "java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=${DEBUG_PORT},suspend=n -jar target/*.jar",
"component": "tools",
"workingDir": "${PROJECT_SOURCE}",
"hotReloadCapable": false
}
}
]
},
"supportedOdoFeatures": {
"dev": true,
"deploy": false,
"debug": true
}
},
"runningIn": {
"deploy": false,
"dev": true
},
"managedBy": "odo"
}
/component/command:
post:
description: Instruct 'odo dev' to perform given command on the component
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
description: Name of the command that should be executed
type: string
enum:
- "push"
example:
action: push
responses:
'200':
description: command was successfully executed
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralSuccess'
example:
message: "push was successfully executed"
components:
schemas:
GeneralSuccess:
type: object
properties:
message:
type: string
GeneralError:
type: object
properties:
message:
type: string

View File

@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@@ -0,0 +1,14 @@
README.md
api/openapi.yaml
go/api.go
go/api_default.go
go/error.go
go/helpers.go
go/impl.go
go/logger.go
go/model__component_command_post_request.go
go/model__component_get_200_response.go
go/model__instance_get_200_response.go
go/model_general_error.go
go/model_general_success.go
go/routers.go

View File

@@ -0,0 +1 @@
6.6.0

View File

@@ -0,0 +1,33 @@
# Go API Server for openapi
API interface for 'odo dev'
## Overview
This server was generated by the [openapi-generator]
(https://openapi-generator.tech) project.
By using the [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate a server stub.
-
To see how to make this your own, look here:
[README](https://openapi-generator.tech)
- API version: 0.1
### Running the server
To run the server, follow these simple steps:
```
go run main.go
```
To run the server in a docker container
```
docker build --network=host -t openapi .
```
Once image is built use
```
docker run --rm -it openapi
```

View File

@@ -0,0 +1,36 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
import (
"context"
"net/http"
)
// DefaultApiRouter defines the required methods for binding the api requests to a responses for the DefaultApi
// The DefaultApiRouter implementation should parse necessary information from the http request,
// pass the data to a DefaultApiServicer to perform the required actions, then write the service results to the http response.
type DefaultApiRouter interface {
ComponentCommandPost(http.ResponseWriter, *http.Request)
ComponentGet(http.ResponseWriter, *http.Request)
InstanceDelete(http.ResponseWriter, *http.Request)
InstanceGet(http.ResponseWriter, *http.Request)
}
// DefaultApiServicer defines the api actions for the DefaultApi service
// This interface intended to stay up to date with the openapi yaml used to generate it,
// while the service implementation can be ignored with the .openapi-generator-ignore file
// and updated with the logic required for the API.
type DefaultApiServicer interface {
ComponentCommandPost(context.Context, ComponentCommandPostRequest) (ImplResponse, error)
ComponentGet(context.Context) (ImplResponse, error)
InstanceDelete(context.Context) (ImplResponse, error)
InstanceGet(context.Context) (ImplResponse, error)
}

View File

@@ -0,0 +1,139 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
import (
"encoding/json"
"net/http"
"strings"
)
// DefaultApiController binds http requests to an api service and writes the service results to the http response
type DefaultApiController struct {
service DefaultApiServicer
errorHandler ErrorHandler
}
// DefaultApiOption for how the controller is set up.
type DefaultApiOption func(*DefaultApiController)
// WithDefaultApiErrorHandler inject ErrorHandler into controller
func WithDefaultApiErrorHandler(h ErrorHandler) DefaultApiOption {
return func(c *DefaultApiController) {
c.errorHandler = h
}
}
// NewDefaultApiController creates a default api controller
func NewDefaultApiController(s DefaultApiServicer, opts ...DefaultApiOption) Router {
controller := &DefaultApiController{
service: s,
errorHandler: DefaultErrorHandler,
}
for _, opt := range opts {
opt(controller)
}
return controller
}
// Routes returns all the api routes for the DefaultApiController
func (c *DefaultApiController) Routes() Routes {
return Routes{
{
"ComponentCommandPost",
strings.ToUpper("Post"),
"/api/v1/component/command",
c.ComponentCommandPost,
},
{
"ComponentGet",
strings.ToUpper("Get"),
"/api/v1/component",
c.ComponentGet,
},
{
"InstanceDelete",
strings.ToUpper("Delete"),
"/api/v1/instance",
c.InstanceDelete,
},
{
"InstanceGet",
strings.ToUpper("Get"),
"/api/v1/instance",
c.InstanceGet,
},
}
}
// ComponentCommandPost -
func (c *DefaultApiController) ComponentCommandPost(w http.ResponseWriter, r *http.Request) {
componentCommandPostRequestParam := ComponentCommandPostRequest{}
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&componentCommandPostRequestParam); err != nil {
c.errorHandler(w, r, &ParsingError{Err: err}, nil)
return
}
if err := AssertComponentCommandPostRequestRequired(componentCommandPostRequestParam); err != nil {
c.errorHandler(w, r, err, nil)
return
}
result, err := c.service.ComponentCommandPost(r.Context(), componentCommandPostRequestParam)
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)
}
// ComponentGet -
func (c *DefaultApiController) ComponentGet(w http.ResponseWriter, r *http.Request) {
result, err := c.service.ComponentGet(r.Context())
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)
}
// InstanceDelete -
func (c *DefaultApiController) InstanceDelete(w http.ResponseWriter, r *http.Request) {
result, err := c.service.InstanceDelete(r.Context())
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)
}
// InstanceGet -
func (c *DefaultApiController) InstanceGet(w http.ResponseWriter, r *http.Request) {
result, err := c.service.InstanceGet(r.Context())
// If an error occurred, encode the error with the status code
if err != nil {
c.errorHandler(w, r, err, &result)
return
}
// If no error, encode the body and the result code
EncodeJSONResponse(result.Body, &result.Code, w)
}

View File

@@ -0,0 +1,62 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
import (
"errors"
"fmt"
"net/http"
)
var (
// ErrTypeAssertionError is thrown when type an interface does not match the asserted type
ErrTypeAssertionError = errors.New("unable to assert type")
)
// ParsingError indicates that an error has occurred when parsing request parameters
type ParsingError struct {
Err error
}
func (e *ParsingError) Unwrap() error {
return e.Err
}
func (e *ParsingError) Error() string {
return e.Err.Error()
}
// RequiredError indicates that an error has occurred when parsing request parameters
type RequiredError struct {
Field string
}
func (e *RequiredError) Error() string {
return fmt.Sprintf("required field '%s' is zero value.", e.Field)
}
// ErrorHandler defines the required method for handling error. You may implement it and inject this into a controller if
// you would like errors to be handled differently from the DefaultErrorHandler
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, result *ImplResponse)
// DefaultErrorHandler defines the default logic on how to handle errors from the controller. Any errors from parsing
// request params will return a StatusBadRequest. Otherwise, the error code originating from the servicer will be used.
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, result *ImplResponse) {
if _, ok := err.(*ParsingError); ok {
// Handle parsing errors
EncodeJSONResponse(err.Error(), func(i int) *int { return &i }(http.StatusBadRequest), w)
} else if _, ok := err.(*RequiredError); ok {
// Handle missing required errors
EncodeJSONResponse(err.Error(), func(i int) *int { return &i }(http.StatusUnprocessableEntity), w)
} else {
// Handle all other errors
EncodeJSONResponse(err.Error(), &result.Code, w)
}
}

View File

@@ -0,0 +1,54 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
import (
"reflect"
)
// Response return a ImplResponse struct filled
func Response(code int, body interface{}) ImplResponse {
return ImplResponse{
Code: code,
Body: body,
}
}
// IsZeroValue checks if the val is the zero-ed value.
func IsZeroValue(val interface{}) bool {
return val == nil || reflect.DeepEqual(val, reflect.Zero(reflect.TypeOf(val)).Interface())
}
// AssertRecurseInterfaceRequired recursively checks each struct in a slice against the callback.
// This method traverse nested slices in a preorder fashion.
func AssertRecurseInterfaceRequired(obj interface{}, callback func(interface{}) error) error {
return AssertRecurseValueRequired(reflect.ValueOf(obj), callback)
}
// AssertRecurseValueRequired checks each struct in the nested slice against the callback.
// This method traverse nested slices in a preorder fashion.
func AssertRecurseValueRequired(value reflect.Value, callback func(interface{}) error) error {
switch value.Kind() {
// If it is a struct we check using callback
case reflect.Struct:
if err := callback(value.Interface()); err != nil {
return err
}
// If it is a slice we continue recursion
case reflect.Slice:
for i := 0; i < value.Len(); i += 1 {
if err := AssertRecurseValueRequired(value.Index(i), callback); err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,16 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
// ImplResponse response defines an error code with the associated body
type ImplResponse struct {
Code int
Body interface{}
}

View File

@@ -0,0 +1,32 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
import (
"log"
"net/http"
"time"
)
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
log.Printf(
"%s %s %s %s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}

View File

@@ -0,0 +1,33 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type ComponentCommandPostRequest struct {
// Name of the command that should be executed
Name string `json:"name,omitempty"`
}
// AssertComponentCommandPostRequestRequired checks if the required fields are not zero-ed
func AssertComponentCommandPostRequestRequired(obj ComponentCommandPostRequest) error {
return nil
}
// AssertRecurseComponentCommandPostRequestRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of ComponentCommandPostRequest (e.g. [][]ComponentCommandPostRequest), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseComponentCommandPostRequestRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aComponentCommandPostRequest, ok := obj.(ComponentCommandPostRequest)
if !ok {
return ErrTypeAssertionError
}
return AssertComponentCommandPostRequestRequired(aComponentCommandPostRequest)
})
}

View File

@@ -0,0 +1,33 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type ComponentGet200Response struct {
// Description of the component. This is the same as output of 'odo describe component -o json'
Component map[string]interface{} `json:"component,omitempty"`
}
// AssertComponentGet200ResponseRequired checks if the required fields are not zero-ed
func AssertComponentGet200ResponseRequired(obj ComponentGet200Response) error {
return nil
}
// AssertRecurseComponentGet200ResponseRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of ComponentGet200Response (e.g. [][]ComponentGet200Response), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseComponentGet200ResponseRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aComponentGet200Response, ok := obj.(ComponentGet200Response)
if !ok {
return ErrTypeAssertionError
}
return AssertComponentGet200ResponseRequired(aComponentGet200Response)
})
}

View File

@@ -0,0 +1,36 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type InstanceGet200Response struct {
// Directory on which this 'odo dev' instance is running
ComponentDirectory string `json:"componentDirectory,omitempty"`
// PID of the this 'odo dev' instance.
Pid int32 `json:"pid,omitempty"`
}
// AssertInstanceGet200ResponseRequired checks if the required fields are not zero-ed
func AssertInstanceGet200ResponseRequired(obj InstanceGet200Response) error {
return nil
}
// AssertRecurseInstanceGet200ResponseRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of InstanceGet200Response (e.g. [][]InstanceGet200Response), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseInstanceGet200ResponseRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aInstanceGet200Response, ok := obj.(InstanceGet200Response)
if !ok {
return ErrTypeAssertionError
}
return AssertInstanceGet200ResponseRequired(aInstanceGet200Response)
})
}

View File

@@ -0,0 +1,31 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type GeneralError struct {
Message string `json:"message,omitempty"`
}
// AssertGeneralErrorRequired checks if the required fields are not zero-ed
func AssertGeneralErrorRequired(obj GeneralError) error {
return nil
}
// AssertRecurseGeneralErrorRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of GeneralError (e.g. [][]GeneralError), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseGeneralErrorRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aGeneralError, ok := obj.(GeneralError)
if !ok {
return ErrTypeAssertionError
}
return AssertGeneralErrorRequired(aGeneralError)
})
}

View File

@@ -0,0 +1,31 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
type GeneralSuccess struct {
Message string `json:"message,omitempty"`
}
// AssertGeneralSuccessRequired checks if the required fields are not zero-ed
func AssertGeneralSuccessRequired(obj GeneralSuccess) error {
return nil
}
// AssertRecurseGeneralSuccessRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of GeneralSuccess (e.g. [][]GeneralSuccess), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseGeneralSuccessRequired(objSlice interface{}) error {
return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
aGeneralSuccess, ok := obj.(GeneralSuccess)
if !ok {
return ErrTypeAssertionError
}
return AssertGeneralSuccessRequired(aGeneralSuccess)
})
}

View File

@@ -0,0 +1,296 @@
/*
* odo dev
*
* API interface for 'odo dev'
*
* API version: 0.1
* Generated by: OpenAPI Generator (https://openapi-generator.tech)
*/
package openapi
import (
"encoding/json"
"errors"
"github.com/gorilla/mux"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
)
// A Route defines the parameters for an api endpoint
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
// Routes are a collection of defined api endpoints
type Routes []Route
// Router defines the required methods for retrieving api routes
type Router interface {
Routes() Routes
}
const errMsgRequiredMissing = "required parameter is missing"
// NewRouter creates a new router for any number of api routers
func NewRouter(routers ...Router) *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, api := range routers {
for _, route := range api.Routes() {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
}
return router
}
// EncodeJSONResponse uses the json encoder to write an interface to the http response with an optional status code
func EncodeJSONResponse(i interface{}, status *int, w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if status != nil {
w.WriteHeader(*status)
} else {
w.WriteHeader(http.StatusOK)
}
if i != nil {
return json.NewEncoder(w).Encode(i)
}
return nil
}
// ReadFormFileToTempFile reads file data from a request form and writes it to a temporary file
func ReadFormFileToTempFile(r *http.Request, key string) (*os.File, error) {
_, fileHeader, err := r.FormFile(key)
if err != nil {
return nil, err
}
return readFileHeaderToTempFile(fileHeader)
}
// ReadFormFilesToTempFiles reads files array data from a request form and writes it to a temporary files
func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) {
if err := r.ParseMultipartForm(32 << 20); err != nil {
return nil, err
}
files := make([]*os.File, 0, len(r.MultipartForm.File[key]))
for _, fileHeader := range r.MultipartForm.File[key] {
file, err := readFileHeaderToTempFile(fileHeader)
if err != nil {
return nil, err
}
files = append(files, file)
}
return files, nil
}
// readFileHeaderToTempFile reads multipart.FileHeader and writes it to a temporary file
func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error) {
formFile, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer formFile.Close()
fileBytes, err := ioutil.ReadAll(formFile)
if err != nil {
return nil, err
}
file, err := ioutil.TempFile("", fileHeader.Filename)
if err != nil {
return nil, err
}
defer func() {
_ = file.Close()
}()
file.Write(fileBytes)
return file, nil
}
// parseFloatParameter parses a string parameter to an int64.
func parseFloatParameter(param string, bitSize int, required bool) (float64, error) {
if param == "" {
if required {
return 0, errors.New(errMsgRequiredMissing)
}
return 0, nil
}
return strconv.ParseFloat(param, bitSize)
}
// parseFloat64Parameter parses a string parameter to an float64.
func parseFloat64Parameter(param string, required bool) (float64, error) {
return parseFloatParameter(param, 64, required)
}
// parseFloat32Parameter parses a string parameter to an float32.
func parseFloat32Parameter(param string, required bool) (float32, error) {
val, err := parseFloatParameter(param, 32, required)
return float32(val), err
}
// parseIntParameter parses a string parameter to an int64.
func parseIntParameter(param string, bitSize int, required bool) (int64, error) {
if param == "" {
if required {
return 0, errors.New(errMsgRequiredMissing)
}
return 0, nil
}
return strconv.ParseInt(param, 10, bitSize)
}
// parseInt64Parameter parses a string parameter to an int64.
func parseInt64Parameter(param string, required bool) (int64, error) {
return parseIntParameter(param, 64, required)
}
// parseInt32Parameter parses a string parameter to an int32.
func parseInt32Parameter(param string, required bool) (int32, error) {
val, err := parseIntParameter(param, 32, required)
return int32(val), err
}
// parseBoolParameter parses a string parameter to a bool
func parseBoolParameter(param string, required bool) (bool, error) {
if param == "" {
if required {
return false, errors.New(errMsgRequiredMissing)
}
return false, nil
}
val, err := strconv.ParseBool(param)
if err != nil {
return false, err
}
return bool(val), nil
}
// parseFloat64ArrayParameter parses a string parameter containing array of values to []Float64.
func parseFloat64ArrayParameter(param, delim string, required bool) ([]float64, error) {
if param == "" {
if required {
return nil, errors.New(errMsgRequiredMissing)
}
return nil, nil
}
str := strings.Split(param, delim)
floats := make([]float64, len(str))
for i, s := range str {
if v, err := strconv.ParseFloat(s, 64); err != nil {
return nil, err
} else {
floats[i] = v
}
}
return floats, nil
}
// parseFloat32ArrayParameter parses a string parameter containing array of values to []float32.
func parseFloat32ArrayParameter(param, delim string, required bool) ([]float32, error) {
if param == "" {
if required {
return nil, errors.New(errMsgRequiredMissing)
}
return nil, nil
}
str := strings.Split(param, delim)
floats := make([]float32, len(str))
for i, s := range str {
if v, err := strconv.ParseFloat(s, 32); err != nil {
return nil, err
} else {
floats[i] = float32(v)
}
}
return floats, nil
}
// parseInt64ArrayParameter parses a string parameter containing array of values to []int64.
func parseInt64ArrayParameter(param, delim string, required bool) ([]int64, error) {
if param == "" {
if required {
return nil, errors.New(errMsgRequiredMissing)
}
return nil, nil
}
str := strings.Split(param, delim)
ints := make([]int64, len(str))
for i, s := range str {
if v, err := strconv.ParseInt(s, 10, 64); err != nil {
return nil, err
} else {
ints[i] = v
}
}
return ints, nil
}
// parseInt32ArrayParameter parses a string parameter containing array of values to []int32.
func parseInt32ArrayParameter(param, delim string, required bool) ([]int32, error) {
if param == "" {
if required {
return nil, errors.New(errMsgRequiredMissing)
}
return nil, nil
}
str := strings.Split(param, delim)
ints := make([]int32, len(str))
for i, s := range str {
if v, err := strconv.ParseInt(s, 10, 32); err != nil {
return nil, err
} else {
ints[i] = int32(v)
}
}
return ints, nil
}

View File

@@ -0,0 +1,63 @@
package apiserver_impl
import (
"context"
"errors"
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"net/http"
)
// DefaultApiService is a service that implements the logic for the DefaultApiServicer
// This service should implement the business logic for every endpoint for the DefaultApi API.
// Include any external packages or services that will be required by this service.
type DefaultApiService struct {
}
// NewDefaultApiService creates a default api service
func NewDefaultApiService() openapi.DefaultApiServicer {
return &DefaultApiService{}
}
// ComponentCommandPost -
func (s *DefaultApiService) ComponentCommandPost(ctx context.Context, componentCommandPostRequest openapi.ComponentCommandPostRequest) (openapi.ImplResponse, error) {
// TODO - update ComponentCommandPost with the required logic for this service method.
// Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.
// TODO: Uncomment the next line to return response Response(200, GeneralSuccess{}) or use other options such as http.Ok ...
// return Response(200, GeneralSuccess{}), nil
return openapi.Response(http.StatusNotImplemented, nil), errors.New("ComponentCommandPost method not implemented")
}
// ComponentGet -
func (s *DefaultApiService) ComponentGet(ctx context.Context) (openapi.ImplResponse, error) {
// TODO - update ComponentGet with the required logic for this service method.
// Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.
// TODO: Uncomment the next line to return response Response(200, ComponentGet200Response{}) or use other options such as http.Ok ...
// return Response(200, ComponentGet200Response{}), nil
return openapi.Response(http.StatusNotImplemented, nil), errors.New("ComponentGet method not implemented")
}
// InstanceDelete -
func (s *DefaultApiService) InstanceDelete(ctx context.Context) (openapi.ImplResponse, error) {
// TODO - update InstanceDelete with the required logic for this service method.
// Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.
// TODO: Uncomment the next line to return response Response(200, GeneralSuccess{}) or use other options such as http.Ok ...
// return Response(200, GeneralSuccess{}), nil
return openapi.Response(http.StatusNotImplemented, nil), errors.New("InstanceDelete method not implemented")
}
// InstanceGet -
func (s *DefaultApiService) InstanceGet(ctx context.Context) (openapi.ImplResponse, error) {
// TODO - update InstanceGet with the required logic for this service method.
// Add api_default_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.
// TODO: Uncomment the next line to return response Response(200, InstanceGet200Response{}) or use other options such as http.Ok ...
// return Response(200, InstanceGet200Response{}), nil
return openapi.Response(http.StatusNotImplemented, nil), errors.New("InstanceGet method not implemented")
}

View File

@@ -0,0 +1,57 @@
package apiserver_impl
import (
"context"
"fmt"
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"github.com/redhat-developer/odo/pkg/state"
"github.com/redhat-developer/odo/pkg/util"
"k8s.io/klog"
"net/http"
)
func StartServer(ctx context.Context, cancelFunc context.CancelFunc, port int, stateClient state.Client) {
defaultApiService := NewDefaultApiService()
defaultApiController := openapi.NewDefaultApiController(defaultApiService)
router := openapi.NewRouter(defaultApiController)
var err error
if port == 0 {
port, err = util.NextFreePort(20000, 30001, nil, "")
if err != nil {
klog.V(0).Infof("Unable to start the API server; encountered error: %v", err)
cancelFunc()
}
}
err = stateClient.SetAPIServerPort(ctx, port)
if err != nil {
klog.V(0).Infof("Unable to start the API server; encountered error: %v", err)
cancelFunc()
}
klog.V(0).Infof("API Server started at localhost:%d/api/v1", port)
server := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: router}
var errChan = make(chan error)
go func() {
err = server.ListenAndServe()
errChan <- err
}()
go func() {
select {
case <-ctx.Done():
klog.V(0).Infof("Shutting down the API server: %v", ctx.Err())
err = server.Shutdown(ctx)
if err != nil {
klog.V(1).Infof("Error while shutting down the API server: %v", err)
}
case err = <-errChan:
klog.V(0).Infof("Stopping the API server; encountered error: %v", err)
cancelFunc()
}
}()
}

View File

@@ -189,7 +189,7 @@ func odoRootCmd(ctx context.Context, name, fullName string, testClientset client
_delete.NewCmdDelete(ctx, _delete.RecommendedCommandName, util.GetFullName(fullName, _delete.RecommendedCommandName), testClientset),
add.NewCmdAdd(add.RecommendedCommandName, util.GetFullName(fullName, add.RecommendedCommandName), testClientset),
remove.NewCmdRemove(remove.RecommendedCommandName, util.GetFullName(fullName, remove.RecommendedCommandName), testClientset),
dev.NewCmdDev(dev.RecommendedCommandName, util.GetFullName(fullName, dev.RecommendedCommandName), testClientset),
dev.NewCmdDev(ctx, dev.RecommendedCommandName, util.GetFullName(fullName, dev.RecommendedCommandName), testClientset),
alizer.NewCmdAlizer(alizer.RecommendedCommandName, util.GetFullName(fullName, alizer.RecommendedCommandName), testClientset),
describe.NewCmdDescribe(ctx, describe.RecommendedCommandName, util.GetFullName(fullName, describe.RecommendedCommandName), testClientset),
registry.NewCmdRegistry(registry.RecommendedCommandName, util.GetFullName(fullName, registry.RecommendedCommandName), testClientset),

View File

@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
apiserver_impl "github.com/redhat-developer/odo/pkg/apiserver-impl"
"github.com/redhat-developer/odo/pkg/odo/cli/feature"
"io"
"path/filepath"
"regexp"
@@ -70,6 +72,8 @@ type DevOptions struct {
portForwardFlag []string
addressFlag string
noCommandsFlag bool
apiServerFlag bool
apiServerPortFlag int
}
var _ genericclioptions.Runnable = (*DevOptions)(nil)
@@ -173,6 +177,12 @@ func (o *DevOptions) Validate(ctx context.Context) error {
return err
}
if o.apiServerFlag && o.apiServerPortFlag != 0 {
if !util.IsPortFree(o.apiServerPortFlag, "") {
return fmt.Errorf("port %d is not free; please try another port", o.apiServerPortFlag)
}
}
return nil
}
@@ -242,6 +252,11 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
return err
}
if o.apiServerFlag {
// Start the server here; it will be shutdown when context is cancelled; or if the server encounters an error
apiserver_impl.StartServer(ctx, o.cancel, o.apiServerPortFlag, o.clientset.StateClient)
}
return o.clientset.DevClient.Start(
o.ctx,
dev.StartOptions{
@@ -282,7 +297,7 @@ func (o *DevOptions) Cleanup(ctx context.Context, commandError error) {
}
// NewCmdDev implements the odo dev command
func NewCmdDev(name, fullName string, testClientset clientset.Clientset) *cobra.Command {
func NewCmdDev(ctx context.Context, name, fullName string, testClientset clientset.Clientset) *cobra.Command {
o := NewDevOptions()
devCmd := &cobra.Command{
Use: name,
@@ -311,6 +326,10 @@ It forwards endpoints with any exposure values ('public', 'internal' or 'none')
devCmd.Flags().StringVar(&o.addressFlag, "address", "127.0.0.1", "Define custom address for port forwarding.")
devCmd.Flags().BoolVar(&o.noCommandsFlag, "no-commands", false, "Do not run any commands; just start the development environment.")
if feature.IsExperimentalModeEnabled(ctx) {
devCmd.Flags().BoolVar(&o.apiServerFlag, "api-server", false, "Start the API Server; this is an experimental feature")
devCmd.Flags().IntVar(&o.apiServerPortFlag, "api-server-port", 0, "Define custom port for API Server; this flag should be used in combination with --api-server flag.")
}
clientset.Add(devCmd,
clientset.BINDING,
clientset.DEV,

View File

@@ -17,6 +17,10 @@ var (
GenericPlatformFlag = OdoFeature{
isExperimental: false,
}
APIServerFlag = OdoFeature{
isExperimental: true,
}
)
// IsEnabled returns whether the specified feature should be enabled or not.

View File

@@ -4,7 +4,6 @@ package cmdline
import (
"context"
"github.com/redhat-developer/odo/pkg/kclient"
)
@@ -15,7 +14,7 @@ type Cmdline interface {
// GetFlags returns a map of flags set
GetFlags() map[string]string
// FlagValue returns the value for a flag
// FlagValue returns the string value for a flag
FlagValue(flagName string) (string, error)
// FlagValueIfSet returns the value for a flag, or an empty string if not set

View File

@@ -59,7 +59,7 @@ func (o *Cobra) GetWorkingDirectory() (string, error) {
return dfutil.GetAbsPath(".")
}
// FlagValueIfSet retrieves the value of the specified flag if it is set for the given command
// FlagValue retrieves the value of the specified flag if it is set for the given command
func (o *Cobra) FlagValue(flagName string) (string, error) {
return o.cmd.Flags().GetString(flagName)
}

View File

@@ -18,4 +18,7 @@ type Client interface {
// SaveExit resets the state file to indicate odo is not running
SaveExit(ctx context.Context) error
// SetAPIServerPort sets the port where API server is listening in the state file and saves it to the file, updating the metadata
SetAPIServerPort(ctx context.Context, port int) error
}

View File

@@ -92,6 +92,17 @@ func (o *State) SaveExit(ctx context.Context) error {
return o.saveCommonIfOwner(pid)
}
func (o *State) SetAPIServerPort(ctx context.Context, port int) error {
var (
pid = odocontext.GetPID(ctx)
platform = fcontext.GetPlatform(ctx, commonflags.PlatformCluster)
)
o.content.APIServerPort = port
o.content.Platform = platform
return o.save(ctx, pid)
}
// save writes the content structure in json format in file
func (o *State) save(ctx context.Context, pid int) error {

View File

@@ -11,4 +11,5 @@ type Content struct {
Platform string `json:"platform"`
// ForwardedPorts are the ports forwarded during odo dev session
ForwardedPorts []api.ForwardedPort `json:"forwardedPorts"`
APIServerPort int `json:"apiServerPort"`
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/ActiveState/termtest/expect"
@@ -110,13 +111,14 @@ import (
*/
type DevSession struct {
session *gexec.Session
stopped bool
console *expect.Console
address string
StdOut string
ErrOut string
Endpoints map[string]string
session *gexec.Session
stopped bool
console *expect.Console
address string
StdOut string
ErrOut string
Endpoints map[string]string
APIServerEndpoint string
}
type DevSessionOpts struct {
@@ -128,6 +130,8 @@ type DevSessionOpts struct {
NoWatch bool
NoCommands bool
CustomAddress string
StartAPIServer bool
APIServerPort int
}
// StartDevMode starts a dev session with `odo dev`
@@ -156,6 +160,12 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
if options.CustomAddress != "" {
args = append(args, "--address", options.CustomAddress)
}
if options.StartAPIServer {
args = append(args, "--api-server")
if options.APIServerPort != 0 {
args = append(args, "--api-server-port", fmt.Sprintf("%d", options.APIServerPort))
}
}
args = append(args, options.CmdlineArgs...)
cmd := Cmd("odo", args...)
cmd.Cmd.Stdin = c.Tty()
@@ -186,6 +196,10 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
result.StdOut = string(outContents)
result.ErrOut = string(errContents)
result.Endpoints = getPorts(string(outContents), options.CustomAddress)
if options.StartAPIServer {
// errContents because the server message is still printed as a log/warning
result.APIServerEndpoint = getAPIServerPort(string(errContents))
}
return result, nil
}
@@ -358,3 +372,12 @@ func getPorts(s, address string) map[string]string {
}
return result
}
// getAPIServerPort returns the address at which api server is running
//
// `I0617 11:40:44.124391 49578 starterserver.go:36] API Server started at localhost:20000/api/v1`
func getAPIServerPort(s string) string {
re := regexp.MustCompile(`(API Server started at localhost:[0-9]+\/api\/v1)`)
matches := re.FindString(s)
return strings.Split(matches, "at ")[1]
}

View File

@@ -0,0 +1,73 @@
package integration
import (
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/tests/helper"
"net/http"
"path/filepath"
)
var _ = Describe("odo dev command with api server tests", func() {
var cmpName string
var commonVar helper.CommonVar
// This is run before every Spec (It)
var _ = BeforeEach(func() {
commonVar = helper.CommonBeforeEach()
cmpName = helper.RandString(6)
helper.Chdir(commonVar.Context)
Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeFalse())
})
// This is run after every Spec (It)
var _ = AfterEach(func() {
helper.CommonAfterEach(commonVar)
})
for _, podman := range []bool{false, true} {
podman := podman
for _, customPort := range []bool{false, true} {
customPort := customPort
When("the component is bootstrapped", helper.LabelPodmanIf(podman, func() {
BeforeEach(func() {
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile.yaml"), filepath.Join(commonVar.Context, "devfile.yaml"), cmpName)
})
When(fmt.Sprintf("odo dev is run with --api-server flag (custom api server port=%v)", customPort), func() {
var (
devSession helper.DevSession
localPort = helper.GetCustomStartPort()
)
BeforeEach(func() {
opts := helper.DevSessionOpts{
RunOnPodman: podman,
StartAPIServer: true,
EnvVars: []string{"ODO_EXPERIMENTAL_MODE=true"},
}
if customPort {
opts.APIServerPort = localPort
}
var err error
devSession, err = helper.StartDevMode(opts)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
devSession.Stop()
devSession.WaitEnd()
})
It("should start the Dev server when --api-server flag is passed", func() {
if customPort {
Expect(devSession.APIServerEndpoint).To(ContainSubstring(fmt.Sprintf("%d", localPort)))
}
url := fmt.Sprintf("http://%s/instance", devSession.APIServerEndpoint)
resp, err := http.Get(url)
Expect(err).ToNot(HaveOccurred())
// TODO: Change this once it is implemented
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusNotImplemented))
})
})
}))
}
}
})