diff --git a/pkg/controller/devopsproject/devopsproject_controller.go b/pkg/controller/devopsproject/devopsproject_controller.go index b08943de5..b05777bd4 100644 --- a/pkg/controller/devopsproject/devopsproject_controller.go +++ b/pkg/controller/devopsproject/devopsproject_controller.go @@ -254,6 +254,12 @@ func (c *Controller) syncHandler(key string) error { ns := c.generateNewNamespace(project) ns, err := c.client.CoreV1().Namespaces().Create(ns) if err != nil { + // devops project name is conflict, cannot create admin namespace + if errors.IsAlreadyExists(err) { + klog.Errorf("Failed to create admin namespace for devopsproject %s, error %v", project.Name, err) + c.eventRecorder.Event(project, v1.EventTypeWarning, "CreateAdminNamespaceFailed", err.Error()) + return err + } klog.V(8).Info(err, fmt.Sprintf("failed to create ns %s ", key)) return err } @@ -369,13 +375,15 @@ func (c *Controller) deleteDevOpsProjectInDevOps(project *devopsv1alpha3.DevOpsP } func (c *Controller) generateNewNamespace(project *devopsv1alpha3.DevOpsProject) *v1.Namespace { + // devops project name and admin namespace name should be the same + // solve the access control problem of devops API v1alpha2 and v1alpha3 ns := &v1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: v1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - GenerateName: project.Name, + Name: project.Name, Labels: map[string]string{ constants.DevOpsProjectLabelKey: project.Name, }, diff --git a/pkg/controller/devopsproject/devopsproject_controller_test.go b/pkg/controller/devopsproject/devopsproject_controller_test.go index faec28cac..97f34b65a 100644 --- a/pkg/controller/devopsproject/devopsproject_controller_test.go +++ b/pkg/controller/devopsproject/devopsproject_controller_test.go @@ -371,13 +371,13 @@ func TestUpdateNsOwnerReference(t *testing.T) { func TestCreateDevOpsProjects(t *testing.T) { f := newFixture(t) project := newDevOpsProject("test", "", true, false) - ns := newNamespace("test-123", "test", true, true) + ns := newNamespace("test", "test", false, true) f.devopsProjectLister = append(f.devopsProjectLister, project) f.objects = append(f.objects, project) f.expectDevOpsProject = []string{""} - - // because generateName not work in fakeClient, so DevOpsProject would not be update - // f.expectUpdateDevOpsProjectAction(project) + expect := project.DeepCopy() + expect.Status.AdminNamespace = "test" + f.expectUpdateDevOpsProjectAction(expect) f.expectCreateNamespaceAction(ns) f.run(getKey(project, t)) } diff --git a/pkg/kapis/devops/v1alpha3/handler.go b/pkg/kapis/devops/v1alpha3/handler.go index cf7ab747d..85c887994 100644 --- a/pkg/kapis/devops/v1alpha3/handler.go +++ b/pkg/kapis/devops/v1alpha3/handler.go @@ -103,6 +103,9 @@ func (h *devopsHandler) CreateDevOpsProject(request *restful.Request, response * if errors.IsNotFound(err) { api.HandleNotFound(response, request, err) return + } else if errors.IsConflict(err) { + api.HandleConflict(response, request, err) + return } api.HandleBadRequest(response, request, err) return diff --git a/pkg/kapis/iam/v1alpha2/handler.go b/pkg/kapis/iam/v1alpha2/handler.go index 1790f238b..8727261c3 100644 --- a/pkg/kapis/iam/v1alpha2/handler.go +++ b/pkg/kapis/iam/v1alpha2/handler.go @@ -1222,7 +1222,7 @@ func (h *iamHandler) ListUserLoginRecords(request *restful.Request, response *re } func handleError(request *restful.Request, response *restful.Response, err error) { - if errors.IsBadRequest(err) { + if errors.IsBadRequest(err) || errors.IsInvalid(err) { api.HandleBadRequest(response, request, err) } else if errors.IsNotFound(err) { api.HandleNotFound(response, request, err) diff --git a/pkg/models/devops/devops.go b/pkg/models/devops/devops.go index 0fd6c6cfe..3a4491c3e 100644 --- a/pkg/models/devops/devops.go +++ b/pkg/models/devops/devops.go @@ -23,13 +23,16 @@ import ( "io" "io/ioutil" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/klog" "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" + devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/client/informers/externalversions" @@ -143,7 +146,30 @@ func convertToHttpParameters(req *http.Request) *devops.HttpParameters { } func (d devopsOperator) CreateDevOpsProject(workspace string, project *v1alpha3.DevOpsProject) (*v1alpha3.DevOpsProject, error) { - project.Annotations[tenantv1alpha1.WorkspaceLabel] = workspace + // All resources of devops project belongs to the namespace of the same name + // The devops project name is used as the name of the admin namespace, using generateName to avoid conflicts + if project.GenerateName == "" { + err := errors.NewInvalid(devopsv1alpha3.SchemeGroupVersion.WithKind(devopsv1alpha3.ResourceKindDevOpsProject).GroupKind(), + "", []*field.Error{field.Required(field.NewPath("metadata.generateName"), "generateName is required")}) + klog.Error(err) + return nil, err + } + // generateName is used as displayName + // ensure generateName is unique in workspace scope + if unique, err := d.isGenerateNameUnique(workspace, project.GenerateName); err != nil { + return nil, err + } else if !unique { + err = errors.NewConflict(devopsv1alpha3.Resource(devopsv1alpha3.ResourceSingularDevOpsProject), + project.GenerateName, fmt.Errorf(project.GenerateName, fmt.Errorf("a devops project named %s already exists in the workspace", project.GenerateName))) + klog.Error(err) + return nil, err + } + // metadata override + if project.Labels == nil { + project.Labels = make(map[string]string, 0) + } + project.Name = "" + project.Labels[tenantv1alpha1.WorkspaceLabel] = workspace return d.ksclient.DevopsV1alpha3().DevOpsProjects().Create(project) } @@ -156,7 +182,10 @@ func (d devopsOperator) DeleteDevOpsProject(workspace string, projectName string } func (d devopsOperator) UpdateDevOpsProject(workspace string, project *v1alpha3.DevOpsProject) (*v1alpha3.DevOpsProject, error) { - project.Annotations[tenantv1alpha1.WorkspaceLabel] = workspace + if project.Labels == nil { + project.Labels = make(map[string]string, 0) + } + project.Labels[tenantv1alpha1.WorkspaceLabel] = workspace return d.ksclient.DevopsV1alpha3().DevOpsProjects().Update(project) } @@ -866,6 +895,21 @@ func (d devopsOperator) ToJson(req *http.Request) (*devops.ResJson, error) { return res, err } +func (d devopsOperator) isGenerateNameUnique(workspace, generateName string) (bool, error) { + selector := labels.Set{tenantv1alpha1.WorkspaceLabel: workspace} + projects, err := d.ksInformers.Devops().V1alpha3().DevOpsProjects().Lister().List(labels.SelectorFromSet(selector)) + if err != nil { + klog.Error(err) + return false, err + } + for _, p := range projects { + if p.GenerateName == generateName { + return false, err + } + } + return true, nil +} + func getInputReqBody(reqBody io.ReadCloser) (newReqBody io.ReadCloser, err error) { var checkBody devops.CheckPlayload var jsonBody []byte