diff --git a/controller/deployments.go b/controller/deployments.go index 28616cf5af..0f0122d901 100644 --- a/controller/deployments.go +++ b/controller/deployments.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "io" - "net/url" - "os" "time" "github.com/fabric8-services/fabric8-wit/app" @@ -17,10 +15,10 @@ import ( "github.com/goadesign/goa" errs "github.com/pkg/errors" - uuid "github.com/satori/go.uuid" + "github.com/satori/go.uuid" "golang.org/x/net/websocket" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/tools/cache" ) @@ -31,17 +29,6 @@ type DeploymentsController struct { ClientGetter } -// ClientGetter creates an instances of clients used by this controller -type ClientGetter interface { - GetKubeClient(ctx context.Context) (kubernetes.KubeClientInterface, error) - GetAndCheckOSIOClient(ctx context.Context) (OpenshiftIOClient, error) -} - -// Default implementation of KubeClientGetter and OSIOClientGetter used by NewDeploymentsController -type defaultClientGetter struct { - config *configuration.Registry -} - // NewDeploymentsController creates a deployments controller. func NewDeploymentsController(service *goa.Service, config *configuration.Registry) *DeploymentsController { return &DeploymentsController{ @@ -61,41 +48,6 @@ func tostring(item interface{}) string { return string(bytes) } -func (g *defaultClientGetter) GetAndCheckOSIOClient(ctx context.Context) (OpenshiftIOClient, error) { - - // defaults - host := "localhost" - scheme := "https" - - req := goa.ContextRequest(ctx) - if req != nil { - // Note - it's probably more efficient to force a loopback host, and only use the port number here - // (on some systems using a non-loopback interface forces a network stack traverse) - host = req.Host - scheme = req.URL.Scheme - } - - // The deployments API communicates with the rest of WIT via the stnadard WIT API. - // This environment variable is used for local development of the deployments API, to point ot a remote WIT. - witURLStr := os.Getenv("FABRIC8_WIT_API_URL") - if witURLStr != "" { - witurl, err := url.Parse(witURLStr) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "FABRIC8_WIT_API_URL": witURLStr, - "err": err, - }, "cannot parse FABRIC8_WIT_API_URL: %s", witURLStr) - return nil, errs.Wrapf(err, "cannot parse FABRIC8_WIT_API_URL: %s", witURLStr) - } - host = witurl.Host - scheme = witurl.Scheme - } - - oc := NewOSIOClient(ctx, scheme, host) - - return oc, nil -} - // getSpaceNameFromSpaceID() converts an OSIO Space UUID to an OpenShift space name. // will return an error if the space is not found. func (c *DeploymentsController) getSpaceNameFromSpaceID(ctx context.Context, spaceID uuid.UUID) (*string, error) { @@ -116,74 +68,6 @@ func (c *DeploymentsController) getSpaceNameFromSpaceID(ctx context.Context, spa return osioSpace.Attributes.Name, nil } -func (g *defaultClientGetter) getNamespaceName(ctx context.Context) (*string, error) { - - osioclient, err := g.GetAndCheckOSIOClient(ctx) - if err != nil { - return nil, err - } - - kubeSpaceAttr, err := osioclient.GetNamespaceByType(ctx, nil, "user") - if err != nil { - return nil, errs.Wrap(err, "unable to retrieve 'user' namespace") - } - if kubeSpaceAttr == nil { - return nil, errors.NewNotFoundError("namespace", "user") - } - - return kubeSpaceAttr.Name, nil -} - -// GetKubeClient creates a kube client for the appropriate cluster assigned to the current user -func (g *defaultClientGetter) GetKubeClient(ctx context.Context) (kubernetes.KubeClientInterface, error) { - - kubeNamespaceName, err := g.getNamespaceName(ctx) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "could not retrieve namespace name") - return nil, errs.Wrap(err, "could not retrieve namespace name") - } - - osioclient, err := g.GetAndCheckOSIOClient(ctx) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "could not create OSIO client") - return nil, err - } - - baseURLProvider, err := NewURLProvider(ctx, g.config, osioclient) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "could not retrieve tenant data") - return nil, errs.Wrap(err, "could not retrieve tenant data") - } - - /* Timeout used per HTTP request to Kubernetes/OpenShift API servers. - * Communication with Hawkular currently uses a hard-coded 30 second - * timeout per request, and does not use this parameter. */ - // create the cluster API client - kubeConfig := &kubernetes.KubeClientConfig{ - BaseURLProvider: baseURLProvider, - UserNamespace: *kubeNamespaceName, - Timeout: g.config.GetDeploymentsHTTPTimeoutSeconds(), - } - kc, err := kubernetes.NewKubeClient(kubeConfig) - if err != nil { - url, _ := baseURLProvider.GetAPIURL() - log.Error(ctx, map[string]interface{}{ - "err": err, - "user_namespace": *kubeNamespaceName, - "cluster": *url, - }, "could not create Kubernetes client object") - return nil, errs.Wrap(err, "could not create Kubernetes client object") - } - return kc, nil -} - -// SetDeployment runs the setDeployment action. func (c *DeploymentsController) SetDeployment(ctx *app.SetDeploymentDeploymentsContext) error { // we double check podcount here, because in the future we might have different query parameters diff --git a/controller/deployments_blackbox_test.go b/controller/deployments_blackbox_test.go index 5a7be32b49..8de97619ec 100644 --- a/controller/deployments_blackbox_test.go +++ b/controller/deployments_blackbox_test.go @@ -53,6 +53,11 @@ type testOSIOClient struct { controller.OpenshiftIOClient } +type testOSClient struct { + fixture *deploymentsTestFixture + kubernetes.OpenShiftRESTAPI +} + func (kc *testKubeClient) Close() { kc.closed = true } @@ -97,6 +102,12 @@ func (fixture *deploymentsTestFixture) GetAndCheckOSIOClient(ctx context.Context }, nil } +func (fixture *deploymentsTestFixture) GetOSClient(ctx context.Context) (kubernetes.OpenShiftRESTAPI, error) { + return &testOSClient{ + fixture: fixture, + }, nil +} + func (c *testOSIOClient) GetSpaceByID(ctx context.Context, spaceID uuid.UUID) (*app.Space, error) { var spaceName *string uuidString := spaceID.String() diff --git a/controller/pipelines.go b/controller/pipelines.go new file mode 100644 index 0000000000..dcc837491f --- /dev/null +++ b/controller/pipelines.go @@ -0,0 +1,92 @@ +package controller + +import ( + "fmt" + "github.com/fabric8-services/fabric8-wit/app" + "github.com/fabric8-services/fabric8-wit/configuration" + "github.com/fabric8-services/fabric8-wit/errors" + "github.com/fabric8-services/fabric8-wit/jsonapi" + "github.com/fabric8-services/fabric8-wit/log" + "github.com/goadesign/goa" + errs "github.com/pkg/errors" + "github.com/satori/go.uuid" +) + +// pipeline implements the pipeline resource. +type PipelinesController struct { + *goa.Controller + Config *configuration.Registry + ClientGetter +} + +func NewPipelineController(service *goa.Service, config *configuration.Registry) *PipelinesController { + return &PipelinesController{ + Controller: service.NewController("PipelinesController"), + Config: config, + ClientGetter: &defaultClientGetter{ + config: config, + }, + } +} + +// Delete a pipelines from given space +func (c *PipelinesController) Delete(ctx *app.DeletePipelinesContext) error { + + osioClient, err := c.GetAndCheckOSIOClient(ctx) + if err != nil { + return jsonapi.JSONErrorResponse(ctx, err) + } + + k8sSpace, err := osioClient.GetNamespaceByType(ctx, nil, "user") + if err != nil { + return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, "unable to retrieve 'user' namespace")) + } + if k8sSpace == nil { + return jsonapi.JSONErrorResponse(ctx, errors.NewNotFoundError("namespace", "user")) + } + + osc, err := c.GetOSClient(ctx) + if err != nil { + return jsonapi.JSONErrorResponse(ctx, err) + } + + userNS := *k8sSpace.Name + spacename, err := c.getSpaceNameFromSpaceID(ctx.SpaceID) + if err != nil { + return jsonapi.JSONErrorResponse(ctx, err) + } + + resp, err := osc.DeleteBuildConfig(userNS, map[string]string{"space": *spacename}) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + "space_name": *spacename, + }, "error occurred while deleting pipeline") + return jsonapi.JSONErrorResponse(ctx, err) + } + + log.Info(ctx, map[string]interface{}{"response": resp}, "deleted pipelines :") + + return ctx.OK([]byte{}) +} + +func (c *PipelinesController) getSpaceNameFromSpaceID(spaceID uuid.UUID) (*string, error) { + // use WIT API to convert Space UUID to Space name + osioclient, err := c.GetAndCheckOSIOClient(c.Context) + if err != nil { + return nil, err + } + + osioSpace, err := osioclient.GetSpaceByID(c.Context, spaceID) + fmt.Printf("spcae %#v", osioSpace) + + if err != nil { + return nil, errs.Wrapf(err, "unable to convert space UUID %s to space name", spaceID) + } + + if osioSpace == nil || osioSpace.Attributes == nil || osioSpace.Attributes.Name == nil { + return nil, errs.Errorf("space UUID %s is not valid a space", spaceID) + } + + return osioSpace.Attributes.Name, nil +} diff --git a/controller/platform_clients.go b/controller/platform_clients.go new file mode 100644 index 0000000000..a679884c89 --- /dev/null +++ b/controller/platform_clients.go @@ -0,0 +1,174 @@ +package controller + +import ( + "context" + "github.com/fabric8-services/fabric8-wit/configuration" + "github.com/fabric8-services/fabric8-wit/errors" + "github.com/fabric8-services/fabric8-wit/kubernetes" + "github.com/fabric8-services/fabric8-wit/log" + "github.com/goadesign/goa" + errs "github.com/pkg/errors" + "net/url" + "os" +) + +// ClientGetter creates an instances of clients used by this controller +type ClientGetter interface { + GetKubeClient(ctx context.Context) (kubernetes.KubeClientInterface, error) + GetAndCheckOSIOClient(ctx context.Context) (OpenshiftIOClient, error) + GetOSClient(ctx context.Context) (kubernetes.OpenShiftRESTAPI, error) +} + +// Default implementation of OSClientGetter and OSIOClientGetter used by NewPipelineController +type defaultClientGetter struct { + config *configuration.Registry +} + +func (g *defaultClientGetter) GetAndCheckOSIOClient(ctx context.Context) (OpenshiftIOClient, error) { + + // defaults + host := "localhost" + scheme := "https" + + req := goa.ContextRequest(ctx) + if req != nil { + // Note - it's probably more efficient to force a loopback host, and only use the port number here + // (on some systems using a non-loopback interface forces a network stack traverse) + host = req.Host + scheme = req.URL.Scheme + } + + // The deployments API communicates with the rest of WIT via the stnadard WIT API. + // This environment variable is used for local development of the deployments API, to point ot a remote WIT. + witURLStr := os.Getenv("FABRIC8_WIT_API_URL") + if witURLStr != "" { + witurl, err := url.Parse(witURLStr) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "Sada": witURLStr, + "err": err, + }, "cannot parse FABRIC8_WIT_API_URL: %s", witURLStr) + return nil, errs.Wrapf(err, "cannot parse FABRIC8_WIT_API_URL: %s", witURLStr) + } + host = witurl.Host + scheme = witurl.Scheme + } + + oc := NewOSIOClient(ctx, scheme, host) + + return oc, nil +} + +func (g *defaultClientGetter) getNamespaceName(ctx context.Context) (*string, error) { + + osioClient, err := g.GetAndCheckOSIOClient(ctx) + if err != nil { + return nil, err + } + + kubeSpaceAttr, err := osioClient.GetNamespaceByType(ctx, nil, "user") + if err != nil { + return nil, errs.Wrap(err, "unable to retrieve 'user' namespace") + } + if kubeSpaceAttr == nil { + return nil, errors.NewNotFoundError("namespace", "user") + } + + return kubeSpaceAttr.Name, nil +} + +// GetKubeClient creates a kube client for the appropriate cluster assigned to the current user +func (g *defaultClientGetter) GetKubeClient(ctx context.Context) (kubernetes.KubeClientInterface, error) { + + k8sNSName, err := g.getNamespaceName(ctx) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + }, "could not retrieve namespace name") + return nil, errs.Wrap(err, "could not retrieve namespace name") + } + + osioclient, err := g.GetAndCheckOSIOClient(ctx) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + }, "could not create OSIO client") + return nil, err + } + + baseURLProvider, err := NewURLProvider(ctx, g.config, osioclient) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + }, "could not retrieve tenant data") + return nil, errs.Wrap(err, "could not retrieve tenant data") + } + + kubeConfig := getK8sConfig(baseURLProvider, k8sNSName, g) + kc, err := kubernetes.NewKubeClient(kubeConfig) + if err != nil { + url, _ := baseURLProvider.GetAPIURL() + log.Error(ctx, map[string]interface{}{ + "err": err, + "user_namespace": *k8sNSName, + "cluster": *url, + }, "could not create Kubernetes client object") + return nil, errs.Wrap(err, "could not create Kubernetes client object") + } + + return kc, nil +} + +// GetOSClient creates a OpenShift client for the appropriate cluster assigned to the current user +func (g *defaultClientGetter) GetOSClient(ctx context.Context) (kubernetes.OpenShiftRESTAPI, error) { + + k8sNSName, err := g.getNamespaceName(ctx) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + }, "could not retrieve namespace name") + return nil, errs.Wrap(err, "could not retrieve namespace name") + } + + osioClient, err := g.GetAndCheckOSIOClient(ctx) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + }, "could not create OSIO client") + return nil, err + } + + baseURLProvider, err := NewURLProvider(ctx, g.config, osioClient) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + }, "could not retrieve tenant data") + return nil, errs.Wrap(err, "could not retrieve tenant data") + } + + kubeConfig := getK8sConfig(baseURLProvider, k8sNSName, g) + oc, err := kubernetes.NewOSClient(kubeConfig) + if err != nil { + url, _ := baseURLProvider.GetAPIURL() + log.Error(ctx, map[string]interface{}{ + "err": err, + "user_namespace": *k8sNSName, + "cluster": *url, + }, "could not create openshift client object") + return nil, errs.Wrap(err, "could not create Kubernetes client object") + } + + return oc, nil +} + +func getK8sConfig(baseURLProvider kubernetes.BaseURLProvider, k8sNSName *string, g *defaultClientGetter) *kubernetes.KubeClientConfig { + /* Timeout used per HTTP request to Kubernetes/OpenShift API servers. + * Communication with Hawkular currently uses a hard-coded 30 second + * timeout per request, and does not use this parameter. */ + kubeConfig := &kubernetes.KubeClientConfig{ + BaseURLProvider: baseURLProvider, + UserNamespace: *k8sNSName, + Timeout: g.config.GetDeploymentsHTTPTimeoutSeconds(), + } + return kubeConfig +} diff --git a/controller/space.go b/controller/space.go index f373eaae25..133536f7c4 100644 --- a/controller/space.go +++ b/controller/space.go @@ -435,6 +435,7 @@ func deleteOpenShiftResource( } } } + if len(errorsList) != 0 { var errString string for _, err = range errorsList { @@ -442,6 +443,15 @@ func deleteOpenShiftResource( } return errors.NewInternalErrorFromString(errString) } + + //delete pipelines from the space + log.Debug(ctx, map[string]interface{}{"label": spaceID}, "deleting pipelines in") + resp, err = cl.DeletePipelines(ctx, client.DeletePipelinesPath(spaceID)) + + if err != nil { + log.Error(ctx, nil, fmt.Sprintf("error occurred while deleting pipelines from space : %v", spaceID)) + } + return nil } diff --git a/controller/space_blackbox_test.go b/controller/space_blackbox_test.go index 121525a5e3..2e181ce609 100644 --- a/controller/space_blackbox_test.go +++ b/controller/space_blackbox_test.go @@ -410,6 +410,7 @@ func (s *SpaceControllerTestSuite) TestDeleteSpace() { "http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace1/deployments/run": {}, "http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace2/deployments/stage": {}, "http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace2/deployments/run": {}, + "http://core/api/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/pipelines": {}, } rDeployments, err := recorder.New("../test/data/deployments/deployments_delete_space.ok") @@ -481,6 +482,7 @@ func (s *SpaceControllerTestSuite) TestDeleteSpace() { "http://core/api/deployments/spaces/4d19e0fb-b558-4160-8768-f41cb8169e95/applications/testspace1/deployments/run": {}, "http://core/api/deployments/spaces/4d19e0fb-b558-4160-8768-f41cb8169e95/applications/testspace2/deployments/stage": {}, "http://core/api/deployments/spaces/4d19e0fb-b558-4160-8768-f41cb8169e95/applications/testspace2/deployments/run": {}, + "http://core/api/spaces/4d19e0fb-b558-4160-8768-f41cb8169e95/pipelines": {}, } rDeployments, err := recorder.New("../test/data/deployments/deployments_delete_space.ok-skip-cluster-false") diff --git a/controller/space_test.go b/controller/space_test.go index 7d9ad21cee..356c4946d7 100644 --- a/controller/space_test.go +++ b/controller/space_test.go @@ -16,25 +16,49 @@ import ( func TestDeleteOpenShiftResource(t *testing.T) { t.Run("ok", func(t *testing.T) { - // given - r, err := recorder.New("../test/data/deployments/deployments_delete_space.ok") - require.NoError(t, err) - defer r.Stop() - - spaceID, err := goauuid.FromString("aec5f659-0680-4633-8599-5f14f1deeabc") - require.NoError(t, err) - ctx := context.Background() - config, err := configuration.New("") - require.NoError(t, err) - - client := &http.Client{ - Transport: r.Transport, - } - - // when - err = deleteOpenShiftResource(client, config, ctx, spaceID) - // then - require.NoError(t, err) + t.Run("with delete pipeline success", func(t *testing.T) { + // given + r, err := recorder.New("../test/data/deployments/deployments_delete_space.ok") + require.NoError(t, err) + defer r.Stop() + + spaceID, err := goauuid.FromString("aec5f659-0680-4633-8599-5f14f1deeabc") + require.NoError(t, err) + ctx := context.Background() + config, err := configuration.New("") + require.NoError(t, err) + + client := &http.Client{ + Transport: r.Transport, + } + + // when + err = deleteOpenShiftResource(client, config, ctx, spaceID) + // then + require.NoError(t, err) + }) + + t.Run("with delete pipeline failure", func(t *testing.T) { + // given + r, err := recorder.New("../test/data/deployments/deployments_delete_space.ok.no_pipeline") + require.NoError(t, err) + defer r.Stop() + + spaceID, err := goauuid.FromString("aec5f659-0680-4633-8599-5f14f1deeabc") + require.NoError(t, err) + ctx := context.Background() + config, err := configuration.New("") + require.NoError(t, err) + + client := &http.Client{ + Transport: r.Transport, + } + + // when + err = deleteOpenShiftResource(client, config, ctx, spaceID) + // then + require.NoError(t, err) + }) }) t.Run("failure", func(t *testing.T) { diff --git a/design/pipelines.go b/design/pipelines.go new file mode 100644 index 0000000000..524563dba7 --- /dev/null +++ b/design/pipelines.go @@ -0,0 +1,26 @@ +package design + +import ( + d "github.com/goadesign/goa/design" + a "github.com/goadesign/goa/design/apidsl" +) + +var _ = a.Resource("pipelines", func() { + a.Parent("space") + a.BasePath("/pipelines") + + // An auth token is required to call the auth API to get an OpenShift auth token. + a.Security("jwt") + + a.Action("delete", func() { + a.Routing( + a.DELETE(""), + ) + a.Description("Delete pipelines under given space") + a.Response(d.OK) + a.Response(d.Unauthorized, JSONAPIErrors) + a.Response(d.InternalServerError, JSONAPIErrors) + a.Response(d.NotFound, JSONAPIErrors) + a.Response(d.BadRequest, JSONAPIErrors) + }) +}) diff --git a/kubernetes/deployments_kubeclient.go b/kubernetes/deployments_kubeclient.go index 4bff8638c4..060f0334d8 100644 --- a/kubernetes/deployments_kubeclient.go +++ b/kubernetes/deployments_kubeclient.go @@ -11,22 +11,21 @@ import ( "strings" "time" - yaml "gopkg.in/yaml.v2" + "github.com/fabric8-services/fabric8-wit/app" + "github.com/fabric8-services/fabric8-wit/errors" + "github.com/fabric8-services/fabric8-wit/log" + errs "github.com/pkg/errors" + "gopkg.in/yaml.v2" kubeErrors "k8s.io/apimachinery/pkg/api/errors" - resource "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/api/resource" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - types "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - v1 "k8s.io/client-go/pkg/api/v1" - rest "k8s.io/client-go/rest" + "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" - - "github.com/fabric8-services/fabric8-wit/app" - "github.com/fabric8-services/fabric8-wit/errors" - "github.com/fabric8-services/fabric8-wit/log" - errs "github.com/pkg/errors" ) // KubeClientConfig holds configuration data needed to create a new KubeClientInterface @@ -86,6 +85,11 @@ type KubeClientInterface interface { KubeAccessControl } +// KubeRESTAPI collects methods that call out to the Kubernetes API server over the network +type KubeRESTAPI interface { + corev1.CoreV1Interface +} + type kubeClient struct { config *KubeClientConfig envMap map[string]string @@ -97,11 +101,6 @@ type kubeClient struct { MetricsGetter } -// KubeRESTAPI collects methods that call out to the Kubernetes API server over the network -type KubeRESTAPI interface { - corev1.CoreV1Interface -} - type kubeAPIClient struct { corev1.CoreV1Interface restConfig *rest.Config @@ -110,6 +109,7 @@ type kubeAPIClient struct { // OpenShiftRESTAPI collects methods that call out to the OpenShift API server over the network type OpenShiftRESTAPI interface { GetBuildConfigs(namespace string, labelSelector string) (map[string]interface{}, error) + DeleteBuildConfig(namespace string, lables map[string]string) (map[string]interface{}, error) GetBuilds(namespace string, labelSelector string) (map[string]interface{}, error) GetDeploymentConfig(namespace string, name string) (map[string]interface{}, error) DeleteDeploymentConfig(namespace string, name string, opts *metaV1.DeleteOptions) (map[string]interface{}, error) @@ -206,6 +206,19 @@ func NewKubeClient(config *KubeClientConfig) (KubeClientInterface, error) { return kubeClient, nil } +func NewOSClient(config *KubeClientConfig) (OpenShiftRESTAPI, error) { + // Use default implementation if no OpenShiftGetter is specified + if config.OpenShiftRESTAPIGetter == nil { + config.OpenShiftRESTAPIGetter = &defaultGetter{} + } + osAPI, err := config.GetOpenShiftRESTAPI(config) + if err != nil { + return nil, err + } + + return osAPI, nil +} + func (*defaultGetter) GetKubeRESTAPI(config *KubeClientConfig) (KubeRESTAPI, error) { url, err := config.GetAPIURL() if err != nil { @@ -773,6 +786,86 @@ func (oc *openShiftAPIClient) GetBuildConfigs(namespace string, labelSelector st return oc.getResource(bcURL, false) } +func (oc *openShiftAPIClient) DeleteBuildConfig(namespace string, labels map[string]string) (map[string]interface{}, error) { + + if namespace == "" { + namespace = oc.config.UserNamespace + } + + var params []string + for k, v := range labels { + params = append(params, k+"="+v) + } + + // The API server rejects deleting buildconfigs by label, so get all + // buildconfigs with the label, and delete one-by-one + bcList, err := oc.GetBuildConfigs(namespace, url.QueryEscape(strings.Join(params[:], ","))) + if err != nil { + return nil, err + } + + kind, ok := bcList["kind"].(string) + if !ok || (kind != "BuildConfigList" && kind != "List") { + return nil, errs.New("no buildconfig list returned from endpoint") + } + + bcs, ok := bcList["items"].([]interface{}) + if !ok { + return nil, errs.New("no list of buildconfig in response") + } + + response := make(map[string]interface{}) + + for _, bc := range bcs { + name, err := getName(bc) + if err != nil { + return nil, err + } + + opts := getDeleteOption() + resourceURI := fmt.Sprintf("/oapi/v1/namespaces/%s/buildconfigs/%s", namespace, name) + resp, err := oc.sendResource(resourceURI, "DELETE", opts) + if err != nil { + return nil, err + } + + response[name] = resp["status"].(interface{}) + } + + return response, nil +} + +func getDeleteOption() *metaV1.DeleteOptions { + policy := metaV1.DeletePropagationForeground + opts := &metaV1.DeleteOptions{ + TypeMeta: metaV1.TypeMeta{ // Normally set automatically by k8s client-go + Kind: "DeleteOptions", + APIVersion: "v1", + }, + PropagationPolicy: &policy, + } + return opts +} + +func getName(r interface{}) (string, error) { + b, ok := r.(map[string]interface{}) + if !ok { + return "", errs.New("BuildConfig is not an object") + } + + metadata, ok := b["metadata"].(map[string]interface{}) + if !ok { + return "", errs.New("BuildConfig has no metadata") + } + + name, ok := metadata["name"].(string) + if !ok { + return "", errs.New("BuildConfig name is missing") + } + + return name, nil +} + // getDeployableEnvironmentNamespace finds a namespace with the corresponding environment name. // Differs from getEnvironmentNamespace in that the environment must be one where the user can deploy // applications diff --git a/kubernetes/deployments_kubeclient_blackbox_test.go b/kubernetes/deployments_kubeclient_blackbox_test.go index 8194a18a94..46554f3677 100644 --- a/kubernetes/deployments_kubeclient_blackbox_test.go +++ b/kubernetes/deployments_kubeclient_blackbox_test.go @@ -70,8 +70,21 @@ func getDefaultKubeClient(fixture *testFixture, transport http.RoundTripper, t * return kc } -// Metrics fakes +func getDefaultOpenShiftClient(fixture *testFixture, transport http.RoundTripper, t *testing.T) kubernetes.OpenShiftRESTAPI { + + config := &kubernetes.KubeClientConfig{ + BaseURLProvider: getDefaultURLProvider("http://api.myCluster", "myToken"), + UserNamespace: "myNamespace", + Transport: transport, + } + + oc, err := kubernetes.NewOSClient(config) + + require.NoError(t, err) + return oc +} +// Metrics fakes type metricsHolder struct { pods []*v1.Pod namespace string @@ -2095,6 +2108,109 @@ func TestWatchEventsInNamespace(t *testing.T) { } } +func TestDeleteBuildConfigs(t *testing.T) { + // DeleteOptions do not change + policy := metav1.DeletePropagationForeground + expectOpts := metav1.DeleteOptions{ + TypeMeta: metav1.TypeMeta{ + Kind: "DeleteOptions", + APIVersion: "v1", + }, + PropagationPolicy: &policy, + } + + testCases := []struct { + testName string + labels map[string]string + namespace string + cassetteName string + expectDeleteURLs map[string]struct{} + shouldFail bool + errorChecker func(error) (bool, error) + }{ + { + testName: "Valid BuildConfigs", + labels: map[string]string{"space": "multiconfig"}, + namespace: "userns", + cassetteName: "deletebuildconfig-valid", + expectDeleteURLs: map[string]struct{}{ + "http://api.myCluster/oapi/v1/namespaces/userns/buildconfigs/vertex": {}, + }, + }, + { + testName: "Empty BuildConfig List", + labels: map[string]string{"space": "emptybc"}, + namespace: "userns", + cassetteName: "deletebuildconfig-valid", + shouldFail: false, + }, + { + testName: "No BuildConfig Resource", + labels: map[string]string{"space": "nobc"}, + namespace: "userns", + cassetteName: "deletebuildconfig-invalid", + shouldFail: true, + errorChecker: func(err error) (bool, error) { + return strings.Contains(err.Error(), "status code 404"), nil + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + r, err := recorder.New(pathToTestJSON + testCase.cassetteName) + require.NoError(t, err, "Failed to open cassette") + r.SetMatcher(func(actual *http.Request, expected cassette.Request) bool { + if cassette.DefaultMatcher(actual, expected) { + // Check request body when sending DELETE + if actual.Method == "DELETE" { + var buf bytes.Buffer + reqBody := actual.Body + _, err := buf.ReadFrom(reqBody) + require.NoError(t, err, "Error reading request body") + defer reqBody.Close() + + // Mark interaction as seen + reqURL := actual.URL.String() + _, pres := testCase.expectDeleteURLs[reqURL] + require.True(t, pres, "Unexpected DELETE request %s", reqURL) + delete(testCase.expectDeleteURLs, reqURL) + + // Check delete options are correct + var deleteOutput metav1.DeleteOptions + err = json.Unmarshal(buf.Bytes(), &deleteOutput) + require.NoError(t, err, "Request body must be DeleteOptions") + require.Equal(t, expectOpts, deleteOutput, "DeleteOptions do not match") + + // Replace body + actual.Body = ioutil.NopCloser(&buf) + } + return true + } + return false + }) + defer r.Stop() + + fixture := &testFixture{} + oc := getDefaultOpenShiftClient(fixture, r.Transport, t) + + _, err = oc.DeleteBuildConfig(testCase.namespace, testCase.labels) + if testCase.shouldFail { + require.Error(t, err, "Expected an error") + if testCase.errorChecker != nil { + matches, _ := testCase.errorChecker(err) + require.True(t, matches, "Error or cause must be the expected type") + } + } else { + require.NoError(t, err, "Unexpected error occurred") + } + + // Check we saw all expected DELETE requests + require.Empty(t, testCase.expectDeleteURLs, "Not all DELETE requests sent: %v", testCase.expectDeleteURLs) + }) + } +} + func checkEventItems(t *testing.T, eventList *v1.EventList, store *cache.FIFO) { for _, item := range eventList.Items { _, exists, _ := store.Get(item) diff --git a/main.go b/main.go index b56ca38bcb..99cabea254 100755 --- a/main.go +++ b/main.go @@ -314,6 +314,10 @@ func main() { deploymentsCtrl := controller.NewDeploymentsController(service, config) app.MountDeploymentsController(service, deploymentsCtrl) + // Mount "pipelines" controller + pipelineCtrl := controller.NewPipelineController(service, config) + app.MountPipelinesController(service, pipelineCtrl) + // Mount "search" controller searchCtrl := controller.NewSearchController(service, appDB, config) app.MountSearchController(service, searchCtrl) diff --git a/test/data/deployments/deployments_delete_space.ok-skip-cluster-false.yaml b/test/data/deployments/deployments_delete_space.ok-skip-cluster-false.yaml index c16f4fdd12..d7bebac99a 100644 --- a/test/data/deployments/deployments_delete_space.ok-skip-cluster-false.yaml +++ b/test/data/deployments/deployments_delete_space.ok-skip-cluster-false.yaml @@ -164,3 +164,9 @@ interactions: # headers: status: 200 OK code: 200 +- request: + url: http://core/api/spaces/4d19e0fb-b558-4160-8768-f41cb8169e95/pipelines + method: DELETE + response: + status: 200OK + code: 200 \ No newline at end of file diff --git a/test/data/deployments/deployments_delete_space.ok.no_pipeline.yaml b/test/data/deployments/deployments_delete_space.ok.no_pipeline.yaml new file mode 100644 index 0000000000..3d657b12da --- /dev/null +++ b/test/data/deployments/deployments_delete_space.ok.no_pipeline.yaml @@ -0,0 +1,166 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + url: http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc + method: GET + response: + body: '{ + "data": { + "attributes": { + "applications": [ + { + "attributes": { + "deployments": [ + { + "attributes": { + "name": "stage", + "pod_total": 1, + "pods": [ + [ + "Running", + "1" + ] + ], + "pods_quota": { + "cpucores": 1, + "memory": 536870912 + }, + "version": "1.0.1" + }, + "id": "stage", + "links": { + "application": "", + "console": "", + "logs": "" + }, + "type": "deployment" + }, + { + "attributes": { + "name": "run", + "pod_total": 1, + "pods": [ + [ + "Running", + "1" + ] + ], + "pods_quota": { + "cpucores": 1, + "memory": 536870912 + }, + "version": "1.0.1" + }, + "id": "run", + "links": { + "application": "", + "console": "", + "logs": "" + }, + "type": "deployment" + } + ], + "name": "testspace1" + }, + "id": "testspace1", + "type": "application" + }, + { + "attributes": { + "deployments": [ + { + "attributes": { + "name": "stage", + "pod_total": 1, + "pods": [ + [ + "Running", + "1" + ] + ], + "pods_quota": { + "cpucores": 1, + "memory": 536870912 + }, + "version": "1.0.1" + }, + "id": "stage", + "links": { + "application": "", + "console": "", + "logs": "" + }, + "type": "deployment" + }, + { + "attributes": { + "name": "run", + "pod_total": 1, + "pods": [ + [ + "Running", + "1" + ] + ], + "pods_quota": { + "cpucores": 1, + "memory": 536870912 + }, + "version": "1.0.1" + }, + "id": "run", + "links": { + "application": "", + "console": "", + "logs": "" + }, + "type": "deployment" + } + ], + "name": "testspace2" + }, + "id": "testspace2", + "type": "application" + } + ], + "name": "testspace" + }, + "id": "aec5f659-0680-4633-8599-5f14f1deeabc", + "type": "space" + } +}' + # headers: + + status: 200 OK + code: 200 +- request: + url: http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace1/deployments/stage + method: DELETE + response: + # headers: + status: 200 OK + code: 200 +- request: + url: http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace1/deployments/run + method: DELETE + response: + # headers: + status: 200 OK + code: 200 +- request: + url: http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace2/deployments/stage + method: DELETE + response: + # headers: + status: 200 OK + code: 200 +- request: + url: http://core/api/deployments/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/applications/testspace2/deployments/run + method: DELETE + response: + # headers: + status: 200 OK + code: 200 diff --git a/test/data/deployments/deployments_delete_space.ok.yaml b/test/data/deployments/deployments_delete_space.ok.yaml index 3d657b12da..249aa9177a 100644 --- a/test/data/deployments/deployments_delete_space.ok.yaml +++ b/test/data/deployments/deployments_delete_space.ok.yaml @@ -164,3 +164,10 @@ interactions: # headers: status: 200 OK code: 200 +- request: + url: http://core/api/spaces/aec5f659-0680-4633-8599-5f14f1deeabc/pipelines + method: DELETE + response: + # headers: + status: 200 OK + code: 200 \ No newline at end of file diff --git a/test/kubernetes/deletebuildconfig-invalid.yaml b/test/kubernetes/deletebuildconfig-invalid.yaml new file mode 100644 index 0000000000..7d0944e369 --- /dev/null +++ b/test/kubernetes/deletebuildconfig-invalid.yaml @@ -0,0 +1,19 @@ +--- +version: 1 +interactions: + # BuildConfigs +- request: + body: "" + form: {} + headers: + Content-Type: + - application/json + url: http://api.myCluster/oapi/v1/namespaces/userns/buildconfigs?labelSelector=space%3Dnobc + method: GET + response: + headers: + Content-Type: + - application/json;charset=UTF-8 + status: 404 Not Found + code: 404 + diff --git a/test/kubernetes/deletebuildconfig-valid.yaml b/test/kubernetes/deletebuildconfig-valid.yaml new file mode 100644 index 0000000000..1b5e8aa9f4 --- /dev/null +++ b/test/kubernetes/deletebuildconfig-valid.yaml @@ -0,0 +1,211 @@ +--- +version: 1 +interactions: + # BuildsConfigs +- request: + body: "" + form: {} + headers: + Content-Type: + - application/json + url: http://api.myCluster/oapi/v1/namespaces/userns/buildconfigs?labelSelector=space%3Dmulticonfig + method: GET + response: + body: | + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "BuildConfig", + "metadata": { + "annotations": { + "che.fabric8.io/stack": "vert.x", + "jenkins.openshift.org/disable-sync-create-on": "jenkins", + "jenkins.openshift.org/generated-by": "jenkins", + "jenkins.openshift.org/job-path": "hrishin/vertexinggg/master" + }, + "creationTimestamp": "2018-11-14T10:40:13Z", + "labels": { + "openshift.io/gitRepository": "vertexinggg", + "space": "multiconfig" + }, + "name": "vertex", + "namespace": "userns", + "resourceVersion": "1331455082", + "selfLink": "/oapi/v1/namespaces/userns/buildconfigs/vertex", + "uid": "aad905b6-e7f9-11e8-ad49-02e52a0be43d" + }, + "spec": { + "failedBuildsHistoryLimit": 5, + "nodeSelector": {}, + "output": {}, + "postCommit": {}, + "resources": {}, + "runPolicy": "Serial", + "source": { + "git": { + "ref": "master", + "uri": "https://github.com/hrishin/vertexinggg.git" + }, + "type": "Git" + }, + "strategy": { + "jenkinsPipelineStrategy": { + "env": [ + { + "name": "BASE_URI", + "value": "https://jenkins.openshift.io" + }, + { + "name": "FABRIC_SPACE", + "value": "multiconfig" + } + ], + "jenkinsfilePath": "Jenkinsfile" + }, + "type": "JenkinsPipeline" + }, + "successfulBuildsHistoryLimit": 5, + "triggers": [ + { + "github": { + "secret": "secret101" + }, + "type": "GitHub" + }, + { + "generic": { + "secret": "secret101" + }, + "type": "Generic" + } + ] + }, + "status": { + "lastVersion": 2 + } + } + ], + "kind": "BuildConfigList", + "metadata": {}, + "resourceVersion": "", + "selfLink": "" + } + + headers: + Content-Type: + - application/json;charset=UTF-8 + status: 200 OK + code: 200 + +- request: + body: "" + form: {} + headers: + Content-Type: + - application/json + url: http://api.myCluster/oapi/v1/namespaces/userns/buildconfigs/vertex + method: DELETE + response: + body: | + { + "apiVersion": "v1", + "kind": "BuildConfig", + "metadata": { + "annotations": { + "che.fabric8.io/stack": "vert.x", + "jenkins.openshift.org/disable-sync-create-on": "jenkins", + "jenkins.openshift.org/generated-by": "jenkins", + "jenkins.openshift.org/job-path": "hrishin/vertexinggg/master" + }, + "creationTimestamp": "2018-11-14T10:40:13Z", + "labels": { + "openshift.io/gitRepository": "vertexinggg", + "space": "multiconfig" + }, + "name": "vertex", + "namespace": "hshinde", + "resourceVersion": "1331455082", + "selfLink": "/oapi/v1/namespaces/hshinde/buildconfigs/vertex", + "uid": "aad905b6-e7f9-11e8-ad49-02e52a0be43d" + }, + "spec": { + "failedBuildsHistoryLimit": 5, + "nodeSelector": {}, + "output": {}, + "postCommit": {}, + "resources": {}, + "runPolicy": "Serial", + "source": { + "git": { + "ref": "master", + "uri": "https://github.com/hrishin/vertexinggg.git" + }, + "type": "Git" + }, + "strategy": { + "jenkinsPipelineStrategy": { + "env": [ + { + "name": "BASE_URI", + "value": "https://jenkins.openshift.io" + }, + { + "name": "FABRIC_SPACE", + "value": "multiconfig" + } + ], + "jenkinsfilePath": "Jenkinsfile" + }, + "type": "JenkinsPipeline" + }, + "successfulBuildsHistoryLimit": 5, + "triggers": [ + { + "github": { + "secret": "secret101" + }, + "type": "GitHub" + }, + { + "generic": { + "secret": "secret101" + }, + "type": "Generic" + } + ] + }, + "status": { + "lastVersion": 2 + } + } + headers: + Content-Type: + - application/json;charset=UTF-8 + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Content-Type: + - application/json + url: http://api.myCluster/oapi/v1/namespaces/userns/buildconfigs?labelSelector=space%3Demptybc + method: GET + response: + body: | + { + "apiVersion": "v1", + "items": [], + "kind": "BuildConfigList", + "metadata": {}, + "resourceVersion": "", + "selfLink": "" + } + + headers: + Content-Type: + - application/json;charset=UTF-8 + status: 200 OK + code: 200