diff options
author | Ritu Sood <Ritu.Sood@intel.com> | 2020-02-24 23:39:28 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2020-02-24 23:39:28 +0000 |
commit | bc6c2db66326040062fca588ea5a23e963b3b784 (patch) | |
tree | f9863b97405fd0a95e2a6b72b7418bb7763c0fad /src/orchestrator | |
parent | 219a7eab4129b3e500bcaf6c62819011580895ba (diff) | |
parent | 95e9ddd082cb2ccd28d9d33ea2f8607bd5c793f5 (diff) |
Merge "Add Controller Register API to Orchestrator"
Diffstat (limited to 'src/orchestrator')
-rw-r--r-- | src/orchestrator/api/api.go | 13 | ||||
-rw-r--r-- | src/orchestrator/api/controllerhandler.go | 104 | ||||
-rw-r--r-- | src/orchestrator/api/controllerhandler_test.go | 233 | ||||
-rw-r--r-- | src/orchestrator/api/projecthandler_test.go | 6 | ||||
-rw-r--r-- | src/orchestrator/cmd/main.go | 8 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/controller.go | 135 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/controller_test.go | 181 | ||||
-rw-r--r-- | src/orchestrator/pkg/module/module.go | 2 |
8 files changed, 674 insertions, 8 deletions
diff --git a/src/orchestrator/api/api.go b/src/orchestrator/api/api.go index 1cb4299e..f0342433 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -22,7 +22,8 @@ import ( var moduleClient *moduleLib.Client // NewRouter creates a router that registers the various urls that are supported -func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient moduleLib.CompositeAppManager) *mux.Router { + +func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient moduleLib.CompositeAppManager, ControllerClient moduleLib.ControllerManager) *mux.Router { router := mux.NewRouter().PathPrefix("/v2").Subrouter() moduleClient = moduleLib.NewClient() @@ -32,6 +33,12 @@ func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient module projHandler := projectHandler{ client: projectClient, } + if ControllerClient == nil { + ControllerClient = moduleClient.Controller + } + controlHandler := controllerHandler{ + client: ControllerClient, + } router.HandleFunc("/projects", projHandler.createHandler).Methods("POST") router.HandleFunc("/projects/{project-name}", projHandler.getHandler).Methods("GET") router.HandleFunc("/projects/{project-name}", projHandler.deleteHandler).Methods("DELETE") @@ -47,5 +54,9 @@ func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient module router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", compAppHandler.getHandler).Methods("GET") router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", compAppHandler.deleteHandler).Methods("DELETE") + router.HandleFunc("/controllers", controlHandler.createHandler).Methods("POST") + router.HandleFunc("/controllers", controlHandler.createHandler).Methods("PUT") + router.HandleFunc("/controllers/{controller-name}", controlHandler.getHandler).Methods("GET") + router.HandleFunc("/controllers/{controller-name}", controlHandler.deleteHandler).Methods("DELETE") return router } diff --git a/src/orchestrator/api/controllerhandler.go b/src/orchestrator/api/controllerhandler.go new file mode 100644 index 00000000..4f98c023 --- /dev/null +++ b/src/orchestrator/api/controllerhandler.go @@ -0,0 +1,104 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * 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 api + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/gorilla/mux" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" +) + +// Used to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type controllerHandler struct { + // Interface that implements controller operations + // We will set this variable with a mock interface for testing + client moduleLib.ControllerManager +} + +// Create handles creation of the controller entry in the database +func (h controllerHandler) createHandler(w http.ResponseWriter, r *http.Request) { + var m moduleLib.Controller + + err := json.NewDecoder(r.Body).Decode(&m) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if m.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateController(m) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular controller Name +// Returns a controller +func (h controllerHandler) getHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["controller-name"] + + ret, err := h.client.GetController(name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular controller Name +func (h controllerHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["controller-name"] + + err := h.client.DeleteController(name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/controllerhandler_test.go b/src/orchestrator/api/controllerhandler_test.go new file mode 100644 index 00000000..f0804107 --- /dev/null +++ b/src/orchestrator/api/controllerhandler_test.go @@ -0,0 +1,233 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * 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 api + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + pkgerrors "github.com/pkg/errors" +) + +//Creating an embedded interface via anonymous variable +//This allows us to make mockDB satisfy the DatabaseConnection +//interface even if we are not implementing all the methods in it +type mockControllerManager struct { + // Items and err will be used to customize each test + // via a localized instantiation of mockControllerManager + Items []moduleLib.Controller + Err error +} + +func (m *mockControllerManager) CreateController(inp moduleLib.Controller) (moduleLib.Controller, error) { + if m.Err != nil { + return moduleLib.Controller{}, m.Err + } + + return m.Items[0], nil +} + +func (m *mockControllerManager) GetController(name string) (moduleLib.Controller, error) { + if m.Err != nil { + return moduleLib.Controller{}, m.Err + } + + return m.Items[0], nil +} + +func (m *mockControllerManager) DeleteController(name string) error { + return m.Err +} + +func TestControllerCreateHandler(t *testing.T) { + testCases := []struct { + label string + reader io.Reader + expected moduleLib.Controller + expectedCode int + controllerClient *mockControllerManager + }{ + { + label: "Missing Body Failure", + expectedCode: http.StatusBadRequest, + controllerClient: &mockControllerManager{}, + }, + { + label: "Create Controller", + expectedCode: http.StatusCreated, + reader: bytes.NewBuffer([]byte(`{ + "name":"testController", + "ip-address":"10.188.234.1", + "port":8080 + }`)), + expected: moduleLib.Controller{ + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + controllerClient: &mockControllerManager{ + //Items that will be returned by the mocked Client + Items: []moduleLib.Controller{ + { + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + }, + }, + }, + { + label: "Missing Controller Name in Request Body", + reader: bytes.NewBuffer([]byte(`{ + "description":"test description" + }`)), + expectedCode: http.StatusBadRequest, + controllerClient: &mockControllerManager{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("POST", "/v2/controllers", testCase.reader) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusCreated + if resp.StatusCode == http.StatusCreated { + got := moduleLib.Controller{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("createHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestControllerGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected moduleLib.Controller + name, version string + expectedCode int + controllerClient *mockControllerManager + }{ + { + label: "Get Controller", + expectedCode: http.StatusOK, + expected: moduleLib.Controller{ + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + name: "testController", + controllerClient: &mockControllerManager{ + Items: []moduleLib.Controller{ + { + Name: "testController", + Host: "10.188.234.1", + Port: 8080, + }, + }, + }, + }, + { + label: "Get Non-Existing Controller", + expectedCode: http.StatusInternalServerError, + name: "nonexistingController", + controllerClient: &mockControllerManager{ + Items: []moduleLib.Controller{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v2/controllers/"+testCase.name, nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.Controller{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestControllerDeleteHandler(t *testing.T) { + + testCases := []struct { + label string + name string + version string + expectedCode int + controllerClient *mockControllerManager + }{ + { + label: "Delete Controller", + expectedCode: http.StatusNoContent, + name: "testController", + controllerClient: &mockControllerManager{}, + }, + { + label: "Delete Non-Existing Controller", + expectedCode: http.StatusInternalServerError, + name: "testController", + controllerClient: &mockControllerManager{ + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("DELETE", "/v2/controllers/"+testCase.name, nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + }) + } +} diff --git a/src/orchestrator/api/projecthandler_test.go b/src/orchestrator/api/projecthandler_test.go index c76764b3..1e273349 100644 --- a/src/orchestrator/api/projecthandler_test.go +++ b/src/orchestrator/api/projecthandler_test.go @@ -119,7 +119,7 @@ func TestProjectCreateHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("POST", "/v2/projects", testCase.reader) - resp := executeRequest(request, NewRouter(testCase.projectClient, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -188,7 +188,7 @@ func TestProjectGetHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("GET", "/v2/projects/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(testCase.projectClient, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { @@ -237,7 +237,7 @@ func TestProjectDeleteHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { request := httptest.NewRequest("DELETE", "/v2/projects/"+testCase.name, nil) - resp := executeRequest(request, NewRouter(testCase.projectClient, nil)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/cmd/main.go b/src/orchestrator/cmd/main.go index 087caba3..a6c72ae3 100644 --- a/src/orchestrator/cmd/main.go +++ b/src/orchestrator/cmd/main.go @@ -22,12 +22,12 @@ import ( "os/signal" "time" + "github.com/gorilla/handlers" "github.com/onap/multicloud-k8s/src/orchestrator/api" "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/auth" "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" - "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" contextDb "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/contextdb" - "github.com/gorilla/handlers" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" ) func main() { @@ -40,14 +40,14 @@ func main() { log.Println(err) log.Fatalln("Exiting...") } - err = contextDb.InitializeContextDatabase() + err = contextDb.InitializeContextDatabase() if err != nil { log.Println("Unable to initialize database connection...") log.Println(err) log.Fatalln("Exiting...") } - httpRouter := api.NewRouter(nil, nil) + httpRouter := api.NewRouter(nil, nil, nil) loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) log.Println("Starting Kubernetes Multicloud API") diff --git a/src/orchestrator/pkg/module/controller.go b/src/orchestrator/pkg/module/controller.go new file mode 100644 index 00000000..35d6f892 --- /dev/null +++ b/src/orchestrator/pkg/module/controller.go @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * 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 module + +import ( + "encoding/json" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// Controller contains the parameters needed for Controllers +// It implements the interface for managing the Controllers +type Controller struct { + Name string `json:"name"` + + Host string `json:"host"` + + Port int64 `json:"port"` +} + +// ControllerKey is the key structure that is used in the database +type ControllerKey struct { + ControllerName string `json:"controller-name"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (mk ControllerKey) String() string { + out, err := json.Marshal(mk) + if err != nil { + return "" + } + + return string(out) +} + +// ControllerManager is an interface exposes the Controller functionality +type ControllerManager interface { + CreateController(ms Controller) (Controller, error) + GetController(name string) (Controller, error) + DeleteController(name string) error +} + +// ControllerClient implements the Manager +// It will also be used to maintain some localized state +type ControllerClient struct { + collectionName string + tagMeta string +} + +// NewControllerClient returns an instance of the ControllerClient +// which implements the Manager +func NewControllerClient() *ControllerClient { + return &ControllerClient{ + collectionName: "controller", + tagMeta: "controllermetadata", + } +} + +// CreateController a new collection based on the Controller +func (mc *ControllerClient) CreateController(m Controller) (Controller, error) { + + //Construct the composite key to select the entry + key := ControllerKey{ + ControllerName: m.Name, + } + + //Check if this Controller already exists + _, err := mc.GetController(m.Name) + if err == nil { + return Controller{}, pkgerrors.New("Controller already exists") + } + + err = db.DBconn.Create(mc.collectionName, key, mc.tagMeta, m) + if err != nil { + return Controller{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return m, nil +} + +// GetController returns the Controller for corresponding name +func (mc *ControllerClient) GetController(name string) (Controller, error) { + + //Construct the composite key to select the entry + key := ControllerKey{ + ControllerName: name, + } + value, err := db.DBconn.Read(mc.collectionName, key, mc.tagMeta) + if err != nil { + return Controller{}, pkgerrors.Wrap(err, "Get Controller") + } + + //value is a byte array + if value != nil { + microserv := Controller{} + err = db.DBconn.Unmarshal(value, µserv) + if err != nil { + return Controller{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return microserv, nil + } + + return Controller{}, pkgerrors.New("Error getting Controller") +} + +// DeleteController the Controller from database +func (mc *ControllerClient) DeleteController(name string) error { + + //Construct the composite key to select the entry + key := ControllerKey{ + ControllerName: name, + } + err := db.DBconn.Delete(name, key, mc.tagMeta) + if err != nil { + return pkgerrors.Wrap(err, "Delete Controller Entry;") + } + return nil +} diff --git a/src/orchestrator/pkg/module/controller_test.go b/src/orchestrator/pkg/module/controller_test.go new file mode 100644 index 00000000..2e783c13 --- /dev/null +++ b/src/orchestrator/pkg/module/controller_test.go @@ -0,0 +1,181 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * 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 module + +import ( + "reflect" + "strings" + "testing" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +func TestCreateController(t *testing.T) { + testCases := []struct { + label string + inp Controller + expectedError string + mockdb *db.MockDB + expected Controller + }{ + { + label: "Create Controller", + inp: Controller{ + Name: "testController", + Host: "132.156.0.10", + Port: 8080, + }, + expected: Controller{ + Name: "testController", + Host: "132.156.0.10", + Port: 8080, + }, + expectedError: "", + mockdb: &db.MockDB{}, + }, + { + label: "Failed Create Controller", + expectedError: "Error Creating Controller", + mockdb: &db.MockDB{ + Err: pkgerrors.New("Error Creating Controller"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewControllerClient() + got, err := impl.CreateController(testCase.inp) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Create returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Create returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Create returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetController(t *testing.T) { + + testCases := []struct { + label string + name string + expectedError string + mockdb *db.MockDB + inp string + expected Controller + }{ + { + label: "Get Controller", + name: "testController", + expected: Controller{ + Name: "testController", + Host: "132.156.0.10", + Port: 8080, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ControllerKey{ControllerName: "testController"}.String(): { + "controllermetadata": []byte( + "{\"name\":\"testController\"," + + "\"host\":\"132.156.0.10\"," + + "\"port\":8080}"), + }, + }, + }, + }, + { + label: "Get Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewControllerClient() + got, err := impl.GetController(testCase.name) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Get returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Get returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Get returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestDeleteController(t *testing.T) { + + testCases := []struct { + label string + name string + expectedError string + mockdb *db.MockDB + }{ + { + label: "Delete Controller", + name: "testController", + mockdb: &db.MockDB{}, + }, + { + label: "Delete Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewControllerClient() + err := impl.DeleteController(testCase.name) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Delete returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Delete returned an unexpected error %s", err) + } + } + }) + } +} diff --git a/src/orchestrator/pkg/module/module.go b/src/orchestrator/pkg/module/module.go index d03e5ffb..a94a4207 100644 --- a/src/orchestrator/pkg/module/module.go +++ b/src/orchestrator/pkg/module/module.go @@ -20,6 +20,7 @@ package module type Client struct { Project *ProjectClient CompositeApp *CompositeAppClient + Controller *ControllerClient // Add Clients for API's here } @@ -28,6 +29,7 @@ func NewClient() *Client { c := &Client{} c.Project = NewProjectClient() c.CompositeApp = NewCompositeAppClient() + c.Controller = NewControllerClient() // Add Client API handlers here return c } |