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
65
66 if !componentStatus.PostStartEventsDone && libdevfile.HasPostStartEvents(devfileObj) {
67 execHandler := component.NewRunHandler(
68 ctx,
69 o.podmanClient,
70 o.execClient,
71 nil,
72
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,
97
98
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,
131
132 o.fs,
133 image.SelectBackend(ctx),
134
135
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
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
170
171
172 err = o.handleLoopbackPorts(ctx, options, pod, fwPorts)
173 if err != nil {
174 return err
175 }
176
177 if options.ForwardLocalhost {
178
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 }
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
199 func (o *DevClient) warnAboutK8sComponents(devfileObj parser.DevfileObj) {
200 var components []string
201
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
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
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
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
296
297
298
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
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