diff --git a/pkg/apis/devops/v1alpha3/credential_types.go b/pkg/apis/devops/v1alpha3/credential_types.go new file mode 100644 index 0000000000000000000000000000000000000000..707fe1323641ff0791509f71ea272c577de45c58 --- /dev/null +++ b/pkg/apis/devops/v1alpha3/credential_types.go @@ -0,0 +1,56 @@ +package v1alpha3 + +import v1 "k8s.io/api/core/v1" + +/** +We use a special type of secret as a credential for DevOps. +This file will not contain CRD, but the credential type constants and their fields. +*/ +const ( + CredentialFinalizerName = "credential.finalizers.kubesphere.io" + DevOpsCredentialPrefix = "credential.devops.kubesphere.io/" + // SecretTypeBasicAuth contains data needed for basic authentication. + // + // Required at least one of fields: + // - Secret.Data["username"] - username used for authentication + // - Secret.Data["password"] - password or token needed for authentication + SecretTypeBasicAuth v1.SecretType = DevOpsCredentialPrefix + "basic-auth" + // BasicAuthUsernameKey is the key of the username for SecretTypeBasicAuth secrets + BasicAuthUsernameKey = "username" + // BasicAuthPasswordKey is the key of the password or token for SecretTypeBasicAuth secrets + BasicAuthPasswordKey = "password" + + // SecretTypeSSHAuth contains data needed for ssh authentication. + // + // Required at least one of fields: + // - Secret.Data["username"] - username used for authentication + // - Secret.Data["passphrase"] - passphrase needed for authentication + // - Secret.Data["privatekey"] - privatekey needed for authentication + SecretTypeSSHAuth v1.SecretType = DevOpsCredentialPrefix + "ssh-auth" + // SSHAuthUsernameKey is the key of the username for SecretTypeSSHAuth secrets + SSHAuthUsernameKey = "username" + // SSHAuthPrivateKey is the key of the passphrase for SecretTypeSSHAuth secrets + SSHAuthPassphraseKey = "passphrase" + // SSHAuthPrivateKey is the key of the privatekey for SecretTypeSSHAuth secrets + SSHAuthPrivateKey = "privatekey" + + // SecretTypeSecretText contains data. + // + // Required at least one of fields: + // - Secret.Data["secret"] - secret + SecretTypeSecretText v1.SecretType = DevOpsCredentialPrefix + "secret-text" + // SecretTextSecretKey is the key of the secret for SecretTypeSecretText secrets + SecretTextSecretKey = "secret" + + // SecretTypeKubeConfig contains data. + // + // Required at least one of fields: + // - Secret.Data["secret"] - secret + SecretTypeKubeConfig v1.SecretType = DevOpsCredentialPrefix + "kubeconfig" + // KubeConfigSecretKey is the key of the secret for SecretTypeKubeConfig secrets + KubeConfigSecretKey = "secret" + // CredentialAutoSyncAnnoKey is used to indicate whether the secret is automatically synchronized to devops. + // In the old version, the credential is stored in jenkins and cannot be obtained. + // This field is set to ensure that the secret is not overwritten by a nil value. + CredentialAutoSyncAnnoKey = DevOpsCredentialPrefix + "autosync" +) diff --git a/pkg/controller/devopscredential/devopscredential_controller.go b/pkg/controller/devopscredential/devopscredential_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..2c7728b76dc19636086e22c908265a38ddf54fe3 --- /dev/null +++ b/pkg/controller/devopscredential/devopscredential_controller.go @@ -0,0 +1,284 @@ +package devopscredential + +import ( + "fmt" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + corev1informer "k8s.io/client-go/informers/core/v1" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + v1core "k8s.io/client-go/kubernetes/typed/core/v1" + corev1lister "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog" + devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" + kubesphereclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + "kubesphere.io/kubesphere/pkg/constants" + devopsClient "kubesphere.io/kubesphere/pkg/simple/client/devops" + "kubesphere.io/kubesphere/pkg/utils/k8sutil" + "kubesphere.io/kubesphere/pkg/utils/sliceutil" + "net/http" + "reflect" + "strings" + "time" +) + +/** + DevOps project controller is used to maintain the state of the DevOps project. +*/ + +type Controller struct { + client clientset.Interface + kubesphereClient kubesphereclient.Interface + + eventBroadcaster record.EventBroadcaster + eventRecorder record.EventRecorder + + secretLister corev1lister.SecretLister + secretSynced cache.InformerSynced + + namespaceLister corev1lister.NamespaceLister + namespaceSynced cache.InformerSynced + + workqueue workqueue.RateLimitingInterface + + workerLoopPeriod time.Duration + + devopsClient devopsClient.Interface +} + +func NewController(client clientset.Interface, + devopsClinet devopsClient.Interface, + namespaceInformer corev1informer.NamespaceInformer, + secretInformer corev1informer.SecretInformer) *Controller { + + broadcaster := record.NewBroadcaster() + broadcaster.StartLogging(func(format string, args ...interface{}) { + klog.Info(fmt.Sprintf(format, args)) + }) + broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: client.CoreV1().Events("")}) + recorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "pipeline-controller"}) + + v := &Controller{ + client: client, + devopsClient: devopsClinet, + workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "pipeline"), + secretLister: secretInformer.Lister(), + secretSynced: secretInformer.Informer().HasSynced, + namespaceLister: namespaceInformer.Lister(), + namespaceSynced: namespaceInformer.Informer().HasSynced, + workerLoopPeriod: time.Second, + } + + v.eventBroadcaster = broadcaster + v.eventRecorder = recorder + + secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + secret := obj.(*v1.Secret) + if strings.HasPrefix(string(secret.Type), devopsv1alpha3.DevOpsCredentialPrefix) { + v.enqueueSecret(obj) + } + }, + UpdateFunc: func(oldObj, newObj interface{}) { + old := oldObj.(*v1.Secret) + new := newObj.(*v1.Secret) + if old.ResourceVersion == new.ResourceVersion { + return + } + if strings.HasPrefix(string(new.Type), devopsv1alpha3.DevOpsCredentialPrefix) { + v.enqueueSecret(newObj) + } + }, + DeleteFunc: func(obj interface{}) { + secret := obj.(*v1.Secret) + if strings.HasPrefix(string(secret.Type), devopsv1alpha3.DevOpsCredentialPrefix) { + v.enqueueSecret(obj) + } + }, + }) + return v +} + +// enqueueSecret takes a Foo resource and converts it into a namespace/name +// string which is then put onto the work workqueue. This method should *not* be +// passed resources of any type other than DevOpsProject. +func (c *Controller) enqueueSecret(obj interface{}) { + var key string + var err error + if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { + utilruntime.HandleError(err) + return + } + c.workqueue.Add(key) +} + +func (c *Controller) processNextWorkItem() bool { + obj, shutdown := c.workqueue.Get() + + if shutdown { + return false + } + + err := func(obj interface{}) error { + defer c.workqueue.Done(obj) + var key string + var ok bool + + if key, ok = obj.(string); !ok { + c.workqueue.Forget(obj) + utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) + return nil + } + if err := c.syncHandler(key); err != nil { + c.workqueue.AddRateLimited(key) + return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error()) + } + c.workqueue.Forget(obj) + klog.V(5).Infof("Successfully synced '%s'", key) + return nil + }(obj) + + if err != nil { + klog.Error(err, "could not reconcile devopsProject") + utilruntime.HandleError(err) + return true + } + + return true +} + +func (c *Controller) worker() { + + for c.processNextWorkItem() { + } +} + +func (c *Controller) Start(stopCh <-chan struct{}) error { + return c.Run(1, stopCh) +} + +func (c *Controller) Run(workers int, stopCh <-chan struct{}) error { + defer utilruntime.HandleCrash() + defer c.workqueue.ShutDown() + + klog.Info("starting pipeline controller") + defer klog.Info("shutting down pipeline controller") + + if !cache.WaitForCacheSync(stopCh, c.secretSynced) { + return fmt.Errorf("failed to wait for caches to sync") + } + + for i := 0; i < workers; i++ { + go wait.Until(c.worker, c.workerLoopPeriod, stopCh) + } + + <-stopCh + return nil +} + +// syncHandler compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the pipeline resource +// with the current status of the resource. +func (c *Controller) syncHandler(key string) error { + nsName, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + klog.Error(err, fmt.Sprintf("could not split copySecret meta %s ", key)) + return nil + } + namespace, err := c.namespaceLister.Get(nsName) + if err != nil { + if errors.IsNotFound(err) { + klog.Info(fmt.Sprintf("namespace '%s' in work queue no longer exists ", key)) + return nil + } + klog.Error(err, fmt.Sprintf("could not get namespace %s ", key)) + return err + } + if !isDevOpsProjectAdminNamespace(namespace) { + err := fmt.Errorf("cound not create credential in normal namespaces %s", namespace.Name) + klog.Warning(err) + return err + } + + secret, err := c.secretLister.Secrets(nsName).Get(name) + if err != nil { + if errors.IsNotFound(err) { + klog.Info(fmt.Sprintf("secret '%s' in work queue no longer exists ", key)) + return nil + } + klog.Error(err, fmt.Sprintf("could not get secret %s ", key)) + return err + } + + copySecret := secret.DeepCopy() + // DeletionTimestamp.IsZero() means copySecret has not been deleted. + if copySecret.ObjectMeta.DeletionTimestamp.IsZero() { + // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers + if !sliceutil.HasString(copySecret.ObjectMeta.Finalizers, devopsv1alpha3.CredentialFinalizerName) { + copySecret.ObjectMeta.Finalizers = append(copySecret.ObjectMeta.Finalizers, devopsv1alpha3.CredentialFinalizerName) + } + // Check secret config exists, otherwise we will create it. + // if secret exists, update config + _, err := c.devopsClient.GetCredentialInProject(nsName, secret.Name) + if err != nil && devopsClient.GetDevOpsStatusCode(err) != http.StatusNotFound { + klog.Error(err, fmt.Sprintf("failed to get secret %s ", key)) + return err + } else if err != nil { + _, err := c.devopsClient.CreateCredentialInProject(nsName, copySecret) + if err != nil { + klog.Error(err, fmt.Sprintf("failed to create secret %s ", key)) + return err + } + } else { + if _, ok := copySecret.Annotations[devopsv1alpha3.CredentialAutoSyncAnnoKey]; ok { + _, err := c.devopsClient.UpdateCredentialInProject(nsName, copySecret) + if err != nil { + klog.Error(err, fmt.Sprintf("failed to update secret %s ", key)) + return err + } + } + } + + } else { + // Finalizers processing logic + if sliceutil.HasString(copySecret.ObjectMeta.Finalizers, devopsv1alpha3.CredentialFinalizerName) { + _, err := c.devopsClient.GetCredentialInProject(nsName, secret.Name) + if err != nil && devopsClient.GetDevOpsStatusCode(err) != http.StatusNotFound { + klog.Error(err, fmt.Sprintf("failed to get secret %s ", key)) + return err + } else if err != nil && devopsClient.GetDevOpsStatusCode(err) == http.StatusNotFound { + } else { + if _, err := c.devopsClient.DeleteCredentialInProject(nsName, secret.Name); err != nil { + klog.Error(err, fmt.Sprintf("failed to delete secret %s in devops", key)) + return err + } + } + copySecret.ObjectMeta.Finalizers = sliceutil.RemoveString(copySecret.ObjectMeta.Finalizers, func(item string) bool { + return item == devopsv1alpha3.CredentialFinalizerName + }) + + } + } + if !reflect.DeepEqual(secret, copySecret) { + _, err = c.client.CoreV1().Secrets(nsName).Update(copySecret) + if err != nil { + klog.Error(err, fmt.Sprintf("failed to update secret %s ", key)) + return err + } + } + + return nil +} + +func isDevOpsProjectAdminNamespace(namespace *v1.Namespace) bool { + _, ok := namespace.Labels[constants.DevOpsProjectLabelKey] + + return ok && k8sutil.IsControlledBy(namespace.OwnerReferences, + devopsv1alpha3.ResourceKindDevOpsProject, "") + +} diff --git a/pkg/controller/devopscredential/devopscredential_controller_test.go b/pkg/controller/devopscredential/devopscredential_controller_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1d00e357f53caf3950d09950a564b204547a7f97 --- /dev/null +++ b/pkg/controller/devopscredential/devopscredential_controller_test.go @@ -0,0 +1,410 @@ +package devopscredential + +import ( + v1 "k8s.io/api/core/v1" + "kubesphere.io/kubesphere/pkg/constants" + fakeDevOps "kubesphere.io/kubesphere/pkg/simple/client/devops/fake" + "reflect" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + kubeinformers "k8s.io/client-go/informers" + k8sfake "k8s.io/client-go/kubernetes/fake" + core "k8s.io/client-go/testing" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + devops "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" +) + +var ( + alwaysReady = func() bool { return true } + noResyncPeriodFunc = func() time.Duration { return 0 } +) + +type fixture struct { + t *testing.T + + kubeclient *k8sfake.Clientset + namespaceLister []*v1.Namespace + secretLister []*v1.Secret + kubeactions []core.Action + + kubeobjects []runtime.Object + // Objects from here preloaded into NewSimpleFake. + objects []runtime.Object + // Objects from here preloaded into devops + initDevOpsProject string + initCredential []*v1.Secret + expectCredential []*v1.Secret +} + +func newFixture(t *testing.T) *fixture { + f := &fixture{} + f.t = t + f.objects = []runtime.Object{} + return f +} + +func newNamespace(name string, projectName string) *v1.Namespace { + ns := &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: v1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{constants.DevOpsProjectLabelKey: projectName}, + }, + } + TRUE := true + ns.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: devops.SchemeGroupVersion.String(), + Kind: devops.ResourceKindDevOpsProject, + Name: projectName, + BlockOwnerDeletion: &TRUE, + Controller: &TRUE, + }, + } + + return ns +} + +func newSecret(namespace, name string, data map[string][]byte, withFinalizers bool, autoSync bool) *v1.Secret { + secret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: devops.ResourceKindPipeline, + APIVersion: devops.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Data: data, + Type: devops.DevOpsCredentialPrefix + "test", + } + if withFinalizers { + secret.Finalizers = append(secret.Finalizers, devops.CredentialFinalizerName) + } + if autoSync{ + if secret.Annotations == nil{ + secret.Annotations = map[string]string{} + } + secret.Annotations[devops.CredentialAutoSyncAnnoKey] = "true" + } + return secret +} + +func newDeletingSecret(namespace, name string) *v1.Secret { + now := metav1.Now() + pipeline := &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: devops.ResourceKindPipeline, + APIVersion: devops.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + DeletionTimestamp: &now, + }, + Type: devops.DevOpsCredentialPrefix + "test", + } + pipeline.Finalizers = append(pipeline.Finalizers, devops.CredentialFinalizerName) + + return pipeline +} + +func (f *fixture) newController() (*Controller, kubeinformers.SharedInformerFactory, *fakeDevOps.Devops) { + f.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...) + + k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc()) + dI := fakeDevOps.NewWithCredentials(f.initDevOpsProject, f.initCredential...) + + c := NewController(f.kubeclient, dI, k8sI.Core().V1().Namespaces(), + k8sI.Core().V1().Secrets()) + + c.secretSynced = alwaysReady + c.eventRecorder = &record.FakeRecorder{} + + for _, f := range f.secretLister { + k8sI.Core().V1().Secrets().Informer().GetIndexer().Add(f) + } + + for _, d := range f.namespaceLister { + k8sI.Core().V1().Namespaces().Informer().GetIndexer().Add(d) + } + + return c, k8sI, dI +} + +func (f *fixture) run(fooName string) { + f.runController(fooName, true, false) +} + +func (f *fixture) runExpectError(fooName string) { + f.runController(fooName, true, true) +} + +func (f *fixture) runController(name string, startInformers bool, expectError bool) { + c, k8sI, dI := f.newController() + if startInformers { + stopCh := make(chan struct{}) + defer close(stopCh) + k8sI.Start(stopCh) + } + + err := c.syncHandler(name) + if !expectError && err != nil { + f.t.Errorf("error syncing foo: %v", err) + } else if expectError && err == nil { + f.t.Error("expected error syncing foo, got nil") + } + + k8sActions := filterInformerActions(f.kubeclient.Actions()) + for i, action := range k8sActions { + if len(f.kubeactions) < i+1 { + f.t.Errorf("%d unexpected actions: %+v", len(k8sActions)-len(f.kubeactions), k8sActions[i:]) + break + } + + expectedAction := f.kubeactions[i] + checkAction(expectedAction, action, f.t) + } + + if len(f.kubeactions) > len(k8sActions) { + f.t.Errorf("%d additional expected actions:%+v", len(f.kubeactions)-len(k8sActions), f.kubeactions[len(k8sActions):]) + } + + if len(dI.Credentials[f.initDevOpsProject]) != len(f.expectCredential) { + f.t.Errorf(" unexpected objects: %v", dI.Projects) + } + for _, credential := range f.expectCredential { + actualCredential := dI.Credentials[f.initDevOpsProject][credential.Name] + if !reflect.DeepEqual(actualCredential, credential) { + f.t.Errorf(" credential %+v not match \n %+v", credential, actualCredential) + } + } +} + +// checkAction verifies that expected and actual actions are equal and both have +// same attached resources +func checkAction(expected, actual core.Action, t *testing.T) { + if !(expected.Matches(actual.GetVerb(), actual.GetResource().Resource) && actual.GetSubresource() == expected.GetSubresource()) { + t.Errorf("Expected\n\t%#v\ngot\n\t%#v", expected, actual) + return + } + + if reflect.TypeOf(actual) != reflect.TypeOf(expected) { + t.Errorf("Action has wrong type. Expected: %t. Got: %t", expected, actual) + return + } + + switch a := actual.(type) { + case core.CreateActionImpl: + e, _ := expected.(core.CreateActionImpl) + expObject := e.GetObject() + object := a.GetObject() + + if !reflect.DeepEqual(expObject, object) { + t.Errorf("Action %s %s has wrong object\nDiff:\n %s", + a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object)) + } + case core.UpdateActionImpl: + e, _ := expected.(core.UpdateActionImpl) + expObject := e.GetObject() + object := a.GetObject() + + if !reflect.DeepEqual(expObject, object) { + t.Errorf("Action %s %s has wrong object\nDiff:\n %s", + a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object)) + } + case core.PatchActionImpl: + e, _ := expected.(core.PatchActionImpl) + expPatch := e.GetPatch() + patch := a.GetPatch() + + if !reflect.DeepEqual(expPatch, patch) { + t.Errorf("Action %s %s has wrong patch\nDiff:\n %s", + a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expPatch, patch)) + } + default: + t.Errorf("Uncaptured Action %s %s, you should explicitly add a case to capture it", + actual.GetVerb(), actual.GetResource().Resource) + } +} + +// filterInformerActions filters list and watch actions for testing resources. +// Since list and watch don't change resource state we can filter it to lower +// nose level in our tests. +func filterInformerActions(actions []core.Action) []core.Action { + ret := []core.Action{} + for _, action := range actions { + if len(action.GetNamespace()) == 0 && + (action.Matches("list", "secrets") || + action.Matches("watch", "secrets") || + action.Matches("list", "namespaces") || + action.Matches("watch", "namespaces")) { + continue + } + ret = append(ret, action) + } + + return ret +} + +func (f *fixture) expectUpdateSecretAction(p *v1.Secret) { + action := core.NewUpdateAction(schema.GroupVersionResource{ + Version: "v1", + Resource: "secrets", + }, p.Namespace, p) + f.kubeactions = append(f.kubeactions, action) +} + +func getKey(p *v1.Secret, t *testing.T) string { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(p) + if err != nil { + t.Errorf("Unexpected error getting key for pipeline %v: %v", p.Name, err) + return "" + } + return key +} + +func TestDoNothing(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + secretName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + secret := newSecret(nsName, secretName, nil, true, true) + + f.secretLister = append(f.secretLister, secret) + f.namespaceLister = append(f.namespaceLister, ns) + f.objects = append(f.objects, secret) + f.initDevOpsProject = nsName + f.initCredential = []*v1.Secret{secret} + f.expectCredential = []*v1.Secret{secret} + + f.run(getKey(secret, t)) +} + +func TestAddCredentialFinalizers(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + secretName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + secret := newSecret(nsName, secretName, nil, false, true) + + expectSecret := newSecret(nsName, secretName, nil, true, true) + + f.secretLister = append(f.secretLister, secret) + f.namespaceLister = append(f.namespaceLister, ns) + f.kubeobjects = append(f.kubeobjects, secret) + f.initDevOpsProject = nsName + f.initCredential = []*v1.Secret{secret} + f.expectCredential = []*v1.Secret{expectSecret} + f.expectUpdateSecretAction(expectSecret) + f.run(getKey(secret, t)) +} + +func TestCreateCredential(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + secretName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + secret := newSecret(nsName, secretName, nil, true, true) + + f.secretLister = append(f.secretLister, secret) + f.namespaceLister = append(f.namespaceLister, ns) + f.kubeobjects = append(f.kubeobjects, secret) + f.initDevOpsProject = nsName + f.expectCredential = []*v1.Secret{secret} + f.run(getKey(secret, t)) +} + +func TestDeleteCredential(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + secretName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + secret := newDeletingSecret(nsName, secretName) + + expectSecret := secret.DeepCopy() + expectSecret.Finalizers = []string{} + f.secretLister = append(f.secretLister, secret) + f.namespaceLister = append(f.namespaceLister, ns) + f.kubeobjects = append(f.kubeobjects, secret) + f.initDevOpsProject = nsName + f.initCredential = []*v1.Secret{secret} + f.expectCredential = []*v1.Secret{} + f.expectUpdateSecretAction(expectSecret) + f.run(getKey(secret, t)) +} + +func TestDeleteNotExistCredential(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + pipelineName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + secret := newDeletingSecret(nsName, pipelineName) + + expectSecret := secret.DeepCopy() + expectSecret.Finalizers = []string{} + f.secretLister = append(f.secretLister, secret) + f.namespaceLister = append(f.namespaceLister, ns) + f.kubeobjects = append(f.kubeobjects, secret) + f.initDevOpsProject = nsName + f.initCredential = []*v1.Secret{} + f.expectCredential = []*v1.Secret{} + f.expectUpdateSecretAction(expectSecret) + f.run(getKey(secret, t)) +} + +func TestUpdateCredential(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + secretName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + initSecret := newSecret(nsName, secretName, nil, true, true) + expectSecret := newSecret(nsName, secretName, map[string][]byte{"a":[]byte("aa")}, true, true) + f.secretLister = append(f.secretLister, expectSecret) + f.namespaceLister = append(f.namespaceLister, ns) + f.kubeobjects = append(f.kubeobjects, expectSecret) + f.initDevOpsProject = nsName + f.initCredential = []*v1.Secret{initSecret} + f.expectCredential = []*v1.Secret{expectSecret} + f.run(getKey(expectSecret, t)) +} + +func TestNotUpdateCredential(t *testing.T) { + f := newFixture(t) + nsName := "test-123" + secretName := "test" + projectName := "test_project" + + ns := newNamespace(nsName, projectName) + initSecret := newSecret(nsName, secretName, nil, true, false) + expectSecret := newSecret(nsName, secretName, map[string][]byte{"a":[]byte("aa")}, true, false) + f.secretLister = append(f.secretLister, expectSecret) + f.namespaceLister = append(f.namespaceLister, ns) + f.kubeobjects = append(f.kubeobjects, expectSecret) + f.initDevOpsProject = nsName + f.initCredential = []*v1.Secret{initSecret} + f.expectCredential = []*v1.Secret{initSecret} + f.run(getKey(expectSecret, t)) +} + diff --git a/pkg/controller/pipeline/pipeline_controller.go b/pkg/controller/pipeline/pipeline_controller.go index 418eb1432d30f7c061bb5e38695852b76e3a01a7..4f6c94bdf1de5303737a5a123c01e12dd39d67f2 100644 --- a/pkg/controller/pipeline/pipeline_controller.go +++ b/pkg/controller/pipeline/pipeline_controller.go @@ -251,7 +251,7 @@ func (c *Controller) syncHandler(key string) error { } } copyPipeline.ObjectMeta.Finalizers = sliceutil.RemoveString(copyPipeline.ObjectMeta.Finalizers, func(item string) bool { - return item == devopsv1alpha3.DevOpsProjectFinalizerName + return item == devopsv1alpha3.PipelineFinalizerName }) } diff --git a/pkg/controller/pipeline/pipeline_controller_test.go b/pkg/controller/pipeline/pipeline_controller_test.go index f0cda3ca033d6576e3772c386e332f3af87c49e2..f02a4a6b6af79d26079fb462dc90f8ae9c0a3c52 100644 --- a/pkg/controller/pipeline/pipeline_controller_test.go +++ b/pkg/controller/pipeline/pipeline_controller_test.go @@ -354,12 +354,15 @@ func TestDeletePipeline(t *testing.T) { ns := newNamespace(nsName, projectName) pipeline := newDeletingPipeline(nsName, pipelineName) + expectPipeline := pipeline.DeepCopy() + expectPipeline.Finalizers = []string{} f.pipelineLister = append(f.pipelineLister, pipeline) f.namespaceLister = append(f.namespaceLister, ns) f.objects = append(f.objects, pipeline) f.initDevOpsProject = nsName f.initPipeline = []*devops.Pipeline{pipeline} f.expectPipeline = []*devops.Pipeline{} + f.expectUpdatePipelineAction(expectPipeline) f.run(getKey(pipeline, t)) } @@ -372,12 +375,15 @@ func TestDeleteNotExistPipeline(t *testing.T) { ns := newNamespace(nsName, projectName) pipeline := newDeletingPipeline(nsName, pipelineName) + expectPipeline := pipeline.DeepCopy() + expectPipeline.Finalizers = []string{} f.pipelineLister = append(f.pipelineLister, pipeline) f.namespaceLister = append(f.namespaceLister, ns) f.objects = append(f.objects, pipeline) f.initDevOpsProject = nsName f.initPipeline = []*devops.Pipeline{} f.expectPipeline = []*devops.Pipeline{} + f.expectUpdatePipelineAction(expectPipeline) f.run(getKey(pipeline, t)) } diff --git a/pkg/kapis/devops/v1alpha2/handler.go b/pkg/kapis/devops/v1alpha2/handler.go index addf7d61e9245a5a4ccb6a3b773ec03af404d991..1f75177d33ed096bfc2afe78601532a78bfad1fb 100644 --- a/pkg/kapis/devops/v1alpha2/handler.go +++ b/pkg/kapis/devops/v1alpha2/handler.go @@ -11,10 +11,9 @@ import ( ) type ProjectPipelineHandler struct { - projectCredentialOperator devops.ProjectCredentialOperator - projectMemberOperator devops.ProjectMemberOperator - devopsOperator devops.DevopsOperator - projectOperator devops.ProjectOperator + projectMemberOperator devops.ProjectMemberOperator + devopsOperator devops.DevopsOperator + projectOperator devops.ProjectOperator } type PipelineSonarHandler struct { @@ -24,10 +23,9 @@ type PipelineSonarHandler struct { func NewProjectPipelineHandler(devopsClient devopsClient.Interface, dbClient *mysql.Database) ProjectPipelineHandler { return ProjectPipelineHandler{ - projectCredentialOperator: devops.NewProjectCredentialOperator(devopsClient, dbClient), - projectMemberOperator: devops.NewProjectMemberOperator(devopsClient, dbClient), - devopsOperator: devops.NewDevopsOperator(devopsClient), - projectOperator: devops.NewProjectOperator(dbClient), + projectMemberOperator: devops.NewProjectMemberOperator(devopsClient, dbClient), + devopsOperator: devops.NewDevopsOperator(devopsClient), + projectOperator: devops.NewProjectOperator(dbClient), } } @@ -39,6 +37,5 @@ func NewPipelineSonarHandler(devopsClient devopsClient.Interface, dbClient *mysq } func NewS2iBinaryHandler(client versioned.Interface, informers externalversions.SharedInformerFactory, s3Client s3.Interface) S2iBinaryHandler { - return S2iBinaryHandler{devops.NewS2iBinaryUploader(client, informers, s3Client)} } diff --git a/pkg/kapis/devops/v1alpha2/project_credential.go b/pkg/kapis/devops/v1alpha2/project_credential.go deleted file mode 100644 index eb452646f2e47144c3ebb9f87200d18ca7c0225e..0000000000000000000000000000000000000000 --- a/pkg/kapis/devops/v1alpha2/project_credential.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2019 The KubeSphere Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha2 - -import ( - "github.com/emicklei/go-restful" - "k8s.io/klog" - "kubesphere.io/kubesphere/pkg/api" - "kubesphere.io/kubesphere/pkg/constants" - "kubesphere.io/kubesphere/pkg/simple/client/devops" -) - -func (h ProjectPipelineHandler) CreateDevOpsProjectCredentialHandler(request *restful.Request, resp *restful.Response) { - - projectId := request.PathParameter("devops") - username := request.HeaderParameter(constants.UserNameHeader) - var credential *devops.Credential - err := request.ReadEntity(&credential) - if err != nil { - klog.Errorf("%+v", err) - api.HandleBadRequest(resp, nil, err) - return - } - credentialId, err := h.projectCredentialOperator.CreateProjectCredential(projectId, username, credential) - - if err != nil { - klog.Errorf("%+v", err) - api.HandleInternalError(resp, nil, err) - return - } - - resp.WriteAsJson(struct { - Name string `json:"name"` - }{Name: credentialId}) - return -} - -func (h ProjectPipelineHandler) UpdateDevOpsProjectCredentialHandler(request *restful.Request, resp *restful.Response) { - - projectId := request.PathParameter("devops") - credentialId := request.PathParameter("credential") - var credential *devops.Credential - err := request.ReadEntity(&credential) - if err != nil { - klog.Errorf("%+v", err) - api.HandleBadRequest(resp, nil, err) - return - } - credentialId, err = h.projectCredentialOperator.UpdateProjectCredential(projectId, credentialId, credential) - - if err != nil { - klog.Errorf("%+v", err) - api.HandleInternalError(resp, nil, err) - return - } - - resp.WriteAsJson(struct { - Name string `json:"name"` - }{Name: credentialId}) - return -} - -func (h ProjectPipelineHandler) DeleteDevOpsProjectCredentialHandler(request *restful.Request, resp *restful.Response) { - - projectId := request.PathParameter("devops") - credentialId := request.PathParameter("credential") - - credentialId, err := h.projectCredentialOperator.DeleteProjectCredential(projectId, credentialId) - - if err != nil { - klog.Errorf("%+v", err) - api.HandleInternalError(resp, nil, err) - return - } - - resp.WriteAsJson(struct { - Name string `json:"name"` - }{Name: credentialId}) - return -} - -func (h ProjectPipelineHandler) GetDevOpsProjectCredentialHandler(request *restful.Request, resp *restful.Response) { - - projectId := request.PathParameter("devops") - credentialId := request.PathParameter("credential") - getContent := request.QueryParameter("content") - response, err := h.projectCredentialOperator.GetProjectCredential(projectId, credentialId, getContent) - - if err != nil { - klog.Errorf("%+v", err) - api.HandleInternalError(resp, nil, err) - return - } - - resp.WriteAsJson(response) - return -} - -func (h ProjectPipelineHandler) GetDevOpsProjectCredentialsHandler(request *restful.Request, resp *restful.Response) { - projectId := request.PathParameter("devops") - - jenkinsCredentials, err := h.projectCredentialOperator.GetProjectCredentials(projectId) - if err != nil { - klog.Errorf("%+v", err) - api.HandleInternalError(resp, nil, err) - return - } - resp.WriteAsJson(jenkinsCredentials) - return -} diff --git a/pkg/kapis/devops/v1alpha2/register.go b/pkg/kapis/devops/v1alpha2/register.go index 88806a656aaf3ab9c0be349237e8ca6798f1e3af..d3f07bf30f5f53726fba22c573d8ca42d94cec06 100644 --- a/pkg/kapis/devops/v1alpha2/register.go +++ b/pkg/kapis/devops/v1alpha2/register.go @@ -156,49 +156,6 @@ func AddToContainer(c *restful.Container, devopsClient devops.Interface, Param(webservice.PathParameter("member", "member's username, e.g. admin")). Writes(devops.ProjectMembership{})) - webservice.Route(webservice.POST("/devops/{devops}/credentials"). - To(projectPipelineHander.CreateDevOpsProjectCredentialHandler). - Doc("Create a credential in the specified DevOps project"). - Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectCredentialTag}). - Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")). - Reads(devops.Credential{})) - - webservice.Route(webservice.PUT("/devops/{devops}/credentials/{credential}"). - To(projectPipelineHander.UpdateDevOpsProjectCredentialHandler). - Doc("Update the specified credential of the DevOps project"). - Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectCredentialTag}). - Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")). - Param(webservice.PathParameter("credential", "credential's ID, e.g. dockerhub-id")). - Reads(devops.Credential{})) - - webservice.Route(webservice.DELETE("/devops/{devops}/credentials/{credential}"). - To(projectPipelineHander.DeleteDevOpsProjectCredentialHandler). - Doc("Delete the specified credential of the DevOps project"). - Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectCredentialTag}). - Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")). - Param(webservice.PathParameter("credential", "credential's ID, e.g. dockerhub-id"))) - - webservice.Route(webservice.GET("/devops/{devops}/credentials/{credential}"). - To(projectPipelineHander.GetDevOpsProjectCredentialHandler). - Doc("Get the specified credential of the DevOps project"). - Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectCredentialTag}). - Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")). - Param(webservice.PathParameter("credential", "credential's ID, e.g. dockerhub-id")). - Param(webservice.QueryParameter("content", ` -Get extra credential content if this query parameter is set. -Specifically, there are three types of info in a credential. One is the basic info that must be returned for each query such as name, id, etc. -The second one is non-encrypted info such as the username of the username-password type of credential, which returns when the "content" parameter is set to non-empty. -The last one is encrypted info, such as the password of the username-password type of credential, which never returns. -`)). - Returns(http.StatusOK, RespOK, devops.Credential{})) - - webservice.Route(webservice.GET("/devops/{devops}/credentials"). - To(projectPipelineHander.GetDevOpsProjectCredentialsHandler). - Doc("Get all credentials of the specified DevOps project"). - Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectCredentialTag}). - Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")). - Returns(http.StatusOK, RespOK, []devops.Credential{})) - // match Jenkisn api "/blue/rest/organizations/jenkins/pipelines/{devops}/{pipeline}" webservice.Route(webservice.GET("/devops/{devops}/pipelines/{pipeline}"). To(projectPipelineHander.GetPipeline). diff --git a/pkg/models/devops/project_credential.go b/pkg/models/devops/project_credential.go deleted file mode 100644 index 9fc22870e509e387e2ab6c21cac80ba25f2bf697..0000000000000000000000000000000000000000 --- a/pkg/models/devops/project_credential.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2019 The KubeSphere Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package devops - -import ( - "github.com/asaskevich/govalidator" - "time" -) - -const ( - ProjectCredentialTableName = "project_credential" - ProjectCredentialIdColumn = "credential_id" - ProjectCredentialDomainColumn = "domain" - ProjectCredentialProjectIdColumn = "project_id" -) - -type ProjectCredential struct { - ProjectId string `json:"project_id"` - CredentialId string `json:"credential_id"` - Domain string `json:"domain"` - Creator string `json:"creator"` - CreateTime time.Time `json:"create_time"` -} - -var ProjectCredentialColumns = GetColumnsFromStruct(&ProjectCredential{}) - -func NewProjectCredential(projectId, credentialId, domain, creator string) *ProjectCredential { - if govalidator.IsNull(domain) { - domain = "_" - } - return &ProjectCredential{ - ProjectId: projectId, - CredentialId: credentialId, - Domain: domain, - Creator: creator, - CreateTime: time.Now(), - } -} diff --git a/pkg/models/devops/project_credential_handler.go b/pkg/models/devops/project_credential_handler.go deleted file mode 100644 index 5640b3fec3a1da7b1fd9c5079185249fb8be9ce6..0000000000000000000000000000000000000000 --- a/pkg/models/devops/project_credential_handler.go +++ /dev/null @@ -1,286 +0,0 @@ -/* -Copyright 2019 The KubeSphere Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package devops - -import ( - "fmt" - "github.com/emicklei/go-restful" - "github.com/gocraft/dbr" - "k8s.io/klog" - "kubesphere.io/kubesphere/pkg/simple/client/devops" - "kubesphere.io/kubesphere/pkg/simple/client/mysql" - - "kubesphere.io/kubesphere/pkg/db" - "net/http" -) - -type ProjectCredentialOperator interface { - CreateProjectCredential(projectId, username string, credentialRequest *devops.Credential) (string, error) - UpdateProjectCredential(projectId, credentialId string, credentialRequest *devops.Credential) (string, error) - DeleteProjectCredential(projectId, credentialId string) (string, error) - GetProjectCredential(projectId, credentialId, getContent string) (*devops.Credential, error) - GetProjectCredentials(projectId string) ([]*devops.Credential, error) -} - -type projectCredentialOperator struct { - devopsClient devops.Interface - db *mysql.Database -} - -func NewProjectCredentialOperator(devopsClient devops.Interface, dbClient *mysql.Database) ProjectCredentialOperator { - return &projectCredentialOperator{devopsClient: devopsClient, db: dbClient} -} - -func (o *projectCredentialOperator) CreateProjectCredential(projectId, username string, credentialRequest *devops.Credential) (string, error) { - switch credentialRequest.Type { - case devops.CredentialTypeUsernamePassword: - if credentialRequest.UsernamePasswordCredential == nil { - err := fmt.Errorf("usename_password should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - case devops.CredentialTypeSsh: - if credentialRequest.SshCredential == nil { - err := fmt.Errorf("ssh should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - case devops.CredentialTypeSecretText: - if credentialRequest.SecretTextCredential == nil { - err := fmt.Errorf("secret_text should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - case devops.CredentialTypeKubeConfig: - if credentialRequest.KubeconfigCredential == nil { - err := fmt.Errorf("kubeconfig should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - default: - err := fmt.Errorf("error unsupport credential type") - klog.Errorf("%+v", err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - - } - credentialId, err := o.devopsClient.CreateCredentialInProject(projectId, credentialRequest) - if err != nil { - klog.Errorf("%+v", err) - return "", err - } - err = o.insertCredentialToDb(projectId, *credentialId, credentialRequest.Domain, username) - if err != nil { - klog.Errorf("%+v", err) - return "", err - } - return *credentialId, nil - -} - -func (o *projectCredentialOperator) UpdateProjectCredential(projectId, credentialId string, credentialRequest *devops.Credential) (string, error) { - - credential, err := o.devopsClient.GetCredentialInProject(projectId, - credentialId, false) - if err != nil { - klog.Errorf("%+v", err) - return "", err - } - switch credential.Type { - case devops.CredentialTypeUsernamePassword: - if credentialRequest.UsernamePasswordCredential == nil { - err := fmt.Errorf("usename_password should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - case devops.CredentialTypeSsh: - if credentialRequest.SshCredential == nil { - err := fmt.Errorf("ssh should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - case devops.CredentialTypeSecretText: - if credentialRequest.SecretTextCredential == nil { - err := fmt.Errorf("secret_text should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - case devops.CredentialTypeKubeConfig: - if credentialRequest.KubeconfigCredential == nil { - err := fmt.Errorf("kubeconfig should not be nil") - klog.Error(err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - default: - err := fmt.Errorf("error unsupport credential type") - klog.Errorf("%+v", err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - credentialRequest.Id = credentialId - _, err = o.devopsClient.UpdateCredentialInProject(projectId, credentialRequest) - if err != nil { - klog.Errorf("%+v", err) - return "", restful.NewError(http.StatusBadRequest, err.Error()) - } - return credentialId, nil - -} - -func (o *projectCredentialOperator) DeleteProjectCredential(projectId, credentialId string) (string, error) { - - _, err := o.devopsClient.GetCredentialInProject(projectId, - credentialId, false) - if err != nil { - klog.Errorf("%+v", err) - return "", err - } - - id, err := o.devopsClient.DeleteCredentialInProject(projectId, credentialId) - if err != nil { - klog.Errorf("%+v", err) - return "", err - } - - deleteConditions := append(make([]dbr.Builder, 0), db.Eq(ProjectCredentialProjectIdColumn, projectId)) - deleteConditions = append(deleteConditions, db.Eq(ProjectCredentialIdColumn, credentialId)) - deleteConditions = append(deleteConditions, db.Eq(ProjectCredentialDomainColumn, "_")) - - _, err = o.db.DeleteFrom(ProjectCredentialTableName). - Where(db.And(deleteConditions...)).Exec() - if err != nil && err != db.ErrNotFound { - klog.Errorf("%+v", err) - return "", err - } - return *id, nil - -} - -func (o *projectCredentialOperator) GetProjectCredential(projectId, credentialId, getContent string) (*devops.Credential, error) { - - content := false - if getContent != "" { - content = true - } - credential, err := o.devopsClient.GetCredentialInProject(projectId, - credentialId, - content) - if err != nil { - klog.Errorf("%+v", err) - return nil, err - } - projectCredential := &ProjectCredential{} - err = o.db.Select(ProjectCredentialColumns...). - From(ProjectCredentialTableName).Where( - db.And(db.Eq(ProjectCredentialProjectIdColumn, projectId), - db.Eq(ProjectCredentialIdColumn, credentialId), - db.Eq(ProjectCredentialDomainColumn, credential.Domain))).LoadOne(projectCredential) - - if err != nil && err != db.ErrNotFound { - klog.Errorf("%+v", err) - return nil, restful.NewError(http.StatusInternalServerError, err.Error()) - } - - response := formatCredentialResponse(credential, projectCredential) - return response, nil - -} - -func (o *projectCredentialOperator) GetProjectCredentials(projectId string) ([]*devops.Credential, error) { - - credentialResponses, err := o.devopsClient.GetCredentialsInProject(projectId) - if err != nil { - klog.Errorf("%+v", err) - return nil, err - } - selectCondition := db.Eq(ProjectCredentialProjectIdColumn, projectId) - projectCredentials := make([]*ProjectCredential, 0) - _, err = o.db.Select(ProjectCredentialColumns...). - From(ProjectCredentialTableName).Where(selectCondition).Load(&projectCredentials) - if err != nil { - klog.Errorf("%+v", err) - return nil, restful.NewError(http.StatusInternalServerError, err.Error()) - } - response := formatCredentialsResponse(credentialResponses, projectCredentials) - return response, nil -} - -func (o *projectCredentialOperator) insertCredentialToDb(projectId, credentialId, domain, username string) error { - - projectCredential := NewProjectCredential(projectId, credentialId, domain, username) - _, err := o.db.InsertInto(ProjectCredentialTableName).Columns(ProjectCredentialColumns...). - Record(projectCredential).Exec() - if err != nil { - klog.Errorf("%+v", err) - return restful.NewError(http.StatusInternalServerError, err.Error()) - } - return nil -} - -func formatCredentialResponse( - credentialResponse *devops.Credential, - dbCredentialResponse *ProjectCredential) *devops.Credential { - response := &devops.Credential{} - response.Id = credentialResponse.Id - response.Description = credentialResponse.Description - response.DisplayName = credentialResponse.DisplayName - if credentialResponse.Fingerprint != nil && credentialResponse.Fingerprint.Hash != "" { - response.Fingerprint = &struct { - FileName string `json:"file_name,omitempty" description:"Credential's display name and description"` - Hash string `json:"hash,omitempty" description:"Credential's hash"` - Usage []*struct { - Name string `json:"name,omitempty" description:"pipeline full name"` - Ranges struct { - Ranges []*struct { - Start int `json:"start,omitempty" description:"Start build number"` - End int `json:"end,omitempty" description:"End build number"` - } `json:"ranges,omitempty"` - } `json:"ranges,omitempty" description:"The build number of all pipelines that use this credential"` - } `json:"usage,omitempty" description:"all usage of Credential"` - }{} - response.Fingerprint.FileName = credentialResponse.Fingerprint.FileName - response.Fingerprint.Hash = credentialResponse.Fingerprint.Hash - for _, usage := range credentialResponse.Fingerprint.Usage { - response.Fingerprint.Usage = append(response.Fingerprint.Usage, usage) - } - } - response.Domain = credentialResponse.Domain - - if dbCredentialResponse != nil { - response.CreateTime = &dbCredentialResponse.CreateTime - response.Creator = dbCredentialResponse.Creator - } - - credentialType, ok := devops.CredentialTypeMap[credentialResponse.Type] - if ok { - response.Type = credentialType - return response - } - response.Type = credentialResponse.Type - return response -} - -func formatCredentialsResponse(credentialsResponse []*devops.Credential, - projectCredentials []*ProjectCredential) []*devops.Credential { - responseSlice := make([]*devops.Credential, 0) - for _, credential := range credentialsResponse { - var dbCredential *ProjectCredential = nil - for _, projectCredential := range projectCredentials { - if projectCredential.CredentialId == credential.Id && - projectCredential.Domain == credential.Domain { - dbCredential = projectCredential - } - } - responseSlice = append(responseSlice, formatCredentialResponse(credential, dbCredential)) - } - return responseSlice -} diff --git a/pkg/simple/client/devops/credential.go b/pkg/simple/client/devops/credential.go index 6a932b70083b611eb943a5964875a5b26847e4cd..bbb5b06904f18dc8ed05fbe09b4b34f70c4e5684 100644 --- a/pkg/simple/client/devops/credential.go +++ b/pkg/simple/client/devops/credential.go @@ -1,6 +1,7 @@ package devops import ( + v1 "k8s.io/api/core/v1" "time" ) @@ -50,28 +51,14 @@ type KubeconfigCredential struct { Content string `json:"content,omitempty" description:"content of kubeconfig"` } -const ( - CredentialTypeUsernamePassword = "username_password" - CredentialTypeSsh = "ssh" - CredentialTypeSecretText = "secret_text" - CredentialTypeKubeConfig = "kubeconfig" -) - -var CredentialTypeMap = map[string]string{ - "SSH Username with private key": CredentialTypeSsh, - "Username with password": CredentialTypeUsernamePassword, - "Secret text": CredentialTypeSecretText, - "Kubernetes configuration (kubeconfig)": CredentialTypeKubeConfig, -} - type CredentialOperator interface { - CreateCredentialInProject(projectId string, credential *Credential) (*string, error) + CreateCredentialInProject(projectId string, credential *v1.Secret) (string, error) - UpdateCredentialInProject(projectId string, credential *Credential) (*string, error) + UpdateCredentialInProject(projectId string, credential *v1.Secret) (string, error) - GetCredentialInProject(projectId, id string, content bool) (*Credential, error) + GetCredentialInProject(projectId, id string) (*Credential, error) GetCredentialsInProject(projectId string) ([]*Credential, error) - DeleteCredentialInProject(projectId, id string) (*string, error) + DeleteCredentialInProject(projectId, id string) (string, error) } diff --git a/pkg/simple/client/devops/fake/fakedevops.go b/pkg/simple/client/devops/fake/fakedevops.go index 5ce79a29755c30ae4ad8358b90259280f1e5d434..8b7d0fe9714597831ff73183e4b57f1b42bc7acb 100644 --- a/pkg/simple/client/devops/fake/fakedevops.go +++ b/pkg/simple/client/devops/fake/fakedevops.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/emicklei/go-restful" "io/ioutil" + v1 "k8s.io/api/core/v1" devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" "kubesphere.io/kubesphere/pkg/simple/client/devops" "net/http" @@ -17,6 +18,8 @@ type Devops struct { Projects map[string]interface{} Pipelines map[string]map[string]*devopsv1alpha3.Pipeline + + Credentials map[string]map[string]*v1.Secret } func New(projects ...string) *Devops { @@ -45,12 +48,28 @@ func NewWithPipelines(project string, pipelines ...*devopsv1alpha3.Pipeline) *De return d } +func NewWithCredentials(project string, credentials ...*v1.Secret) *Devops { + d := &Devops{ + Data: nil, + Projects: map[string]interface{}{}, + Credentials: map[string]map[string]*v1.Secret{}, + } + + d.Projects[project] = true + d.Credentials[project] = map[string]*v1.Secret{} + for _, f := range credentials { + d.Credentials[project][f.Name] = f + } + return d +} + func (d *Devops) CreateDevOpsProject(projectId string) (string, error) { if _, ok := d.Projects[projectId]; ok { return projectId, nil } d.Projects[projectId] = true d.Pipelines[projectId] = map[string]*devopsv1alpha3.Pipeline{} + d.Credentials[projectId] = map[string]*v1.Secret{} return projectId, nil } @@ -58,6 +77,7 @@ func (d *Devops) DeleteDevOpsProject(projectId string) error { if _, ok := d.Projects[projectId]; ok { delete(d.Projects, projectId) delete(d.Pipelines, projectId) + delete(d.Credentials, projectId) return nil } else { return &devops.ErrorResponse{ @@ -264,19 +284,128 @@ func (d *Devops) ToJson(httpParameters *devops.HttpParameters) (*devops.ResJson, } // CredentialOperator -func (d *Devops) CreateCredentialInProject(projectId string, credential *devops.Credential) (*string, error) { - return nil, nil +func (d *Devops) CreateCredentialInProject(projectId string, credential *v1.Secret) (string, error) { + if _, ok := d.Credentials[projectId][credential.Name]; ok { + err := fmt.Errorf("credential name [%s] has been used", credential.Name) + return "", restful.NewError(http.StatusConflict, err.Error()) + } + d.Credentials[projectId][credential.Name] = credential + return credential.Name, nil } -func (d *Devops) UpdateCredentialInProject(projectId string, credential *devops.Credential) (*string, error) { - return nil, nil +func (d *Devops) UpdateCredentialInProject(projectId string, credential *v1.Secret) (string, error) { + if _, ok := d.Credentials[projectId][credential.Name]; !ok { + err := &devops.ErrorResponse{ + Body: []byte{}, + Response: &http.Response{ + Status: "404 Not Found", + StatusCode: 404, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + ContentLength: 50, + Header: http.Header{ + "Foo": []string{"Bar"}, + }, + Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used + Request: &http.Request{ + Method: "", + URL: &url.URL{ + Scheme: "", + Opaque: "", + User: nil, + Host: "", + Path: "", + RawPath: "", + ForceQuery: false, + RawQuery: "", + Fragment: "", + }, + }, + }, + Message: "", + } + return "", err + } + d.Credentials[projectId][credential.Name] = credential + return credential.Name, nil } -func (d *Devops) GetCredentialInProject(projectId, id string, content bool) (*devops.Credential, error) { - return nil, nil + +func (d *Devops) GetCredentialInProject(projectId, id string) (*devops.Credential, error) { + if _, ok := d.Credentials[projectId][id]; !ok { + err := &devops.ErrorResponse{ + Body: []byte{}, + Response: &http.Response{ + Status: "404 Not Found", + StatusCode: 404, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + ContentLength: 50, + Header: http.Header{ + "Foo": []string{"Bar"}, + }, + Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used + Request: &http.Request{ + Method: "", + URL: &url.URL{ + Scheme: "", + Opaque: "", + User: nil, + Host: "", + Path: "", + RawPath: "", + ForceQuery: false, + RawQuery: "", + Fragment: "", + }, + }, + }, + Message: "", + } + return nil, err + } + return &devops.Credential{Id: id}, nil } func (d *Devops) GetCredentialsInProject(projectId string) ([]*devops.Credential, error) { return nil, nil } -func (d *Devops) DeleteCredentialInProject(projectId, id string) (*string, error) { return nil, nil } +func (d *Devops) DeleteCredentialInProject(projectId, id string) (string, error) { + if _, ok := d.Credentials[projectId][id]; !ok { + err := &devops.ErrorResponse{ + Body: []byte{}, + Response: &http.Response{ + Status: "404 Not Found", + StatusCode: 404, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + ContentLength: 50, + Header: http.Header{ + "Foo": []string{"Bar"}, + }, + Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used + Request: &http.Request{ + Method: "", + URL: &url.URL{ + Scheme: "", + Opaque: "", + User: nil, + Host: "", + Path: "", + RawPath: "", + ForceQuery: false, + RawQuery: "", + Fragment: "", + }, + }, + }, + Message: "", + } + return "", err + } + delete(d.Credentials[projectId], id) + return "", nil +} // BuildGetter func (d *Devops) GetProjectPipelineBuildByType(projectId, pipelineId string, status string) (*devops.Build, error) { @@ -306,6 +435,7 @@ func (d *Devops) CreateProjectPipeline(projectId string, pipeline *devopsv1alpha d.Pipelines[projectId][pipeline.Name] = pipeline return "", nil } + func (d *Devops) DeleteProjectPipeline(projectId string, pipelineId string) (string, error) { if _, ok := d.Pipelines[projectId][pipelineId]; !ok { err := &devops.ErrorResponse{ @@ -343,6 +473,7 @@ func (d *Devops) DeleteProjectPipeline(projectId string, pipelineId string) (str delete(d.Pipelines[projectId], pipelineId) return "", nil } + func (d *Devops) UpdateProjectPipeline(projectId string, pipeline *devopsv1alpha3.Pipeline) (string, error) { if _, ok := d.Pipelines[projectId][pipeline.Name]; !ok { err := &devops.ErrorResponse{ @@ -380,6 +511,7 @@ func (d *Devops) UpdateProjectPipeline(projectId string, pipeline *devopsv1alpha d.Pipelines[projectId][pipeline.Name] = pipeline return "", nil } + func (d *Devops) GetProjectPipelineConfig(projectId, pipelineId string) (*devopsv1alpha3.Pipeline, error) { if _, ok := d.Pipelines[projectId][pipelineId]; !ok { err := &devops.ErrorResponse{ diff --git a/pkg/simple/client/devops/jenkins/credential.go b/pkg/simple/client/devops/jenkins/credential.go index e5dff570b0c3480e5926b1545e8fc7b93ad8cd8e..b67c657747d91b5e8743ad9010a8d3696e42afe6 100644 --- a/pkg/simple/client/devops/jenkins/credential.go +++ b/pkg/simple/client/devops/jenkins/credential.go @@ -16,13 +16,13 @@ package jenkins import ( "errors" "fmt" - "github.com/PuerkitoBio/goquery" "github.com/emicklei/go-restful" + v1 "k8s.io/api/core/v1" "k8s.io/klog" + devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" "kubesphere.io/kubesphere/pkg/simple/client/devops" "net/http" "strconv" - "strings" ) const SSHCrenditalStaplerClass = "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey" @@ -99,10 +99,15 @@ type CredentialResponse struct { Domain string `json:"domain"` } -func NewSshCredential(id, username, passphrase, privateKey, description string) *SshCredential { +func NewSshCredential(secret *v1.Secret) *SshCredential { + id := secret.Name + username := string(secret.Data[devopsv1alpha3.SSHAuthUsernameKey]) + passphrase := string(secret.Data[devopsv1alpha3.SSHAuthPassphraseKey]) + privatekey := string(secret.Data[devopsv1alpha3.SSHAuthPrivateKey]) + keySource := PrivateKeySource{ StaplerClass: DirectSSHCrenditalStaplerClass, - PrivateKey: privateKey, + PrivateKey: privatekey, } return &SshCredential{ @@ -111,48 +116,52 @@ func NewSshCredential(id, username, passphrase, privateKey, description string) Username: username, Passphrase: passphrase, KeySource: keySource, - Description: description, StaplerClass: SSHCrenditalStaplerClass, } } -func NewUsernamePasswordCredential(id, username, password, description string) *UsernamePasswordCredential { +func NewUsernamePasswordCredential(secret *v1.Secret) *UsernamePasswordCredential { + id := secret.Name + username := string(secret.Data[devopsv1alpha3.BasicAuthUsernameKey]) + password := string(secret.Data[devopsv1alpha3.BasicAuthPasswordKey]) return &UsernamePasswordCredential{ Scope: GLOBALScope, Id: id, Username: username, Password: password, - Description: description, StaplerClass: UsernamePassswordCredentialStaplerClass, } } -func NewSecretTextCredential(id, secret, description string) *SecretTextCredential { +func NewSecretTextCredential(secret *v1.Secret) *SecretTextCredential { + id := secret.Name + secretContent := string(secret.Data[devopsv1alpha3.SecretTextSecretKey]) return &SecretTextCredential{ Scope: GLOBALScope, Id: id, - Secret: secret, - Description: description, + Secret: secretContent, StaplerClass: SecretTextCredentialStaplerClass, } } -func NewKubeconfigCredential(id, content, description string) *KubeconfigCredential { +func NewKubeconfigCredential(secret *v1.Secret) *KubeconfigCredential { + id := secret.Name + secretContent := string(secret.Data[devopsv1alpha3.KubeConfigSecretKey]) + credentialSource := KubeconfigSource{ StaplerClass: DirectKubeconfigCredentialStaperClass, - Content: content, + Content: secretContent, } return &KubeconfigCredential{ Scope: GLOBALScope, Id: id, - Description: description, KubeconfigSource: credentialSource, StaplerClass: KubeconfigCredentialStaplerClass, } } -func (j *Jenkins) GetCredentialInProject(projectId, id string, content bool) (*devops.Credential, error) { +func (j *Jenkins) GetCredentialInProject(projectId, id string) (*devops.Credential, error) { responseStruct := &devops.Credential{} domain := "_" @@ -169,54 +178,6 @@ func (j *Jenkins) GetCredentialInProject(projectId, id string, content bool) (*d return nil, errors.New(strconv.Itoa(response.StatusCode)) } responseStruct.Domain = domain - if content { - - } - contentString := "" - response, err = j.Requester.GetHtml( - fmt.Sprintf("/job/%s/credentials/store/folder/domain/%s/credential/%s/update", projectId, domain, id), - &contentString, nil) - if err != nil { - return nil, err - } - if response.StatusCode != http.StatusOK { - return nil, errors.New(strconv.Itoa(response.StatusCode)) - } - stringReader := strings.NewReader(contentString) - doc, err := goquery.NewDocumentFromReader(stringReader) - if err != nil { - klog.Errorf("%+v", err) - return nil, restful.NewError(http.StatusInternalServerError, err.Error()) - } - switch responseStruct.Type { - case devops.CredentialTypeKubeConfig: - content := &devops.KubeconfigCredential{} - doc.Find("textarea[name*=content]").Each(func(i int, selection *goquery.Selection) { - value := selection.Text() - content.Content = value - }) - responseStruct.KubeconfigCredential = content - case devops.CredentialTypeUsernamePassword: - content := &devops.UsernamePasswordCredential{} - doc.Find("input[name*=username]").Each(func(i int, selection *goquery.Selection) { - value, _ := selection.Attr("value") - content.Username = value - }) - - responseStruct.UsernamePasswordCredential = content - case devops.CredentialTypeSsh: - content := &devops.SshCredential{} - doc.Find("input[name*=username]").Each(func(i int, selection *goquery.Selection) { - value, _ := selection.Attr("value") - content.Username = value - }) - - doc.Find("textarea[name*=privateKey]").Each(func(i int, selection *goquery.Selection) { - value := selection.Text() - content.PrivateKey = value - }) - responseStruct.SshCredential = content - } return responseStruct, nil } @@ -243,30 +204,23 @@ func (j *Jenkins) GetCredentialsInProject(projectId string) ([]*devops.Credentia } -func (j *Jenkins) CreateCredentialInProject(projectId string, credential *devops.Credential) (*string, error) { +func (j *Jenkins) CreateCredentialInProject(projectId string, credential *v1.Secret) (string, error) { var request interface{} responseString := "" switch credential.Type { - case devops.CredentialTypeUsernamePassword: - request = NewUsernamePasswordCredential(credential.Id, - credential.UsernamePasswordCredential.Username, credential.UsernamePasswordCredential.Password, - credential.Description) - - case devops.CredentialTypeSsh: - request = NewSshCredential(credential.Id, - credential.SshCredential.Username, credential.SshCredential.Passphrase, - credential.SshCredential.PrivateKey, credential.Description) - case devops.CredentialTypeSecretText: - request = NewSecretTextCredential(credential.Id, - credential.SecretTextCredential.Secret, credential.Description) - case devops.CredentialTypeKubeConfig: - request = NewKubeconfigCredential(credential.Id, - credential.KubeconfigCredential.Content, credential.Description) + case devopsv1alpha3.SecretTypeBasicAuth: + request = NewUsernamePasswordCredential(credential) + case devopsv1alpha3.SecretTypeSSHAuth: + request = NewSshCredential(credential) + case devopsv1alpha3.SecretTypeSecretText: + request = NewSecretTextCredential(credential) + case devopsv1alpha3.SecretTypeKubeConfig: + request = NewKubeconfigCredential(credential) default: err := fmt.Errorf("error unsupport credential type") klog.Errorf("%+v", err) - return nil, restful.NewError(http.StatusBadRequest, err.Error()) + return "", restful.NewError(http.StatusBadRequest, err.Error()) } response, err := j.Requester.Post( @@ -277,65 +231,58 @@ func (j *Jenkins) CreateCredentialInProject(projectId string, credential *devops }), }) if err != nil { - return nil, err + return "", err } if response.StatusCode != http.StatusOK { - return nil, errors.New(strconv.Itoa(response.StatusCode)) + return "", errors.New(strconv.Itoa(response.StatusCode)) } - return &credential.Id, nil + return credential.Name, nil } -func (j *Jenkins) UpdateCredentialInProject(projectId string, credential *devops.Credential) (*string, error) { +func (j *Jenkins) UpdateCredentialInProject(projectId string, credential *v1.Secret) (string, error) { requestContent := "" switch credential.Type { - case devops.CredentialTypeUsernamePassword: - requestStruct := NewUsernamePasswordCredential(credential.Id, - credential.UsernamePasswordCredential.Username, credential.UsernamePasswordCredential.Password, - credential.Description) + case devopsv1alpha3.SecretTypeBasicAuth: + requestStruct := NewUsernamePasswordCredential(credential) requestContent = makeJson(requestStruct) - - case devops.CredentialTypeSsh: - requestStruct := NewSshCredential(credential.Id, - credential.SshCredential.Username, credential.SshCredential.Passphrase, - credential.SshCredential.PrivateKey, credential.Description) + case devopsv1alpha3.SecretTypeSSHAuth: + requestStruct := NewSshCredential(credential) requestContent = makeJson(requestStruct) - case devops.CredentialTypeSecretText: - requestStruct := NewSecretTextCredential(credential.Id, - credential.SecretTextCredential.Secret, credential.Description) + case devopsv1alpha3.SecretTypeSecretText: + requestStruct := NewSecretTextCredential(credential) requestContent = makeJson(requestStruct) - case devops.CredentialTypeKubeConfig: - requestStruct := NewKubeconfigCredential(credential.Id, - credential.KubeconfigCredential.Content, credential.Description) + case devopsv1alpha3.SecretTypeKubeConfig: + requestStruct := NewKubeconfigCredential(credential) requestContent = makeJson(requestStruct) default: err := fmt.Errorf("error unsupport credential type") klog.Errorf("%+v", err) - return nil, restful.NewError(http.StatusBadRequest, err.Error()) + return "", restful.NewError(http.StatusBadRequest, err.Error()) } response, err := j.Requester.Post( - fmt.Sprintf("/job/%s/credentials/store/folder/domain/_/credential/%s/updateSubmit", projectId, credential.Id), + fmt.Sprintf("/job/%s/credentials/store/folder/domain/_/credential/%s/updateSubmit", projectId, credential.Name), nil, nil, map[string]string{ "json": requestContent, }) if err != nil { - return nil, err + return "", err } if response.StatusCode != http.StatusOK { - return nil, errors.New(strconv.Itoa(response.StatusCode)) + return "", errors.New(strconv.Itoa(response.StatusCode)) } - return &credential.Id, nil + return credential.Name, nil } -func (j *Jenkins) DeleteCredentialInProject(projectId, id string) (*string, error) { +func (j *Jenkins) DeleteCredentialInProject(projectId, id string) (string, error) { response, err := j.Requester.Post( fmt.Sprintf("/job/%s/credentials/store/folder/domain/_/credential/%s/doDelete", projectId, id), nil, nil, nil) if err != nil { - return nil, err + return "", err } if response.StatusCode != http.StatusOK { - return nil, errors.New(strconv.Itoa(response.StatusCode)) + return "", errors.New(strconv.Itoa(response.StatusCode)) } - return &id, nil + return id, nil }