Implement API endpoints (#6915)

* Pass odo context to api server

* Get /instance

* DELETE /instance implementation

* Move describe logic to pkg/component/describe

* Get /component implementation

* POST /component/command implementation

* Fix example by replacing action with name

* Fix integration test

* Integration tests

* Add comment for PushWatcher

* Test DELETE /instance without --no-watch

* Apply suggestions from code review

Co-authored-by: Armel Soro <armel@rm3l.org>

* Return an error if not ready for push

* Fix windows tests

* Fix tests for Windows

---------

Co-authored-by: Armel Soro <armel@rm3l.org>
This commit is contained in:
Philippe Martin
2023-06-26 16:00:49 +02:00
committed by GitHub
parent 535ee0a105
commit 94e32303bd
10 changed files with 514 additions and 272 deletions

View File

@@ -188,7 +188,7 @@ paths:
enum:
- "push"
example:
action: push
name: push
responses:
'200':
description: command was successfully executed

View File

@@ -14,7 +14,6 @@ To see how to make this your own, look here:
- API version: 0.1
### Running the server
To run the server, follow these simple steps:

View File

@@ -2,62 +2,87 @@ package apiserver_impl
import (
"context"
"errors"
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"fmt"
"net/http"
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"github.com/redhat-developer/odo/pkg/component/describe"
"github.com/redhat-developer/odo/pkg/kclient"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/podman"
"github.com/redhat-developer/odo/pkg/state"
)
// 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 {
cancel context.CancelFunc
pushWatcher chan<- struct{}
kubeClient kclient.ClientInterface
podmanClient podman.Client
stateClient state.Client
}
// NewDefaultApiService creates a default api service
func NewDefaultApiService() openapi.DefaultApiServicer {
return &DefaultApiService{}
func NewDefaultApiService(
cancel context.CancelFunc,
pushWatcher chan<- struct{},
kubeClient kclient.ClientInterface,
podmanClient podman.Client,
stateClient state.Client,
) openapi.DefaultApiServicer {
return &DefaultApiService{
cancel: cancel,
pushWatcher: pushWatcher,
kubeClient: kubeClient,
podmanClient: podmanClient,
stateClient: stateClient,
}
}
// 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.
switch componentCommandPostRequest.Name {
case "push":
select {
case s.pushWatcher <- struct{}{}:
return openapi.Response(http.StatusOK, openapi.GeneralSuccess{
Message: "push was successfully executed",
}), nil
default:
return openapi.Response(http.StatusTooManyRequests, openapi.GeneralError{
Message: "a push operation is not possible at this time. Please retry later",
}), nil
}
// 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")
default:
return openapi.Response(http.StatusBadRequest, nil), fmt.Errorf("command name %q not supported. Supported values are: %q", componentCommandPostRequest.Name, "push")
}
}
// 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")
value, _, err := describe.DescribeDevfileComponent(ctx, s.kubeClient, s.podmanClient, s.stateClient)
if err != nil {
return openapi.Response(http.StatusInternalServerError, ""), fmt.Errorf("error getting the description of the component: %w", err)
}
return openapi.Response(http.StatusOK, value), nil
}
// 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")
s.cancel()
return openapi.Response(http.StatusOK, openapi.GeneralSuccess{
Message: fmt.Sprintf("'odo dev' instance with pid: %d is shutting down.", odocontext.GetPID(ctx)),
}), nil
}
// 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")
response := openapi.InstanceGet200Response{
Pid: int32(odocontext.GetPID(ctx)),
ComponentDirectory: odocontext.GetWorkingDirectory(ctx),
}
return openapi.Response(http.StatusOK, response), nil
}

View File

