diff --git a/.golangci.yaml b/.golangci.yaml index e5707f4394f..a4ad2d39068 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -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. diff --git a/Makefile b/Makefile index 51a49129103..e53c7f7e1f8 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/go.mod b/go.mod index b53fc02e788..fa4a2c5f557 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/ododevapispec.yaml b/ododevapispec.yaml new file mode 100644 index 00000000000..e4bb37245ed --- /dev/null +++ b/ododevapispec.yaml @@ -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 \ No newline at end of file diff --git a/pkg/apiserver-gen/.openapi-generator-ignore b/pkg/apiserver-gen/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/pkg/apiserver-gen/.openapi-generator-ignore @@ -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 diff --git a/pkg/apiserver-gen/.openapi-generator/FILES b/pkg/apiserver-gen/.openapi-generator/FILES new file mode 100644 index 00000000000..dcaf4f3948c --- /dev/null +++ b/pkg/apiserver-gen/.openapi-generator/FILES @@ -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 diff --git a/pkg/apiserver-gen/.openapi-generator/VERSION b/pkg/apiserver-gen/.openapi-generator/VERSION new file mode 100644 index 00000000000..cd802a1ec4e --- /dev/null +++ b/pkg/apiserver-gen/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.6.0 \ No newline at end of file diff --git a/pkg/apiserver-gen/README.md b/pkg/apiserver-gen/README.md new file mode 100644 index 00000000000..b17264fc746 --- /dev/null +++ b/pkg/apiserver-gen/README.md @@ -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 +``` diff --git a/pkg/apiserver-gen/go/api.go b/pkg/apiserver-gen/go/api.go new file mode 100644 index 00000000000..c8e13b62193 --- /dev/null +++ b/pkg/apiserver-gen/go/api.go @@ -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) +} diff --git a/pkg/apiserver-gen/go/api_default.go b/pkg/apiserver-gen/go/api_default.go new file mode 100644 index 00000000000..18988caae88 --- /dev/null +++ b/pkg/apiserver-gen/go/api_default.go @@ -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) + +} diff --git a/pkg/apiserver-gen/go/error.go b/pkg/apiserver-gen/go/error.go new file mode 100644 index 00000000000..7561a1df2fd --- /dev/null +++ b/pkg/apiserver-gen/go/error.go @@ -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) + } +} diff --git a/pkg/apiserver-gen/go/helpers.go b/pkg/apiserver-gen/go/helpers.go new file mode 100644 index 00000000000..48f03265ee0 --- /dev/null +++ b/pkg/apiserver-gen/go/helpers.go @@ -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 +} diff --git a/pkg/apiserver-gen/go/impl.go b/pkg/apiserver-gen/go/impl.go new file mode 100644 index 00000000000..9ca7add4e99 --- /dev/null +++ b/pkg/apiserver-gen/go/impl.go @@ -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{} +} diff --git a/pkg/apiserver-gen/go/logger.go b/pkg/apiserver-gen/go/logger.go new file mode 100644 index 00000000000..5f54be8c3d7 --- /dev/null +++ b/pkg/apiserver-gen/go/logger.go @@ -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), + ) + }) +} diff --git a/pkg/apiserver-gen/go/model__component_command_post_request.go b/pkg/apiserver-gen/go/model__component_command_post_request.go new file mode 100644 index 00000000000..26e4fc63be3 --- /dev/null +++ b/pkg/apiserver-gen/go/model__component_command_post_request.go @@ -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) + }) +} diff --git a/pkg/apiserver-gen/go/model__component_get_200_response.go b/pkg/apiserver-gen/go/model__component_get_200_response.go new file mode 100644 index 00000000000..fc416bd3848 --- /dev/null +++ b/pkg/apiserver-gen/go/model__component_get_200_response.go @@ -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) + }) +} diff --git a/pkg/apiserver-gen/go/model__instance_get_200_response.go b/pkg/apiserver-gen/go/model__instance_get_200_response.go new file mode 100644 index 00000000000..1485bd19b4e --- /dev/null +++ b/pkg/apiserver-gen/go/model__instance_get_200_response.go @@ -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) + }) +} diff --git a/pkg/apiserver-gen/go/model_general_error.go b/pkg/apiserver-gen/go/model_general_error.go new file mode 100644 index 00000000000..2d47ac83f40 --- /dev/null +++ b/pkg/apiserver-gen/go/model_general_error.go @@ -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) + }) +} diff --git a/pkg/apiserver-gen/go/model_general_success.go b/pkg/apiserver-gen/go/model_general_success.go new file mode 100644 index 00000000000..3ef5848b2bb --- /dev/null +++ b/pkg/apiserver-gen/go/model_general_success.go @@ -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) + }) +} diff --git a/pkg/apiserver-gen/go/routers.go b/pkg/apiserver-gen/go/routers.go new file mode 100644 index 00000000000..e698f8ce1e9 --- /dev/null +++ b/pkg/apiserver-gen/go/routers.go @@ -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 +} diff --git a/pkg/apiserver-impl/api_default_service.go b/pkg/apiserver-impl/api_default_service.go new file mode 100644 index 00000000000..3af09574950 --- /dev/null +++ b/pkg/apiserver-impl/api_default_service.go @@ -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") +} diff --git a/pkg/apiserver-impl/starterserver.go b/pkg/apiserver-impl/starterserver.go new file mode 100644 index 00000000000..09174e2d329 --- /dev/null +++ b/pkg/apiserver-impl/starterserver.go @@ -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() + } + }() +} diff --git a/pkg/odo/cli/cli.go b/pkg/odo/cli/cli.go index 67f02f77638..6f706ad138a 100644 --- a/pkg/odo/cli/cli.go +++ b/pkg/odo/cli/cli.go @@ -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), diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index c3aff5f9843..9f258960a4d 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -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, diff --git a/pkg/odo/cli/feature/features.go b/pkg/odo/cli/feature/features.go index 7968b811838..cf4137eb33c 100644 --- a/pkg/odo/cli/feature/features.go +++ b/pkg/odo/cli/feature/features.go @@ -17,6 +17,10 @@ var ( GenericPlatformFlag = OdoFeature{ isExperimental: false, } + + APIServerFlag = OdoFeature{ + isExperimental: true, + } ) // IsEnabled returns whether the specified feature should be enabled or not. diff --git a/pkg/odo/cmdline/cmdline.go b/pkg/odo/cmdline/cmdline.go index 4afe67b6ccf..0cad31fd8b3 100644 --- a/pkg/odo/cmdline/cmdline.go +++ b/pkg/odo/cmdline/cmdline.go @@ -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 diff --git a/pkg/odo/cmdline/cobra.go b/pkg/odo/cmdline/cobra.go index 3fa24bfdb6b..b58c3851df2 100644 --- a/pkg/odo/cmdline/cobra.go +++ b/pkg/odo/cmdline/cobra.go @@ -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) } diff --git a/pkg/state/interface.go b/pkg/state/interface.go index 9029000786f..fd687c1173e 100644 --- a/pkg/state/interface.go +++ b/pkg/state/interface.go @@ -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 } diff --git a/pkg/state/state.go b/pkg/state/state.go index 71e4d5db109..dc72933ebbc 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -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 { diff --git a/pkg/state/types.go b/pkg/state/types.go index 0aefcca4048..feedf52b7b2 100644 --- a/pkg/state/types.go +++ b/pkg/state/types.go @@ -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"` } diff --git a/tests/helper/helper_dev.go b/tests/helper/helper_dev.go index 428d71bc74d..2e80dd02c53 100644 --- a/tests/helper/helper_dev.go +++ b/tests/helper/helper_dev.go @@ -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] +} diff --git a/tests/integration/cmd_dev_api_server_test.go b/tests/integration/cmd_dev_api_server_test.go new file mode 100644 index 00000000000..237fd4390ca --- /dev/null +++ b/tests/integration/cmd_dev_api_server_test.go @@ -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)) + }) + }) + })) + } + } +})