diff --git a/ododevapispec.yaml b/ododevapispec.yaml index e4bb37245ed..2930bfcd174 100644 --- a/ododevapispec.yaml +++ b/ododevapispec.yaml @@ -188,7 +188,7 @@ paths: enum: - "push" example: - action: push + name: push responses: '200': description: command was successfully executed diff --git a/pkg/apiserver-gen/README.md b/pkg/apiserver-gen/README.md index b17264fc746..e79af8f3f8d 100644 --- a/pkg/apiserver-gen/README.md +++ b/pkg/apiserver-gen/README.md @@ -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: diff --git a/pkg/apiserver-impl/api_default_service.go b/pkg/apiserver-impl/api_default_service.go index 3af09574950..418e6267945 100644 --- a/pkg/apiserver-impl/api_default_service.go +++ b/pkg/apiserver-impl/api_default_service.go @@ -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 } diff --git a/pkg/apiserver-impl/starterserver.go b/pkg/apiserver-impl/starterserver.go index 09174e2d329..7aa09173245 100644 --- a/pkg/apiserver-impl/starterserver.go +++ b/pkg/apiserver-impl/starterserver.go @@ -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{} +} + +func StartServer( + ctx context.Context, + cancelFunc context.CancelFunc, + port int, + kubernetesClient kclient.ClientInterface, + podmanClient podman.Client, + stateClient state.Client, +) ApiServer { - defaultApiService := NewDefaultApiService() + 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, + } } diff --git a/pkg/component/describe/describe.go b/pkg/component/describe/describe.go new file mode 100644 index 00000000000..c5a3e4e6fbf --- /dev/null +++ b/pkg/component/describe/describe.go @@ -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) + } + } + } + } +} diff --git a/pkg/dev/interface.go b/pkg/dev/interface.go index 4776c26e4ca..cc3630a0278 100644 --- a/pkg/dev/interface.go +++ b/pkg/dev/interface.go @@ -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 diff --git a/pkg/odo/cli/describe/component.go b/pkg/odo/cli/describe/component.go index b79ee56fe09..01ff33bce76 100644 --- a/pkg/odo/cli/describe/component.go +++ b/pkg/odo/cli/describe/component.go @@ -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 { diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index b02e7bca892..58e6d0c63ce 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -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, }, diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index 26cbb9f5e31..992fc5ee9cc 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -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: diff --git a/tests/integration/cmd_dev_api_server_test.go b/tests/integration/cmd_dev_api_server_test.go index 237fd4390ca..e55483f353d 100644 --- a/tests/integration/cmd_dev_api_server_test.go +++ b/tests/integration/cmd_dev_api_server_test.go @@ -1,12 +1,18 @@ package integration import ( + "bytes" "fmt" + "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" - "net/http" - "path/filepath" + "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")) + }) + }) + }) + })) + })) } })