@@ -3,16 +3,38 @@ package apiserver_impl
import (
"context"
"fmt"
"net"
"net/http"
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/podman"
"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) {
type ApiServer struct {
PushWatcher <-chan struct{}
}
defaultApiService := NewDefaultApiService()
func StartServer(
ctx context.Context,
cancelFunc context.CancelFunc,
port int,
kubernetesClient kclient.ClientInterface,
podmanClient podman.Client,
stateClient state.Client,
) ApiServer {
pushWatcher := make(chan struct{})
defaultApiService := NewDefaultApiService(
cancelFunc,
pushWatcher,
kubernetesClient,
podmanClient,
stateClient,
)
defaultApiController := openapi.NewDefaultApiController(defaultApiService)
router := openapi.NewRouter(defaultApiController)
@@ -38,6 +60,9 @@ func StartServer(ctx context.Context, cancelFunc context.CancelFunc, port int, s
server := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: router}
var errChan = make(chan error)
go func() {
server.BaseContext = func(net.Listener) context.Context {
return ctx
}
err = server.ListenAndServe()
errChan <- err
}()
@@ -54,4 +79,8 @@ func StartServer(ctx context.Context, cancelFunc context.CancelFunc, port int, s
cancelFunc()
}
}()
return ApiServer{
PushWatcher: pushWatcher,
}
}

View File

