forked from rhd-gitops-example/odo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
component.go
1536 lines (1304 loc) · 52.7 KB
/
component.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
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package component
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
"github.com/pkg/errors"
applabels "github.com/openshift/odo/pkg/application/labels"
"github.com/openshift/odo/pkg/catalog"
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/config"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/util/validation"
"github.com/openshift/odo/pkg/preference"
"github.com/openshift/odo/pkg/storage"
urlpkg "github.com/openshift/odo/pkg/url"
"github.com/openshift/odo/pkg/util"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// componentSourceURLAnnotation is an source url from which component was build
// it can be also file://
const componentSourceURLAnnotation = "app.openshift.io/vcs-uri"
const ComponentSourceTypeAnnotation = "app.kubernetes.io/component-source-type"
const componentRandomNamePartsMaxLen = 12
const componentNameMaxRetries = 3
const componentNameMaxLen = -1
// GetComponentDir returns source repo name
// Parameters:
// path: git url or source path or binary path
// paramType: One of CreateType as in GIT/LOCAL/BINARY
// Returns: directory name
func GetComponentDir(path string, paramType config.SrcType) (string, error) {
retVal := ""
switch paramType {
case config.GIT:
retVal = strings.TrimSuffix(path[strings.LastIndex(path, "/")+1:], ".git")
case config.LOCAL:
retVal = filepath.Base(path)
case config.BINARY:
filename := filepath.Base(path)
var extension = filepath.Ext(filename)
retVal = filename[0 : len(filename)-len(extension)]
default:
currDir, err := os.Getwd()
if err != nil {
return "", errors.Wrapf(err, "unable to generate a random name as getting current directory failed")
}
retVal = filepath.Base(currDir)
}
retVal = strings.TrimSpace(util.GetDNS1123Name(strings.ToLower(retVal)))
return retVal, nil
}
// GetDefaultComponentName generates a unique component name
// Parameters: desired default component name(w/o prefix) and slice of existing component names
// Returns: Unique component name and error if any
func GetDefaultComponentName(componentPath string, componentPathType config.SrcType, componentType string, existingComponentList ComponentList) (string, error) {
var prefix string
// Get component names from component list
var existingComponentNames []string
for _, component := range existingComponentList.Items {
existingComponentNames = append(existingComponentNames, component.Name)
}
// Fetch config
cfg, err := preference.New()
if err != nil {
return "", errors.Wrap(err, "unable to generate random component name")
}
// If there's no prefix in config file, or its value is empty string use safe default - the current directory along with component type
if cfg.OdoSettings.NamePrefix == nil || *cfg.OdoSettings.NamePrefix == "" {
prefix, err = GetComponentDir(componentPath, componentPathType)
if err != nil {
return "", errors.Wrap(err, "unable to generate random component name")
}
prefix = util.TruncateString(prefix, componentRandomNamePartsMaxLen)
} else {
// Set the required prefix into componentName
prefix = *cfg.OdoSettings.NamePrefix
}
// Generate unique name for the component using prefix and unique random suffix
componentName, err := util.GetRandomName(
fmt.Sprintf("%s-%s", componentType, prefix),
componentNameMaxLen,
existingComponentNames,
componentNameMaxRetries,
)
if err != nil {
return "", errors.Wrap(err, "unable to generate random component name")
}
return util.GetDNS1123Name(componentName), nil
}
// validateSourceType check if given sourceType is supported
func validateSourceType(sourceType string) bool {
validSourceTypes := []string{
"git",
"local",
"binary",
}
for _, valid := range validSourceTypes {
if valid == sourceType {
return true
}
}
return false
}
// CreateFromGit inputPorts is the array containing the string port values
// inputPorts is the array containing the string port values
// envVars is the array containing the environment variables
func CreateFromGit(client *occlient.Client, params occlient.CreateArgs) error {
// Create the labels
labels := componentlabels.GetLabels(params.Name, params.ApplicationName, true)
// Parse componentImageType before adding to labels
_, imageName, imageTag, _, err := occlient.ParseImageName(params.ImageName)
if err != nil {
return errors.Wrap(err, "unable to parse image name")
}
// save component type as label
labels[componentlabels.ComponentTypeLabel] = imageName
labels[componentlabels.ComponentTypeVersion] = imageTag
// save source path as annotation
annotations := map[string]string{componentSourceURLAnnotation: params.SourcePath}
annotations[ComponentSourceTypeAnnotation] = "git"
// Namespace the component
namespacedOpenShiftObject, err := util.NamespaceOpenShiftObject(params.Name, params.ApplicationName)
if err != nil {
return errors.Wrapf(err, "unable to create namespaced name")
}
// Create CommonObjectMeta to be passed in
commonObjectMeta := metav1.ObjectMeta{
Name: namespacedOpenShiftObject,
Labels: labels,
Annotations: annotations,
}
err = client.NewAppS2I(params, commonObjectMeta)
if err != nil {
return errors.Wrapf(err, "unable to create git component %s", namespacedOpenShiftObject)
}
return nil
}
// GetComponentPorts provides slice of ports used by the component in the form port_no/protocol
func GetComponentPorts(client *occlient.Client, componentName string, applicationName string) (ports []string, err error) {
componentLabels := componentlabels.GetLabels(componentName, applicationName, false)
componentSelector := util.ConvertLabelsToSelector(componentLabels)
dc, err := client.GetOneDeploymentConfigFromSelector(componentSelector)
if err != nil {
return nil, errors.Wrapf(err, "unable to fetch deployment configs for the selector %v", componentSelector)
}
for _, container := range dc.Spec.Template.Spec.Containers {
for _, port := range container.Ports {
ports = append(ports, fmt.Sprintf("%v/%v", port.ContainerPort, port.Protocol))
}
}
return ports, nil
}
// GetComponentLinkedSecretNames provides a slice containing the names of the secrets that are present in envFrom
func GetComponentLinkedSecretNames(client *occlient.Client, componentName string, applicationName string) (secretNames []string, err error) {
componentLabels := componentlabels.GetLabels(componentName, applicationName, false)
componentSelector := util.ConvertLabelsToSelector(componentLabels)
dc, err := client.GetOneDeploymentConfigFromSelector(componentSelector)
if err != nil {
return nil, errors.Wrapf(err, "unable to fetch deployment configs for the selector %v", componentSelector)
}
for _, env := range dc.Spec.Template.Spec.Containers[0].EnvFrom {
if env.SecretRef != nil {
secretNames = append(secretNames, env.SecretRef.Name)
}
}
return secretNames, nil
}
// CreateFromPath create new component with source or binary from the given local path
// sourceType indicates the source type of the component and can be either local or binary
// envVars is the array containing the environment variables
func CreateFromPath(client *occlient.Client, params occlient.CreateArgs) error {
// Create the labels to be used
labels := componentlabels.GetLabels(params.Name, params.ApplicationName, true)
// Parse componentImageType before adding to labels
_, imageName, imageTag, _, err := occlient.ParseImageName(params.ImageName)
if err != nil {
return errors.Wrap(err, "unable to parse image name")
}
// save component type as label
labels[componentlabels.ComponentTypeLabel] = imageName
labels[componentlabels.ComponentTypeVersion] = imageTag
// save source path as annotation
sourceURL := util.GenFileURL(params.SourcePath)
annotations := map[string]string{componentSourceURLAnnotation: sourceURL}
annotations[ComponentSourceTypeAnnotation] = string(params.SourceType)
// Namespace the component
namespacedOpenShiftObject, err := util.NamespaceOpenShiftObject(params.Name, params.ApplicationName)
if err != nil {
return errors.Wrapf(err, "unable to create namespaced name")
}
// Create CommonObjectMeta to be passed in
commonObjectMeta := metav1.ObjectMeta{
Name: namespacedOpenShiftObject,
Labels: labels,
Annotations: annotations,
}
// Bootstrap the deployment with SupervisorD
err = client.BootstrapSupervisoredS2I(params, commonObjectMeta)
if err != nil {
return err
}
if params.Wait {
// if wait flag is present then extract the podselector
// use the podselector for calling WaitAndGetPod
selectorLabels, err := util.NamespaceOpenShiftObject(labels[componentlabels.ComponentLabel], labels["app"])
if err != nil {
return err
}
podSelector := fmt.Sprintf("deploymentconfig=%s", selectorLabels)
_, err = client.WaitAndGetPod(podSelector, corev1.PodRunning, "Waiting for component to start")
if err != nil {
return err
}
return nil
}
return nil
}
// Delete whole component
func Delete(client *occlient.Client, componentName string, applicationName string) error {
// Loading spinner
s := log.Spinnerf("Deleting component %s", componentName)
defer s.End(false)
labels := componentlabels.GetLabels(componentName, applicationName, false)
err := client.Delete(labels)
if err != nil {
return errors.Wrapf(err, "error deleting component %s", componentName)
}
s.End(true)
return nil
}
// getEnvFromPodEnvs loops through the passed slice of pod#EnvVars and gets the value corresponding to the key passed, returns empty stirng if not available
func getEnvFromPodEnvs(envName string, podEnvs []corev1.EnvVar) string {
for _, podEnv := range podEnvs {
if podEnv.Name == envName {
return podEnv.Value
}
}
return ""
}
// getS2IPaths returns slice of s2i paths of odo interest
// Parameters:
// podEnvs: Slice of env vars extracted from pod template
// Returns:
// Slice of s2i paths extracted from passed parameters
func getS2IPaths(podEnvs []corev1.EnvVar) []string {
retVal := []string{}
// List of s2i Paths exported for use in container pod for working with source/binary
s2iPathEnvs := []string{
occlient.EnvS2IDeploymentDir,
occlient.EnvS2ISrcOrBinPath,
occlient.EnvS2IWorkingDir,
occlient.EnvS2ISrcBackupDir,
}
// For each of the required env var
for _, s2iPathEnv := range s2iPathEnvs {
// try to fetch the value of required env from the ones set already in the component container like for the case of watch or multiple pushes
envVal := getEnvFromPodEnvs(s2iPathEnv, podEnvs)
isEnvValPresent := false
if envVal != "" {
for _, e := range retVal {
if envVal == e {
isEnvValPresent = true
break
}
}
if !isEnvValPresent {
// If `src` not in path, append it
if filepath.Base(envVal) != "src" {
envVal = filepath.Join(envVal, "src")
}
retVal = append(retVal, envVal)
}
}
}
return retVal
}
// CreateComponent creates component as per the passed component settings
// Parameters:
// client: occlient instance
// componentConfig: the component configuration that holds all details of component
// context: the component context indicating the location of component config and hence its source as well
// stdout: io.Writer instance to write output to
// Returns:
// err: errors if any
func CreateComponent(client *occlient.Client, componentConfig config.LocalConfigInfo, context string, stdout io.Writer) (err error) {
cmpName := componentConfig.GetName()
cmpType := componentConfig.GetType()
cmpSrcType := componentConfig.GetSourceType()
cmpPorts := componentConfig.GetPorts()
cmpSrcRef := componentConfig.GetRef()
appName := componentConfig.GetApplication()
envVarsList := componentConfig.GetEnvVars()
addDebugPortToEnv(&envVarsList, componentConfig)
// create and get the storage to be created/mounted during the component creation
storageList := getStorageFromConfig(&componentConfig)
storageToBeMounted, _, err := storage.Push(client, storageList, componentConfig.GetName(), componentConfig.GetApplication(), false)
if err != nil {
return err
}
log.Successf("Initializing component")
createArgs := occlient.CreateArgs{
Name: cmpName,
ImageName: cmpType,
ApplicationName: appName,
EnvVars: envVarsList.ToStringSlice(),
StorageToBeMounted: storageToBeMounted,
}
createArgs.SourceType = cmpSrcType
createArgs.SourcePath = componentConfig.GetSourceLocation()
if len(cmpPorts) > 0 {
createArgs.Ports = cmpPorts
}
createArgs.Resources, err = occlient.GetResourceRequirementsFromCmpSettings(componentConfig)
if err != nil {
return errors.Wrap(err, "failed to create component")
}
s := log.Spinner("Creating component")
defer s.End(false)
switch cmpSrcType {
case config.GIT:
// Use Git
if cmpSrcRef != "" {
createArgs.SourceRef = cmpSrcRef
}
createArgs.Wait = true
createArgs.StdOut = stdout
if err = CreateFromGit(
client,
createArgs,
); err != nil {
return errors.Wrapf(err, "failed to create component with args %+v", createArgs)
}
s.End(true)
// Trigger build
if err = Build(client, createArgs.Name, createArgs.ApplicationName, createArgs.Wait, createArgs.StdOut, false); err != nil {
return errors.Wrapf(err, "failed to build component with args %+v", createArgs)
}
// deploy the component and wait for it to complete
// desiredRevision is 1 as this is the first push
if err = Deploy(client, createArgs, 1); err != nil {
return errors.Wrapf(err, "failed to deploy component with args %+v", createArgs)
}
case config.LOCAL:
fileInfo, err := os.Stat(createArgs.SourcePath)
if err != nil {
return errors.Wrapf(err, "failed to get info of path %+v of component %+v", createArgs.SourcePath, createArgs.Name)
}
if !fileInfo.IsDir() {
return fmt.Errorf("component creation with args %+v as path needs to be a directory", createArgs)
}
// Create
if err = CreateFromPath(client, createArgs); err != nil {
return errors.Wrapf(err, "failed to create component with args %+v", createArgs)
}
case config.BINARY:
if err = CreateFromPath(client, createArgs); err != nil {
return errors.Wrapf(err, "failed to create component with args %+v", createArgs)
}
default:
// If the user does not provide anything (local, git or binary), use the current absolute path and deploy it
createArgs.SourceType = config.LOCAL
dir, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "failed to create component with current directory as source for the component")
}
createArgs.SourcePath = dir
if err = CreateFromPath(client, createArgs); err != nil {
return errors.Wrapf(err, "")
}
}
s.End(true)
return
}
// CheckComponentMandatoryParams checks mandatory parammeters for component
func CheckComponentMandatoryParams(componentSettings config.ComponentSettings) error {
var req_fields string
if componentSettings.Name == nil {
req_fields = fmt.Sprintf("%s name", req_fields)
}
if componentSettings.Application == nil {
req_fields = fmt.Sprintf("%s application", req_fields)
}
if componentSettings.Project == nil {
req_fields = fmt.Sprintf("%s project name", req_fields)
}
if componentSettings.SourceType == nil {
req_fields = fmt.Sprintf("%s source type", req_fields)
}
if componentSettings.SourceLocation == nil {
req_fields = fmt.Sprintf("%s source location", req_fields)
}
if componentSettings.Type == nil {
req_fields = fmt.Sprintf("%s type", req_fields)
}
if len(req_fields) > 0 {
return fmt.Errorf("missing mandatory parameters:%s", req_fields)
}
return nil
}
// ValidateComponentCreateRequest validates request for component creation and returns errors if any
// Returns:
// errors if any
func ValidateComponentCreateRequest(client *occlient.Client, componentSettings config.ComponentSettings, contextDir string) (err error) {
// Check the mandatory parameters first
err = CheckComponentMandatoryParams(componentSettings)
if err != nil {
return err
}
// Parse the image name
_, componentType, _, componentVersion := util.ParseComponentImageName(*componentSettings.Type)
// Check to see if the catalog type actually exists
exists, err := catalog.ComponentExists(client, componentType, componentVersion)
if err != nil {
return errors.Wrapf(err, "failed to check component of type %s", componentType)
}
if !exists {
return fmt.Errorf("failed to find component of type %s and version %s", componentType, componentVersion)
}
// Validate component name
err = validation.ValidateName(*componentSettings.Name)
if err != nil {
return errors.Wrapf(err, "failed to check component of name %s", *componentSettings.Name)
}
// If component is of type local, check if the source path is valid
if *componentSettings.SourceType == config.LOCAL {
glog.V(4).Infof("Checking source location: %s", *(componentSettings.SourceLocation))
srcLocInfo, err := os.Stat(*(componentSettings.SourceLocation))
if err != nil {
return errors.Wrap(err, "failed to create component. Please view the settings used using the command `odo config view`")
}
if !srcLocInfo.IsDir() {
return fmt.Errorf("source path for component created for local source needs to be a directory")
}
}
if *componentSettings.SourceType == config.BINARY {
// if relative path starts with ../ (or windows equivalent), it means that binary file is not inside the context
if strings.HasPrefix(*(componentSettings.SourceLocation), fmt.Sprintf("..%c", filepath.Separator)) {
return fmt.Errorf("%s binary needs to be inside of the context directory (%s)", *(componentSettings.SourceLocation), contextDir)
}
}
return
}
// ApplyConfig applies the component config onto component dc
// Parameters:
// client: occlient instance
// appName: Name of application of which the component is a part
// componentName: Name of the component which is being patched with config
// componentConfig: Component configuration
// cmpExist: true if components exists in the cluster
// Returns:
// err: Errors if any else nil
func ApplyConfig(client *occlient.Client, componentConfig config.LocalConfigInfo, stdout io.Writer, cmpExist bool) (err error) {
// if component exist then only call the update function
if cmpExist {
if err = Update(client, componentConfig, componentConfig.GetSourceLocation(), stdout); err != nil {
return err
}
}
showChanges, err := checkIfURLChangesWillBeMade(client, componentConfig)
if err != nil {
return err
}
if showChanges {
log.Info("\nApplying URL changes")
// Create any URLs that have been added to the component
err = ApplyConfigCreateURL(client, componentConfig)
if err != nil {
return err
}
// Delete any URLs
err = applyConfigDeleteURL(client, componentConfig)
if err != nil {
return err
}
}
return
}
// ApplyConfigDeleteURL applies url config deletion onto component
func applyConfigDeleteURL(client *occlient.Client, componentConfig config.LocalConfigInfo) (err error) {
urlList, err := urlpkg.ListPushed(client, componentConfig.GetName(), componentConfig.GetApplication())
if err != nil {
return err
}
localURLList := componentConfig.GetURL()
for _, u := range urlList.Items {
if !checkIfURLPresentInConfig(localURLList, u.Name) {
err = urlpkg.Delete(client, u.Name, componentConfig.GetApplication())
if err != nil {
return err
}
log.Successf("URL %s successfully deleted", u.Name)
}
}
return nil
}
func checkIfURLPresentInConfig(localURL []config.ConfigURL, url string) bool {
for _, u := range localURL {
if u.Name == url {
return true
}
}
return false
}
// ApplyConfigCreateURL applies url config onto component
func ApplyConfigCreateURL(client *occlient.Client, componentConfig config.LocalConfigInfo) error {
urls := componentConfig.GetURL()
for _, urlo := range urls {
exist, err := urlpkg.Exists(client, urlo.Name, componentConfig.GetName(), componentConfig.GetApplication())
if err != nil {
return errors.Wrapf(err, "unable to check url")
}
if exist {
log.Successf("URL %s already exists", urlo.Name)
} else {
host, err := urlpkg.Create(client, urlo.Name, urlo.Port, componentConfig.GetName(), componentConfig.GetApplication())
if err != nil {
return errors.Wrapf(err, "unable to create url")
}
log.Successf("URL %s: %s created", urlo.Name, host)
}
}
return nil
}
// PushLocal push local code to the cluster and trigger build there.
// During copying binary components, path represent base directory path to binary and files contains path of binary
// During copying local source components, path represent base directory path whereas files is empty
// During `odo watch`, path represent base directory path whereas files contains list of changed Files
// Parameters:
// componentName is name of the component to update sources to
// applicationName is the name of the application of which the component is a part
// path is base path of the component source/binary
// files is list of changed files captured during `odo watch` as well as binary file path
// delFiles is the list of files identified as deleted
// isForcePush indicates if the sources to be updated are due to a push in which case its a full source directory push or only push of identified sources
// globExps are the glob expressions which are to be ignored during the push
// show determines whether or not to show the log (passed in by po.show argument within /cmd)
// Returns
// Error if any
func PushLocal(client *occlient.Client, componentName string, applicationName string, path string, out io.Writer, files []string, delFiles []string, isForcePush bool, globExps []string, show bool) error {
glog.V(4).Infof("PushLocal: componentName: %s, applicationName: %s, path: %s, files: %s, delFiles: %s, isForcePush: %+v", componentName, applicationName, path, files, delFiles, isForcePush)
// Edge case: check to see that the path is NOT empty.
emptyDir, err := 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 DeploymentConfig for component
componentLabels := componentlabels.GetLabels(componentName, applicationName, false)
componentSelector := util.ConvertLabelsToSelector(componentLabels)
dc, err := client.GetOneDeploymentConfigFromSelector(componentSelector)
if err != nil {
return errors.Wrap(err, "unable to get deployment for component")
}
// Find Pod for component
podSelector := fmt.Sprintf("deploymentconfig=%s", dc.Name)
// Wait for Pod to be in running state otherwise we can't sync data to it.
pod, err := client.WaitAndGetPod(podSelector, corev1.PodRunning, "Waiting for component to start")
if err != nil {
return errors.Wrapf(err, "error while waiting for pod %s", podSelector)
}
// Get S2I Source/Binary Path from Pod Env variables created at the time of component create
s2iSrcPath := getEnvFromPodEnvs(occlient.EnvS2ISrcOrBinPath, pod.Spec.Containers[0].Env)
if s2iSrcPath == "" {
s2iSrcPath = occlient.DefaultS2ISrcOrBinPath
}
targetPath := fmt.Sprintf("%s/src", s2iSrcPath)
// Sync the files to the pod
s := log.Spinner("Syncing files to the component")
defer s.End(false)
// If there are files identified as deleted, propagate them to the component pod
if len(delFiles) > 0 {
glog.V(4).Infof("propogating deletion of files %s to pod", strings.Join(delFiles, " "))
/*
Delete files observed by watch to have been deleted from each of s2i directories like:
deployment dir: In interpreted runtimes like python, source is copied over to deployment dir so delete needs to happen here as well
destination dir: This is the directory where s2i expects source to be copied for it be built and deployed
working dir: Directory where, sources are copied over from deployment dir from where the s2i builds and deploys source.
Deletes need to happen here as well otherwise, even if the latest source is copied over, the stale source files remain
source backup dir: Directory used for backing up source across multiple iterations of push and watch in component container
In case of python, s2i image moves sources from destination dir to workingdir which means sources are deleted from destination dir
So, during the subsequent watch pushing new diff to component pod, the source as a whole doesn't exist at destination dir and hence needs
to be backed up.
*/
err := client.PropagateDeletes(pod.Name, delFiles, getS2IPaths(pod.Spec.Containers[0].Env))
if err != nil {
return errors.Wrapf(err, "unable to propagate file deletions %+v", delFiles)
}
}
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 = client.CopyFile(path, pod.Name, targetPath, files, globExps)
if err != nil {
s.End(false)
return errors.Wrap(err, "unable push files to pod")
}
}
s.End(true)
if show {
s = log.SpinnerNoSpin("Building component")
} else {
s = log.Spinner("Building component")
}
// use pipes to write output from ExecCMDInContainer in yellow to 'out' io.Writer
pipeReader, pipeWriter := io.Pipe()
var cmdOutput string
// This Go routine will automatically pipe the output from ExecCMDInContainer to
// our logger.
go func() {
scanner := bufio.NewScanner(pipeReader)
for scanner.Scan() {
line := scanner.Text()
if log.IsDebug() || show {
_, err := fmt.Fprintln(out, line)
if err != nil {
log.Errorf("Unable to print to stdout: %v", err)
}
}
cmdOutput += fmt.Sprintln(line)
}
}()
err = client.ExecCMDInContainer(pod.Name,
// We will use the assemble-and-restart script located within the supervisord container we've created
[]string{"/opt/odo/bin/assemble-and-restart"},
pipeWriter, pipeWriter, nil, false)
if err != nil {
// If we fail, log the output
log.Errorf("Unable to build files\n%v", cmdOutput)
s.End(false)
return errors.Wrap(err, "unable to execute assemble script")
}
s.End(true)
return nil
}
// Build component from BuildConfig.
// If 'wait' is true than it waits for build to successfully complete.
// If 'wait' is false than this function won't return error even if build failed.
// 'show' will determine whether or not the log will be shown to the user (while building)
func Build(client *occlient.Client, componentName string, applicationName string, wait bool, stdout io.Writer, show bool) error {
// Loading spinner
// No loading spinner if we're showing the logging output
s := log.Spinnerf("Triggering build from git")
defer s.End(false)
// Namespace the component
namespacedOpenShiftObject, err := util.NamespaceOpenShiftObject(componentName, applicationName)
if err != nil {
return errors.Wrapf(err, "unable to create namespaced name")
}
buildName, err := client.StartBuild(namespacedOpenShiftObject)
if err != nil {
return errors.Wrapf(err, "unable to rebuild %s", componentName)
}
s.End(true)
// Retrieve the Build Log and write to buffer if debug is disabled, else we we output to stdout / debug.
var b bytes.Buffer
if !log.IsDebug() && !show {
stdout = bufio.NewWriter(&b)
}
if wait {
if show {
s = log.SpinnerNoSpin("Waiting for build to finish")
} else {
s = log.Spinner("Waiting for build to finish")
}
defer s.End(false)
if err := client.FollowBuildLog(buildName, stdout); err != nil {
return errors.Wrapf(err, "unable to follow logs for %s", buildName)
}
if err := client.WaitForBuildToFinish(buildName); err != nil {
return errors.Wrapf(err, "unable to build %s, error: %s", buildName, b.String())
}
s.End(true)
}
return nil
}
// Deploy deploys the component
// it starts a new deployment and wait for the new dc to be available
// desiredRevision is the desired version of the deployment config to wait for
func Deploy(client *occlient.Client, params occlient.CreateArgs, desiredRevision int64) error {
// Loading spinner
s := log.Spinnerf("Deploying component %s", params.Name)
defer s.End(false)
// Namespace the component
namespacedOpenShiftObject, err := util.NamespaceOpenShiftObject(params.Name, params.ApplicationName)
if err != nil {
return errors.Wrapf(err, "unable to create namespaced name")
}
// start the deployment
// the build must be finished before this call and the new image must be successfully updated
_, err = client.StartDeployment(namespacedOpenShiftObject)
if err != nil {
return errors.Wrapf(err, "unable to create DeploymentConfig for %s", namespacedOpenShiftObject)
}
// Watch / wait for deployment config to update annotations
_, err = client.WaitAndGetDC(namespacedOpenShiftObject, desiredRevision, occlient.OcUpdateTimeout, occlient.IsDCRolledOut)
if err != nil {
return errors.Wrapf(err, "unable to wait for DeploymentConfig %s to update", namespacedOpenShiftObject)
}
s.End(true)
return nil
}
// GetComponentType returns type of component in given application and project
func GetComponentType(client *occlient.Client, componentName string, applicationName string) (string, error) {
// filter according to component and application name
selector := fmt.Sprintf("%s=%s,%s=%s", componentlabels.ComponentLabel, componentName, applabels.ApplicationLabel, applicationName)
componentImageTypes, err := client.GetDeploymentConfigLabelValues(componentlabels.ComponentTypeLabel, selector)
if err != nil {
return "", errors.Wrap(err, "unable to get type of %s component")
}
if len(componentImageTypes) < 1 {
// no type returned
return "", errors.Wrap(err, "unable to find type of %s component")
}
// check if all types are the same
// it should be as we are secting only exactly one component, and it doesn't make sense
// to have one component labeled with different component type labels
for _, componentImageType := range componentImageTypes {
if componentImageTypes[0] != componentImageType {
return "", errors.Wrap(err, "data mismatch: %s component has objects with different types")
}
}
return componentImageTypes[0], nil
}
// List lists components in active application
func List(client *occlient.Client, applicationName string, localConfigInfo *config.LocalConfigInfo) (ComponentList, error) {
var applicationSelector string
if applicationName != "" {
applicationSelector = fmt.Sprintf("%s=%s", applabels.ApplicationLabel, applicationName)
}
project, err := client.GetProject(client.Namespace)
if err != nil {
return ComponentList{}, err
}
var components []Component
componentNamesMap := make(map[string]bool)
if project != nil {
// retrieve all the deployment configs that are associated with this application
dcList, err := client.GetDeploymentConfigsFromSelector(applicationSelector)
if err != nil {
return ComponentList{}, errors.Wrapf(err, "unable to list components")
}
// extract the labels we care about from each component
for _, elem := range dcList {
component, err := GetComponent(client, elem.Labels[componentlabels.ComponentLabel], applicationName, client.Namespace)
if err != nil {
return ComponentList{}, errors.Wrap(err, "Unable to get component")
}
component.Status.State = "Pushed"
components = append(components, component)
componentNamesMap[component.Name] = true
}
}
if localConfigInfo != nil {
component, err := GetComponentFromConfig(*localConfigInfo)
if err != nil {
return GetMachineReadableFormatForList(components), err
}
_, ok := componentNamesMap[component.Name]
if component.Name != "" && !ok && component.Spec.App == applicationName && component.Namespace == client.Namespace {
components = append(components, component)
}
if len(components) == 0 {
return GetMachineReadableFormatForList(components), nil
}
}
compoList := GetMachineReadableFormatForList(components)
return compoList, nil
}
// GetComponentFromConfig returns the component on the config if it exists
func GetComponentFromConfig(localConfig config.LocalConfigInfo) (Component, error) {
if localConfig.ConfigFileExists() {
component := getMachineReadableFormat(localConfig.GetName(), localConfig.GetType())
component.Namespace = localConfig.GetProject()
component.Spec = ComponentSpec{
App: localConfig.GetApplication(),
Type: localConfig.GetType(),
Source: localConfig.GetSourceLocation(),
Ports: localConfig.GetPorts(),
}
if localConfig.GetSourceType() == "local" || localConfig.GetSourceType() == "binary" {
component.Spec.Source = util.GenFileURL(localConfig.GetSourceLocation())
}
component.Status = ComponentStatus{
State: "Not Pushed",
}
for _, localURL := range localConfig.GetURL() {
component.Spec.URL = append(component.Spec.URL, localURL.Name)
}
for _, localEnv := range localConfig.GetEnvVars() {
component.Spec.Env = append(component.Spec.Env, corev1.EnvVar{Name: localEnv.Name, Value: localEnv.Value})
}
for _, localStorage := range localConfig.GetStorage() {
component.Spec.Storage = append(component.Spec.Storage, localStorage.Name)
}
return component, nil
}
return Component{}, nil
}
// ListIfPathGiven lists all available component in given path directory
func ListIfPathGiven(client *occlient.Client, paths []string) (ComponentList, error) {
var components []Component
var err error
for _, path := range paths {
err = filepath.Walk(path, func(path string, f os.FileInfo, err error) error {
if f != nil && strings.Contains(f.Name(), ".odo") {
data, err := config.NewLocalConfigInfo(filepath.Dir(path))
if err != nil {
return err
}
// if the .odo folder doesn't contain a proper config file
if data.GetName() == "" || data.GetApplication() == "" || data.GetProject() == "" {
return nil
}
// since the config file maybe belong to a component of a different project
client.Namespace = data.GetProject()
exist, err := Exists(client, data.GetName(), data.GetApplication())
if err != nil {
return err
}
con, _ := filepath.Abs(filepath.Dir(path))
a := getMachineReadableFormat(data.GetName(), data.GetType())
a.Namespace = data.GetProject()
a.Spec.App = data.GetApplication()
a.Spec.Source = data.GetSourceLocation()
a.Spec.Ports = data.GetPorts()
a.Status.Context = con
state := "Not Pushed"
if exist {
state = "Pushed"
}
a.Status.State = state
components = append(components, a)
}
return nil
})
}
return GetMachineReadableFormatForList(components), err
}