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
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
154
155
156
157
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
164
165 baseDir := filepath.Dir(path)
166
167
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
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
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
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