@@ -0,0 +1,249 @@
package describe
import (
"context"
"errors"
"fmt"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/generator"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
clierrors "github.com/redhat-developer/odo/pkg/odo/cli/errors"
"github.com/redhat-developer/odo/pkg/odo/cli/feature"
"github.com/redhat-developer/odo/pkg/odo/commonflags"
fcontext "github.com/redhat-developer/odo/pkg/odo/commonflags/context"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/podman"
"github.com/redhat-developer/odo/pkg/state"
"k8s.io/klog"
)
// DescribeDevfileComponent describes the component defined by the devfile in the current directory
func DescribeDevfileComponent(
ctx context.Context,
kubeClient kclient.ClientInterface,
podmanClient podman.Client,
stateClient state.Client,
) (result api.Component, devfile *parser.DevfileObj, err error) {
var (
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
componentName = odocontext.GetComponentName(ctx)
)
isPlatformFeatureEnabled := feature.IsEnabled(ctx, feature.GenericPlatformFlag)
platform := fcontext.GetPlatform(ctx, "")
switch platform {
case "":
if kubeClient == nil {
log.Warning(kclient.NewNoConnectionError())
}
if isPlatformFeatureEnabled && podmanClient == nil {
log.Warning(podman.NewPodmanNotFoundError(nil))
}
case commonflags.PlatformCluster:
if kubeClient == nil {
return api.Component{}, nil, kclient.NewNoConnectionError()
}
podmanClient = nil
case commonflags.PlatformPodman:
if podmanClient == nil {
return api.Component{}, nil, podman.NewPodmanNotFoundError(nil)
}
kubeClient = nil
}
// TODO(feloy) Pass PID with `--pid` flag
allFwdPorts, err := stateClient.GetForwardedPorts(ctx)
if err != nil {
return api.Component{}, nil, err
}
if isPlatformFeatureEnabled {
for i := range allFwdPorts {
if allFwdPorts[i].Platform == "" {
allFwdPorts[i].Platform = commonflags.PlatformCluster
}
}
}
var forwardedPorts []api.ForwardedPort
switch platform {
case "":
if isPlatformFeatureEnabled {
// Read ports from all platforms
forwardedPorts = allFwdPorts
} else {
// Limit to cluster ports only
for _, p := range allFwdPorts {
if p.Platform == "" || p.Platform == commonflags.PlatformCluster {
forwardedPorts = append(forwardedPorts, p)
}
}
}
case commonflags.PlatformCluster:
for _, p := range allFwdPorts {
if p.Platform == "" || p.Platform == commonflags.PlatformCluster {
forwardedPorts = append(forwardedPorts, p)
}
}
case commonflags.PlatformPodman:
for _, p := range allFwdPorts {
if p.Platform == commonflags.PlatformPodman {
forwardedPorts = append(forwardedPorts, p)
}
}
}
runningOn, err := GetRunningOn(ctx, componentName, kubeClient, podmanClient)
if err != nil {
return api.Component{}, nil, err
}
var ingresses []api.ConnectionData
var routes []api.ConnectionData
if kubeClient != nil {
ingresses, routes, err = component.ListRoutesAndIngresses(kubeClient, componentName, odocontext.GetApplication(ctx))
if err != nil {
err = clierrors.NewWarning("failed to get ingresses/routes", err)
// Do not return the error yet, as it is only a warning
}
}
cmp := api.Component{
DevfilePath: devfilePath,
DevfileData: api.GetDevfileData(*devfileObj),
DevForwardedPorts: forwardedPorts,
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,
ManagedBy: "odo",
Ingresses: ingresses,
Routes: routes,
}
if !isPlatformFeatureEnabled {
// Display RunningOn field only if the feature is enabled
cmp.RunningOn = nil
}
updateWithRemoteSourceLocation(&cmp)
return cmp, devfileObj, err
}
// DescribeNamedComponent describes a component given its name
func DescribeNamedComponent(
ctx context.Context,
name string,
kubeClient kclient.ClientInterface,
podmanClient podman.Client,
) (result api.Component, devfileObj *parser.DevfileObj, err error) {
isPlatformFeatureEnabled := feature.IsEnabled(ctx, feature.GenericPlatformFlag)
platform := fcontext.GetPlatform(ctx, "")
switch platform {
case "":
if isPlatformFeatureEnabled {
//Get info from all platforms
if kubeClient == nil {
log.Warning(kclient.NewNoConnectionError())
}
if podmanClient == nil {
log.Warning(podman.NewPodmanNotFoundError(nil))
}
} else {
if kubeClient == nil {
return api.Component{}, nil, kclient.NewNoConnectionError()
}
podmanClient = nil
}
case commonflags.PlatformCluster:
if kubeClient == nil {
return api.Component{}, nil, kclient.NewNoConnectionError()
}
podmanClient = nil
case commonflags.PlatformPodman:
if podmanClient == nil {
return api.Component{}, nil, podman.NewPodmanNotFoundError(nil)
}
kubeClient = nil
}
runningOn, err := GetRunningOn(ctx, name, kubeClient, podmanClient)
if err != nil {
return api.Component{}, nil, err
}
devfile, err := component.GetDevfileInfo(ctx, kubeClient, podmanClient, name)
if err != nil {
return api.Component{}, nil, err
}
var ingresses []api.ConnectionData
var routes []api.ConnectionData
if kubeClient != nil {
ingresses, routes, err = component.ListRoutesAndIngresses(kubeClient, name, odocontext.GetApplication(ctx))
if err != nil {
return api.Component{}, nil, fmt.Errorf("failed to get ingresses/routes: %w", err)
}
}
cmp := api.Component{
DevfileData: &api.DevfileData{
Devfile: devfile.Data,
},
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,
ManagedBy: "odo",
Ingresses: ingresses,
Routes: routes,
}
if !feature.IsEnabled(ctx, feature.GenericPlatformFlag) {
// Display RunningOn field only if the feature is enabled
cmp.RunningOn = nil
}
return cmp, &devfile, nil
}
func GetRunningOn(ctx context.Context, n string, kubeClient kclient.ClientInterface, podmanClient podman.Client) (map[string]api.RunningModes, error) {
var runningOn map[string]api.RunningModes
runningModesMap, err := component.GetRunningModes(ctx, kubeClient, podmanClient, n)
if err != nil {
if !errors.As(err, &component.NoComponentFoundError{}) {
return nil, err
}
// it is ok if the component is not deployed
runningModesMap = nil
}
if runningModesMap != nil {
runningOn = make(map[string]api.RunningModes, len(runningModesMap))
if kubeClient != nil && runningModesMap[kubeClient] != nil {
runningOn[commonflags.PlatformCluster] = runningModesMap[kubeClient]
}
if podmanClient != nil && runningModesMap[podmanClient] != nil {
runningOn[commonflags.PlatformPodman] = runningModesMap[podmanClient]
}
}
return runningOn, nil
}
func updateWithRemoteSourceLocation(cmp *api.Component) {
components, err := cmp.DevfileData.Devfile.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType},
})
if err != nil {
return
}
for _, comp := range components {
if comp.Container.GetMountSources() {
if comp.Container.SourceMapping == "" {
comp.Container.SourceMapping = generator.DevfileSourceVolumeMount
err = cmp.DevfileData.Devfile.UpdateComponent(comp)
if err != nil {
klog.V(2).Infof("error occurred while updating the component %s; cause: %s", comp.Name, err)
}
}
}
}
}

