...

Source file src/github.com/redhat-developer/odo/pkg/dev/podmandev/reconcile.go

Documentation: github.com/redhat-developer/odo/pkg/dev/podmandev

     1  package podmandev
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
    12  	"github.com/devfile/library/v2/pkg/devfile/parser"
    13  	"github.com/fatih/color"
    14  
    15  	"github.com/redhat-developer/odo/pkg/api"
    16  	"github.com/redhat-developer/odo/pkg/component"
    17  	envcontext "github.com/redhat-developer/odo/pkg/config/context"
    18  	"github.com/redhat-developer/odo/pkg/dev"
    19  	"github.com/redhat-developer/odo/pkg/dev/common"
    20  	"github.com/redhat-developer/odo/pkg/devfile/image"
    21  	"github.com/redhat-developer/odo/pkg/libdevfile"
    22  	"github.com/redhat-developer/odo/pkg/log"
    23  	odocontext "github.com/redhat-developer/odo/pkg/odo/context"
    24  	"github.com/redhat-developer/odo/pkg/port"
    25  	"github.com/redhat-developer/odo/pkg/watch"
    26  
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/equality"
    29  	"k8s.io/klog"
    30  )
    31  
    32  func (o *DevClient) reconcile(
    33  	ctx context.Context,
    34  	parameters common.PushParameters,
    35  	componentStatus *watch.ComponentStatus,
    36  ) error {
    37  	var (
    38  		componentName = odocontext.GetComponentName(ctx)
    39  		devfilePath   = odocontext.GetDevfilePath(ctx)
    40  		path          = filepath.Dir(devfilePath)
    41  		options       = parameters.StartOptions
    42  		devfileObj    = parameters.Devfile
    43  	)
    44  
    45  	o.warnAboutK8sComponents(devfileObj)
    46  
    47  	err := o.buildPushAutoImageComponents(ctx, devfileObj)
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	pod, fwPorts, err := o.deployPod(ctx, options, devfileObj)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	o.deployedPod = pod
    57  	componentStatus.SetState(watch.StateReady)
    58  
    59  	execRequired, err := o.syncFiles(ctx, options, pod, path)
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	// PostStart events from the devfile will only be executed when the component
    65  	// didn't previously exist
    66  	if !componentStatus.PostStartEventsDone && libdevfile.HasPostStartEvents(devfileObj) {
    67  		execHandler := component.NewRunHandler(
    68  			ctx,
    69  			o.podmanClient,
    70  			o.execClient,
    71  			nil, // TODO(feloy) set this value when we want to support exec on new container on podman
    72  			// TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PostStart commands
    73  			nil, nil,
    74  			component.HandlerOptions{
    75  				PodName:           pod.Name,
    76  				ContainersRunning: component.GetContainersNames(pod),
    77  				Msg:               "Executing post-start command in container",
    78  			},
    79  		)
    80  		err = libdevfile.ExecPostStartEvents(ctx, devfileObj, execHandler)
    81  		if err != nil {
    82  			return err
    83  		}
    84  	}
    85  	componentStatus.PostStartEventsDone = true
    86  
    87  	innerLoopWithCommands := !parameters.StartOptions.SkipCommands
    88  	var hasRunOrDebugCmd bool
    89  	if innerLoopWithCommands {
    90  		if execRequired {
    91  			doExecuteBuildCommand := func() error {
    92  				execHandler := component.NewRunHandler(
    93  					ctx,
    94  					o.podmanClient,
    95  					o.execClient,
    96  					nil, // TODO(feloy) set this value when we want to support exec on new container on podman
    97  
    98  					// TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PreStop events
    99  					nil, nil, component.HandlerOptions{
   100  						PodName:           pod.Name,
   101  						ComponentExists:   componentStatus.RunExecuted,
   102  						ContainersRunning: component.GetContainersNames(pod),
   103  						Msg:               "Building your application in container",
   104  					},
   105  				)
   106  				return libdevfile.Build(ctx, devfileObj, options.BuildCommand, execHandler)
   107  			}
   108  
   109  			err = doExecuteBuildCommand()
   110  			if err != nil {
   111  				return err
   112  			}
   113  
   114  			cmdKind := devfilev1.RunCommandGroupKind
   115  			cmdName := options.RunCommand
   116  			if options.Debug {
   117  				cmdKind = devfilev1.DebugCommandGroupKind
   118  				cmdName = options.DebugCommand
   119  			}
   120  			_, hasRunOrDebugCmd, err = libdevfile.GetCommand(parameters.Devfile, cmdName, cmdKind)
   121  			if err != nil {
   122  				return err
   123  			}
   124  
   125  			if hasRunOrDebugCmd {
   126  				cmdHandler := component.NewRunHandler(
   127  					ctx,
   128  					o.podmanClient,
   129  					o.execClient,
   130  					nil, // TODO(feloy) set this value when we want to support exec on new container on podman
   131  
   132  					o.fs,
   133  					image.SelectBackend(ctx),
   134  
   135  					// TODO(feloy) set to deploy Kubernetes/Openshift components
   136  					component.HandlerOptions{
   137  						PodName:           pod.Name,
   138  						ComponentExists:   componentStatus.RunExecuted,
   139  						ContainersRunning: component.GetContainersNames(pod),
   140  					},
   141  				)
   142  				err = libdevfile.ExecuteCommandByNameAndKind(ctx, devfileObj, cmdName, cmdKind, cmdHandler, false)
   143  				if err != nil {
   144  					return err
   145  				}
   146  				componentStatus.RunExecuted = true
   147  			} else {
   148  				msg := fmt.Sprintf("Missing default %v command", cmdKind)
   149  				if cmdName != "" {
   150  					msg = fmt.Sprintf("Missing %v command with name %q", cmdKind, cmdName)
   151  				}
   152  				log.Warning(msg)
   153  			}
   154  		}
   155  	}
   156  
   157  	if innerLoopWithCommands && hasRunOrDebugCmd && len(fwPorts) != 0 {
   158  		// Check that the application is actually listening on the ports declared in the Devfile, so we are sure that port-forwarding will work
   159  		appReadySpinner := log.Spinner("Waiting for the application to be ready")
   160  		err = o.checkAppPorts(ctx, pod.Name, fwPorts)
   161  		appReadySpinner.End(err == nil)
   162  		if err != nil {
   163  			log.Warningf("Port forwarding might not work correctly: %v", err)
   164  			log.Warning("Running `odo logs --follow --platform podman` might help in identifying the problem.")
   165  			fmt.Fprintln(options.Out)
   166  		}
   167  	}
   168  
   169  	// By default, Podman will not forward to container applications listening on the loopback interface.
   170  	// So we are trying to detect such cases and act accordingly.
   171  	// See https://github.com/redhat-developer/odo/issues/6510#issuecomment-1439986558
   172  	err = o.handleLoopbackPorts(ctx, options, pod, fwPorts)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	if options.ForwardLocalhost {
   178  		// Port-forwarding is enabled by executing dedicated socat commands
   179  		err = o.portForwardClient.StartPortForwarding(ctx, devfileObj, componentName, options.Debug, options.RandomPorts, options.Out, options.ErrOut, fwPorts, options.CustomAddress)
   180  		if err != nil {
   181  			return common.NewErrPortForward(err)
   182  		}
   183  	} // else port-forwarding is done via the main container ports in the pod spec
   184  
   185  	for _, fwPort := range fwPorts {
   186  		s := fmt.Sprintf("Forwarding from %s:%d -> %d", fwPort.LocalAddress, fwPort.LocalPort, fwPort.ContainerPort)
   187  		fmt.Fprintf(options.Out, " -  %s", log.SboldColor(color.FgGreen, s))
   188  	}
   189  	err = o.stateClient.SetForwardedPorts(ctx, fwPorts)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	componentStatus.SetState(watch.StateReady)
   195  	return nil
   196  }
   197  
   198  // warnAboutApplyComponents prints a warning if the Devfile contains standalone K8s components (not referenced by any Apply commands). These resources are currently applied when running in the cluster mode, but not on Podman.
   199  func (o *DevClient) warnAboutK8sComponents(devfileObj parser.DevfileObj) {
   200  	var components []string
   201  	// get all standalone k8s components for a given commandGK
   202  	k8sComponents, _ := libdevfile.GetK8sAndOcComponentsToPush(devfileObj, false)
   203  
   204  	if len(k8sComponents) == 0 {
   205  		return
   206  	}
   207  
   208  	for _, comp := range k8sComponents {
   209  		components = append(components, comp.Name)
   210  	}
   211  
   212  	log.Warningf("Kubernetes components are not supported on Podman. Skipping: %v.", strings.Join(components, ", "))
   213  }
   214  
   215  func (o *DevClient) buildPushAutoImageComponents(ctx context.Context, devfileObj parser.DevfileObj) error {
   216  	components, err := libdevfile.GetImageComponentsToPushAutomatically(devfileObj)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	for _, c := range components {
   222  		err = image.BuildPushSpecificImage(ctx, image.SelectBackend(ctx), o.fs, c, envcontext.GetEnvConfig(ctx).PushImages)
   223  		if err != nil {
   224  			return err
   225  		}
   226  	}
   227  	return nil
   228  }
   229  
   230  // deployPod deploys the component as a Pod in podman
   231  func (o *DevClient) deployPod(ctx context.Context, options dev.StartOptions, devfileObj parser.DevfileObj) (*corev1.Pod, []api.ForwardedPort, error) {
   232  
   233  	spinner := log.Spinner("Deploying pod")
   234  	defer spinner.End(false)
   235  
   236  	pod, fwPorts, err := o.createPodFromComponent(
   237  		ctx,
   238  		options.Debug,
   239  		options.BuildCommand,
   240  		options.RunCommand,
   241  		options.DebugCommand,
   242  		options.ForwardLocalhost,
   243  		options.RandomPorts,
   244  		options.CustomForwardedPorts,
   245  		o.usedPorts,
   246  		options.CustomAddress,
   247  		devfileObj,
   248  	)
   249  	if err != nil {
   250  		return nil, nil, err
   251  	}
   252  	o.usedPorts = getUsedPorts(fwPorts)
   253  
   254  	if equality.Semantic.DeepEqual(o.deployedPod, pod) {
   255  		klog.V(4).Info("pod is already deployed as required")
   256  		spinner.End(true)
   257  		return o.deployedPod, fwPorts, nil
   258  	}
   259  
   260  	// Delete previous pod, if running
   261  	if o.deployedPod != nil {
   262  		err = o.podmanClient.CleanupPodResources(o.deployedPod, false)
   263  		if err != nil {
   264  			return nil, nil, err
   265  		}
   266  	} else {
   267  		err = o.checkVolumesFree(pod)
   268  		if err != nil {
   269  			return nil, nil, err
   270  		}
   271  	}
   272  
   273  	err = o.podmanClient.PlayKube(pod)
   274  	if err != nil {
   275  		// there are cases when pod is created even if there is an error with the pod def; for e.g. incorrect image
   276  		if podMap, _ := o.podmanClient.PodLs(); podMap[pod.Name] {
   277  			o.deployedPod = &corev1.Pod{}
   278  			o.deployedPod.SetName(pod.Name)
   279  		}
   280  		return nil, nil, err
   281  	}
   282  
   283  	spinner.End(true)
   284  	return pod, fwPorts, nil
   285  }
   286  
   287  func (o *DevClient) checkAppPorts(ctx context.Context, podName string, portsToFwd []api.ForwardedPort) error {
   288  	containerPortsMapping := make(map[string][]int)
   289  	for _, p := range portsToFwd {
   290  		containerPortsMapping[p.ContainerName] = append(containerPortsMapping[p.ContainerName], p.ContainerPort)
   291  	}
   292  	return port.CheckAppPortsListening(ctx, o.execClient, podName, containerPortsMapping, 1*time.Minute)
   293  }
   294  
   295  // handleLoopbackPorts tries to detect if any of the ports to forward (in fwPorts) is actually bound to the loopback interface within the specified pod.
   296  // If that is the case, it will either return an error if options.IgnoreLocalhost is false, or no error otherwise.
   297  //
   298  // Note that this method should be called after the process representing the application (run or debug command) is actually started in the pod.
   299  func (o *DevClient) handleLoopbackPorts(ctx context.Context, options dev.StartOptions, pod *corev1.Pod, fwPorts []api.ForwardedPort) error {
   300  	if len(pod.Spec.Containers) == 0 {
   301  		return nil
   302  	}
   303  
   304  	loopbackPorts, err := port.DetectRemotePortsBoundOnLoopback(ctx, o.execClient, pod.Name, pod.Spec.Containers[0].Name, fwPorts)
   305  	if err != nil {
   306  		log.Warningf("unable to detect container ports bound on the loopback interface: %v", err)
   307  	}
   308  
   309  	if len(loopbackPorts) == 0 {
   310  		return nil
   311  	}
   312  
   313  	klog.V(5).Infof("detected %d ports bound on the loopback interface in the pod: %v", len(loopbackPorts), loopbackPorts)
   314  	list := make([]string, 0, len(loopbackPorts))
   315  	for _, p := range loopbackPorts {
   316  		list = append(list, fmt.Sprintf("%s (%d)", p.PortName, p.ContainerPort))
   317  	}
   318  	msg := fmt.Sprintf(`Detected that the following port(s) can be reached only via the container loopback interface: %s.
   319  Port forwarding on Podman currently does not work with applications listening on the loopback interface.
   320  Either change the application to make those port(s) reachable on all interfaces (0.0.0.0), or rerun 'odo dev' with `, strings.Join(list, ", "))
   321  	if options.IgnoreLocalhost {
   322  		msg += "'--forward-localhost' to make port-forwarding work with such ports."
   323  	} else {
   324  		msg += `any of the following options:
   325  - --ignore-localhost: no error will be returned by odo, but forwarding to those ports might not work on Podman.
   326  - --forward-localhost: odo will inject a dedicated side container to redirect traffic to such ports.`
   327  	}
   328  	if options.IgnoreLocalhost {
   329  		// ForwardLocalhost should not be true at this point.
   330  		log.Warningf(msg)
   331  	} else if !options.ForwardLocalhost {
   332  		log.Errorf(msg)
   333  		return errors.New("cannot make port forwarding work with ports bound to the loopback interface only")
   334  	}
   335  
   336  	return nil
   337  }
   338  

View as plain text