Simplify devfile Kubernetes adapter (#6762)

* Get values from context

* Move Devfile param to WatchParams and biuld adapter only once

* Move pkg/devfile/adapters/kubernetes/* into pkg/dev/kubedev

* Rename Push to reconcile and split in 2 parts: components and innreloop

* Pass out ans errout as startOptions

* Embed StartOptions into PushParameters

* Embed StartOptions into WatchParameters

* Fix passing startoptions

* Deduplicate options (out, ...)

* Revert adding unwanted files

* Fix wait app ready
This commit is contained in:
Philippe Martin
2023-04-25 11:13:19 +02:00
committed by GitHub
parent 6aacbe242b
commit e9c5f86563
28 changed files with 790 additions and 899 deletions

View File

@@ -18,7 +18,7 @@ import (
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/configAutomount"
"github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/storage"
"github.com/redhat-developer/odo/pkg/dev/kubedev/storage"
"github.com/redhat-developer/odo/pkg/devfile/image"
"github.com/redhat-developer/odo/pkg/kclient"
odolabels "github.com/redhat-developer/odo/pkg/labels"

View File

@@ -1,4 +1,4 @@
package adapters
package common
import (
"path/filepath"

View File

@@ -1,4 +1,4 @@
package adapters
package common
import (
"testing"

View File

@@ -1,4 +1,4 @@
package adapters
package common
import "fmt"

17
pkg/dev/common/types.go Normal file
View File

@@ -0,0 +1,17 @@
package common
import (
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/dev"
)
// PushParameters is a struct containing the parameters to be used when pushing to a devfile component
type PushParameters struct {
StartOptions dev.StartOptions
Devfile parser.DevfileObj
WatchFiles []string // Optional: WatchFiles is the list of changed files detected by odo watch. If empty or nil, odo will check .odo/odo-file-index.json to determine changed files
WatchDeletedFiles []string // Optional: WatchDeletedFiles is the list of deleted files detected by odo watch. If empty or nil, odo will check .odo/odo-file-index.json to determine deleted files
Show bool // Show tells whether the devfile command output should be shown on stdout
DevfileScanIndexForWatch bool // DevfileScanIndexForWatch is true if watch's push should regenerate the index file during SyncFiles, false otherwise. See 'pkg/sync/adapter.go' for details
}

View File

@@ -2,8 +2,9 @@ package dev
import (
"context"
"github.com/redhat-developer/odo/pkg/api"
"io"
"github.com/redhat-developer/odo/pkg/api"
)
type StartOptions struct {
@@ -31,6 +32,9 @@ type StartOptions struct {
ForwardLocalhost bool
// Variables to override in the Devfile
Variables map[string]string
Out io.Writer
ErrOut io.Writer
}
type Client interface {
@@ -39,8 +43,6 @@ type Client interface {
// It logs messages and errors to out and errOut.
Start(
ctx context.Context,
out io.Writer,
errOut io.Writer,
options StartOptions,
) error

View File

@@ -1,4 +1,4 @@
package component
package kubedev
import (
"context"

View File

@@ -0,0 +1,216 @@
package kubedev
import (
"context"
"fmt"
"path/filepath"
"time"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/log"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/port"
"github.com/redhat-developer/odo/pkg/sync"
"github.com/redhat-developer/odo/pkg/util"
"github.com/redhat-developer/odo/pkg/watch"
"k8s.io/klog"
)
func (o *DevClient) innerloop(ctx context.Context, parameters common.PushParameters, componentStatus *watch.ComponentStatus) error {
var (
appName = odocontext.GetApplication(ctx)
componentName = odocontext.GetComponentName(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
)
// Now the Deployment has a Ready replica, we can get the Pod to work inside it
pod, err := o.kubernetesClient.GetPodUsingComponentName(componentName)
if err != nil {
return fmt.Errorf("unable to get pod for component %s: %w", componentName, err)
}
// Find at least one pod with the source volume mounted, error out if none can be found
containerName, syncFolder, err := common.GetFirstContainerWithSourceVolume(pod.Spec.Containers)
if err != nil {
return fmt.Errorf("error while retrieving container from pod %s with a mounted project volume: %w", pod.GetName(), err)
}
s := log.Spinner("Syncing files into the container")
defer s.End(false)
// Get commands
pushDevfileCommands, err := o.getPushDevfileCommands(parameters)
if err != nil {
return fmt.Errorf("failed to validate devfile build and run commands: %w", err)
}
podChanged := componentStatus.State == watch.StateWaitDeployment
// Get a sync adapter. Check if project files have changed and sync accordingly
compInfo := sync.ComponentInfo{
ComponentName: componentName,
ContainerName: containerName,
PodName: pod.GetName(),
SyncFolder: syncFolder,
}
cmdKind := devfilev1.RunCommandGroupKind
cmdName := parameters.StartOptions.RunCommand
if parameters.StartOptions.Debug {
cmdKind = devfilev1.DebugCommandGroupKind
cmdName = parameters.StartOptions.DebugCommand
}
syncParams := sync.SyncParameters{
Path: path,
WatchFiles: parameters.WatchFiles,
WatchDeletedFiles: parameters.WatchDeletedFiles,
IgnoredFiles: parameters.StartOptions.IgnorePaths,
DevfileScanIndexForWatch: parameters.DevfileScanIndexForWatch,
CompInfo: compInfo,
ForcePush: !o.deploymentExists || podChanged,
Files: common.GetSyncFilesFromAttributes(pushDevfileCommands[cmdKind]),
}
execRequired, err := o.syncClient.SyncFiles(ctx, syncParams)
if err != nil {
componentStatus.State = watch.StateReady
return fmt.Errorf("failed to sync to component with name %s: %w", componentName, err)
}
s.End(true)
// PostStart events from the devfile will only be executed when the component
// didn't previously exist
if !componentStatus.PostStartEventsDone && libdevfile.HasPostStartEvents(parameters.Devfile) {
err = libdevfile.ExecPostStartEvents(ctx, parameters.Devfile, component.NewExecHandler(o.kubernetesClient, o.execClient, appName, componentName, pod.Name, "Executing post-start command in container", parameters.Show))
if err != nil {
return err
}
}
componentStatus.PostStartEventsDone = true
cmd, err := libdevfile.ValidateAndGetCommand(parameters.Devfile, cmdName, cmdKind)
if err != nil {
return err
}
commandType, err := parsercommon.GetCommandType(cmd)
if err != nil {
return err
}
var running bool
var isComposite bool
cmdHandler := runHandler{
fs: o.filesystem,
execClient: o.execClient,
kubeClient: o.kubernetesClient,
appName: appName,
componentName: componentName,
devfile: parameters.Devfile,
path: path,
podName: pod.GetName(),
ctx: ctx,
}
if commandType == devfilev1.ExecCommandType {
running, err = cmdHandler.IsRemoteProcessForCommandRunning(ctx, cmd, pod.Name)
if err != nil {
return err
}
} else if commandType == devfilev1.CompositeCommandType {
// this handler will run each command in this composite command individually,
// and will determine whether each command is running or not.
isComposite = true
} else {
return fmt.Errorf("unsupported type %q for Devfile command %s, only exec and composite are handled",
commandType, cmd.Id)
}
cmdHandler.componentExists = running || isComposite
klog.V(4).Infof("running=%v, execRequired=%v",
running, execRequired)
if isComposite || !running || execRequired {
// Invoke the build command once (before calling libdevfile.ExecuteCommandByNameAndKind), as, if cmd is a composite command,
// the handler we pass will be called for each command in that composite command.
doExecuteBuildCommand := func() error {
execHandler := component.NewExecHandler(o.kubernetesClient, o.execClient, appName, componentName, pod.Name,
"Building your application in container", parameters.Show)
return libdevfile.Build(ctx, parameters.Devfile, parameters.StartOptions.BuildCommand, execHandler)
}
if running {
if cmd.Exec == nil || !util.SafeGetBool(cmd.Exec.HotReloadCapable) {
if err = doExecuteBuildCommand(); err != nil {
return err
}
}
} else {
if err = doExecuteBuildCommand(); err != nil {
return err
}
}
err = libdevfile.ExecuteCommandByNameAndKind(ctx, parameters.Devfile, cmdName, cmdKind, &cmdHandler, false)
if err != nil {
return err
}
}
if podChanged || o.portsChanged {
o.portForwardClient.StopPortForwarding(ctx, componentName)
}
// Check that the application is actually listening on the ports declared in the Devfile, so we are sure that port-forwarding will work
appReadySpinner := log.Spinner("Waiting for the application to be ready")
err = o.checkAppPorts(ctx, pod.Name, o.portsToForward)
appReadySpinner.End(err == nil)
if err != nil {
log.Warningf("Port forwarding might not work correctly: %v", err)
log.Warning("Running `odo logs --follow` might help in identifying the problem.")
fmt.Fprintln(log.GetStdout())
}
err = o.portForwardClient.StartPortForwarding(ctx, parameters.Devfile, componentName, parameters.StartOptions.Debug, parameters.StartOptions.RandomPorts, log.GetStdout(), parameters.StartOptions.ErrOut, parameters.StartOptions.CustomForwardedPorts)
if err != nil {
return common.NewErrPortForward(err)
}
componentStatus.EndpointsForwarded = o.portForwardClient.GetForwardedPorts()
componentStatus.State = watch.StateReady
return nil
}
func (o *DevClient) getPushDevfileCommands(parameters common.PushParameters) (map[devfilev1.CommandGroupKind]devfilev1.Command, error) {
pushDevfileCommands, err := libdevfile.ValidateAndGetPushCommands(parameters.Devfile, parameters.StartOptions.BuildCommand, parameters.StartOptions.RunCommand)
if err != nil {
return nil, fmt.Errorf("failed to validate devfile build and run commands: %w", err)
}
if parameters.StartOptions.Debug {
pushDevfileDebugCommands, e := libdevfile.ValidateAndGetCommand(parameters.Devfile, parameters.StartOptions.DebugCommand, devfilev1.DebugCommandGroupKind)
if e != nil {
return nil, fmt.Errorf("debug command is not valid: %w", e)
}
pushDevfileCommands[devfilev1.DebugCommandGroupKind] = pushDevfileDebugCommands
}
return pushDevfileCommands, nil
}
func (o *DevClient) checkAppPorts(ctx context.Context, podName string, portsToFwd map[string][]devfilev1.Endpoint) error {
containerPortsMapping := make(map[string][]int)
for c, ports := range portsToFwd {
for _, p := range ports {
containerPortsMapping[c] = append(containerPortsMapping[c], p.TargetPort)
}
}
return port.CheckAppPortsListening(ctx, o.execClient, podName, containerPortsMapping, 1*time.Minute)
}

View File

@@ -3,30 +3,26 @@ package kubedev
import (
"context"
"fmt"
"io"
"path/filepath"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/redhat-developer/odo/pkg/binding"
_delete "github.com/redhat-developer/odo/pkg/component/delete"
"github.com/redhat-developer/odo/pkg/configAutomount"
"github.com/redhat-developer/odo/pkg/dev"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/devfile/location"
"github.com/redhat-developer/odo/pkg/exec"
"github.com/redhat-developer/odo/pkg/kclient"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/portForward"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/sync"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
"github.com/redhat-developer/odo/pkg/watch"
"k8s.io/klog"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/component"
"github.com/redhat-developer/odo/pkg/devfile/location"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/watch"
)
const (
@@ -47,6 +43,13 @@ type DevClient struct {
execClient exec.Client
deleteClient _delete.Client
configAutomountClient configAutomount.Client
// deploymentExists is true when the deployment is already created when calling createComponents
deploymentExists bool
// portsChanged is true of ports have changed since the last call to createComponents
portsChanged bool
// portsToForward lists the port to forward during inner loop (TODO move port forward to createComponents)
portsToForward map[string][]devfilev1.Endpoint
}
var _ dev.Client = (*DevClient)(nil)
@@ -79,116 +82,52 @@ func NewDevClient(
func (o *DevClient) Start(
ctx context.Context,
out io.Writer,
errOut io.Writer,
options dev.StartOptions,
) error {
klog.V(4).Infoln("Creating new adapter")
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)
devfileObj = odocontext.GetDevfileObj(ctx)
)
adapter := component.NewKubernetesAdapter(
o.kubernetesClient,
o.prefClient,
o.portForwardClient,
o.bindingClient,
o.syncClient,
o.execClient,
o.configAutomountClient,
component.AdapterContext{
ComponentName: componentName,
Context: path,
AppName: odocontext.GetApplication(ctx),
Devfile: *devfileObj,
FS: o.filesystem,
})
pushParameters := adapters.PushParameters{
Path: path,
IgnoredFiles: options.IgnorePaths,
Debug: options.Debug,
DevfileBuildCmd: options.BuildCommand,
DevfileRunCmd: options.RunCommand,
RandomPorts: options.RandomPorts,
CustomForwardedPorts: options.CustomForwardedPorts,
ErrOut: errOut,
pushParameters := common.PushParameters{
StartOptions: options,
Devfile: *devfileObj,
}
klog.V(4).Infoln("Creating inner-loop resources for the component")
componentStatus := watch.ComponentStatus{
ImageComponentsAutoApplied: make(map[string]v1alpha2.ImageComponent),
ImageComponentsAutoApplied: make(map[string]devfilev1.ImageComponent),
}
err := adapter.Push(ctx, pushParameters, &componentStatus)
err := o.reconcile(ctx, pushParameters, &componentStatus)
if err != nil {
return err
}
klog.V(4).Infoln("Successfully created inner-loop resources")
watchParameters := watch.WatchParameters{
DevfilePath: devfilePath,
Path: path,
ComponentName: componentName,
ApplicationName: odocontext.GetApplication(ctx),
DevfileWatchHandler: o.regenerateAdapterAndPush,
FileIgnores: options.IgnorePaths,
InitialDevfileObj: *devfileObj,
Debug: options.Debug,
DevfileBuildCmd: options.BuildCommand,
DevfileRunCmd: options.RunCommand,
Variables: options.Variables,
RandomPorts: options.RandomPorts,
CustomForwardedPorts: options.CustomForwardedPorts,
WatchFiles: options.WatchFiles,
WatchCluster: true,
ErrOut: errOut,
PromptMessage: promptMessage,
StartOptions: options,
DevfileWatchHandler: o.regenerateAdapterAndPush,
WatchCluster: true,
PromptMessage: promptMessage,
}
return o.watchClient.WatchAndPush(out, watchParameters, ctx, componentStatus)
return o.watchClient.WatchAndPush(ctx, watchParameters, componentStatus)
}
// RegenerateAdapterAndPush regenerates the adapter and pushes the files to remote pod
func (o *DevClient) regenerateAdapterAndPush(ctx context.Context, pushParams adapters.PushParameters, watchParams watch.WatchParameters, componentStatus *watch.ComponentStatus) error {
var adapter component.ComponentAdapter
// RegenerateAdapterAndPush get the new devfile and pushes the files to remote pod
func (o *DevClient) regenerateAdapterAndPush(ctx context.Context, pushParams common.PushParameters, componentStatus *watch.ComponentStatus) error {
adapter, err := o.regenerateComponentAdapterFromWatchParams(watchParams)
devObj, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), pushParams.StartOptions.Variables)
if err != nil {
return fmt.Errorf("unable to generate component from watch parameters: %w", err)
}
err = adapter.Push(ctx, pushParams, componentStatus)
pushParams.Devfile = devObj
err = o.reconcile(ctx, pushParams, componentStatus)
if err != nil {
return fmt.Errorf("watch command was unable to push component: %w", err)
}
return nil
}
func (o *DevClient) regenerateComponentAdapterFromWatchParams(parameters watch.WatchParameters) (component.ComponentAdapter, error) {
devObj, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), parameters.Variables)
if err != nil {
return nil, err
}
return component.NewKubernetesAdapter(
o.kubernetesClient,
o.prefClient,
o.portForwardClient,
o.bindingClient,
o.syncClient,
o.execClient,
o.configAutomountClient,
component.AdapterContext{
ComponentName: parameters.ComponentName,
Context: parameters.Path,
AppName: parameters.ApplicationName,
Devfile: devObj,
FS: o.filesystem,
},
), nil
}

View File

@@ -1,34 +1,35 @@
package component
package kubedev
import (
"context"
"errors"
"testing"
"github.com/devfile/library/v2/pkg/devfile/generator"
"github.com/devfile/library/v2/pkg/devfile/parser/data"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/redhat-developer/odo/pkg/configAutomount"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/util"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/generator"
devfileParser "github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/testingutil"
"github.com/devfile/library/v2/pkg/devfile/parser/data"
"github.com/redhat-developer/odo/pkg/configAutomount"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/kclient"
odolabels "github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/preference"
odoTestingUtil "github.com/redhat-developer/odo/pkg/testingutil"
"github.com/redhat-developer/odo/pkg/util"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
ktesting "k8s.io/client-go/testing"
)
@@ -84,7 +85,7 @@ func TestCreateOrUpdateComponent(t *testing.T) {
var comp devfilev1.Component
if tt.componentType != "" {
odolabels.SetProjectType(deployment.Annotations, string(tt.componentType))
comp = testingutil.GetFakeContainerComponent("component")
comp = odoTestingUtil.GetFakeContainerComponent("component")
}
devObj := devfileParser.DevfileObj{
Data: func() data.DevfileData {
@@ -106,12 +107,6 @@ func TestCreateOrUpdateComponent(t *testing.T) {
}(),
}
adapterCtx := AdapterContext{
ComponentName: testComponentName,
AppName: testAppName,
Devfile: devObj,
}
fkclient, fkclientset := kclient.FakeNew()
fkclientset.Kubernetes.PrependReactor("patch", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
@@ -134,8 +129,14 @@ func TestCreateOrUpdateComponent(t *testing.T) {
fakePrefClient.EXPECT().GetEphemeralSourceVolume().AnyTimes()
fakeConfigAutomount := configAutomount.NewMockClient(ctrl)
fakeConfigAutomount.EXPECT().GetAutomountingVolumes().AnyTimes()
componentAdapter := NewKubernetesAdapter(fkclient, fakePrefClient, nil, nil, nil, nil, fakeConfigAutomount, adapterCtx)
_, _, err := componentAdapter.createOrUpdateComponent(tt.running, libdevfile.DevfileCommands{}, nil)
client := NewDevClient(fkclient, fakePrefClient, nil, nil, nil, nil, nil, nil, nil, fakeConfigAutomount)
ctx := context.Background()
ctx = odocontext.WithApplication(ctx, "app")
ctx = odocontext.WithComponentName(ctx, "my-component")
ctx = odocontext.WithDevfilePath(ctx, "/path/to/devfile")
_, _, err := client.createOrUpdateComponent(ctx, common.PushParameters{
Devfile: devObj,
}, tt.running, libdevfile.DevfileCommands{}, nil)
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
@@ -240,14 +241,14 @@ func TestAdapter_generateDeploymentObjectMeta(t *testing.T) {
fakeClient, _ := kclient.FakeNew()
fakeClient.Namespace = "project-0"
a := Adapter{
kubeClient: fakeClient,
AdapterContext: AdapterContext{
ComponentName: tt.fields.componentName,
AppName: tt.fields.appName,
},
a := DevClient{
kubernetesClient: fakeClient,
}
got, err := a.generateDeploymentObjectMeta(tt.fields.deployment, tt.args.labels, tt.args.annotations)
ctx := context.Background()
ctx = odocontext.WithApplication(ctx, "app")
ctx = odocontext.WithComponentName(ctx, "nodejs")
ctx = odocontext.WithDevfilePath(ctx, "/path/to/devfile")
got, err := a.generateDeploymentObjectMeta(ctx, tt.fields.deployment, tt.args.labels, tt.args.annotations)
if (err != nil) != tt.wantErr {
t.Errorf("generateDeploymentObjectMeta() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -436,8 +437,8 @@ func TestAdapter_deleteRemoteResources(t *testing.T) {
if tt.fields.kubeClientCustomizer != nil {
tt.fields.kubeClientCustomizer(kubeClient)
}
a := Adapter{
kubeClient: kubeClient,
a := DevClient{
kubernetesClient: kubeClient,
}
if err := a.deleteRemoteResources(tt.args.objectsToRemove); (err != nil) != tt.wantErr {
t.Errorf("deleteRemoteResources() error = %v, wantErr %v", err, tt.wantErr)

View File

@@ -0,0 +1,26 @@
package kubedev
import (
"context"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/watch"
)
// reconcile updates the component if a matching component exists or creates one if it doesn't exist
// Once the component has started, it will sync the source code to it.
// The componentStatus will be modified to reflect the status of the component when the function returns
func (o *DevClient) reconcile(ctx context.Context, parameters common.PushParameters, componentStatus *watch.ComponentStatus) (err error) {
// podOK indicates if the pod is ready to use for the inner loop
var podOK bool
podOK, err = o.createComponents(ctx, parameters, componentStatus)
if err != nil {
return err
}
if !podOK {
return nil
}
return o.innerloop(ctx, parameters, componentStatus)
}

View File

@@ -50,15 +50,15 @@ func (mr *MockClientMockRecorder) CleanupResources(ctx, out interface{}) *gomock
}
// Start mocks base method.
func (m *MockClient) Start(ctx context.Context, out, errOut io.Writer, options StartOptions) error {
func (m *MockClient) Start(ctx context.Context, options StartOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Start", ctx, out, errOut, options)
ret := m.ctrl.Call(m, "Start", ctx, options)
ret0, _ := ret[0].(error)
return ret0
}
// Start indicates an expected call of Start.
func (mr *MockClientMockRecorder) Start(ctx, out, errOut, options interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) Start(ctx, options interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockClient)(nil).Start), ctx, out, errOut, options)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockClient)(nil).Start), ctx, options)
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/utils"
"github.com/redhat-developer/odo/pkg/dev/kubedev/utils"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/odo/commonflags"

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"path/filepath"
"strings"
@@ -16,7 +15,6 @@ import (
"github.com/redhat-developer/odo/pkg/dev"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/devfile/location"
"github.com/redhat-developer/odo/pkg/exec"
"github.com/redhat-developer/odo/pkg/libdevfile"
@@ -77,53 +75,32 @@ func NewDevClient(
func (o *DevClient) Start(
ctx context.Context,
out io.Writer,
errOut io.Writer,
options dev.StartOptions,
) error {
var (
appName = odocontext.GetApplication(ctx)
componentName = odocontext.GetComponentName(ctx)
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentStatus = watch.ComponentStatus{
ImageComponentsAutoApplied: make(map[string]devfilev1.ImageComponent),
}
)
err := o.reconcile(ctx, out, errOut, options, &componentStatus)
err := o.reconcile(ctx, options, &componentStatus)
if err != nil {
return err
}
watch.PrintInfoMessage(out, path, options.WatchFiles, promptMessage)
watch.PrintInfoMessage(options.Out, path, options.WatchFiles, promptMessage)
watchParameters := watch.WatchParameters{
DevfilePath: devfilePath,
Path: path,
ComponentName: componentName,
ApplicationName: appName,
InitialDevfileObj: *devfileObj,
DevfileWatchHandler: o.watchHandler,
FileIgnores: options.IgnorePaths,
Debug: options.Debug,
DevfileBuildCmd: options.BuildCommand,
DevfileRunCmd: options.RunCommand,
Variables: options.Variables,
RandomPorts: options.RandomPorts,
IgnoreLocalhost: options.IgnoreLocalhost,
ForwardLocalhost: options.ForwardLocalhost,
CustomForwardedPorts: options.CustomForwardedPorts,
WatchFiles: options.WatchFiles,
WatchCluster: false,
Out: out,
ErrOut: errOut,
PromptMessage: promptMessage,
StartOptions: options,
DevfileWatchHandler: o.watchHandler,
WatchCluster: false,
PromptMessage: promptMessage,
}
return o.watchClient.WatchAndPush(out, watchParameters, ctx, componentStatus)
return o.watchClient.WatchAndPush(ctx, watchParameters, componentStatus)
}
// syncFiles syncs the local source files in path into the pod's source volume
@@ -165,7 +142,7 @@ func (o *DevClient) syncFiles(ctx context.Context, options dev.StartOptions, pod
CompInfo: compInfo,
ForcePush: true,
Files: adapters.GetSyncFilesFromAttributes(devfileCmd),
Files: common.GetSyncFilesFromAttributes(devfileCmd),
}
execRequired, err := o.syncClient.SyncFiles(ctx, syncParams)
if err != nil {
@@ -193,28 +170,15 @@ func (o *DevClient) checkVolumesFree(pod *corev1.Pod) error {
return nil
}
func (o *DevClient) watchHandler(ctx context.Context, pushParams adapters.PushParameters, watchParams watch.WatchParameters, componentStatus *watch.ComponentStatus) error {
printWarningsOnDevfileChanges(ctx, watchParams)
startOptions := dev.StartOptions{
IgnorePaths: watchParams.FileIgnores,
Debug: watchParams.Debug,
BuildCommand: watchParams.DevfileBuildCmd,
RunCommand: watchParams.DevfileRunCmd,
RandomPorts: watchParams.RandomPorts,
IgnoreLocalhost: watchParams.IgnoreLocalhost,
ForwardLocalhost: watchParams.ForwardLocalhost,
CustomForwardedPorts: watchParams.CustomForwardedPorts,
WatchFiles: watchParams.WatchFiles,
Variables: watchParams.Variables,
}
return o.reconcile(ctx, watchParams.Out, watchParams.ErrOut, startOptions, componentStatus)
func (o *DevClient) watchHandler(ctx context.Context, pushParams common.PushParameters, componentStatus *watch.ComponentStatus) error {
printWarningsOnDevfileChanges(ctx, pushParams.StartOptions)
return o.reconcile(ctx, pushParams.StartOptions, componentStatus)
}
func printWarningsOnDevfileChanges(ctx context.Context, parameters watch.WatchParameters) {
func printWarningsOnDevfileChanges(ctx context.Context, options dev.StartOptions) {
var warning string
currentDevfile := odocontext.GetDevfileObj(ctx)
newDevfile, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), parameters.Variables)
newDevfile, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), options.Variables)
if err != nil {
warning = fmt.Sprintf("error while reading the Devfile. Please restart 'odo dev' if you made any changes to the Devfile. Error message is: %v", err)
} else {
@@ -239,6 +203,6 @@ func printWarningsOnDevfileChanges(ctx context.Context, parameters watch.WatchPa
}
}
if warning != "" {
log.Fwarning(parameters.Out, warning+"\n")
log.Fwarning(options.Out, warning+"\n")
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"time"
@@ -17,7 +16,7 @@ import (
"github.com/redhat-developer/odo/pkg/component"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/dev"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/devfile/image"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/log"
@@ -32,8 +31,6 @@ import (
func (o *DevClient) reconcile(
ctx context.Context,
out io.Writer,
errOut io.Writer,
options dev.StartOptions,
componentStatus *watch.ComponentStatus,
) error {
@@ -130,7 +127,7 @@ func (o *DevClient) reconcile(
if err != nil {
log.Warningf("Port forwarding might not work correctly: %v", err)
log.Warning("Running `odo logs --follow --platform podman` might help in identifying the problem.")
fmt.Fprintln(out)
fmt.Fprintln(options.Out)
}
// By default, Podman will not forward to container applications listening on the loopback interface.
@@ -143,15 +140,15 @@ func (o *DevClient) reconcile(
if options.ForwardLocalhost {
// Port-forwarding is enabled by executing dedicated socat commands
err = o.portForwardClient.StartPortForwarding(ctx, *devfileObj, componentName, options.Debug, options.RandomPorts, out, errOut, fwPorts)
err = o.portForwardClient.StartPortForwarding(ctx, *devfileObj, componentName, options.Debug, options.RandomPorts, options.Out, options.ErrOut, fwPorts)
if err != nil {
return adapters.NewErrPortForward(err)
return common.NewErrPortForward(err)
}
} // else port-forwarding is done via the main container ports in the pod spec
for _, fwPort := range fwPorts {
s := fmt.Sprintf("Forwarding from %s:%d -> %d", fwPort.LocalAddress, fwPort.LocalPort, fwPort.ContainerPort)
fmt.Fprintf(out, " - %s", log.SboldColor(color.FgGreen, s))
fmt.Fprintf(options.Out, " - %s", log.SboldColor(color.FgGreen, s))
}
err = o.stateClient.SetForwardedPorts(ctx, fwPorts)
if err != nil {

View File

@@ -1,13 +0,0 @@
package component
import (
"context"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/watch"
)
// ComponentAdapter defines the functions that platform-specific adapters must implement
type ComponentAdapter interface {
Push(ctx context.Context, parameters adapters.PushParameters, componentStatus *watch.ComponentStatus) error
}

View File

@@ -1,152 +0,0 @@
package component
import (
"context"
"fmt"
"reflect"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/devfile/image"
"github.com/redhat-developer/odo/pkg/kclient"
odolabels "github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/service"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
"github.com/redhat-developer/odo/pkg/watch"
)
// getComponentDeployment returns the deployment associated with the component, if deployed
// and indicate if the deployment has been found
func (a *Adapter) getComponentDeployment() (*appsv1.Deployment, bool, error) {
// Get the Dev deployment:
// Since `odo deploy` can theoretically deploy a deployment as well with the same instance name
// we make sure that we are retrieving the deployment with the Dev mode, NOT Deploy.
selectorLabels := odolabels.GetSelector(a.ComponentName, a.AppName, odolabels.ComponentDevMode, true)
deployment, err := a.kubeClient.GetOneDeploymentFromSelector(selectorLabels)
if err != nil {
if _, ok := err.(*kclient.DeploymentNotFoundError); !ok {
return nil, false, fmt.Errorf("unable to determine if component %s exists: %w", a.ComponentName, err)
}
}
componentExists := deployment != nil
return deployment, componentExists, nil
}
func (a *Adapter) buildPushAutoImageComponents(ctx context.Context, fs filesystem.Filesystem, devfileObj parser.DevfileObj, compStatus *watch.ComponentStatus) error {
components, err := libdevfile.GetImageComponentsToPushAutomatically(devfileObj)
if err != nil {
return err
}
for _, c := range components {
if c.Image == nil {
return fmt.Errorf("component %q should be an Image Component", c.Name)
}
alreadyApplied, ok := compStatus.ImageComponentsAutoApplied[c.Name]
if ok && reflect.DeepEqual(*c.Image, alreadyApplied) {
klog.V(1).Infof("Skipping image component %q; already applied and not changed", c.Name)
continue
}
err = image.BuildPushSpecificImage(ctx, fs, c, true)
if err != nil {
return err
}
compStatus.ImageComponentsAutoApplied[c.Name] = *c.Image
}
// Remove keys that might no longer be valid
devfileHasCompFn := func(n string) bool {
for _, c := range components {
if c.Name == n {
return true
}
}
return false
}
for n := range compStatus.ImageComponentsAutoApplied {
if !devfileHasCompFn(n) {
delete(compStatus.ImageComponentsAutoApplied, n)
}
}
return nil
}
// pushDevfileKubernetesComponents gets the Kubernetes components from the Devfile and push them to the cluster
// adding the specified labels and ownerreference to them
func (a *Adapter) pushDevfileKubernetesComponents(
labels map[string]string,
mode string,
reference metav1.OwnerReference,
) ([]devfilev1.Component, error) {
// fetch the "kubernetes inlined components" to create them on cluster
// from odo standpoint, these components contain yaml manifest of ServiceBinding
k8sComponents, err := libdevfile.GetK8sAndOcComponentsToPush(a.Devfile, false)
if err != nil {
return nil, fmt.Errorf("error while trying to fetch service(s) from devfile: %w", err)
}
// validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster
err = component.ValidateResourcesExist(a.kubeClient, a.Devfile, k8sComponents, a.Context)
if err != nil {
return nil, err
}
// Set the annotations for the component type
annotations := make(map[string]string)
odolabels.SetProjectType(annotations, component.GetComponentTypeFromDevfileMetadata(a.AdapterContext.Devfile.Data.GetMetadata()))
// create the Kubernetes objects from the manifest and delete the ones not in the devfile
err = service.PushKubernetesResources(a.kubeClient, a.Devfile, k8sComponents, labels, annotations, a.Context, mode, reference)
if err != nil {
return nil, fmt.Errorf("failed to create Kubernetes resources associated with the component: %w", err)
}
return k8sComponents, nil
}
func (a *Adapter) getPushDevfileCommands(parameters adapters.PushParameters) (map[devfilev1.CommandGroupKind]devfilev1.Command, error) {
pushDevfileCommands, err := libdevfile.ValidateAndGetPushCommands(a.Devfile, parameters.DevfileBuildCmd, parameters.DevfileRunCmd)
if err != nil {
return nil, fmt.Errorf("failed to validate devfile build and run commands: %w", err)
}
if parameters.Debug {
pushDevfileDebugCommands, e := libdevfile.ValidateAndGetCommand(a.Devfile, parameters.DevfileDebugCmd, devfilev1.DebugCommandGroupKind)
if e != nil {
return nil, fmt.Errorf("debug command is not valid: %w", e)
}
pushDevfileCommands[devfilev1.DebugCommandGroupKind] = pushDevfileDebugCommands
}
return pushDevfileCommands, nil
}
func (a *Adapter) updatePVCsOwnerReferences(ownerReference metav1.OwnerReference) error {
// list the latest state of the PVCs
pvcs, err := a.kubeClient.ListPVCs(fmt.Sprintf("%v=%v", "component", a.ComponentName))
if err != nil {
return err
}
// update the owner reference of the PVCs with the deployment
for i := range pvcs {
if pvcs[i].OwnerReferences != nil || pvcs[i].DeletionTimestamp != nil {
continue
}
err = a.kubeClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error {
return a.kubeClient.UpdateStorageOwnerReference(&pvcs[i], ownerRef)
})
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,23 +0,0 @@
package adapters
import (
"github.com/redhat-developer/odo/pkg/api"
"io"
)
// PushParameters is a struct containing the parameters to be used when pushing to a devfile component
type PushParameters struct {
Path string // Path refers to the parent folder containing the source code to push up to a component
WatchFiles []string // Optional: WatchFiles is the list of changed files detected by odo watch. If empty or nil, odo will check .odo/odo-file-index.json to determine changed files
WatchDeletedFiles []string // Optional: WatchDeletedFiles is the list of deleted files detected by odo watch. If empty or nil, odo will check .odo/odo-file-index.json to determine deleted files
IgnoredFiles []string // IgnoredFiles is the list of files to not push up to a component
Show bool // Show tells whether the devfile command output should be shown on stdout
DevfileBuildCmd string // DevfileBuildCmd takes the build command through the command line and overwrites devfile build command
DevfileRunCmd string // DevfileRunCmd takes the run command through the command line and overwrites devfile run command
DevfileDebugCmd string // DevfileDebugCmd takes the debug command through the command line and overwrites the devfile debug command
DevfileScanIndexForWatch bool // DevfileScanIndexForWatch is true if watch's push should regenerate the index file during SyncFiles, false otherwise. See 'pkg/sync/adapter.go' for details
Debug bool // Runs the component in debug mode
RandomPorts bool // True to forward containers ports on local random ports
CustomForwardedPorts []api.ForwardedPort // Optional: CustomForwardedPorts configuration to be used to customize the port forwarding; if nil, we automatically select ports
ErrOut io.Writer // Writer to output forwarded port information
}

View File

@@ -238,8 +238,6 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
return o.clientset.DevClient.Start(
o.ctx,
o.out,
o.errOut,
dev.StartOptions{
IgnorePaths: o.ignorePaths,
Debug: o.debugFlag,
@@ -251,6 +249,8 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
ForwardLocalhost: o.forwardLocalhostFlag,
Variables: variables,
CustomForwardedPorts: o.forwardedPorts,
Out: o.out,
ErrOut: o.errOut,
},
)
}

View File

@@ -2,7 +2,6 @@ package watch
import (
"context"
"io"
)
type Client interface {
@@ -11,5 +10,5 @@ type Client interface {
// componentStatus is a variable to store the status of the component, and that will be exchanged between
// parts of code (unfortunately, tthere is no place to store the status of the component in some Kubernetes resource
// as it is generally done for a Kubernetes resource)
WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context, componentStatus ComponentStatus) error
WatchAndPush(ctx context.Context, parameters WatchParameters, componentStatus ComponentStatus) error
}

View File

@@ -6,7 +6,6 @@ package watch
import (
context "context"
io "io"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
@@ -36,15 +35,15 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// WatchAndPush mocks base method.
func (m *MockClient) WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context, componentStatus ComponentStatus) error {
func (m *MockClient) WatchAndPush(ctx context.Context, parameters WatchParameters, componentStatus ComponentStatus) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WatchAndPush", out, parameters, ctx, componentStatus)
ret := m.ctrl.Call(m, "WatchAndPush", ctx, parameters, componentStatus)
ret0, _ := ret[0].(error)
return ret0
}
// WatchAndPush indicates an expected call of WatchAndPush.
func (mr *MockClientMockRecorder) WatchAndPush(out, parameters, ctx, componentStatus interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) WatchAndPush(ctx, parameters, componentStatus interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchAndPush", reflect.TypeOf((*MockClient)(nil).WatchAndPush), out, parameters, ctx, componentStatus)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchAndPush", reflect.TypeOf((*MockClient)(nil).WatchAndPush), ctx, parameters, componentStatus)
}

View File

@@ -4,20 +4,20 @@ import (
"context"
"errors"
"fmt"
"github.com/redhat-developer/odo/pkg/api"
"io"
"os"
"path/filepath"
"reflect"
"time"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/dev"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/log"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/fsnotify/fsnotify"
gitignore "github.com/sabhiram/go-gitignore"
@@ -59,54 +59,19 @@ func NewWatchClient(kubeClient kclient.ClientInterface) *WatchClient {
// WatchParameters is designed to hold the controllables and attributes that the watch function works on
type WatchParameters struct {
// Name of component that is to be watched
ComponentName string
// Name of application, the component is part of
ApplicationName string
// DevfilePath is the path of the devfile
DevfilePath string
// The path to the source of component(local or binary)
Path string
// List/Slice of files/folders in component source, the updates to which need not be pushed to component deployed pod
FileIgnores []string
StartOptions dev.StartOptions
// Custom function that can be used to push detected changes to remote pod. For more info about what each of the parameters to this function, please refer, pkg/component/component.go#PushLocal
// WatchHandler func(kclient.ClientInterface, string, string, string, io.Writer, []string, []string, bool, []string, bool) error
// Custom function that can be used to push detected changes to remote devfile pod. For more info about what each of the parameters to this function, please refer, pkg/devfile/adapters/interface.go#PlatformAdapter
DevfileWatchHandler func(context.Context, adapters.PushParameters, WatchParameters, *ComponentStatus) error
DevfileWatchHandler func(context.Context, common.PushParameters, *ComponentStatus) error
// Parameter whether or not to show build logs
Show bool
// DevfileBuildCmd takes the build command through the command line and overwrites devfile build command
DevfileBuildCmd string
// DevfileRunCmd takes the run command through the command line and overwrites devfile run command
DevfileRunCmd string
// DevfileDebugCmd takes the debug command through the command line and overwrites the devfile debug command
DevfileDebugCmd string
// InitialDevfileObj is used to compare the devfile between the very first run of odo dev and subsequent ones
InitialDevfileObj parser.DevfileObj
// Debug indicates if the debug command should be started after sync, or the run command by default
Debug bool
// DebugPort indicates which debug port to use for pushing after sync
DebugPort int
// Variables override Devfile variables
Variables map[string]string
// RandomPorts is true to forward containers ports on local random ports
RandomPorts bool
// Optional: sCustomForwardedPorts configuration to be used to customize the port forwarding; if nil, we automatically select ports
CustomForwardedPorts []api.ForwardedPort
// IgnoreLocalhost indicates whether to proceed with port-forwarding regardless of any container ports being bound to the container loopback interface.
// Applicable to Podman only.
IgnoreLocalhost bool
// WatchFiles indicates to watch for file changes and sync changes to the container
WatchFiles bool
// ForwardLocalhost indicates whether to try to make port-forwarding work with container apps listening on the loopback interface.
ForwardLocalhost bool
// WatchCluster indicates to watch Cluster-related objects (Deployment, Pod, etc)
WatchCluster bool
// ErrOut is a Writer to output forwarded port information
Out io.Writer
// ErrOut is a Writer to output forwarded port information
ErrOut io.Writer
// PromptMessage
PromptMessage string
}
@@ -118,14 +83,22 @@ type evaluateChangesFunc func(events []fsnotify.Event, path string, fileIgnores
// processEventsFunc processes the events received on the watcher. It uses the WatchParameters to trigger watch handler and writes to out
// It returns a Duration after which to recall in case of error
type processEventsFunc func(ctx context.Context, changedFiles, deletedPaths []string, parameters WatchParameters, out io.Writer, componentStatus *ComponentStatus, backoff *ExpBackoff) (*time.Duration, error)
type processEventsFunc func(ctx context.Context, parameters WatchParameters, changedFiles, deletedPaths []string, componentStatus *ComponentStatus, backoff *ExpBackoff) (*time.Duration, error)
func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context, componentStatus ComponentStatus) error {
klog.V(4).Infof("starting WatchAndPush, path: %s, component: %s, ignores %s", parameters.Path, parameters.ComponentName, parameters.FileIgnores)
func (o *WatchClient) WatchAndPush(ctx context.Context, parameters WatchParameters, componentStatus ComponentStatus) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)
appName = odocontext.GetApplication(ctx)
)
klog.V(4).Infof("starting WatchAndPush, path: %s, component: %s, ignores %s", path, componentName, parameters.StartOptions.IgnorePaths)
var err error
if parameters.WatchFiles {
o.sourcesWatcher, err = getFullSourcesWatcher(parameters.Path, parameters.FileIgnores)
if parameters.StartOptions.WatchFiles {
o.sourcesWatcher, err = getFullSourcesWatcher(path, parameters.StartOptions.IgnorePaths)
if err != nil {
return err
}
@@ -138,7 +111,7 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct
defer o.sourcesWatcher.Close()
if parameters.WatchCluster {
selector := labels.GetSelector(parameters.ComponentName, parameters.ApplicationName, labels.ComponentDevMode, true)
selector := labels.GetSelector(componentName, appName, labels.ComponentDevMode, true)
o.deploymentWatcher, err = o.kubeClient.DeploymentWatcher(ctx, selector)
if err != nil {
return fmt.Errorf("error watching deployment: %v", err)
@@ -157,13 +130,13 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct
if err != nil {
return err
}
if parameters.WatchFiles {
if parameters.StartOptions.WatchFiles {
var devfileFiles []string
devfileFiles, err = libdevfile.GetReferencedLocalFiles(parameters.InitialDevfileObj)
devfileFiles, err = libdevfile.GetReferencedLocalFiles(*devfileObj)
if err != nil {
return err
}
devfileFiles = append(devfileFiles, parameters.DevfilePath)
devfileFiles = append(devfileFiles, devfilePath)
for _, f := range devfileFiles {
err = o.devfileWatcher.Add(f)
if err != nil {
@@ -179,14 +152,14 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct
return err
}
if isForbidden {
log.Fwarning(out, "Unable to watch Events resource, warning Events won't be displayed")
log.Fwarning(parameters.StartOptions.Out, "Unable to watch Events resource, warning Events won't be displayed")
}
} else {
o.warningsWatcher = NewNoOpWatcher()
}
o.keyWatcher = getKeyWatcher(ctx, out)
return o.eventWatcher(ctx, parameters, out, evaluateFileChanges, o.processEvents, componentStatus)
o.keyWatcher = getKeyWatcher(ctx, parameters.StartOptions.Out)
return o.eventWatcher(ctx, parameters, evaluateFileChanges, o.processEvents, componentStatus)
}
// eventWatcher loops till the context's Done channel indicates it to stop looping, at which point it performs cleanup.
@@ -195,12 +168,19 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct
func (o *WatchClient) eventWatcher(
ctx context.Context,
parameters WatchParameters,
out io.Writer,
evaluateChangesHandler evaluateChangesFunc,
processEventsHandler processEventsFunc,
componentStatus ComponentStatus,
) error {
var (
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)
appName = odocontext.GetApplication(ctx)
out = parameters.StartOptions.Out
)
expBackoff := NewExpBackoff()
var events []fsnotify.Event
@@ -245,7 +225,7 @@ func (o *WatchClient) eventWatcher(
var changedFiles, deletedPaths []string
if !o.forceSync {
// first find the files that have changed (also includes the ones newly created) or deleted
changedFiles, deletedPaths = evaluateChangesHandler(events, parameters.Path, parameters.FileIgnores, o.sourcesWatcher)
changedFiles, deletedPaths = evaluateChangesHandler(events, path, parameters.StartOptions.IgnorePaths, o.sourcesWatcher)
// process the changes and sync files with remote pod
if len(changedFiles) == 0 && len(deletedPaths) == 0 {
continue
@@ -254,7 +234,7 @@ func (o *WatchClient) eventWatcher(
componentStatus.State = StateSyncOutdated
fmt.Fprintf(out, "Pushing files...\n\n")
retry, err := processEventsHandler(ctx, changedFiles, deletedPaths, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, parameters, changedFiles, deletedPaths, &componentStatus, expBackoff)
o.forceSync = false
if err != nil {
return err
@@ -292,7 +272,7 @@ func (o *WatchClient) eventWatcher(
}
case <-deployTimer.C:
retry, err := processEventsHandler(ctx, nil, nil, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, parameters, nil, nil, &componentStatus, expBackoff)
if err != nil {
return err
}
@@ -308,7 +288,7 @@ func (o *WatchClient) eventWatcher(
case <-devfileTimer.C:
fmt.Fprintf(out, "Updating Component...\n\n")
retry, err := processEventsHandler(ctx, nil, nil, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, parameters, nil, nil, &componentStatus, expBackoff)
if err != nil {
return err
}
@@ -320,7 +300,7 @@ func (o *WatchClient) eventWatcher(
}
case <-retryTimer.C:
retry, err := processEventsHandler(ctx, nil, nil, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, parameters, nil, nil, &componentStatus, expBackoff)
if err != nil {
return err
}
@@ -351,7 +331,7 @@ func (o *WatchClient) eventWatcher(
switch kevent := ev.Object.(type) {
case *corev1.Event:
podName := kevent.InvolvedObject.Name
selector := labels.GetSelector(parameters.ComponentName, parameters.ApplicationName, labels.ComponentDevMode, true)
selector := labels.GetSelector(componentName, appName, labels.ComponentDevMode, true)
matching, err := o.kubeClient.IsPodNameMatchingSelector(ctx, podName, selector)
if err != nil {
return err
@@ -438,12 +418,17 @@ func evaluateFileChanges(events []fsnotify.Event, path string, fileIgnores []str
func (o *WatchClient) processEvents(
ctx context.Context,
changedFiles, deletedPaths []string,
parameters WatchParameters,
out io.Writer,
changedFiles, deletedPaths []string,
componentStatus *ComponentStatus,
backoff *ExpBackoff,
) (*time.Duration, error) {
var (
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
out = parameters.StartOptions.Out
)
for _, file := range removeDuplicates(append(changedFiles, deletedPaths...)) {
fmt.Fprintf(out, "\nFile %s changed\n", file)
}
@@ -452,22 +437,14 @@ func (o *WatchClient) processEvents(
klog.V(4).Infof("Copying files %s to pod", changedFiles)
pushParams := adapters.PushParameters{
Path: parameters.Path,
pushParams := common.PushParameters{
StartOptions: parameters.StartOptions,
WatchFiles: changedFiles,
WatchDeletedFiles: deletedPaths,
IgnoredFiles: parameters.FileIgnores,
DevfileBuildCmd: parameters.DevfileBuildCmd,
DevfileRunCmd: parameters.DevfileRunCmd,
DevfileDebugCmd: parameters.DevfileDebugCmd,
DevfileScanIndexForWatch: !hasFirstSuccessfulPushOccurred,
Debug: parameters.Debug,
RandomPorts: parameters.RandomPorts,
CustomForwardedPorts: parameters.CustomForwardedPorts,
ErrOut: parameters.ErrOut,
}
oldStatus := *componentStatus
err := parameters.DevfileWatchHandler(ctx, pushParams, parameters, componentStatus)
err := parameters.DevfileWatchHandler(ctx, pushParams, componentStatus)
if err != nil {
if isFatal(err) {
return nil, err
@@ -485,7 +462,7 @@ func (o *WatchClient) processEvents(
fmt.Fprintf(out, "Updated Kubernetes config\n")
}
} else {
if parameters.WatchFiles {
if parameters.StartOptions.WatchFiles {
fmt.Fprintf(out, "%s - %s\n\n", PushErrorString, err.Error())
} else {
return nil, err
@@ -498,7 +475,7 @@ func (o *WatchClient) processEvents(
if oldStatus.State != StateReady && componentStatus.State == StateReady ||
!reflect.DeepEqual(oldStatus.EndpointsForwarded, componentStatus.EndpointsForwarded) {
PrintInfoMessage(out, parameters.Path, parameters.WatchFiles, parameters.PromptMessage)
PrintInfoMessage(out, path, parameters.StartOptions.WatchFiles, parameters.PromptMessage)
}
return nil, nil
}
@@ -563,5 +540,5 @@ func PrintInfoMessage(out io.Writer, path string, watchFiles bool, promptMessage
}
func isFatal(err error) bool {
return errors.As(err, &adapters.ErrPortForward{})
return errors.As(err, &common.ErrPortForward{})
}

View File

@@ -4,13 +4,15 @@ import (
"bytes"
"context"
"fmt"
"io"
"testing"
"time"
"k8s.io/apimachinery/pkg/watch"
"github.com/fsnotify/fsnotify"
"github.com/redhat-developer/odo/pkg/dev"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
)
func evaluateChangesHandler(events []fsnotify.Event, path string, fileIgnores []string, watcher *fsnotify.Watcher) ([]string, []string) {
@@ -40,8 +42,8 @@ func evaluateChangesHandler(events []fsnotify.Event, path string, fileIgnores []
return changedFiles, deletedPaths
}
func processEventsHandler(ctx context.Context, changedFiles, deletedPaths []string, _ WatchParameters, out io.Writer, componentStatus *ComponentStatus, backo *ExpBackoff) (*time.Duration, error) {
fmt.Fprintf(out, "changedFiles %s deletedPaths %s\n", changedFiles, deletedPaths)
func processEventsHandler(ctx context.Context, params WatchParameters, changedFiles, deletedPaths []string, componentStatus *ComponentStatus, backo *ExpBackoff) (*time.Duration, error) {
fmt.Fprintf(params.StartOptions.Out, "changedFiles %s deletedPaths %s\n", changedFiles, deletedPaths)
return nil, nil
}
@@ -89,7 +91,11 @@ func Test_eventWatcher(t *testing.T) {
{
name: "Case 3: Delete file, no error",
args: args{
parameters: WatchParameters{FileIgnores: []string{"file1"}},
parameters: WatchParameters{
StartOptions: dev.StartOptions{
IgnorePaths: []string{"file1"},
},
},
},
wantOut: "Pushing files...\n\nchangedFiles [] deletedPaths [file1 file2]\n",
wantErr: true,
@@ -113,6 +119,9 @@ func Test_eventWatcher(t *testing.T) {
fileWatcher, _ := fsnotify.NewWatcher()
var cancel context.CancelFunc
ctx, cancel := context.WithCancel(context.Background())
ctx = odocontext.WithDevfilePath(ctx, "/path/to/devfile")
ctx = odocontext.WithApplication(ctx, "odo")
ctx = odocontext.WithComponentName(ctx, "my-component")
out := &bytes.Buffer{}
go func() {
@@ -139,7 +148,9 @@ func Test_eventWatcher(t *testing.T) {
devfileWatcher: fileWatcher,
keyWatcher: make(chan byte),
}
err := o.eventWatcher(ctx, tt.args.parameters, out, evaluateChangesHandler, processEventsHandler, componentStatus)
tt.args.parameters.StartOptions.Out = out
err := o.eventWatcher(ctx, tt.args.parameters, evaluateChangesHandler, processEventsHandler, componentStatus)
if (err != nil) != tt.wantErr {
t.Errorf("eventWatcher() error = %v, wantErr %v", err, tt.wantErr)
return