1 package state
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "regexp"
12
13 "github.com/mitchellh/go-ps"
14 "k8s.io/klog"
15
16 "github.com/redhat-developer/odo/pkg/api"
17 "github.com/redhat-developer/odo/pkg/odo/cli/feature"
18 "github.com/redhat-developer/odo/pkg/odo/commonflags"
19 fcontext "github.com/redhat-developer/odo/pkg/odo/commonflags/context"
20 odocontext "github.com/redhat-developer/odo/pkg/odo/context"
21 "github.com/redhat-developer/odo/pkg/testingutil/filesystem"
22 "github.com/redhat-developer/odo/pkg/testingutil/system"
23 )
24
25 type State struct {
26 content Content
27 fs filesystem.Filesystem
28 system system.System
29 }
30
31 var _ Client = (*State)(nil)
32
33 func NewStateClient(fs filesystem.Filesystem, system system.System) *State {
34 return &State{
35 fs: fs,
36 system: system,
37 }
38 }
39
40 func (o *State) Init(ctx context.Context) error {
41 var (
42 pid = odocontext.GetPID(ctx)
43 platform = fcontext.GetPlatform(ctx, commonflags.PlatformCluster)
44 )
45 o.content.PID = pid
46 o.content.Platform = platform
47 return o.save(ctx, pid)
48
49 }
50
51 func (o *State) SetForwardedPorts(ctx context.Context, fwPorts []api.ForwardedPort) error {
52 var (
53 pid = odocontext.GetPID(ctx)
54 platform = fcontext.GetPlatform(ctx, commonflags.PlatformCluster)
55 )
56
57 o.content.ForwardedPorts = fwPorts
58 o.content.PID = pid
59 o.content.Platform = platform
60 return o.save(ctx, pid)
61 }
62
63 func (o *State) GetForwardedPorts(ctx context.Context) ([]api.ForwardedPort, error) {
64 var (
65 result []api.ForwardedPort
66 platforms []string
67 platform = fcontext.GetPlatform(ctx, "")
68 )
69 if platform == "" {
70 platforms = []string{commonflags.PlatformCluster, commonflags.PlatformPodman}
71 } else {
72 platforms = []string{platform}
73 }
74
75 for _, platform = range platforms {
76 content, err := o.read(platform)
77 if err != nil {
78 if errors.Is(err, fs.ErrNotExist) {
79 continue
80 }
81 return nil, err
82 }
83 result = append(result, content.ForwardedPorts...)
84 }
85 return result, nil
86 }
87
88 func (o *State) SaveExit(ctx context.Context) error {
89 var (
90 pid = odocontext.GetPID(ctx)
91 )
92 o.content.ForwardedPorts = nil
93 o.content.PID = 0
94 o.content.Platform = ""
95 o.content.APIServerPort = 0
96 err := o.delete(pid)
97 if err != nil {
98 return err
99 }
100 return o.saveCommonIfOwner(pid)
101 }
102
103 func (o *State) SetAPIServerPort(ctx context.Context, port int) error {
104 var (
105 pid = odocontext.GetPID(ctx)
106 platform = fcontext.GetPlatform(ctx, commonflags.PlatformCluster)
107 )
108
109 o.content.APIServerPort = port
110 o.content.Platform = platform
111 return o.save(ctx, pid)
112 }
113
114 func (o *State) GetAPIServerPorts(ctx context.Context) ([]api.DevControlPlane, error) {
115 var (
116 result []api.DevControlPlane
117 platforms []string
118 platform = fcontext.GetPlatform(ctx, "")
119 )
120 if platform == "" {
121 platforms = []string{commonflags.PlatformCluster, commonflags.PlatformPodman}
122 } else {
123 platforms = []string{platform}
124 }
125
126 for _, platform = range platforms {
127 content, err := o.read(platform)
128 if err != nil {
129 if errors.Is(err, fs.ErrNotExist) {
130 continue
131 }
132 return nil, err
133 }
134 if content.APIServerPort == 0 {
135 continue
136 }
137 controlPlane := api.DevControlPlane{
138 Platform: platform,
139 LocalPort: content.APIServerPort,
140 APIServerPath: "/api/v1/",
141 }
142 if feature.IsEnabled(ctx, feature.UIServer) {
143 controlPlane.WebInterfacePath = "/"
144 }
145 result = append(result, controlPlane)
146 }
147 return result, nil
148 }
149
150
151 func (o *State) save(ctx context.Context, pid int) error {
152
153 err := o.checkFirstInPlatform(ctx)
154 if err != nil {
155 return err
156 }
157
158 err = o.saveCommonIfOwner(pid)
159 if err != nil {
160 return err
161 }
162
163 return o.writeStateFile(getFilename(pid))
164 }
165
166 func (o *State) writeStateFile(path string) error {
167 jsonContent, err := json.MarshalIndent(o.content, "", " ")
168 if err != nil {
169 return err
170 }
171
172 dir := filepath.Dir(path)
173 err = os.MkdirAll(dir, 0750)
174 if err != nil {
175 return err
176 }
177 return o.fs.WriteFile(path, jsonContent, 0644)
178 }
179
180
181 func (o *State) read(platform string) (Content, error) {
182
183 var content Content
184
185
186 entries, err := o.fs.ReadDir(_dirpath)
187 if err != nil {
188 return Content{}, nil
189 }
190 re := regexp.MustCompile(`^devstate\.[0-9]*\.json$`)
191 for _, entry := range entries {
192 if !re.MatchString(entry.Name()) {
193 continue
194 }
195 jsonContent, err := o.fs.ReadFile(filepath.Join(_dirpath, entry.Name()))
196 if err != nil {
197 return Content{}, err
198 }
199
200 _ = json.Unmarshal(jsonContent, &content)
201 if content.Platform == platform {
202 break
203 } else {
204 content = Content{}
205 }
206 }
207 if content.Platform == "" {
208 return Content{}, fs.ErrNotExist
209 }
210 return content, nil
211 }
212
213 func (o *State) delete(pid int) error {
214 return o.fs.Remove(getFilename(pid))
215 }
216
217 func getFilename(pid int) string {
218 return fmt.Sprintf(_filepathPid, pid)
219 }
220
221 func (o *State) saveCommonIfOwner(pid int) error {
222
223 ok, err := o.isFreeOrOwnedBy(pid)
224 if err != nil {
225 return err
226 }
227 if !ok {
228 return nil
229 }
230
231 return o.writeStateFile(_filepath)
232 }
233
234 func (o *State) isFreeOrOwnedBy(pid int) (bool, error) {
235 jsonContent, err := o.fs.ReadFile(_filepath)
236 if err != nil {
237 if errors.Is(err, os.ErrNotExist) {
238
239 return true, nil
240 }
241 return false, err
242 }
243 var savedContent Content
244
245 _ = json.Unmarshal(jsonContent, &savedContent)
246 if savedContent.PID == 0 {
247
248 return true, nil
249 }
250 if savedContent.PID == pid {
251
252 return true, nil
253 }
254
255 exists, err := o.system.PidExists(savedContent.PID)
256 if err != nil {
257 return false, err
258 }
259 if !exists {
260
261 return true, nil
262 }
263
264 return false, nil
265 }
266
267 func (o *State) checkFirstInPlatform(ctx context.Context) error {
268 var (
269 pid = odocontext.GetPID(ctx)
270 platform = fcontext.GetPlatform(ctx, "cluster")
271 )
272
273 re := regexp.MustCompile(`^devstate\.[0-9]*\.json$`)
274 entries, err := o.fs.ReadDir(_dirpath)
275 if err != nil {
276 if errors.Is(err, os.ErrNotExist) {
277
278 return nil
279 }
280 return err
281 }
282 for _, entry := range entries {
283 if !re.MatchString(entry.Name()) {
284 continue
285 }
286 jsonContent, err := o.fs.ReadFile(filepath.Join(_dirpath, entry.Name()))
287 if err != nil {
288 return err
289 }
290 var content Content
291
292 _ = json.Unmarshal(jsonContent, &content)
293
294 if content.Platform != platform {
295 continue
296 }
297
298 if content.PID == pid {
299 continue
300 }
301 exists, err := o.system.PidExists(content.PID)
302 if err != nil {
303 return err
304 }
305 if exists {
306 var process ps.Process
307 process, err = o.system.FindProcess(content.PID)
308 if err != nil {
309 klog.V(4).Infof("process %d exists but is not accessible, ignoring", content.PID)
310 continue
311 }
312 if process.Executable() != "odo" && process.Executable() != "odo.exe" {
313 klog.V(4).Infof("process %d exists but is not odo, ignoring", content.PID)
314 continue
315 }
316
317 return NewErrAlreadyRunningOnPlatform(platform, content.PID)
318 }
319 }
320 return nil
321 }
322
323 func (o *State) GetOrphanFiles(ctx context.Context) ([]string, error) {
324 var (
325 pid = odocontext.GetPID(ctx)
326 result []string
327 )
328
329 re := regexp.MustCompile(`^devstate\.?[0-9]*\.json$`)
330 entries, err := o.fs.ReadDir(_dirpath)
331 if err != nil {
332 if errors.Is(err, os.ErrNotExist) {
333
334 return nil, nil
335 }
336 return nil, err
337 }
338 for _, entry := range entries {
339 if !re.MatchString(entry.Name()) {
340 continue
341 }
342 filename, err := getFullFilename(entry)
343 if err != nil {
344 return nil, err
345 }
346
347 jsonContent, err := o.fs.ReadFile(filepath.Join(_dirpath, entry.Name()))
348 if err != nil {
349 return nil, err
350 }
351 var content Content
352
353 _ = json.Unmarshal(jsonContent, &content)
354
355 if content.PID == pid {
356 continue
357 }
358 if content.PID == 0 {
359
360 continue
361 }
362 exists, err := o.system.PidExists(content.PID)
363 if err != nil {
364 return nil, err
365 }
366 if exists {
367 var process ps.Process
368 process, err = o.system.FindProcess(content.PID)
369 if err != nil {
370 klog.V(4).Infof("process %d exists but is not accessible => orphan", content.PID)
371 result = append(result, filename)
372 continue
373 }
374 if process == nil {
375 klog.V(4).Infof("process %d does not exist => orphan", content.PID)
376 result = append(result, filename)
377 continue
378 }
379 if process.Executable() != "odo" && process.Executable() != "odo.exe" {
380 klog.V(4).Infof("process %d exists but is not odo => orphan", content.PID)
381 result = append(result, filename)
382 continue
383 }
384
385 klog.V(4).Infof("process %d exists and is odo => not orphan", content.PID)
386 continue
387 }
388 klog.V(4).Infof("process %d does not exist => orphan", content.PID)
389 result = append(result, filename)
390 }
391 return result, nil
392 }
393
394 func getFullFilename(entry fs.FileInfo) (string, error) {
395 return filepath.Abs(filepath.Join(_dirpath, entry.Name()))
396 }
397
View as plain text