...

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

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

     1  package kclient
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	projectv1 "github.com/openshift/api/project/v1"
    10  	corev1 "k8s.io/api/core/v1"
    11  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/fields"
    14  	"k8s.io/apimachinery/pkg/watch"
    15  	"k8s.io/klog"
    16  )
    17  
    18  const (
    19  	// timeout for waiting for project deletion
    20  	waitForProjectDeletionTimeOut = 3 * time.Minute
    21  )
    22  
    23  // GetProject returns project based on the name of the project
    24  // errors related to project not being found or forbidden are translated to nil project for compatibility
    25  func (c *Client) GetProject(projectName string) (*projectv1.Project, error) {
    26  	prj, err := c.projectClient.Projects().Get(context.TODO(), projectName, metav1.GetOptions{})
    27  	if err != nil {
    28  		istatus, ok := err.(kerrors.APIStatus)
    29  		if ok {
    30  			status := istatus.Status()
    31  			if status.Reason == metav1.StatusReasonNotFound || status.Reason == metav1.StatusReasonForbidden {
    32  				return nil, nil
    33  			}
    34  		} else {
    35  			return nil, err
    36  		}
    37  
    38  	}
    39  	return prj, err
    40  
    41  }
    42  
    43  // ListProjects return list of existing projects that user has access to.
    44  func (c *Client) ListProjects() (*projectv1.ProjectList, error) {
    45  	return c.projectClient.Projects().List(context.TODO(), metav1.ListOptions{})
    46  }
    47  
    48  // ListProjectNames return list of existing project names that user has access to.
    49  func (c *Client) ListProjectNames() ([]string, error) {
    50  	projects, err := c.ListProjects()
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	var projectNames []string
    56  	for _, p := range projects.Items {
    57  		projectNames = append(projectNames, p.Name)
    58  	}
    59  	return projectNames, nil
    60  }
    61  
    62  // DeleteProject deletes given project
    63  //
    64  // NOTE:
    65  // There is a very specific edge case that may happen during project deletion when deleting a project and then immediately creating another.
    66  // Unfortunately, despite the watch interface, we cannot safely determine if the project is 100% deleted. See this link:
    67  // https://stackoverflow.com/questions/48208001/deleted-openshift-online-pro-project-has-left-a-trace-so-cannot-create-project-o
    68  // Will Gordon (Engineer @ Red Hat) describes the issue:
    69  //
    70  // "Projects are deleted asynchronously after you send the delete command. So it's possible that the deletion just hasn't been reconciled yet. It should happen within a minute or so, so try again.
    71  // Also, please be aware that in a multitenant environment, like OpenShift Online, you are prevented from creating a project with the same name as any other project in the cluster, even if it's not your own. So if you can't create the project, it's possible that someone has already created a project with the same name."
    72  func (c *Client) DeleteProject(name string, wait bool) error {
    73  
    74  	// Instantiate watcher for our "wait" function
    75  	var watcher watch.Interface
    76  	var err error
    77  
    78  	// If --wait has been passed, we will wait for the project to fully be deleted
    79  	if wait {
    80  		watcher, err = c.projectClient.Projects().Watch(context.TODO(), metav1.ListOptions{
    81  			FieldSelector: fields.Set{"metadata.name": name}.AsSelector().String(),
    82  		})
    83  		if err != nil {
    84  			return fmt.Errorf("unable to watch project: %w", err)
    85  		}
    86  		defer watcher.Stop()
    87  	}
    88  
    89  	// Delete the project
    90  	err = c.projectClient.Projects().Delete(context.TODO(), name, metav1.DeleteOptions{})
    91  	if err != nil {
    92  		return fmt.Errorf("unable to delete project: %w", err)
    93  	}
    94  
    95  	// If watcher has been created (wait was passed) we will create a go routine and actually **wait**
    96  	// until *EVERYTHING* is successfully deleted.
    97  	if watcher != nil {
    98  
    99  		// Project channel
   100  		// Watch error channel
   101  		projectChannel := make(chan *projectv1.Project)
   102  		watchErrorChannel := make(chan error)
   103  
   104  		// Create a go routine to run in the background
   105  		go func() {
   106  
   107  			for {
   108  
   109  				// If watch unexpected has been closed..
   110  				val, ok := <-watcher.ResultChan()
   111  				if !ok {
   112  					//return fmt.Errorf("received unexpected signal %+v on project watch channel", val)
   113  					watchErrorChannel <- fmt.Errorf("watch channel was closed unexpectedly: %+v", val)
   114  					break
   115  				}
   116  
   117  				// So we depend on val.Type as val.Object.Status.Phase is just empty string and not a mapped value constant
   118  				if projectStatus, ok := val.Object.(*projectv1.Project); ok {
   119  
   120  					klog.V(3).Infof("Status of delete of project %s is '%s'", name, projectStatus.Status.Phase)
   121  
   122  					switch projectStatus.Status.Phase {
   123  					//projectStatus.Status.Phase can only be "Terminating" or "Active" or ""
   124  					case "":
   125  						if val.Type == watch.Deleted {
   126  							projectChannel <- projectStatus
   127  							break
   128  						}
   129  						if val.Type == watch.Error {
   130  							watchErrorChannel <- fmt.Errorf("failed watching the deletion of project %s", name)
   131  							break
   132  						}
   133  					}
   134  
   135  				} else {
   136  					watchErrorChannel <- errors.New("unable to convert event object to Project")
   137  					break
   138  				}
   139  
   140  			}
   141  			close(projectChannel)
   142  			close(watchErrorChannel)
   143  		}()
   144  
   145  		select {
   146  		case val := <-projectChannel:
   147  			klog.V(3).Infof("Deletion information for project: %+v", val)
   148  			return nil
   149  		case err := <-watchErrorChannel:
   150  			return err
   151  		case <-time.After(waitForProjectDeletionTimeOut):
   152  			return fmt.Errorf("waited %s but couldn't delete project %s in time", waitForProjectDeletionTimeOut, name)
   153  		}
   154  
   155  	}
   156  
   157  	// Return nil since we don't bother checking for the watcher..
   158  	return nil
   159  }
   160  
   161  // CreateNewProject creates project with given projectName
   162  func (c *Client) CreateNewProject(projectName string, wait bool) error {
   163  	// Instantiate watcher before requesting new project
   164  	// If watcher is created after the project it can lead to situation when the project is created before the watcher.
   165  	// When this happens, it gets stuck waiting for event that already happened.
   166  	var watcher watch.Interface
   167  	var err error
   168  	if wait {
   169  		watcher, err = c.projectClient.Projects().Watch(context.TODO(), metav1.ListOptions{
   170  			FieldSelector: fields.Set{"metadata.name": projectName}.AsSelector().String(),
   171  		})
   172  		if err != nil {
   173  			return fmt.Errorf("unable to watch new project %s creation: %w", projectName, err)
   174  		}
   175  		defer watcher.Stop()
   176  	}
   177  
   178  	projectRequest := &projectv1.ProjectRequest{
   179  		ObjectMeta: metav1.ObjectMeta{
   180  			Name: projectName,
   181  		},
   182  	}
   183  	_, err = c.projectClient.ProjectRequests().Create(context.TODO(), projectRequest, metav1.CreateOptions{FieldManager: FieldManager})
   184  	if err != nil {
   185  		return fmt.Errorf("unable to create new project %s: %w", projectName, err)
   186  	}
   187  
   188  	if watcher != nil {
   189  		for {
   190  			val, ok := <-watcher.ResultChan()
   191  			if !ok {
   192  				break
   193  			}
   194  			if prj, ok := val.Object.(*projectv1.Project); ok {
   195  				klog.V(3).Infof("Status of creation of project %s is %s", prj.Name, prj.Status.Phase)
   196  				switch prj.Status.Phase {
   197  				//prj.Status.Phase can only be "Terminating" or "Active" or ""
   198  				case corev1.NamespaceActive:
   199  					if val.Type == watch.Added {
   200  						klog.V(3).Infof("Project %s now exists", prj.Name)
   201  						return nil
   202  					}
   203  					if val.Type == watch.Error {
   204  						return fmt.Errorf("failed watching the deletion of project %s", prj.Name)
   205  					}
   206  				}
   207  			}
   208  		}
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  // IsProjectSupported checks if Project resource type is present on the cluster
   215  func (c *Client) IsProjectSupported() (bool, error) {
   216  	return c.IsResourceSupported("project.openshift.io", "v1", "projects")
   217  }
   218  
   219  // GetCurrentProjectName returns the current project name
   220  func (c *Client) GetCurrentProjectName() string {
   221  	return c.Namespace
   222  }
   223  

View as plain text