-
Notifications
You must be signed in to change notification settings - Fork 243
/
adapter.go
590 lines (503 loc) · 21 KB
/
adapter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
package component
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/golang/glog"
"github.com/pkg/errors"
"github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/adapters/kubernetes/storage"
"github.com/openshift/odo/pkg/devfile/adapters/kubernetes/utils"
"github.com/openshift/odo/pkg/exec"
versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/kclient"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/sync"
"github.com/openshift/odo/pkg/util"
)
// New instantiantes a component adapter
func New(adapterContext common.AdapterContext, client kclient.Client) Adapter {
return Adapter{
Client: client,
AdapterContext: adapterContext,
}
}
// Adapter is a component adapter implementation for Kubernetes
type Adapter struct {
Client kclient.Client
common.AdapterContext
devfileBuildCmd string
devfileRunCmd string
}
// 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.
func (a Adapter) Push(parameters common.PushParameters) (err error) {
componentExists := utils.ComponentExists(a.Client, a.ComponentName)
globExps := util.GetAbsGlobExps(parameters.Path, parameters.IgnoredFiles)
a.devfileBuildCmd = parameters.DevfileBuildCmd
a.devfileRunCmd = parameters.DevfileRunCmd
deletedFiles := []string{}
changedFiles := []string{}
isForcePush := false
podChanged := false
var podName string
// If the component already exists, retrieve the pod's name before it's potentially updated
if componentExists {
pod, err := a.waitAndGetComponentPod(true)
if err != nil {
return errors.Wrapf(err, "unable to get pod for component %s", a.ComponentName)
}
podName = pod.GetName()
}
// Validate the devfile build and run commands
pushDevfileCommands, err := common.ValidateAndGetPushDevfileCommands(a.Devfile.Data, a.devfileBuildCmd, a.devfileRunCmd)
if err != nil {
return errors.Wrap(err, "failed to validate devfile build and run commands")
}
err = a.createOrUpdateComponent(componentExists)
if err != nil {
return errors.Wrap(err, "unable to create or update component")
}
_, err = a.Client.WaitForDeploymentRollout(a.ComponentName)
if err != nil {
return errors.Wrap(err, "error while waiting for deployment rollout")
}
// Wait for Pod to be in running state otherwise we can't sync data or exec commands to it.
pod, err := a.waitAndGetComponentPod(false)
if err != nil {
return errors.Wrapf(err, "unable to get pod for component %s", a.ComponentName)
}
// Compare the name of the pod with the one before the rollout. If they differ, it means there's a new pod and a force push is required
if componentExists && podName != pod.GetName() {
podChanged = true
}
// Sync source code to the component
// If syncing for the first time, sync the entire source directory
// If syncing to an already running component, sync the deltas
// If syncing from an odo watch process, skip this step, as we already have the list of changed and deleted files.
if !podChanged && !parameters.ForceBuild && len(parameters.WatchFiles) == 0 && len(parameters.WatchDeletedFiles) == 0 {
absIgnoreRules := util.GetAbsGlobExps(parameters.Path, parameters.IgnoredFiles)
spinner := log.NewStatus(log.GetStdout())
defer spinner.End(true)
if componentExists {
spinner.Start("Checking file changes for pushing", false)
} else {
// if the component doesn't exist, we don't check for changes in the files
// thus we show a different message
spinner.Start("Checking files for pushing", false)
}
// Before running the indexer, make sure the .odo folder exists (or else the index file will not get created)
odoFolder := filepath.Join(parameters.Path, ".odo")
if _, err := os.Stat(odoFolder); os.IsNotExist(err) {
err = os.Mkdir(odoFolder, 0750)
if err != nil {
return errors.Wrap(err, "unable to create directory")
}
}
// run the indexer and find the modified/added/deleted/renamed files
filesChanged, filesDeleted, err := util.RunIndexer(parameters.Path, absIgnoreRules)
spinner.End(true)
if err != nil {
return errors.Wrap(err, "unable to run indexer")
}
// If the component already exists, sync only the files that changed
if componentExists {
// apply the glob rules from the .gitignore/.odo file
// and ignore the files on which the rules apply and filter them out
filesChangedFiltered, filesDeletedFiltered := util.FilterIgnores(filesChanged, filesDeleted, absIgnoreRules)
// Remove the relative file directory from the list of deleted files
// in order to make the changes correctly within the Kubernetes pod
deletedFiles, err = util.RemoveRelativePathFromFiles(filesDeletedFiltered, parameters.Path)
if err != nil {
return errors.Wrap(err, "unable to remove relative path from list of changed/deleted files")
}
glog.V(4).Infof("List of files to be deleted: +%v", deletedFiles)
changedFiles = filesChangedFiltered
if len(filesChangedFiltered) == 0 && len(filesDeletedFiltered) == 0 {
// no file was modified/added/deleted/renamed, thus return without building
log.Success("No file changes detected, skipping build. Use the '-f' flag to force the build.")
return nil
}
}
} else if len(parameters.WatchFiles) > 0 || len(parameters.WatchDeletedFiles) > 0 {
changedFiles = parameters.WatchFiles
deletedFiles = parameters.WatchDeletedFiles
}
if parameters.ForceBuild || !componentExists || podChanged {
isForcePush = true
}
// Sync the local source code to the component
err = a.pushLocal(parameters.Path,
changedFiles,
deletedFiles,
isForcePush,
globExps,
pod.GetName(),
pod.Spec.Containers,
)
if err != nil {
return errors.Wrapf(err, "Failed to sync to component with name %s", a.ComponentName)
}
err = a.execDevfile(pushDevfileCommands, componentExists, parameters.Show, pod.GetName(), pod.Spec.Containers)
if err != nil {
return err
}
return nil
}
// DoesComponentExist returns true if a component with the specified name exists, false otherwise
func (a Adapter) DoesComponentExist(cmpName string) bool {
return utils.ComponentExists(a.Client, cmpName)
}
func (a Adapter) createOrUpdateComponent(componentExists bool) (err error) {
componentName := a.ComponentName
labels := map[string]string{
"component": componentName,
}
containers, err := utils.GetContainers(a.Devfile)
if err != nil {
return err
}
if len(containers) == 0 {
return fmt.Errorf("No valid components found in the devfile")
}
containers, err = utils.UpdateContainersWithSupervisord(a.Devfile, containers, a.devfileRunCmd)
if err != nil {
return err
}
objectMeta := kclient.CreateObjectMeta(componentName, a.Client.Namespace, labels, nil)
podTemplateSpec := kclient.GeneratePodTemplateSpec(objectMeta, containers)
kclient.AddBootstrapSupervisordInitContainer(podTemplateSpec)
componentAliasToVolumes := utils.GetVolumes(a.Devfile)
var uniqueStorages []common.Storage
volumeNameToPVCName := make(map[string]string)
processedVolumes := make(map[string]bool)
// Get a list of all the unique volume names and generate their PVC names
for _, volumes := range componentAliasToVolumes {
for _, vol := range volumes {
if _, ok := processedVolumes[*vol.Name]; !ok {
processedVolumes[*vol.Name] = true
// Generate the PVC Names
glog.V(3).Infof("Generating PVC name for %v", *vol.Name)
generatedPVCName, err := storage.GeneratePVCNameFromDevfileVol(*vol.Name, componentName)
if err != nil {
return err
}
// Check if we have an existing PVC with the labels, overwrite the generated name with the existing name if present
existingPVCName, err := storage.GetExistingPVC(&a.Client, *vol.Name, componentName)
if err != nil {
return err
}
if len(existingPVCName) > 0 {
glog.V(3).Infof("Found an existing PVC for %v, PVC %v will be re-used", *vol.Name, existingPVCName)
generatedPVCName = existingPVCName
}
pvc := common.Storage{
Name: generatedPVCName,
Volume: vol,
}
uniqueStorages = append(uniqueStorages, pvc)
volumeNameToPVCName[*vol.Name] = generatedPVCName
}
}
}
// Add PVC and Volume Mounts to the podTemplateSpec
err = kclient.AddPVCAndVolumeMount(podTemplateSpec, volumeNameToPVCName, componentAliasToVolumes)
if err != nil {
return err
}
deploymentSpec := kclient.GenerateDeploymentSpec(*podTemplateSpec)
var containerPorts []corev1.ContainerPort
for _, c := range deploymentSpec.Template.Spec.Containers {
if len(containerPorts) == 0 {
containerPorts = c.Ports
} else {
containerPorts = append(containerPorts, c.Ports...)
}
}
serviceSpec := kclient.GenerateServiceSpec(objectMeta.Name, containerPorts)
glog.V(3).Infof("Creating deployment %v", deploymentSpec.Template.GetName())
glog.V(3).Infof("The component name is %v", componentName)
if utils.ComponentExists(a.Client, componentName) {
// If the component already exists, get the resource version of the deploy before updating
glog.V(3).Info("The component already exists, attempting to update it")
deployment, err := a.Client.UpdateDeployment(*deploymentSpec)
if err != nil {
return err
}
glog.V(3).Infof("Successfully updated component %v", componentName)
oldSvc, err := a.Client.KubeClient.CoreV1().Services(a.Client.Namespace).Get(componentName, metav1.GetOptions{})
objectMetaTemp := objectMeta
ownerReference := kclient.GenerateOwnerReference(deployment)
objectMetaTemp.OwnerReferences = append(objectMeta.OwnerReferences, ownerReference)
if err != nil {
// no old service was found, create a new one
if len(serviceSpec.Ports) > 0 {
_, err = a.Client.CreateService(objectMetaTemp, *serviceSpec)
if err != nil {
return err
}
glog.V(3).Infof("Successfully created Service for component %s", componentName)
}
} else {
if len(serviceSpec.Ports) > 0 {
serviceSpec.ClusterIP = oldSvc.Spec.ClusterIP
objectMetaTemp.ResourceVersion = oldSvc.GetResourceVersion()
_, err = a.Client.UpdateService(objectMetaTemp, *serviceSpec)
if err != nil {
return err
}
glog.V(3).Infof("Successfully update Service for component %s", componentName)
} else {
err = a.Client.KubeClient.CoreV1().Services(a.Client.Namespace).Delete(componentName, &metav1.DeleteOptions{})
if err != nil {
return err
}
}
}
} else {
deployment, err := a.Client.CreateDeployment(*deploymentSpec)
if err != nil {
return err
}
glog.V(3).Infof("Successfully created component %v", componentName)
ownerReference := kclient.GenerateOwnerReference(deployment)
objectMetaTemp := objectMeta
objectMetaTemp.OwnerReferences = append(objectMeta.OwnerReferences, ownerReference)
if len(serviceSpec.Ports) > 0 {
_, err = a.Client.CreateService(objectMetaTemp, *serviceSpec)
if err != nil {
return err
}
glog.V(3).Infof("Successfully created Service for component %s", componentName)
}
}
// Get the storage adapter and create the volumes if it does not exist
stoAdapter := storage.New(a.AdapterContext, a.Client)
err = stoAdapter.Create(uniqueStorages)
if err != nil {
return err
}
return nil
}
// pushLocal syncs source code from the user's disk to the component
func (a Adapter) pushLocal(path string, files []string, delFiles []string, isForcePush bool, globExps []string, podName string, containers []corev1.Container) error {
glog.V(4).Infof("Push: componentName: %s, path: %s, files: %s, delFiles: %s, isForcePush: %+v", a.ComponentName, path, files, delFiles, isForcePush)
// Edge case: check to see that the path is NOT empty.
emptyDir, err := util.IsEmpty(path)
if err != nil {
return errors.Wrapf(err, "Unable to check directory: %s", path)
} else if emptyDir {
return errors.New(fmt.Sprintf("Directory / file %s is empty", path))
}
// Find at least one pod with the source volume mounted, error out if none can be found
containerName, err := getFirstContainerWithSourceVolume(containers)
if err != nil {
return errors.Wrapf(err, "error while retrieving container from pod: %s", podName)
}
// Sync the files to the pod
s := log.Spinner("Syncing files to the component")
defer s.End(false)
// If there's only one project defined in the devfile, sync to `/projects/project-name`, otherwise sync to /projects
syncFolder, err := getSyncFolder(a.Devfile.Data.GetProjects())
if err != nil {
return errors.Wrapf(err, "unable to sync the files to the component")
}
if syncFolder != kclient.OdoSourceVolumeMount {
// Need to make sure the folder already exists on the component or else sync will fail
glog.V(4).Infof("Creating %s on the remote container if it doesn't already exist", syncFolder)
cmdArr := getCmdToCreateSyncFolder(syncFolder)
err = exec.ExecuteCommand(&a.Client, podName, containerName, cmdArr, false)
if err != nil {
return err
}
}
// If there were any files deleted locally, delete them remotely too.
if len(delFiles) > 0 {
cmdArr := getCmdToDeleteFiles(delFiles, syncFolder)
err = exec.ExecuteCommand(&a.Client, podName, containerName, cmdArr, false)
if err != nil {
return err
}
}
if !isForcePush {
if len(files) == 0 && len(delFiles) == 0 {
// nothing to push
s.End(true)
return nil
}
}
if isForcePush || len(files) > 0 {
glog.V(4).Infof("Copying files %s to pod", strings.Join(files, " "))
err = sync.CopyFile(&a.Client, path, podName, containerName, syncFolder, files, globExps)
if err != nil {
s.End(false)
return errors.Wrap(err, "unable push files to pod")
}
}
s.End(true)
return nil
}
func (a Adapter) waitAndGetComponentPod(hideSpinner bool) (*corev1.Pod, error) {
podSelector := fmt.Sprintf("component=%s", a.ComponentName)
watchOptions := metav1.ListOptions{
LabelSelector: podSelector,
}
// Wait for Pod to be in running state otherwise we can't sync data to it.
pod, err := a.Client.WaitAndGetPod(watchOptions, corev1.PodRunning, "Waiting for component to start", hideSpinner)
if err != nil {
return nil, errors.Wrapf(err, "error while waiting for pod %s", podSelector)
}
return pod, nil
}
// Push syncs source code from the user's disk to the component
func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand, componentExists, show bool, podName string, containers []corev1.Container) (err error) {
var buildRequired bool
var s *log.Status
if len(pushDevfileCommands) == 1 {
// if there is one command, it is the mandatory run command. No need to build.
buildRequired = false
} else if len(pushDevfileCommands) == 2 {
// if there are two commands, it is the optional build command and the mandatory run command, set buildRequired to true
buildRequired = true
} else {
return fmt.Errorf("error executing devfile commands - there should be at least 1 command or at most 2 commands, currently there are %v commands", len(pushDevfileCommands))
}
for i := 0; i < len(pushDevfileCommands); i++ {
command := pushDevfileCommands[i]
// Exec the devBuild command if buildRequired is true
if (command.Name == string(common.DefaultDevfileBuildCommand) || command.Name == a.devfileBuildCmd) && buildRequired {
glog.V(3).Infof("Executing devfile command %v", command.Name)
for _, action := range command.Actions {
// Change to the workdir and execute the command
var cmdArr []string
if action.Workdir != nil {
cmdArr = []string{"/bin/sh", "-c", "cd " + *action.Workdir + " && " + *action.Command}
} else {
cmdArr = []string{"/bin/sh", "-c", *action.Command}
}
if show {
s = log.SpinnerNoSpin("Executing " + command.Name + " command " + fmt.Sprintf("%q", *action.Command))
} else {
s = log.Spinner("Executing " + command.Name + " command " + fmt.Sprintf("%q", *action.Command))
}
defer s.End(false)
err = exec.ExecuteCommand(&a.Client, podName, *action.Component, cmdArr, show)
if err != nil {
s.End(false)
return err
}
s.End(true)
}
// Reset the for loop counter and iterate through all the devfile commands again for others
i = -1
// Set the buildRequired to false since we already executed the build command
buildRequired = false
} else if (command.Name == string(common.DefaultDevfileRunCommand) || command.Name == a.devfileRunCmd) && !buildRequired {
// Always check for buildRequired is false, since the command may be iterated out of order and we always want to execute devBuild first if buildRequired is true. If buildRequired is false, then we don't need to build and we can execute the devRun command
glog.V(3).Infof("Executing devfile command %v", command.Name)
for _, action := range command.Actions {
// Check if the devfile run component containers have supervisord as the entrypoint.
// Start the supervisord if the odo component does not exist
if !componentExists {
err = a.InitRunContainerSupervisord(*action.Component, podName, containers)
if err != nil {
return
}
}
// Exec the supervisord ctl stop and start for the devrun program
type devRunExecutable struct {
command []string
}
devRunExecs := []devRunExecutable{
{
command: []string{common.SupervisordBinaryPath, "ctl", "stop", "all"},
},
{
command: []string{common.SupervisordBinaryPath, "ctl", "start", string(common.DefaultDevfileRunCommand)},
},
}
s = log.Spinner("Executing " + command.Name + " command " + fmt.Sprintf("%q", *action.Command))
defer s.End(false)
for _, devRunExec := range devRunExecs {
err = exec.ExecuteCommand(&a.Client, podName, *action.Component, devRunExec.command, show)
if err != nil {
s.End(false)
return
}
}
s.End(true)
}
}
}
return
}
// InitRunContainerSupervisord initializes the supervisord in the container if
// the container has entrypoint that is not supervisord
func (a Adapter) InitRunContainerSupervisord(containerName, podName string, containers []corev1.Container) (err error) {
for _, container := range containers {
if container.Name == containerName && !reflect.DeepEqual(container.Command, []string{common.SupervisordBinaryPath}) {
command := []string{common.SupervisordBinaryPath, "-c", common.SupervisordConfFile, "-d"}
err = exec.ExecuteCommand(&a.Client, podName, containerName, command, true)
}
}
return
}
// getFirstContainerWithSourceVolume returns the first container that set mountSources: true
// Because the source volume is shared across all components that need it, we only need to sync once,
// so we only need to find one container. If no container was found, that means there's no
// container to sync to, so return an error
func getFirstContainerWithSourceVolume(containers []corev1.Container) (string, error) {
for _, c := range containers {
for _, vol := range c.VolumeMounts {
if vol.Name == kclient.OdoSourceVolume {
return c.Name, nil
}
}
}
return "", fmt.Errorf("In order to sync files, odo requires at least one component in a devfile to set 'mountSources: true'")
}
// getSyncFolder returns the folder that we need to sync the source files to
// If there's exactly one project defined in the devfile, and clonePath isn't set return `/projects/<projectName>`
// If there's exactly one project, and clonePath is set, return `/projects/<clonePath>`
// If the clonePath is an absolute path or contains '..', return an error
// Otherwise (zero projects or many), return `/projects`
func getSyncFolder(projects []versionsCommon.DevfileProject) (string, error) {
if len(projects) == 1 {
project := projects[0]
// If the clonepath is set to a value, set it to be the sync folder
// As some devfiles rely on the code being synced to the folder in the clonepath
if project.ClonePath != nil {
if strings.HasPrefix(*project.ClonePath, "/") {
return "", fmt.Errorf("the clonePath in the devfile must be a relative path")
}
if strings.Contains(*project.ClonePath, "..") {
return "", fmt.Errorf("the clonePath in the devfile cannot escape the projects root. Don't use .. to try and do that")
}
return filepath.ToSlash(filepath.Join(kclient.OdoSourceVolumeMount, *project.ClonePath)), nil
}
return filepath.ToSlash(filepath.Join(kclient.OdoSourceVolumeMount, projects[0].Name)), nil
}
return kclient.OdoSourceVolumeMount, nil
}
// getCmdToCreateSyncFolder returns the command used to create the remote sync folder on the running container
func getCmdToCreateSyncFolder(syncFolder string) []string {
return []string{"mkdir", "-p", syncFolder}
}
// getCmdToDeleteFiles returns the command used to delete the remote files on the container that are marked for deletion
func getCmdToDeleteFiles(delFiles []string, syncFolder string) []string {
rmPaths := util.GetRemoteFilesMarkedForDeletion(delFiles, syncFolder)
glog.V(4).Infof("remote files marked for deletion are %+v", rmPaths)
cmdArr := []string{"rm", "-rf"}
return append(cmdArr, rmPaths...)
}
// Delete deletes the component
func (a Adapter) Delete(labels map[string]string) error {
if !utils.ComponentExists(a.Client, a.ComponentName) {
return errors.Errorf("the component %s doesn't exist on the cluster", a.ComponentName)
}
return a.Client.DeleteDeployment(labels)
}