...

Source file src/github.com/redhat-developer/odo/pkg/init/backend/interactive.go

Documentation: github.com/redhat-developer/odo/pkg/init/backend

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
    12  	"github.com/devfile/library/v2/pkg/devfile/parser"
    13  	parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
    14  	dfutil "github.com/devfile/library/v2/pkg/util"
    15  	"k8s.io/klog"
    16  
    17  	"github.com/redhat-developer/odo/pkg/alizer"
    18  	"github.com/redhat-developer/odo/pkg/api"
    19  	"github.com/redhat-developer/odo/pkg/init/asker"
    20  	"github.com/redhat-developer/odo/pkg/log"
    21  	"github.com/redhat-developer/odo/pkg/registry"
    22  	"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
    23  )
    24  
    25  const (
    26  	STATE_ASK_ARCHITECTURES = iota
    27  	STATE_ASK_LANG
    28  	STATE_ASK_TYPE
    29  	STATE_ASK_VERSION
    30  	STATE_END
    31  )
    32  
    33  // InteractiveBackend is a backend that will ask information interactively using the `asker` package
    34  type InteractiveBackend struct {
    35  	askerClient    asker.Asker
    36  	registryClient registry.Client
    37  	alizerClient   alizer.Client
    38  }
    39  
    40  var _ InitBackend = (*InteractiveBackend)(nil)
    41  
    42  func NewInteractiveBackend(askerClient asker.Asker, registryClient registry.Client, alizerClient alizer.Client) *InteractiveBackend {
    43  	return &InteractiveBackend{
    44  		askerClient:    askerClient,
    45  		registryClient: registryClient,
    46  		alizerClient:   alizerClient,
    47  	}
    48  }
    49  
    50  func (o *InteractiveBackend) Validate(flags map[string]string, fs filesystem.Filesystem, dir string) error {
    51  	return nil
    52  }
    53  
    54  func (o *InteractiveBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DetectionResult, error) {
    55  	result := &api.DetectionResult{}
    56  	var devfileEntries registry.DevfileStackList
    57  	state := STATE_ASK_ARCHITECTURES
    58  	var lang string
    59  	archs := []string{"amd64"}
    60  	var err error
    61  	var details api.DevfileStack
    62  loop:
    63  	for {
    64  		switch state {
    65  
    66  		case STATE_ASK_ARCHITECTURES:
    67  			archs, err = o.askerClient.AskArchitectures(knownArchitectures, archs)
    68  			if err != nil {
    69  				return nil, err
    70  			}
    71  			state = STATE_ASK_LANG
    72  
    73  		case STATE_ASK_LANG:
    74  			filter := strings.Join(archs, ",")
    75  			devfileEntries, _ = o.registryClient.ListDevfileStacks(ctx, "", "", filter, false, false)
    76  			langs := devfileEntries.GetLanguages()
    77  			var back bool
    78  			back, lang, err = o.askerClient.AskLanguage(langs)
    79  			if err != nil {
    80  				return nil, err
    81  			}
    82  			if back {
    83  				state = STATE_ASK_ARCHITECTURES
    84  				continue loop
    85  			}
    86  			state = STATE_ASK_TYPE
    87  
    88  		case STATE_ASK_TYPE:
    89  			types := devfileEntries.GetProjectTypes(lang)
    90  			var back bool
    91  			back, details, err = o.askerClient.AskType(types)
    92  			if err != nil {
    93  				return nil, err
    94  			}
    95  			if back {
    96  				state = STATE_ASK_LANG
    97  				continue loop
    98  			}
    99  			result.DevfileRegistry = details.Registry.Name
   100  			result.Devfile = details.Name
   101  			if len(details.Versions) > 1 {
   102  				state = STATE_ASK_VERSION
   103  			} else {
   104  				state = STATE_END
   105  			}
   106  		case STATE_ASK_VERSION:
   107  			var back bool
   108  			back, version, err := o.askerClient.AskVersion(details.Versions)
   109  			if err != nil {
   110  				return nil, err
   111  			}
   112  			if back {
   113  				state = STATE_ASK_TYPE
   114  				continue loop
   115  			}
   116  			result.DevfileVersion = version
   117  			state = STATE_END
   118  		case STATE_END:
   119  			break loop
   120  		}
   121  	}
   122  
   123  	return result, nil
   124  }
   125  
   126  func (o *InteractiveBackend) SelectStarterProject(devfile parser.DevfileObj, flags map[string]string) (*v1alpha2.StarterProject, error) {
   127  	starterProjects, err := devfile.Data.GetStarterProjects(parsercommon.DevfileOptions{})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	sort.Slice(starterProjects, func(i, j int) bool {
   133  		return starterProjects[i].Name < starterProjects[j].Name
   134  	})
   135  
   136  	names := make([]string, 0, len(starterProjects))
   137  	for _, starterProject := range starterProjects {
   138  		names = append(names, starterProject.Name)
   139  	}
   140  
   141  	ok, starter, err := o.askerClient.AskStarterProject(names)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	if !ok {
   146  		return nil, nil
   147  	}
   148  	return &starterProjects[starter], nil
   149  }
   150  
   151  func (o *InteractiveBackend) PersonalizeName(devfile parser.DevfileObj, flags map[string]string) (string, error) {
   152  
   153  	// We will retrieve the name using alizer and then suggest it as the default name.
   154  	// 1. Check the pom.xml / package.json / etc. for the project name
   155  	// 2. If not, use the directory name instead
   156  
   157  	// Get the absolute path to the directory from the Devfile context
   158  	path := devfile.Ctx.GetAbsPath()
   159  	if path == "" {
   160  		return "", fmt.Errorf("unable to get the absolute path to the directory: %q", path)
   161  	}
   162  
   163  	// Pass in the BASE directory (not the file name of devfile.yaml)
   164  	// Convert path to base dir not file name
   165  	baseDir := filepath.Dir(path)
   166  
   167  	// Detect the name
   168  	name, err := o.alizerClient.DetectName(baseDir)
   169  	if err != nil {
   170  		return "", fmt.Errorf("detecting name using alizer: %w", err)
   171  	}
   172  
   173  	klog.V(4).Infof("Detected name via alizer: %q from path: %q", name, baseDir)
   174  
   175  	if name == "" {
   176  		return "", fmt.Errorf("unable to detect the name")
   177  	}
   178  
   179  	var userReturnedName string
   180  	// keep asking the name until the user enters a valid name
   181  	for {
   182  		userReturnedName, err = o.askerClient.AskName(name)
   183  		if err != nil {
   184  			return "", err
   185  		}
   186  		validK8sNameErr := dfutil.ValidateK8sResourceName("name", userReturnedName)
   187  		if validK8sNameErr == nil {
   188  			break
   189  		}
   190  		log.Error(validK8sNameErr)
   191  	}
   192  	return userReturnedName, nil
   193  }
   194  
   195  func (o *InteractiveBackend) PersonalizeDevfileConfig(devfileobj parser.DevfileObj) (parser.DevfileObj, error) {
   196  	config, err := getPortsAndEnvVar(devfileobj)
   197  	var zeroDevfile parser.DevfileObj
   198  	if err != nil {
   199  		return zeroDevfile, err
   200  	}
   201  
   202  	var selectContainerAnswer string
   203  	containerOptions := config.GetContainers()
   204  	containerOptions = append(containerOptions, "NONE - configuration is correct")
   205  
   206  	for selectContainerAnswer != "NONE - configuration is correct" {
   207  		PrintConfiguration(config)
   208  		selectContainerAnswer, err = o.askerClient.AskContainerName(containerOptions)
   209  		if err != nil {
   210  			return zeroDevfile, err
   211  		}
   212  
   213  		selectedContainer := config[selectContainerAnswer]
   214  		if selectContainerAnswer == "NONE - configuration is correct" {
   215  			break
   216  		}
   217  
   218  		var configOps asker.OperationOnContainer
   219  		for configOps.Ops != "Nothing" {
   220  			configOps, err = o.askerClient.AskPersonalizeConfiguration(selectedContainer)
   221  			if err != nil {
   222  				return zeroDevfile, err
   223  			}
   224  			switch configOps.Ops {
   225  			case "Add":
   226  				switch configOps.Kind {
   227  				case "Port":
   228  					var newPort string
   229  					newPort, err = o.askerClient.AskAddPort()
   230  					if err != nil {
   231  						return zeroDevfile, err
   232  					}
   233  
   234  					err = devfileobj.Data.SetPorts(map[string][]string{selectContainerAnswer: {newPort}})
   235  					if err != nil {
   236  						return zeroDevfile, err
   237  					}
   238  					selectedContainer.Ports = append(selectedContainer.Ports, newPort)
   239  
   240  				case "EnvVar":
   241  					var newEnvNameAnswer, newEnvValueAnswer string
   242  					newEnvNameAnswer, newEnvValueAnswer, err = o.askerClient.AskAddEnvVar()
   243  					if err != nil {
   244  						return zeroDevfile, err
   245  					}
   246  					err = devfileobj.Data.AddEnvVars(map[string][]v1alpha2.EnvVar{selectContainerAnswer: {{
   247  						Name:  newEnvNameAnswer,
   248  						Value: newEnvValueAnswer,
   249  					}}})
   250  					if err != nil {
   251  						return zeroDevfile, err
   252  					}
   253  					selectedContainer.Envs[newEnvNameAnswer] = newEnvValueAnswer
   254  				}
   255  			case "Delete":
   256  				switch configOps.Kind {
   257  				case "Port":
   258  					portToDelete := configOps.Key
   259  					indexToDelete := -1
   260  					for i, port := range selectedContainer.Ports {
   261  						if port == portToDelete {
   262  							indexToDelete = i
   263  						}
   264  					}
   265  					if indexToDelete == -1 {
   266  						log.Warningf(fmt.Sprintf("unable to delete port %q, not found", portToDelete))
   267  					}
   268  					err = devfileobj.Data.RemovePorts(map[string][]string{selectContainerAnswer: {portToDelete}})
   269  					if err != nil {
   270  						return zeroDevfile, err
   271  					}
   272  					selectedContainer.Ports = append(selectedContainer.Ports[:indexToDelete], selectedContainer.Ports[indexToDelete+1:]...)
   273  
   274  				case "EnvVar":
   275  					envToDelete := configOps.Key
   276  					if _, ok := selectedContainer.Envs[envToDelete]; !ok {
   277  						log.Warningf(fmt.Sprintf("unable to delete env %q, not found", envToDelete))
   278  					}
   279  					err = devfileobj.Data.RemoveEnvVars(map[string][]string{selectContainerAnswer: {envToDelete}})
   280  					if err != nil {
   281  						return zeroDevfile, err
   282  					}
   283  					delete(selectedContainer.Envs, envToDelete)
   284  				}
   285  			case "Nothing":
   286  			default:
   287  				return zeroDevfile, fmt.Errorf("unknown configuration selected %q", fmt.Sprintf("%v %v %v", configOps.Ops, configOps.Kind, configOps.Key))
   288  			}
   289  			// Update the current configuration
   290  			config[selectContainerAnswer] = selectedContainer
   291  		}
   292  	}
   293  	return devfileobj, nil
   294  }
   295  
   296  func (o *InteractiveBackend) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string) (parser.DevfileObj, error) {
   297  	return handleApplicationPorts(log.GetStdout(), devfileobj, ports)
   298  }
   299  
   300  func PrintConfiguration(config asker.DevfileConfiguration) {
   301  
   302  	var keys []string
   303  	for key := range config {
   304  		keys = append(keys, key)
   305  	}
   306  	sort.Strings(keys)
   307  
   308  	for _, key := range keys {
   309  		container := config[key]
   310  		log.Sectionf("Container Configuration %q:", key)
   311  
   312  		stdout := log.GetStdout()
   313  
   314  		fmt.Fprintf(stdout, "  OPEN PORTS:\n")
   315  
   316  		for _, value := range container.Ports {
   317  			fmt.Fprintf(stdout, "    - %s\n", value)
   318  		}
   319  
   320  		fmt.Fprintf(stdout, "  ENVIRONMENT VARIABLES:\n")
   321  
   322  		keys := make([]string, 0, len(container.Envs))
   323  		for key := range container.Envs {
   324  			keys = append(keys, key)
   325  		}
   326  		sort.Strings(keys)
   327  		for _, key := range keys {
   328  			fmt.Fprintf(stdout, "    - %s = %s\n", key, container.Envs[key])
   329  		}
   330  
   331  	}
   332  
   333  	// Make sure we add a newline at the end
   334  	fmt.Println()
   335  }
   336  
   337  func getPortsAndEnvVar(obj parser.DevfileObj) (asker.DevfileConfiguration, error) {
   338  	var config = asker.DevfileConfiguration{}
   339  	components, err := obj.Data.GetComponents(parsercommon.DevfileOptions{ComponentOptions: parsercommon.ComponentOptions{ComponentType: v1alpha2.ContainerComponentType}})
   340  	if err != nil {
   341  		return config, err
   342  	}
   343  	for _, component := range components {
   344  		var ports = []string{}
   345  		var envMap = map[string]string{}
   346  
   347  		for _, ep := range component.Container.Endpoints {
   348  			ports = append(ports, strconv.Itoa(ep.TargetPort))
   349  		}
   350  		for _, env := range component.Container.Env {
   351  			envMap[env.Name] = env.Value
   352  		}
   353  		config[component.Name] = asker.ContainerConfiguration{
   354  			Ports: ports,
   355  			Envs:  envMap,
   356  		}
   357  	}
   358  	return config, nil
   359  }
   360  

View as plain text