View File

@@ -37,6 +37,8 @@ type StartOptions struct {
ForwardLocalhost bool
// Variables to override in the Devfile
Variables map[string]string
// PushWatcher is a channel that will emit an event when Pushing files to the component is requested
PushWatcher <-chan struct{}
Out io.Writer
ErrOut io.Writer

View File

@@ -7,15 +7,13 @@ import (
"strings"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/generator"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/spf13/cobra"
"k8s.io/klog"
ktemplates "k8s.io/kubectl/pkg/util/templates"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/component/describe"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
clierrors "github.com/redhat-developer/odo/pkg/odo/cli/errors"
@@ -26,7 +24,6 @@ import (
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
"github.com/redhat-developer/odo/pkg/podman"
)
// ComponentRecommendedCommandName is the recommended component sub-command name
@@ -128,232 +125,9 @@ func (o *ComponentOptions) RunForJsonOutput(ctx context.Context) (out interface{
func (o *ComponentOptions) run(ctx context.Context) (result api.Component, devfileObj *parser.DevfileObj, err error) {
if o.nameFlag != "" {
return o.describeNamedComponent(ctx, o.nameFlag)
return describe.DescribeNamedComponent(ctx, o.nameFlag, o.clientset.KubernetesClient, o.clientset.PodmanClient)
}
return o.describeDevfileComponent(ctx)
}
// describeNamedComponent describes a component given its name
func (o *ComponentOptions) describeNamedComponent(ctx context.Context, name string) (result api.Component, devfileObj *parser.DevfileObj, err error) {
var (
kubeClient = o.clientset.KubernetesClient
podmanClient = o.clientset.PodmanClient
)
isPlatformFeatureEnabled := feature.IsEnabled(ctx, feature.GenericPlatformFlag)
platform := fcontext.GetPlatform(ctx, "")
switch platform {
case "":
if isPlatformFeatureEnabled {
//Get info from all platforms
if kubeClient == nil {
log.Warning(kclient.NewNoConnectionError())
}
if podmanClient == nil {
log.Warning(podman.NewPodmanNotFoundError(nil))
}
} else {
if kubeClient == nil {
return api.Component{}, nil, kclient.NewNoConnectionError()
}
podmanClient = nil
}
case commonflags.PlatformCluster:
if kubeClient == nil {
return api.Component{}, nil, kclient.NewNoConnectionError()
}
podmanClient = nil
case commonflags.PlatformPodman:
if podmanClient == nil {
return api.Component{}, nil, podman.NewPodmanNotFoundError(nil)
}
kubeClient = nil
}
runningOn, err := getRunningOn(ctx, name, kubeClient, podmanClient)
if err != nil {
return api.Component{}, nil, err
}
devfile, err := component.GetDevfileInfo(ctx, kubeClient, podmanClient, name)
if err != nil {
return api.Component{}, nil, err
}
var ingresses []api.ConnectionData
var routes []api.ConnectionData
if kubeClient != nil {
ingresses, routes, err = component.ListRoutesAndIngresses(kubeClient, name, odocontext.GetApplication(ctx))
if err != nil {
return api.Component{}, nil, fmt.Errorf("failed to get ingresses/routes: %w", err)
}
}
cmp := api.Component{
DevfileData: &api.DevfileData{
Devfile: devfile.Data,
},
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,
ManagedBy: "odo",
Ingresses: ingresses,
Routes: routes,
}
if !feature.IsEnabled(ctx, feature.GenericPlatformFlag) {
// Display RunningOn field only if the feature is enabled
cmp.RunningOn = nil
}
return cmp, &devfile, nil
}
// describeDevfileComponent describes the component defined by the devfile in the current directory
func (o *ComponentOptions) describeDevfileComponent(ctx context.Context) (result api.Component, devfile *parser.DevfileObj, err error) {
var (
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
componentName = odocontext.GetComponentName(ctx)
)
var (
kubeClient = o.clientset.KubernetesClient
podmanClient = o.clientset.PodmanClient
)
isPlatformFeatureEnabled := feature.IsEnabled(ctx, feature.GenericPlatformFlag)
platform := fcontext.GetPlatform(ctx, "")
switch platform {
case "":
if kubeClient == nil {
log.Warning(kclient.NewNoConnectionError())
}
if isPlatformFeatureEnabled && podmanClient == nil {
log.Warning(podman.NewPodmanNotFoundError(nil))
}
case commonflags.PlatformCluster:
if kubeClient == nil {
return api.Component{}, nil, kclient.NewNoConnectionError()
}
podmanClient = nil
case commonflags.PlatformPodman:
if podmanClient == nil {
return api.Component{}, nil, podman.NewPodmanNotFoundError(nil)
}
kubeClient = nil
}
// TODO(feloy) Pass PID with `--pid` flag
allFwdPorts, err := o.clientset.StateClient.GetForwardedPorts(ctx)
if err != nil {
return api.Component{}, nil, err
}
if isPlatformFeatureEnabled {
for i := range allFwdPorts {
if allFwdPorts[i].Platform == "" {
allFwdPorts[i].Platform = commonflags.PlatformCluster
}
}
}
var forwardedPorts []api.ForwardedPort
switch platform {
case "":
if isPlatformFeatureEnabled {
// Read ports from all platforms
forwardedPorts = allFwdPorts
} else {
// Limit to cluster ports only
for _, p := range allFwdPorts {
if p.Platform == "" || p.Platform == commonflags.PlatformCluster {
forwardedPorts = append(forwardedPorts, p)
}
}
}
case commonflags.PlatformCluster:
for _, p := range allFwdPorts {
if p.Platform == "" || p.Platform == commonflags.PlatformCluster {
forwardedPorts = append(forwardedPorts, p)
}
}
case commonflags.PlatformPodman:
for _, p := range allFwdPorts {
if p.Platform == commonflags.PlatformPodman {
forwardedPorts = append(forwardedPorts, p)
}
}
}
runningOn, err := getRunningOn(ctx, componentName, kubeClient, podmanClient)
if err != nil {
return api.Component{}, nil, err
}
var ingresses []api.ConnectionData
var routes []api.ConnectionData
if kubeClient != nil {
ingresses, routes, err = component.ListRoutesAndIngresses(kubeClient, componentName, odocontext.GetApplication(ctx))
if err != nil {
err = clierrors.NewWarning("failed to get ingresses/routes", err)
// Do not return the error yet, as it is only a warning
}
}
cmp := api.Component{
DevfilePath: devfilePath,
DevfileData: api.GetDevfileData(*devfileObj),
DevForwardedPorts: forwardedPorts,
RunningIn: api.MergeRunningModes(runningOn),
RunningOn: runningOn,
ManagedBy: "odo",
Ingresses: ingresses,
Routes: routes,
}
if !isPlatformFeatureEnabled {
// Display RunningOn field only if the feature is enabled
cmp.RunningOn = nil
}
updateWithRemoteSourceLocation(&cmp)
return cmp, devfileObj, err
}
func updateWithRemoteSourceLocation(cmp *api.Component) {
components, err := cmp.DevfileData.Devfile.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType},
})
if err != nil {
return
}
for _, comp := range components {
if comp.Container.GetMountSources() {
if comp.Container.SourceMapping == "" {
comp.Container.SourceMapping = generator.DevfileSourceVolumeMount
err = cmp.DevfileData.Devfile.UpdateComponent(comp)
if err != nil {
klog.V(2).Infof("error occurred while updating the component %s; cause: %s", comp.Name, err)
}
}
}
}
}
func getRunningOn(ctx context.Context, n string, kubeClient kclient.ClientInterface, podmanClient podman.Client) (map[string]api.RunningModes, error) {
var runningOn map[string]api.RunningModes
runningModesMap, err := component.GetRunningModes(ctx, kubeClient, podmanClient, n)
if err != nil {
if !errors.As(err, &component.NoComponentFoundError{}) {
return nil, err
}
// it is ok if the component is not deployed
runningModesMap = nil
}
if runningModesMap != nil {
runningOn = make(map[string]api.RunningModes, len(runningModesMap))
if kubeClient != nil && runningModesMap[kubeClient] != nil {
runningOn[commonflags.PlatformCluster] = runningModesMap[kubeClient]
}
if podmanClient != nil && runningModesMap[podmanClient] != nil {
runningOn[commonflags.PlatformPodman] = runningModesMap[podmanClient]
}
}
return runningOn, nil
return describe.DescribeDevfileComponent(ctx, o.clientset.KubernetesClient, o.clientset.PodmanClient, o.clientset.StateClient)
}
func printHumanReadableOutput(ctx context.Context, cmp api.Component, devfileObj *parser.DevfileObj) error {

View File

@@ -113,7 +113,6 @@ func (o *DevOptions) PreInit() string {
func (o *DevOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args []string) error {
// Define this first so that if user hits Ctrl+c very soon after running odo dev, odo doesn't panic
o.ctx, o.cancel = context.WithCancel(ctx)
return nil
}
@@ -258,9 +257,17 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
return err
}
var apiServer apiserver_impl.ApiServer
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)
apiServer = apiserver_impl.StartServer(
ctx,
o.cancel,
o.apiServerPortFlag,
o.clientset.KubernetesClient,
o.clientset.PodmanClient,
o.clientset.StateClient,
)
}
return o.clientset.DevClient.Start(
@@ -278,6 +285,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
Variables: variables,
CustomForwardedPorts: o.forwardedPorts,
CustomAddress: o.addressFlag,
PushWatcher: apiServer.PushWatcher,
Out: o.out,
ErrOut: o.errOut,
},

