diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go index 945238b8861..20c202632ea 100644 --- a/pkg/deploy/deploy.go +++ b/pkg/deploy/deploy.go @@ -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" diff --git a/pkg/devfile/adapters/attributes.go b/pkg/dev/common/attributes.go similarity index 97% rename from pkg/devfile/adapters/attributes.go rename to pkg/dev/common/attributes.go index 061a8d3b6e8..49cad270a34 100644 --- a/pkg/devfile/adapters/attributes.go +++ b/pkg/dev/common/attributes.go @@ -1,4 +1,4 @@ -package adapters +package common import ( "path/filepath" diff --git a/pkg/devfile/adapters/attributes_test.go b/pkg/dev/common/attributes_test.go similarity index 99% rename from pkg/devfile/adapters/attributes_test.go rename to pkg/dev/common/attributes_test.go index d4afbefdf82..b926c1d9c04 100644 --- a/pkg/devfile/adapters/attributes_test.go +++ b/pkg/dev/common/attributes_test.go @@ -1,4 +1,4 @@ -package adapters +package common import ( "testing" diff --git a/pkg/devfile/adapters/errors.go b/pkg/dev/common/errors.go similarity index 93% rename from pkg/devfile/adapters/errors.go rename to pkg/dev/common/errors.go index d0bde259ebb..c7bc0851e91 100644 --- a/pkg/devfile/adapters/errors.go +++ b/pkg/dev/common/errors.go @@ -1,4 +1,4 @@ -package adapters +package common import "fmt" diff --git a/pkg/dev/common/types.go b/pkg/dev/common/types.go new file mode 100644 index 00000000000..cca5956cf53 --- /dev/null +++ b/pkg/dev/common/types.go @@ -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 +} diff --git a/pkg/dev/interface.go b/pkg/dev/interface.go index 03318ee4c3b..9578babb570 100644 --- a/pkg/dev/interface.go +++ b/pkg/dev/interface.go @@ -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 diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/dev/kubedev/components.go similarity index 51% rename from pkg/devfile/adapters/kubernetes/component/adapter.go rename to pkg/dev/kubedev/components.go index 17531a76ed5..38f1cafc963 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/dev/kubedev/components.go @@ -1,117 +1,64 @@ -package component +package kubedev import ( "context" "errors" "fmt" + "path/filepath" "reflect" "strings" - "time" - devfilefs "github.com/devfile/library/v2/pkg/testingutil/filesystem" "golang.org/x/sync/errgroup" - kerrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/pointer" - "github.com/redhat-developer/odo/pkg/binding" + devfilev1 "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" + parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" + devfilefs "github.com/devfile/library/v2/pkg/testingutil/filesystem" + dfutil "github.com/devfile/library/v2/pkg/util" + "github.com/redhat-developer/odo/pkg/component" - "github.com/redhat-developer/odo/pkg/configAutomount" "github.com/redhat-developer/odo/pkg/dev/common" - "github.com/redhat-developer/odo/pkg/devfile/adapters" - "github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/storage" - "github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/utils" - "github.com/redhat-developer/odo/pkg/exec" + "github.com/redhat-developer/odo/pkg/dev/kubedev/storage" + "github.com/redhat-developer/odo/pkg/dev/kubedev/utils" + "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/log" - "github.com/redhat-developer/odo/pkg/machineoutput" - "github.com/redhat-developer/odo/pkg/port" - "github.com/redhat-developer/odo/pkg/portForward" - "github.com/redhat-developer/odo/pkg/preference" + odocontext "github.com/redhat-developer/odo/pkg/odo/context" "github.com/redhat-developer/odo/pkg/service" storagepkg "github.com/redhat-developer/odo/pkg/storage" - "github.com/redhat-developer/odo/pkg/sync" "github.com/redhat-developer/odo/pkg/testingutil/filesystem" "github.com/redhat-developer/odo/pkg/util" "github.com/redhat-developer/odo/pkg/watch" - devfilev1 "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" - parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" - dfutil "github.com/devfile/library/v2/pkg/util" - appsv1 "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/klog" + "k8s.io/utils/pointer" ) -// Adapter is a component adapter implementation for Kubernetes -type Adapter struct { - kubeClient kclient.ClientInterface - prefClient preference.Client - portForwardClient portForward.Client - bindingClient binding.Client - syncClient sync.Client - execClient exec.Client - configAutomountClient configAutomount.Client - - AdapterContext - logger machineoutput.MachineEventLoggingClient -} - -// AdapterContext is a construct that is common to all adapters -type AdapterContext struct { - ComponentName string // ComponentName is the odo component name, it is NOT related to any devfile components - Context string // Context is the given directory containing the source code and configs - AppName string // the application name associated to a component - Devfile parser.DevfileObj // Devfile is the object returned by the Devfile parser - FS filesystem.Filesystem // FS is the object used for building image component if present -} - -var _ ComponentAdapter = (*Adapter)(nil) - -// NewKubernetesAdapter returns a Devfile adapter for the targeted platform -func NewKubernetesAdapter( - kubernetesClient kclient.ClientInterface, - prefClient preference.Client, - portForwardClient portForward.Client, - bindingClient binding.Client, - syncClient sync.Client, - execClient exec.Client, - configAutomountClient configAutomount.Client, - context AdapterContext, -) Adapter { - return Adapter{ - kubeClient: kubernetesClient, - prefClient: prefClient, - portForwardClient: portForwardClient, - bindingClient: bindingClient, - syncClient: syncClient, - execClient: execClient, - configAutomountClient: configAutomountClient, - AdapterContext: context, - logger: machineoutput.NewMachineEventLoggingClient(), - } -} - -// Push 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 (a Adapter) Push(ctx context.Context, parameters adapters.PushParameters, componentStatus *watch.ComponentStatus) (err error) { +// createComponents creates the components into the cluster +// returns true if the pod is created +func (o *DevClient) createComponents(ctx context.Context, parameters common.PushParameters, componentStatus *watch.ComponentStatus) (bool, error) { + var ( + appName = odocontext.GetApplication(ctx) + componentName = odocontext.GetComponentName(ctx) + ) // preliminary checks - err = dfutil.ValidateK8sResourceName("component name", a.ComponentName) + err := dfutil.ValidateK8sResourceName("component name", componentName) if err != nil { - return err + return false, err } - err = dfutil.ValidateK8sResourceName("component namespace", a.kubeClient.GetCurrentNamespace()) + err = dfutil.ValidateK8sResourceName("component namespace", o.kubernetesClient.GetCurrentNamespace()) if err != nil { - return err + return false, err } if componentStatus.State == watch.StateSyncOutdated { @@ -120,14 +67,15 @@ func (a Adapter) Push(ctx context.Context, parameters adapters.PushParameters, c } klog.V(4).Infof("component state: %q\n", componentStatus.State) - err = a.buildPushAutoImageComponents(ctx, a.FS, a.Devfile, componentStatus) + err = o.buildPushAutoImageComponents(ctx, o.filesystem, parameters.Devfile, componentStatus) if err != nil { - return err + return false, err } - deployment, deploymentExists, err := a.getComponentDeployment() + var deployment *appsv1.Deployment + deployment, o.deploymentExists, err = o.getComponentDeployment(ctx) if err != nil { - return err + return false, err } if componentStatus.State != watch.StateWaitDeployment && componentStatus.State != watch.StateReady { @@ -135,277 +83,191 @@ func (a Adapter) Push(ctx context.Context, parameters adapters.PushParameters, c } // Set the mode to Dev since we are using "odo dev" here - runtime := component.GetComponentRuntimeFromDevfileMetadata(a.Devfile.Data.GetMetadata()) - labels := odolabels.GetLabels(a.ComponentName, a.AppName, runtime, odolabels.ComponentDevMode, false) + runtime := component.GetComponentRuntimeFromDevfileMetadata(parameters.Devfile.Data.GetMetadata()) + labels := odolabels.GetLabels(componentName, appName, runtime, odolabels.ComponentDevMode, false) var updated bool - deployment, updated, err = a.createOrUpdateComponent(deploymentExists, libdevfile.DevfileCommands{ - BuildCmd: parameters.DevfileBuildCmd, - RunCmd: parameters.DevfileRunCmd, - DebugCmd: parameters.DevfileDebugCmd, + deployment, updated, err = o.createOrUpdateComponent(ctx, parameters, o.deploymentExists, libdevfile.DevfileCommands{ + BuildCmd: parameters.StartOptions.BuildCommand, + RunCmd: parameters.StartOptions.RunCommand, + DebugCmd: parameters.StartOptions.DebugCommand, }, deployment) if err != nil { - return fmt.Errorf("unable to create or update component: %w", err) + return false, fmt.Errorf("unable to create or update component: %w", err) } ownerReference := generator.GetOwnerReference(deployment) // Delete remote resources that are not present in the Devfile - selector := odolabels.GetSelector(a.ComponentName, a.AppName, odolabels.ComponentDevMode, false) + selector := odolabels.GetSelector(componentName, appName, odolabels.ComponentDevMode, false) - objectsToRemove, serviceBindingSecretsToRemove, err := a.getRemoteResourcesNotPresentInDevfile(selector) + objectsToRemove, serviceBindingSecretsToRemove, err := o.getRemoteResourcesNotPresentInDevfile(ctx, parameters, selector) if err != nil { - return fmt.Errorf("unable to determine resources to delete: %w", err) + return false, fmt.Errorf("unable to determine resources to delete: %w", err) } - err = a.deleteRemoteResources(objectsToRemove) + err = o.deleteRemoteResources(objectsToRemove) if err != nil { - return fmt.Errorf("unable to delete remote resources: %w", err) + return false, fmt.Errorf("unable to delete remote resources: %w", err) } // this is mainly useful when the Service Binding Operator is not installed; // and the service binding secrets must be deleted manually since they are created by odo if len(serviceBindingSecretsToRemove) != 0 { - err = a.deleteServiceBindingSecrets(serviceBindingSecretsToRemove, deployment) + err = o.deleteServiceBindingSecrets(serviceBindingSecretsToRemove, deployment) if err != nil { - return fmt.Errorf("unable to delete service binding secrets: %w", err) + return false, fmt.Errorf("unable to delete service binding secrets: %w", err) } } // Create all the K8s components defined in the devfile - _, err = a.pushDevfileKubernetesComponents(labels, odolabels.ComponentDevMode, ownerReference) + _, err = o.pushDevfileKubernetesComponents(ctx, parameters, labels, odolabels.ComponentDevMode, ownerReference) if err != nil { - return err + return false, err } - err = a.updatePVCsOwnerReferences(ownerReference) + err = o.updatePVCsOwnerReferences(ctx, ownerReference) if err != nil { - return err + return false, err } if updated { klog.V(4).Infof("Deployment has been updated to generation %d. Waiting new event...\n", deployment.GetGeneration()) componentStatus.State = watch.StateWaitDeployment - return nil + return false, nil } numberReplicas := deployment.Status.ReadyReplicas if numberReplicas != 1 { klog.V(4).Infof("Deployment has %d ready replicas. Waiting new event...\n", numberReplicas) componentStatus.State = watch.StateWaitDeployment - return nil + return false, nil } - injected, err := a.bindingClient.CheckServiceBindingsInjectionDone(a.ComponentName, a.AppName) + injected, err := o.bindingClient.CheckServiceBindingsInjectionDone(componentName, appName) if err != nil { - return err + return false, err } if !injected { klog.V(4).Infof("Waiting for all service bindings to be injected...\n") - return errors.New("some servicebindings are not injected") + return false, errors.New("some servicebindings are not injected") } // Check if endpoints changed in Devfile - portsToForward, err := libdevfile.GetDevfileContainerEndpointMapping(a.Devfile, parameters.Debug) + o.portsToForward, err = libdevfile.GetDevfileContainerEndpointMapping(parameters.Devfile, parameters.StartOptions.Debug) if err != nil { - return err + return false, err } - portsChanged := !reflect.DeepEqual(portsToForward, a.portForwardClient.GetForwardedPorts()) + o.portsChanged = !reflect.DeepEqual(o.portsToForward, o.portForwardClient.GetForwardedPorts()) - if componentStatus.State == watch.StateReady && !portsChanged { + if componentStatus.State == watch.StateReady && !o.portsChanged { // If the deployment is already in Ready State, no need to continue - return nil - } - - // Now the Deployment has a Ready replica, we can get the Pod to work inside it - pod, err := a.kubeClient.GetPodUsingComponentName(a.ComponentName) - if err != nil { - return fmt.Errorf("unable to get pod for component %s: %w", a.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 := a.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: a.ComponentName, - ContainerName: containerName, - PodName: pod.GetName(), - SyncFolder: syncFolder, - } - - cmdKind := devfilev1.RunCommandGroupKind - cmdName := parameters.DevfileRunCmd - if parameters.Debug { - cmdKind = devfilev1.DebugCommandGroupKind - cmdName = parameters.DevfileDebugCmd - } - - syncParams := sync.SyncParameters{ - Path: parameters.Path, - WatchFiles: parameters.WatchFiles, - WatchDeletedFiles: parameters.WatchDeletedFiles, - IgnoredFiles: parameters.IgnoredFiles, - DevfileScanIndexForWatch: parameters.DevfileScanIndexForWatch, - - CompInfo: compInfo, - ForcePush: !deploymentExists || podChanged, - Files: adapters.GetSyncFilesFromAttributes(pushDevfileCommands[cmdKind]), - } - - execRequired, err := a.syncClient.SyncFiles(ctx, syncParams) - if err != nil { - componentStatus.State = watch.StateReady - return fmt.Errorf("failed to sync to component with name %s: %w", a.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(a.Devfile) { - err = libdevfile.ExecPostStartEvents(ctx, a.Devfile, component.NewExecHandler(a.kubeClient, a.execClient, a.AppName, a.ComponentName, pod.Name, "Executing post-start command in container", parameters.Show)) - if err != nil { - return err - } + return false, nil } - componentStatus.PostStartEventsDone = true + return true, nil +} - cmd, err := libdevfile.ValidateAndGetCommand(a.Devfile, cmdName, cmdKind) +func (o *DevClient) buildPushAutoImageComponents(ctx context.Context, fs filesystem.Filesystem, devfileObj parser.DevfileObj, compStatus *watch.ComponentStatus) error { + components, err := libdevfile.GetImageComponentsToPushAutomatically(devfileObj) if err != nil { return err } - commandType, err := parsercommon.GetCommandType(cmd) - if err != nil { - return err - } - var running bool - var isComposite bool - cmdHandler := runHandler{ - fs: a.FS, - execClient: a.execClient, - kubeClient: a.kubeClient, - appName: a.AppName, - componentName: a.ComponentName, - devfile: a.Devfile, - path: parameters.Path, - podName: pod.GetName(), - ctx: ctx, - } - - if commandType == devfilev1.ExecCommandType { - running, err = cmdHandler.IsRemoteProcessForCommandRunning(ctx, cmd, pod.Name) + 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 } - } 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) + compStatus.ImageComponentsAutoApplied[c.Name] = *c.Image } - 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(a.kubeClient, a.execClient, a.AppName, a.ComponentName, pod.Name, - "Building your application in container", parameters.Show) - return libdevfile.Build(ctx, a.Devfile, parameters.DevfileBuildCmd, 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 + // Remove keys that might no longer be valid + devfileHasCompFn := func(n string) bool { + for _, c := range components { + if c.Name == n { + return true } } - err = libdevfile.ExecuteCommandByNameAndKind(ctx, a.Devfile, cmdName, cmdKind, &cmdHandler, false) - if err != nil { - return err + return false + } + for n := range compStatus.ImageComponentsAutoApplied { + if !devfileHasCompFn(n) { + delete(compStatus.ImageComponentsAutoApplied, n) } } - if podChanged || portsChanged { - a.portForwardClient.StopPortForwarding(ctx, a.ComponentName) - } + return nil +} - // 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 = a.checkAppPorts(ctx, pod.Name, 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()) - } +// getComponentDeployment returns the deployment associated with the component, if deployed +// and indicate if the deployment has been found +func (o *DevClient) getComponentDeployment(ctx context.Context) (*appsv1.Deployment, bool, error) { + var ( + componentName = odocontext.GetComponentName(ctx) + appName = odocontext.GetApplication(ctx) + ) + + // 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(componentName, appName, odolabels.ComponentDevMode, true) + deployment, err := o.kubernetesClient.GetOneDeploymentFromSelector(selectorLabels) - err = a.portForwardClient.StartPortForwarding(ctx, a.Devfile, a.ComponentName, parameters.Debug, parameters.RandomPorts, log.GetStdout(), parameters.ErrOut, parameters.CustomForwardedPorts) if err != nil { - return adapters.NewErrPortForward(err) + if _, ok := err.(*kclient.DeploymentNotFoundError); !ok { + return nil, false, fmt.Errorf("unable to determine if component %s exists: %w", componentName, err) + } } - componentStatus.EndpointsForwarded = a.portForwardClient.GetForwardedPorts() - - componentStatus.State = watch.StateReady - return nil + componentExists := deployment != nil + return deployment, componentExists, nil } // createOrUpdateComponent creates the deployment or updates it if it already exists // with the expected spec. // Returns the new deployment and if the generation of the deployment has been updated -func (a *Adapter) createOrUpdateComponent( +func (o *DevClient) createOrUpdateComponent( + ctx context.Context, + parameters common.PushParameters, componentExists bool, commands libdevfile.DevfileCommands, deployment *appsv1.Deployment, ) (*appsv1.Deployment, bool, error) { - componentName := a.ComponentName + var ( + appName = odocontext.GetApplication(ctx) + componentName = odocontext.GetComponentName(ctx) + devfilePath = odocontext.GetDevfilePath(ctx) + path = filepath.Dir(devfilePath) + ) - runtime := component.GetComponentRuntimeFromDevfileMetadata(a.Devfile.Data.GetMetadata()) + runtime := component.GetComponentRuntimeFromDevfileMetadata(parameters.Devfile.Data.GetMetadata()) // Set the labels - labels := odolabels.GetLabels(componentName, a.AppName, runtime, odolabels.ComponentDevMode, true) + labels := odolabels.GetLabels(componentName, appName, runtime, odolabels.ComponentDevMode, true) annotations := make(map[string]string) - odolabels.SetProjectType(annotations, component.GetComponentTypeFromDevfileMetadata(a.AdapterContext.Devfile.Data.GetMetadata())) + odolabels.SetProjectType(annotations, component.GetComponentTypeFromDevfileMetadata(parameters.Devfile.Data.GetMetadata())) odolabels.AddCommonAnnotations(annotations) klog.V(4).Infof("We are deploying these annotations: %s", annotations) - deploymentObjectMeta, err := a.generateDeploymentObjectMeta(deployment, labels, annotations) + deploymentObjectMeta, err := o.generateDeploymentObjectMeta(ctx, deployment, labels, annotations) if err != nil { return nil, false, err } - policy, err := a.kubeClient.GetCurrentNamespacePolicy() + policy, err := o.kubernetesClient.GetCurrentNamespacePolicy() if err != nil { return nil, false, err } - podTemplateSpec, err := generator.GetPodTemplateSpec(a.Devfile, generator.PodTemplateParams{ + podTemplateSpec, err := generator.GetPodTemplateSpec(parameters.Devfile, generator.PodTemplateParams{ ObjectMeta: deploymentObjectMeta, PodSecurityAdmissionPolicy: policy, }) @@ -419,13 +281,13 @@ func (a *Adapter) createOrUpdateComponent( initContainers := podTemplateSpec.Spec.InitContainers - containers, err = utils.UpdateContainersEntrypointsIfNeeded(a.Devfile, containers, commands.BuildCmd, commands.RunCmd, commands.DebugCmd) + containers, err = utils.UpdateContainersEntrypointsIfNeeded(parameters.Devfile, containers, commands.BuildCmd, commands.RunCmd, commands.DebugCmd) if err != nil { return nil, false, err } // Returns the volumes to add to the PodTemplate and adds volumeMounts to the containers and initContainers - volumes, err := a.buildVolumes(containers, initContainers) + volumes, err := o.buildVolumes(ctx, parameters, containers, initContainers) if err != nil { return nil, false, err } @@ -449,7 +311,7 @@ func (a *Adapter) createOrUpdateComponent( originalGeneration = deployment.GetGeneration() } - deployment, err = generator.GetDeployment(a.Devfile, deployParams) + deployment, err = generator.GetDeployment(parameters.Devfile, deployParams) if err != nil { return nil, false, err } @@ -457,7 +319,7 @@ func (a *Adapter) createOrUpdateComponent( deployment.Annotations = make(map[string]string) } - if vcsUri := util.GetGitOriginPath(a.Context); vcsUri != "" { + if vcsUri := util.GetGitOriginPath(path); vcsUri != "" { deployment.Annotations["app.openshift.io/vcs-uri"] = vcsUri } @@ -466,16 +328,16 @@ func (a *Adapter) createOrUpdateComponent( serviceAnnotations["service.binding/backend_ip"] = "path={.spec.clusterIP}" serviceAnnotations["service.binding/backend_port"] = "path={.spec.ports},elementType=sliceOfMaps,sourceKey=name,sourceValue=port" - serviceName, err := util.NamespaceKubernetesObjectWithTrim(componentName, a.AppName, 63) + serviceName, err := util.NamespaceKubernetesObjectWithTrim(componentName, appName, 63) if err != nil { return nil, false, err } - serviceObjectMeta := generator.GetObjectMeta(serviceName, a.kubeClient.GetCurrentNamespace(), labels, serviceAnnotations) + serviceObjectMeta := generator.GetObjectMeta(serviceName, o.kubernetesClient.GetCurrentNamespace(), labels, serviceAnnotations) serviceParams := generator.ServiceParams{ ObjectMeta: serviceObjectMeta, SelectorLabels: selectorLabels, } - svc, err := generator.GetService(a.Devfile, serviceParams, parsercommon.DevfileOptions{}) + svc, err := generator.GetService(parameters.Devfile, serviceParams, parsercommon.DevfileOptions{}) if err != nil { return nil, false, err @@ -485,27 +347,27 @@ func (a *Adapter) createOrUpdateComponent( if componentExists { // If the component already exists, get the resource version of the deploy before updating klog.V(2).Info("The component already exists, attempting to update it") - if a.kubeClient.IsSSASupported() { + if o.kubernetesClient.IsSSASupported() { klog.V(4).Info("Applying deployment") - deployment, err = a.kubeClient.ApplyDeployment(*deployment) + deployment, err = o.kubernetesClient.ApplyDeployment(*deployment) } else { klog.V(4).Info("Updating deployment") - deployment, err = a.kubeClient.UpdateDeployment(*deployment) + deployment, err = o.kubernetesClient.UpdateDeployment(*deployment) } if err != nil { return nil, false, err } klog.V(2).Infof("Successfully updated component %v", componentName) ownerReference := generator.GetOwnerReference(deployment) - err = a.createOrUpdateServiceForComponent(svc, componentName, ownerReference) + err = o.createOrUpdateServiceForComponent(ctx, svc, ownerReference) if err != nil { return nil, false, err } } else { - if a.kubeClient.IsSSASupported() { - deployment, err = a.kubeClient.ApplyDeployment(*deployment) + if o.kubernetesClient.IsSSASupported() { + deployment, err = o.kubernetesClient.ApplyDeployment(*deployment) } else { - deployment, err = a.kubeClient.CreateDeployment(*deployment) + deployment, err = o.kubernetesClient.CreateDeployment(*deployment) } if err != nil { @@ -516,9 +378,9 @@ func (a *Adapter) createOrUpdateComponent( if len(svc.Spec.Ports) > 0 { ownerReference := generator.GetOwnerReference(deployment) originOwnerRefs := svc.OwnerReferences - err = a.kubeClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { + err = o.kubernetesClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { svc.OwnerReferences = append(originOwnerRefs, ownerRef) - _, err = a.kubeClient.CreateService(*svc) + _, err = o.kubernetesClient.CreateService(*svc) return err }) if err != nil { @@ -533,136 +395,17 @@ func (a *Adapter) createOrUpdateComponent( return deployment, newGeneration != originalGeneration, nil } -// buildVolumes: -// - (side effect on cluster) creates the PVC for the project sources if Epehemeral preference is false -// - (side effect on cluster) creates the PVCs for non-ephemeral volumes defined in the Devfile -// - (side effect on input parameters) adds volumeMounts to containers and initContainers for the PVCs and Ephemeral volumes -// - (side effect on input parameters) adds volumeMounts for automounted volumes -// => Returns the list of Volumes to add to the PodTemplate -func (a *Adapter) buildVolumes(containers, initContainers []corev1.Container) ([]corev1.Volume, error) { - - runtime := component.GetComponentRuntimeFromDevfileMetadata(a.Devfile.Data.GetMetadata()) - - storageClient := storagepkg.NewClient(a.ComponentName, a.AppName, storagepkg.ClientOptions{ - Client: a.kubeClient, - Runtime: runtime, - }) - - // Create the PVC for the project sources, if not ephemeral - err := storage.HandleOdoSourceStorage(a.kubeClient, storageClient, a.ComponentName, a.prefClient.GetEphemeralSourceVolume()) - if err != nil { - return nil, err - } - - // Create PVCs for non-ephemeral Volumes defined in the Devfile - // and returns the Ephemeral volumes defined in the Devfile - ephemerals, err := storagepkg.Push(storageClient, a.Devfile) - if err != nil { - return nil, err - } - - // get all the PVCs from the cluster belonging to the component - // These PVCs have been created earlier with `storage.HandleOdoSourceStorage` and `storagepkg.Push` - pvcs, err := a.kubeClient.ListPVCs(fmt.Sprintf("%v=%v", "component", a.ComponentName)) - if err != nil { - return nil, err - } - - var allVolumes []corev1.Volume - - // Get the name of the PVC for project sources + a map of (storageName => VolumeInfo) - // odoSourcePVCName will be empty when Ephemeral preference is true - odoSourcePVCName, volumeNameToVolInfo, err := storage.GetVolumeInfos(pvcs) - if err != nil { - return nil, err - } - - // Add the volumes for the projects source and the Odo-specific directory - odoMandatoryVolumes := utils.GetOdoContainerVolumes(odoSourcePVCName) - allVolumes = append(allVolumes, odoMandatoryVolumes...) - - // Add the volumeMounts for the project sources volume and the Odo-specific volume into the containers - utils.AddOdoProjectVolume(containers) - utils.AddOdoMandatoryVolume(containers) - - // Get PVC volumes and Volume Mounts - pvcVolumes, err := storage.GetPersistentVolumesAndVolumeMounts(a.Devfile, containers, initContainers, volumeNameToVolInfo, parsercommon.DevfileOptions{}) - if err != nil { - return nil, err - } - allVolumes = append(allVolumes, pvcVolumes...) - - ephemeralVolumes, err := storage.GetEphemeralVolumesAndVolumeMounts(a.Devfile, containers, initContainers, ephemerals, parsercommon.DevfileOptions{}) - if err != nil { - return nil, err - } - allVolumes = append(allVolumes, ephemeralVolumes...) - - automountVolumes, err := storage.GetAutomountVolumes(a.configAutomountClient, containers, initContainers) - if err != nil { - return nil, err - } - allVolumes = append(allVolumes, automountVolumes...) - - return allVolumes, nil -} - -func (a *Adapter) createOrUpdateServiceForComponent(svc *corev1.Service, componentName string, ownerReference metav1.OwnerReference) error { - oldSvc, err := a.kubeClient.GetOneService(a.ComponentName, a.AppName, true) - originOwnerReferences := svc.OwnerReferences - if err != nil { - // no old service was found, create a new one - if len(svc.Spec.Ports) > 0 { - err = a.kubeClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { - svc.OwnerReferences = append(originOwnerReferences, ownerReference) - _, err = a.kubeClient.CreateService(*svc) - return err - }) - if err != nil { - return err - } - klog.V(2).Infof("Successfully created Service for component %s", componentName) - } - return nil - } - if len(svc.Spec.Ports) > 0 { - svc.Spec.ClusterIP = oldSvc.Spec.ClusterIP - svc.ResourceVersion = oldSvc.GetResourceVersion() - err = a.kubeClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { - svc.OwnerReferences = append(originOwnerReferences, ownerRef) - _, err = a.kubeClient.UpdateService(*svc) - return err - }) - if err != nil { - return err - } - klog.V(2).Infof("Successfully update Service for component %s", componentName) - return nil - } - // delete the old existing service if the component currently doesn't expose any ports - return a.kubeClient.DeleteService(oldSvc.Name) -} - -// generateDeploymentObjectMeta generates a ObjectMeta object for the given deployment's name, labels and annotations -// if no deployment exists, it creates a new deployment name -func (a Adapter) generateDeploymentObjectMeta(deployment *appsv1.Deployment, labels map[string]string, annotations map[string]string) (metav1.ObjectMeta, error) { - if deployment != nil { - return generator.GetObjectMeta(deployment.Name, a.kubeClient.GetCurrentNamespace(), labels, annotations), nil - } else { - deploymentName, err := util.NamespaceKubernetesObject(a.ComponentName, a.AppName) - if err != nil { - return metav1.ObjectMeta{}, err - } - return generator.GetObjectMeta(deploymentName, a.kubeClient.GetCurrentNamespace(), labels, annotations), nil - } -} - // getRemoteResourcesNotPresentInDevfile compares the list of Devfile K8s component and remote K8s resources // and returns a list of the remote resources not present in the Devfile and in case the SBO is not installed, a list of service binding secrets that must be deleted; // it ignores the core components (such as deployments, svc, pods; all resources with `component:` label) -func (a Adapter) getRemoteResourcesNotPresentInDevfile(selector string) (objectsToRemove, serviceBindingSecretsToRemove []unstructured.Unstructured, err error) { - currentNamespace := a.kubeClient.GetCurrentNamespace() - allRemoteK8sResources, err := a.kubeClient.GetAllResourcesFromSelector(selector, currentNamespace) +func (o DevClient) getRemoteResourcesNotPresentInDevfile(ctx context.Context, parameters common.PushParameters, selector string) (objectsToRemove, serviceBindingSecretsToRemove []unstructured.Unstructured, err error) { + var ( + devfilePath = odocontext.GetDevfilePath(ctx) + path = filepath.Dir(devfilePath) + ) + + currentNamespace := o.kubernetesClient.GetCurrentNamespace() + allRemoteK8sResources, err := o.kubernetesClient.GetAllResourcesFromSelector(selector, currentNamespace) if err != nil { return nil, nil, fmt.Errorf("unable to fetch remote resources: %w", err) } @@ -683,7 +426,7 @@ func (a Adapter) getRemoteResourcesNotPresentInDevfile(selector string) (objects } var devfileK8sResources []devfilev1.Component - devfileK8sResources, err = libdevfile.GetK8sAndOcComponentsToPush(a.Devfile, true) + devfileK8sResources, err = libdevfile.GetK8sAndOcComponentsToPush(parameters.Devfile, true) if err != nil { return nil, nil, fmt.Errorf("unable to obtain resources from the Devfile: %w", err) } @@ -692,14 +435,14 @@ func (a Adapter) getRemoteResourcesNotPresentInDevfile(selector string) (objects var devfileK8sResourcesUnstructured []unstructured.Unstructured for _, devfileK := range devfileK8sResources { var devfileKUnstructuredList []unstructured.Unstructured - devfileKUnstructuredList, err = libdevfile.GetK8sComponentAsUnstructuredList(a.Devfile, devfileK.Name, a.Context, devfilefs.DefaultFs{}) + devfileKUnstructuredList, err = libdevfile.GetK8sComponentAsUnstructuredList(parameters.Devfile, devfileK.Name, path, devfilefs.DefaultFs{}) if err != nil { return nil, nil, fmt.Errorf("unable to read the resource: %w", err) } devfileK8sResourcesUnstructured = append(devfileK8sResourcesUnstructured, devfileKUnstructuredList...) } - isSBOSupported, err := a.kubeClient.IsServiceBindingSupported() + isSBOSupported, err := o.kubernetesClient.IsServiceBindingSupported() if err != nil { return nil, nil, fmt.Errorf("error in determining support for the Service Binding Operator: %w", err) } @@ -739,7 +482,7 @@ func (a Adapter) getRemoteResourcesNotPresentInDevfile(selector string) (objects } // deleteRemoteResources takes a list of remote resources to be deleted -func (a Adapter) deleteRemoteResources(objectsToRemove []unstructured.Unstructured) error { +func (o DevClient) deleteRemoteResources(objectsToRemove []unstructured.Unstructured) error { if len(objectsToRemove) == 0 { return nil } @@ -757,12 +500,12 @@ func (a Adapter) deleteRemoteResources(objectsToRemove []unstructured.Unstructur // See https://golang.org/doc/faq#closures_and_goroutines for more details. objectToRemove := objectToRemove g.Go(func() error { - gvr, err := a.kubeClient.GetGVRFromGVK(objectToRemove.GroupVersionKind()) + gvr, err := o.kubernetesClient.GetGVRFromGVK(objectToRemove.GroupVersionKind()) if err != nil { return fmt.Errorf("unable to get information about resource: %s/%s: %w", objectToRemove.GetKind(), objectToRemove.GetName(), err) } - err = a.kubeClient.DeleteDynamicResource(objectToRemove.GetName(), gvr, true) + err = o.kubernetesClient.DeleteDynamicResource(objectToRemove.GetName(), gvr, true) if err != nil { if !(kerrors.IsNotFound(err) || kerrors.IsMethodNotSupported(err)) { return fmt.Errorf("unable to delete resource: %s/%s: %w", objectToRemove.GetKind(), objectToRemove.GetName(), err) @@ -783,19 +526,19 @@ func (a Adapter) deleteRemoteResources(objectsToRemove []unstructured.Unstructur // deleteServiceBindingSecrets takes a list of Service Binding secrets(unstructured) that should be deleted; // this is helpful when Service Binding Operator is not installed on the cluster -func (a Adapter) deleteServiceBindingSecrets(serviceBindingSecretsToRemove []unstructured.Unstructured, deployment *appsv1.Deployment) error { +func (o DevClient) deleteServiceBindingSecrets(serviceBindingSecretsToRemove []unstructured.Unstructured, deployment *appsv1.Deployment) error { for _, secretToRemove := range serviceBindingSecretsToRemove { spinner := log.Spinnerf("Deleting Kubernetes resource: %s/%s", secretToRemove.GetKind(), secretToRemove.GetName()) defer spinner.End(false) - err := service.UnbindWithLibrary(a.kubeClient, secretToRemove, deployment) + err := service.UnbindWithLibrary(o.kubernetesClient, secretToRemove, deployment) if err != nil { return fmt.Errorf("failed to unbind secret %q from the application", secretToRemove.GetName()) } // since the library currently doesn't delete the secret after unbinding // delete the secret manually - err = a.kubeClient.DeleteSecret(secretToRemove.GetName(), a.kubeClient.GetCurrentNamespace()) + err = o.kubernetesClient.DeleteSecret(secretToRemove.GetName(), o.kubernetesClient.GetCurrentNamespace()) if err != nil { if !kerrors.IsNotFound(err) { return fmt.Errorf("unable to delete Kubernetes resource: %s/%s: %s", secretToRemove.GetKind(), secretToRemove.GetName(), err.Error()) @@ -807,15 +550,203 @@ func (a Adapter) deleteServiceBindingSecrets(serviceBindingSecretsToRemove []uns return nil } -func (a *Adapter) 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) +// pushDevfileKubernetesComponents gets the Kubernetes components from the Devfile and push them to the cluster +// adding the specified labels and ownerreference to them +func (o *DevClient) pushDevfileKubernetesComponents( + ctx context.Context, + parameters common.PushParameters, + labels map[string]string, + mode string, + reference metav1.OwnerReference, +) ([]devfilev1.Component, error) { + var ( + devfilePath = odocontext.GetDevfilePath(ctx) + path = filepath.Dir(devfilePath) + ) + + // fetch the "kubernetes inlined components" to create them on cluster + // from odo standpoint, these components contain yaml manifest of ServiceBinding + k8sComponents, err := libdevfile.GetK8sAndOcComponentsToPush(parameters.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(o.kubernetesClient, parameters.Devfile, k8sComponents, path) + if err != nil { + return nil, err + } + + // Set the annotations for the component type + annotations := make(map[string]string) + odolabels.SetProjectType(annotations, component.GetComponentTypeFromDevfileMetadata(parameters.Devfile.Data.GetMetadata())) + + // create the Kubernetes objects from the manifest and delete the ones not in the devfile + err = service.PushKubernetesResources(o.kubernetesClient, parameters.Devfile, k8sComponents, labels, annotations, path, mode, reference) + if err != nil { + return nil, fmt.Errorf("failed to create Kubernetes resources associated with the component: %w", err) + } + return k8sComponents, nil +} + +func (o *DevClient) updatePVCsOwnerReferences(ctx context.Context, ownerReference metav1.OwnerReference) error { + var ( + componentName = odocontext.GetComponentName(ctx) + ) + + // list the latest state of the PVCs + pvcs, err := o.kubernetesClient.ListPVCs(fmt.Sprintf("%v=%v", "component", 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 = o.kubernetesClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { + return o.kubernetesClient.UpdateStorageOwnerReference(&pvcs[i], ownerRef) + }) + if err != nil { + return err + } + } + return nil +} + +// generateDeploymentObjectMeta generates a ObjectMeta object for the given deployment's name, labels and annotations +// if no deployment exists, it creates a new deployment name +func (o DevClient) generateDeploymentObjectMeta(ctx context.Context, deployment *appsv1.Deployment, labels map[string]string, annotations map[string]string) (metav1.ObjectMeta, error) { + var ( + appName = odocontext.GetApplication(ctx) + componentName = odocontext.GetComponentName(ctx) + ) + if deployment != nil { + return generator.GetObjectMeta(deployment.Name, o.kubernetesClient.GetCurrentNamespace(), labels, annotations), nil + } else { + deploymentName, err := util.NamespaceKubernetesObject(componentName, appName) + if err != nil { + return metav1.ObjectMeta{}, err } + return generator.GetObjectMeta(deploymentName, o.kubernetesClient.GetCurrentNamespace(), labels, annotations), nil + } +} + +// buildVolumes: +// - (side effect on cluster) creates the PVC for the project sources if Epehemeral preference is false +// - (side effect on cluster) creates the PVCs for non-ephemeral volumes defined in the Devfile +// - (side effect on input parameters) adds volumeMounts to containers and initContainers for the PVCs and Ephemeral volumes +// - (side effect on input parameters) adds volumeMounts for automounted volumes +// => Returns the list of Volumes to add to the PodTemplate +func (o *DevClient) buildVolumes(ctx context.Context, parameters common.PushParameters, containers, initContainers []corev1.Container) ([]corev1.Volume, error) { + var ( + appName = odocontext.GetApplication(ctx) + componentName = odocontext.GetComponentName(ctx) + ) + + runtime := component.GetComponentRuntimeFromDevfileMetadata(parameters.Devfile.Data.GetMetadata()) + + storageClient := storagepkg.NewClient(componentName, appName, storagepkg.ClientOptions{ + Client: o.kubernetesClient, + Runtime: runtime, + }) + + // Create the PVC for the project sources, if not ephemeral + err := storage.HandleOdoSourceStorage(o.kubernetesClient, storageClient, componentName, o.prefClient.GetEphemeralSourceVolume()) + if err != nil { + return nil, err + } + + // Create PVCs for non-ephemeral Volumes defined in the Devfile + // and returns the Ephemeral volumes defined in the Devfile + ephemerals, err := storagepkg.Push(storageClient, parameters.Devfile) + if err != nil { + return nil, err + } + + // get all the PVCs from the cluster belonging to the component + // These PVCs have been created earlier with `storage.HandleOdoSourceStorage` and `storagepkg.Push` + pvcs, err := o.kubernetesClient.ListPVCs(fmt.Sprintf("%v=%v", "component", componentName)) + if err != nil { + return nil, err + } + + var allVolumes []corev1.Volume + + // Get the name of the PVC for project sources + a map of (storageName => VolumeInfo) + // odoSourcePVCName will be empty when Ephemeral preference is true + odoSourcePVCName, volumeNameToVolInfo, err := storage.GetVolumeInfos(pvcs) + if err != nil { + return nil, err + } + + // Add the volumes for the projects source and the Odo-specific directory + odoMandatoryVolumes := utils.GetOdoContainerVolumes(odoSourcePVCName) + allVolumes = append(allVolumes, odoMandatoryVolumes...) + + // Add the volumeMounts for the project sources volume and the Odo-specific volume into the containers + utils.AddOdoProjectVolume(containers) + utils.AddOdoMandatoryVolume(containers) + + // Get PVC volumes and Volume Mounts + pvcVolumes, err := storage.GetPersistentVolumesAndVolumeMounts(parameters.Devfile, containers, initContainers, volumeNameToVolInfo, parsercommon.DevfileOptions{}) + if err != nil { + return nil, err + } + allVolumes = append(allVolumes, pvcVolumes...) + + ephemeralVolumes, err := storage.GetEphemeralVolumesAndVolumeMounts(parameters.Devfile, containers, initContainers, ephemerals, parsercommon.DevfileOptions{}) + if err != nil { + return nil, err + } + allVolumes = append(allVolumes, ephemeralVolumes...) + + automountVolumes, err := storage.GetAutomountVolumes(o.configAutomountClient, containers, initContainers) + if err != nil { + return nil, err } - return port.CheckAppPortsListening(ctx, a.execClient, podName, containerPortsMapping, 1*time.Minute) + allVolumes = append(allVolumes, automountVolumes...) + + return allVolumes, nil } -// PushCommandsMap stores the commands to be executed as per their types. -type PushCommandsMap map[devfilev1.CommandGroupKind]devfilev1.Command +func (o *DevClient) createOrUpdateServiceForComponent(ctx context.Context, svc *corev1.Service, ownerReference metav1.OwnerReference) error { + var ( + appName = odocontext.GetApplication(ctx) + componentName = odocontext.GetComponentName(ctx) + ) + oldSvc, err := o.kubernetesClient.GetOneService(componentName, appName, true) + originOwnerReferences := svc.OwnerReferences + if err != nil { + // no old service was found, create a new one + if len(svc.Spec.Ports) > 0 { + err = o.kubernetesClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { + svc.OwnerReferences = append(originOwnerReferences, ownerReference) + _, err = o.kubernetesClient.CreateService(*svc) + return err + }) + if err != nil { + return err + } + klog.V(2).Infof("Successfully created Service for component %s", componentName) + } + return nil + } + if len(svc.Spec.Ports) > 0 { + svc.Spec.ClusterIP = oldSvc.Spec.ClusterIP + svc.ResourceVersion = oldSvc.GetResourceVersion() + err = o.kubernetesClient.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { + svc.OwnerReferences = append(originOwnerReferences, ownerRef) + _, err = o.kubernetesClient.UpdateService(*svc) + return err + }) + if err != nil { + return err + } + klog.V(2).Infof("Successfully update Service for component %s", componentName) + return nil + } + // delete the old existing service if the component currently doesn't expose any ports + return o.kubernetesClient.DeleteService(oldSvc.Name) +} diff --git a/pkg/devfile/adapters/kubernetes/component/handler.go b/pkg/dev/kubedev/handler.go similarity index 99% rename from pkg/devfile/adapters/kubernetes/component/handler.go rename to pkg/dev/kubedev/handler.go index c4114d7a7cc..0d9ac926e36 100644 --- a/pkg/devfile/adapters/kubernetes/component/handler.go +++ b/pkg/dev/kubedev/handler.go @@ -1,4 +1,4 @@ -package component +package kubedev import ( "context" diff --git a/pkg/dev/kubedev/innerloop.go b/pkg/dev/kubedev/innerloop.go new file mode 100644 index 00000000000..3b49fc12ed0 --- /dev/null +++ b/pkg/dev/kubedev/innerloop.go @@ -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) +} diff --git a/pkg/dev/kubedev/kubedev.go b/pkg/dev/kubedev/kubedev.go index 47972932461..961c96bd7fc 100644 --- a/pkg/dev/kubedev/kubedev.go +++ b/pkg/dev/kubedev/kubedev.go @@ -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 -} diff --git a/pkg/devfile/adapters/kubernetes/component/adapter_test.go b/pkg/dev/kubedev/push_test.go similarity index 93% rename from pkg/devfile/adapters/kubernetes/component/adapter_test.go rename to pkg/dev/kubedev/push_test.go index d987508b6f8..82d59b30069 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter_test.go +++ b/pkg/dev/kubedev/push_test.go @@ -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) diff --git a/pkg/dev/kubedev/reconcile.go b/pkg/dev/kubedev/reconcile.go new file mode 100644 index 00000000000..fab6ca1b0c7 --- /dev/null +++ b/pkg/dev/kubedev/reconcile.go @@ -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) +} diff --git a/pkg/devfile/adapters/kubernetes/storage/utils.go b/pkg/dev/kubedev/storage/utils.go similarity index 100% rename from pkg/devfile/adapters/kubernetes/storage/utils.go rename to pkg/dev/kubedev/storage/utils.go diff --git a/pkg/devfile/adapters/kubernetes/storage/utils_test.go b/pkg/dev/kubedev/storage/utils_test.go similarity index 100% rename from pkg/devfile/adapters/kubernetes/storage/utils_test.go rename to pkg/dev/kubedev/storage/utils_test.go diff --git a/pkg/devfile/adapters/kubernetes/utils/utils.go b/pkg/dev/kubedev/utils/utils.go similarity index 100% rename from pkg/devfile/adapters/kubernetes/utils/utils.go rename to pkg/dev/kubedev/utils/utils.go diff --git a/pkg/devfile/adapters/kubernetes/utils/utils_test.go b/pkg/dev/kubedev/utils/utils_test.go similarity index 100% rename from pkg/devfile/adapters/kubernetes/utils/utils_test.go rename to pkg/dev/kubedev/utils/utils_test.go diff --git a/pkg/dev/mock.go b/pkg/dev/mock.go index c687fe98985..c615af8e388 100644 --- a/pkg/dev/mock.go +++ b/pkg/dev/mock.go @@ -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) } diff --git a/pkg/dev/podmandev/pod.go b/pkg/dev/podmandev/pod.go index df26ba9e597..3402dd92811 100644 --- a/pkg/dev/podmandev/pod.go +++ b/pkg/dev/podmandev/pod.go @@ -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" diff --git a/pkg/dev/podmandev/podmandev.go b/pkg/dev/podmandev/podmandev.go index e46a4d579df..7db0c996049 100644 --- a/pkg/dev/podmandev/podmandev.go +++ b/pkg/dev/podmandev/podmandev.go @@ -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") } } diff --git a/pkg/dev/podmandev/reconcile.go b/pkg/dev/podmandev/reconcile.go index 01a654a83f1..ccfab4b7dda 100644 --- a/pkg/dev/podmandev/reconcile.go +++ b/pkg/dev/podmandev/reconcile.go @@ -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 { diff --git a/pkg/devfile/adapters/kubernetes/component/interface.go b/pkg/devfile/adapters/kubernetes/component/interface.go deleted file mode 100644 index 40b3a3c88d5..00000000000 --- a/pkg/devfile/adapters/kubernetes/component/interface.go +++ /dev/null @@ -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 -} diff --git a/pkg/devfile/adapters/kubernetes/component/push.go b/pkg/devfile/adapters/kubernetes/component/push.go deleted file mode 100644 index 4587da2b880..00000000000 --- a/pkg/devfile/adapters/kubernetes/component/push.go +++ /dev/null @@ -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 -} diff --git a/pkg/devfile/adapters/types.go b/pkg/devfile/adapters/types.go deleted file mode 100644 index 570facf8290..00000000000 --- a/pkg/devfile/adapters/types.go +++ /dev/null @@ -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 -} diff --git a/pkg/odo/cli/dev/dev.go b/pkg/odo/cli/dev/dev.go index 2e35f481471..4a24fc8e6ff 100644 --- a/pkg/odo/cli/dev/dev.go +++ b/pkg/odo/cli/dev/dev.go @@ -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, }, ) } diff --git a/pkg/watch/interface.go b/pkg/watch/interface.go index 544c0e052c2..1abff35149a 100644 --- a/pkg/watch/interface.go +++ b/pkg/watch/interface.go @@ -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 } diff --git a/pkg/watch/mock.go b/pkg/watch/mock.go index 5b615b9ab95..49d9756db4b 100644 --- a/pkg/watch/mock.go +++ b/pkg/watch/mock.go @@ -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) } diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index ef789e43444..5e75167c2c0 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -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(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) + ) -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) + 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{}) } diff --git a/pkg/watch/watch_test.go b/pkg/watch/watch_test.go index 33247f6f4d2..c72eb12715a 100644 --- a/pkg/watch/watch_test.go +++ b/pkg/watch/watch_test.go @@ -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