Files
odo/pkg/kclient/projects.go
Philippe Martin 24fd02673d Remove github.com/pkg/errors dependency (#5557)
* Change errors.Wrapf

* Replace errors.Wrap

* Dont use pkg/errors (except error.Cause)

* Fix errors on Windows (do not test system underlying message)

* Replace errors.Cause

* Review
2022-03-21 11:32:42 +01:00

223 lines
7.2 KiB
Go

package kclient
import (
"context"
"errors"
"fmt"
"time"
projectv1 "github.com/openshift/api/project/v1"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/klog"
)
const (
// timeout for waiting for project deletion
waitForProjectDeletionTimeOut = 3 * time.Minute
)
// GetProject returns project based on the name of the project
// errors related to project not being found or forbidden are translated to nil project for compatibility
func (c *Client) GetProject(projectName string) (*projectv1.Project, error) {
prj, err := c.projectClient.Projects().Get(context.TODO(), projectName, metav1.GetOptions{})
if err != nil {
istatus, ok := err.(kerrors.APIStatus)
if ok {
status := istatus.Status()
if status.Reason == metav1.StatusReasonNotFound || status.Reason == metav1.StatusReasonForbidden {
return nil, nil
}
} else {
return nil, err
}
}
return prj, err
}
// ListProjects return list of existing projects that user has access to.
func (c *Client) ListProjects() (*projectv1.ProjectList, error) {
return c.projectClient.Projects().List(context.TODO(), metav1.ListOptions{})
}
// ListProjectNames return list of existing project names that user has access to.
func (c *Client) ListProjectNames() ([]string, error) {
projects, err := c.ListProjects()
if err != nil {
return nil, err
}
var projectNames []string
for _, p := range projects.Items {
projectNames = append(projectNames, p.Name)
}
return projectNames, nil
}
// DeleteProject deletes given project
//
// NOTE:
// There is a very specific edge case that may happen during project deletion when deleting a project and then immediately creating another.
// Unfortunately, despite the watch interface, we cannot safely determine if the project is 100% deleted. See this link:
// https://stackoverflow.com/questions/48208001/deleted-openshift-online-pro-project-has-left-a-trace-so-cannot-create-project-o
// Will Gordon (Engineer @ Red Hat) describes the issue:
//
// "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.
// 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."
func (c *Client) DeleteProject(name string, wait bool) error {
// Instantiate watcher for our "wait" function
var watcher watch.Interface
var err error
// If --wait has been passed, we will wait for the project to fully be deleted
if wait {
watcher, err = c.projectClient.Projects().Watch(context.TODO(), metav1.ListOptions{
FieldSelector: fields.Set{"metadata.name": name}.AsSelector().String(),
})
if err != nil {
return fmt.Errorf("unable to watch project: %w", err)
}
defer watcher.Stop()
}
// Delete the project
err = c.projectClient.Projects().Delete(context.TODO(), name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("unable to delete project: %w", err)
}
// If watcher has been created (wait was passed) we will create a go routine and actually **wait**
// until *EVERYTHING* is successfully deleted.
if watcher != nil {
// Project channel
// Watch error channel
projectChannel := make(chan *projectv1.Project)
watchErrorChannel := make(chan error)
// Create a go routine to run in the background
go func() {
for {
// If watch unexpected has been closed..
val, ok := <-watcher.ResultChan()
if !ok {
//return fmt.Errorf("received unexpected signal %+v on project watch channel", val)
watchErrorChannel <- fmt.Errorf("watch channel was closed unexpectedly: %+v", val)
break
}
// So we depend on val.Type as val.Object.Status.Phase is just empty string and not a mapped value constant
if projectStatus, ok := val.Object.(*projectv1.Project); ok {
klog.V(3).Infof("Status of delete of project %s is '%s'", name, projectStatus.Status.Phase)
switch projectStatus.Status.Phase {
//projectStatus.Status.Phase can only be "Terminating" or "Active" or ""
case "":
if val.Type == watch.Deleted {
projectChannel <- projectStatus
break
}
if val.Type == watch.Error {
watchErrorChannel <- fmt.Errorf("failed watching the deletion of project %s", name)
break
}
}
} else {
watchErrorChannel <- errors.New("unable to convert event object to Project")
break
}
}
close(projectChannel)
close(watchErrorChannel)
}()
select {
case val := <-projectChannel:
klog.V(3).Infof("Deletion information for project: %+v", val)
return nil
case err := <-watchErrorChannel:
return err
case <-time.After(waitForProjectDeletionTimeOut):
return fmt.Errorf("waited %s but couldn't delete project %s in time", waitForProjectDeletionTimeOut, name)
}
}
// Return nil since we don't bother checking for the watcher..
return nil
}
// CreateNewProject creates project with given projectName
func (c *Client) CreateNewProject(projectName string, wait bool) error {
// Instantiate watcher before requesting new project
// If watcher is created after the project it can lead to situation when the project is created before the watcher.
// When this happens, it gets stuck waiting for event that already happened.
var watcher watch.Interface
var err error
if wait {
watcher, err = c.projectClient.Projects().Watch(context.TODO(), metav1.ListOptions{
FieldSelector: fields.Set{"metadata.name": projectName}.AsSelector().String(),
})
if err != nil {
return fmt.Errorf("unable to watch new project %s creation: %w", projectName, err)
}
defer watcher.Stop()
}
projectRequest := &projectv1.ProjectRequest{
ObjectMeta: metav1.ObjectMeta{
Name: projectName,
},
}
_, err = c.projectClient.ProjectRequests().Create(context.TODO(), projectRequest, metav1.CreateOptions{FieldManager: FieldManager})
if err != nil {
return fmt.Errorf("unable to create new project %s: %w", projectName, err)
}
if watcher != nil {
for {
val, ok := <-watcher.ResultChan()
if !ok {
break
}
if prj, ok := val.Object.(*projectv1.Project); ok {
klog.V(3).Infof("Status of creation of project %s is %s", prj.Name, prj.Status.Phase)
switch prj.Status.Phase {
//prj.Status.Phase can only be "Terminating" or "Active" or ""
case corev1.NamespaceActive:
if val.Type == watch.Added {
klog.V(3).Infof("Project %s now exists", prj.Name)
return nil
}
if val.Type == watch.Error {
return fmt.Errorf("failed watching the deletion of project %s", prj.Name)
}
}
}
}
}
return nil
}
// IsProjectSupported checks if Project resource type is present on the cluster
func (c *Client) IsProjectSupported() (bool, error) {
return c.IsResourceSupported("project.openshift.io", "v1", "projects")
}
// GetCurrentProjectName returns the current project name
func (c *Client) GetCurrentProjectName() string {
return c.Namespace
}