View File

@@ -257,6 +257,10 @@ func (o *WatchClient) eventWatcher(
sourcesTimer.Reset(100 * time.Millisecond)
}
case <-parameters.StartOptions.PushWatcher:
o.forceSync = true
sourcesTimer.Reset(100 * time.Millisecond)
case ev := <-o.deploymentWatcher.ResultChan():
switch obj := ev.Object.(type) {
case *appsv1.Deployment:

View File

@@ -1,12 +1,18 @@
package integration
import (
"bytes"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/tests/helper"
"io"
"net/http"
"path/filepath"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/tests/helper"
"k8s.io/utils/pointer"
)
var _ = Describe("odo dev command with api server tests", func() {
@@ -63,11 +69,157 @@ var _ = Describe("odo dev command with api server tests", func() {
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))
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
})
})
}))
}
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("odo dev is run with --api-server flag", func() {
var (
devSession helper.DevSession
)
BeforeEach(func() {
opts := helper.DevSessionOpts{
RunOnPodman: podman,
StartAPIServer: true,
EnvVars: []string{"ODO_EXPERIMENTAL_MODE=true"},
}
var err error
devSession, err = helper.StartDevMode(opts)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
devSession.Stop()
devSession.WaitEnd()
})
It("should serve endpoints", func() {
By("GETting /instance", func() {
url := fmt.Sprintf("http://%s/instance", devSession.APIServerEndpoint)
resp, err := http.Get(url)
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
strBody := string(body)
helper.JsonPathExist(strBody, "pid")
helper.JsonPathContentIs(strBody, "componentDirectory", commonVar.Context)
})
By("GETting /component", func() {
url := fmt.Sprintf("http://%s/component", devSession.APIServerEndpoint)
resp, err := http.Get(url)
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
strBody := string(body)
helper.JsonPathContentIs(strBody, "devfilePath", filepath.Join(commonVar.Context, "devfile.yaml"))
helper.JsonPathContentIs(strBody, "devfileData.devfile.metadata.name", cmpName)
helper.JsonPathContentIs(strBody, "devfileData.supportedOdoFeatures.dev", "true")
helper.JsonPathContentIs(strBody, "devfileData.supportedOdoFeatures.deploy", "false")
helper.JsonPathContentIs(strBody, "devfileData.supportedOdoFeatures.debug", "false")
helper.JsonPathContentIs(strBody, "managedBy", "odo")
if podman {
helper.JsonPathDoesNotExist(strBody, "runningOn.cluster")
helper.JsonPathExist(strBody, "runningOn.podman")
helper.JsonPathContentIs(strBody, "runningOn.podman.dev", "true")
helper.JsonPathContentIs(strBody, "runningOn.podman.deploy", "false")
} else {
helper.JsonPathDoesNotExist(strBody, "runningOn.podman")
helper.JsonPathExist(strBody, "runningOn.cluster")
helper.JsonPathContentIs(strBody, "runningOn.cluster.dev", "true")
helper.JsonPathContentIs(strBody, "runningOn.cluster.deploy", "false")
}
})
})
When("/component/command endpoint is POSTed", func() {
BeforeEach(func() {
url := fmt.Sprintf("http://%s/component/command", devSession.APIServerEndpoint)
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(`{"name": "push"}`)))
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
})
It("should trigger a push", func() {
err := devSession.WaitSync()
Expect(err).ToNot(HaveOccurred())
})
})
When("/instance endpoint is DELETEd", func() {
BeforeEach(func() {
url := fmt.Sprintf("http://%s/instance", devSession.APIServerEndpoint)
req, err := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer([]byte{}))
Expect(err).ToNot(HaveOccurred())
client := &http.Client{}
resp, err := client.Do(req)
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
})
It("should terminate the dev session", func() {
devSession.WaitEnd()
fmt.Println("<<< Session terminated >>>")
})
})
})
When("odo is executed with --no-watch and --api-server flags", helper.LabelPodmanIf(podman, func() {
var devSession helper.DevSession
BeforeEach(func() {
var err error
args := []string{"--no-watch"}
devSession, err = helper.StartDevMode(helper.DevSessionOpts{
CmdlineArgs: args,
RunOnPodman: podman,
StartAPIServer: true,
EnvVars: []string{"ODO_EXPERIMENTAL_MODE=true"},
})
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
devSession.Stop()
devSession.WaitEnd()
})
When("a file in component directory is modified", func() {
BeforeEach(func() {
helper.ReplaceString(filepath.Join(commonVar.Context, "server.js"), "App started", "App is super started")
devSession.CheckNotSynced(10 * time.Second)
})
When("/component/command endpoint is POSTed", func() {
BeforeEach(func() {
url := fmt.Sprintf("http://%s/component/command", devSession.APIServerEndpoint)
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(`{"name": "push"}`)))
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
})
It("should trigger a push", func() {
err := devSession.WaitSync()
Expect(err).ToNot(HaveOccurred())
component := helper.NewComponent(cmpName, "app", labels.ComponentDevMode, commonVar.Project, commonVar.CliRunner)
execResult, _ := component.Exec("runtime", []string{"cat", "/projects/server.js"}, pointer.Bool(true))
Expect(execResult).To(ContainSubstring("App is super started"))
})
})
})
}))
}))
}
})