mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
* 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
223 lines
7.2 KiB
Go
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
|
|
}
|