...

Source file src/github.com/redhat-developer/odo/pkg/state/state.go

Documentation: github.com/redhat-developer/odo/pkg/state

     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  	// TODO(feloy) When other data is persisted into the state file, it will be needed to read the file first
    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 // if the state file does not exist, no ports are forwarded
    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 // if the state file does not exist, no API Servers are listening
   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  // save writes the content structure in json format in file
   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  	// .odo directory is supposed to exist, don't create it
   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  // read returns the content of the devstate.${PID}.json file for the given platform
   181  func (o *State) read(platform string) (Content, error) {
   182  
   183  	var content Content
   184  
   185  	// We could use Glob, but it is not implemented by the Filesystem abstraction
   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  		// Ignore error, to handle empty file
   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  			// File not found, it is free
   239  			return true, nil
   240  		}
   241  		return false, err
   242  	}
   243  	var savedContent Content
   244  	// Ignore error, to handle empty file
   245  	_ = json.Unmarshal(jsonContent, &savedContent)
   246  	if savedContent.PID == 0 {
   247  		// PID is 0 in file, it is free
   248  		return true, nil
   249  	}
   250  	if savedContent.PID == pid {
   251  		// File is owned by process
   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  		// Process already finished
   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  			// No file found => no problem
   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  		// Ignore error, to handle empty file
   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  			// Process exists => problem
   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  			// No file found => no orphan files
   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  		// Ignore error, to handle empty file
   353  		_ = json.Unmarshal(jsonContent, &content)
   354  
   355  		if content.PID == pid {
   356  			continue
   357  		}
   358  		if content.PID == 0 {
   359  			// This is devstate.json with pid=0
   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  			// Process exists => not orphan
   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