mirror of
				https://github.com/redhat-developer/odo.git
				synced 2025-10-19 03:06:19 +03:00 
			
		
		
		
	Add odo logs (#5760)
				
					
				
			* Add odo logs * Nolint for random number generation * Changes based on Philippe's PR review * Add logs for `odo logs` * Add nolint at the right place to fix unit tests * Changes based on PR feedback * Name the key in unstructured.Unstructured * Name containers with same names as c, c1, c2 * Remove unused struct field * Modify documentation to follow general pattern * Undo the changes done in earlier commits * odo logs help message is accurate * Update docs/website/versioned_docs/version-3.0.0/command-reference/logs.md Co-authored-by: Parthvi Vala <pvala@redhat.com> * Fixes broken link rendering * Correct the example used in odo logs doc * Make container name clearer in odo logs output * Wrap at 120 chars, not 80 * Fixes to the document after rebase mistake Co-authored-by: Parthvi Vala <pvala@redhat.com>
This commit is contained in:
		| @@ -0,0 +1,168 @@ | ||||
| --- | ||||
| title: odo logs | ||||
| --- | ||||
|  | ||||
| ## Description | ||||
|  | ||||
| `odo logs` is used to display the logs for all the containers odo created for the component under current working  | ||||
| directory. | ||||
|  | ||||
| ## Running the command  | ||||
|  | ||||
| If you haven't already done so, you must [initialize](../command-reference/init) your source code with the `odo  | ||||
| init` command. Next, run the `odo dev` command so that odo can create the resources on the Kubernetes cluster. | ||||
|  | ||||
| Consider a devfile.yaml like below which was used to create inner loop resources using `odo dev`. Notice that  | ||||
| multiple containers have been named as `main` to show how `odo logs` would display logs when more than one  | ||||
| containers have the same name: | ||||
| ```yaml | ||||
| metadata: | ||||
|   description: Stack with Node.js 14 | ||||
|   displayName: Node.js Runtime | ||||
|   icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg | ||||
|   language: javascript | ||||
|   name: node | ||||
|   projectType: nodejs | ||||
|   tags: | ||||
|     - NodeJS | ||||
|     - Express | ||||
|     - ubi8 | ||||
|   version: 1.0.1 | ||||
| schemaVersion: 2.0.0 | ||||
| starterProjects: | ||||
|   - git: | ||||
|       remotes: | ||||
|         origin: https://github.com/odo-devfiles/nodejs-ex.git | ||||
|     name: nodejs-starter | ||||
| commands: | ||||
| - exec: | ||||
|     commandLine: npm install | ||||
|     component: runtime | ||||
|     group: | ||||
|       isDefault: true | ||||
|       kind: build | ||||
|     workingDir: ${PROJECT_SOURCE} | ||||
|   id: install | ||||
| - exec: | ||||
|     commandLine: npm start | ||||
|     component: runtime | ||||
|     group: | ||||
|       isDefault: true | ||||
|       kind: run | ||||
|     workingDir: ${PROJECT_SOURCE} | ||||
|   id: run | ||||
| - exec: | ||||
|     commandLine: npm run debug | ||||
|     component: runtime | ||||
|     group: | ||||
|       isDefault: true | ||||
|       kind: debug | ||||
|     workingDir: ${PROJECT_SOURCE} | ||||
|   id: debug | ||||
| - exec: | ||||
|     commandLine: npm test | ||||
|     component: runtime | ||||
|     group: | ||||
|       isDefault: true | ||||
|       kind: test | ||||
|     workingDir: ${PROJECT_SOURCE} | ||||
|   id: test | ||||
| components: | ||||
| - container: | ||||
|     endpoints: | ||||
|     - name: http-3000 | ||||
|       targetPort: 3000 | ||||
|     image: registry.access.redhat.com/ubi8/nodejs-14:latest | ||||
|     memoryLimit: 1024Mi | ||||
|     mountSources: true | ||||
|   name: runtime | ||||
| - name: infinitepodone | ||||
|   kubernetes:   | ||||
|     inlined: | | ||||
|       apiVersion: v1 | ||||
|       kind: Pod | ||||
|       metadata: | ||||
|         name: infinitepodone | ||||
|       spec: | ||||
|         containers: | ||||
|           - name: main | ||||
|             image: docker.io/dharmit/infiniteloop | ||||
| - name: infinitedeployment | ||||
|   kubernetes: | ||||
|     inlined: | | ||||
|       apiVersion: apps/v1 | ||||
|       kind: Deployment | ||||
|       metadata: | ||||
|         name: infinitedeployment | ||||
|       spec: | ||||
|         replicas: 1 | ||||
|         selector: | ||||
|           matchLabels: | ||||
|             app: infinite | ||||
|         template: | ||||
|           metadata: | ||||
|             labels: | ||||
|               app: infinite | ||||
|           spec: | ||||
|             containers: | ||||
|             - name: main | ||||
|               image: docker.io/dharmit/infiniteloop | ||||
|  ``` | ||||
| When you do `odo dev`, odo creates pods for: | ||||
| 1. The component named `node` itself. Containers for this are created using `.components.container`. | ||||
| 2. Kubernetes component named `infinitepodone` | ||||
| 3. Kubernetes component named `infinitedeployment`. As can be seen under `.spec.template.spec.containers` for this  | ||||
|    particular component, it creates one container for it. | ||||
|  | ||||
| When you run `odo logs`, you should see logs from all these containers. Each line is prefixed with  | ||||
| `<container-name>:` to easily distinguish which the container the logs belong to. Since we named multiple  | ||||
| containers in the `devfile.yaml` as `main`, `odo logs` has distinguished these containers as `main` and `main[1]`: | ||||
| ```shell | ||||
| $ odo logs | ||||
| main: Fri May 27 06:17:30 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:31 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:32 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:33 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:34 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:35 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:36 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:37 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:38 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:39 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:40 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:41 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:42 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:44 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:45 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:46 UTC 2022 - this is infinite for loop | ||||
| main: Fri May 27 06:17:47 UTC 2022 - this is infinite for loop | ||||
| runtime: time="2022-05-27T06:17:36Z" level=info msg="create process:devrun"  | ||||
| runtime: time="2022-05-27T06:17:36Z" level=info msg="create process:debugrun"  | ||||
| runtime: time="2022-05-27T06:17:36Z" level=info msg="try to start program" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:36Z" level=info msg="success to start program" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:37Z" level=debug msg="no auth required"  | ||||
| runtime: time="2022-05-27T06:17:37Z" level=debug msg="wait program exit" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:37Z" level=info msg="program stopped with status:exit status 0" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:37Z" level=info msg="Don't start the stopped program because its autorestart flag is false" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:41Z" level=debug msg="no auth required"  | ||||
| runtime: time="2022-05-27T06:17:41Z" level=debug msg="succeed to find process:devrun"  | ||||
| runtime: time="2022-05-27T06:17:41Z" level=info msg="try to start program" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:41Z" level=info msg="success to start program" program=devrun  | ||||
| runtime: ODO_COMMAND_RUN is npm start | ||||
| runtime: Changing directory to ${PROJECT_SOURCE} | ||||
| runtime: Executing command cd ${PROJECT_SOURCE} && npm start | ||||
| runtime:  | ||||
| runtime: > nodejs-starter@1.0.0 start /projects | ||||
| runtime: > node server.js | ||||
| runtime:  | ||||
| runtime: App started on PORT 3000 | ||||
| runtime: time="2022-05-27T06:17:42Z" level=debug msg="wait program exit" program=devrun  | ||||
| runtime: time="2022-05-27T06:17:43Z" level=debug msg="no auth required"  | ||||
| main1: Fri May 27 06:17:34 UTC 2022 - this is infinite for loop | ||||
| main1: Fri May 27 06:17:35 UTC 2022 - this is infinite for loop | ||||
| main1: Fri May 27 06:17:36 UTC 2022 - this is infinite for loop | ||||
| main1: Fri May 27 06:17:37 UTC 2022 - this is infinite for loop | ||||
| main1: Fri May 27 06:17:38 UTC 2022 - this is infinite for loop | ||||
| main1: Fri May 27 06:17:39 UTC 2022 - this is infinite for loop | ||||
| main1: Fri May 27 06:17:40 UTC 2022 - this is infinite for loop | ||||
| ``` | ||||
| @@ -5,155 +5,38 @@ | ||||
| package binding | ||||
|  | ||||
| import ( | ||||
| 	reflect "reflect" | ||||
|  | ||||
| 	parser "github.com/devfile/library/pkg/devfile/parser" | ||||
| 	gomock "github.com/golang/mock/gomock" | ||||
| 	api "github.com/redhat-developer/odo/pkg/api" | ||||
| 	unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
| 	reflect "reflect" | ||||
| ) | ||||
|  | ||||
| // MockClient is a mock of Client interface | ||||
| // MockClient is a mock of Client interface. | ||||
| type MockClient struct { | ||||
| 	ctrl     *gomock.Controller | ||||
| 	recorder *MockClientMockRecorder | ||||
| } | ||||
|  | ||||
| // MockClientMockRecorder is the mock recorder for MockClient | ||||
| // MockClientMockRecorder is the mock recorder for MockClient. | ||||
| type MockClientMockRecorder struct { | ||||
| 	mock *MockClient | ||||
| } | ||||
|  | ||||
| // NewMockClient creates a new mock instance | ||||
| // NewMockClient creates a new mock instance. | ||||
| func NewMockClient(ctrl *gomock.Controller) *MockClient { | ||||
| 	mock := &MockClient{ctrl: ctrl} | ||||
| 	mock.recorder = &MockClientMockRecorder{mock} | ||||
| 	return mock | ||||
| } | ||||
|  | ||||
| // EXPECT returns an object that allows the caller to indicate expected use | ||||
| // EXPECT returns an object that allows the caller to indicate expected use. | ||||
| func (m *MockClient) EXPECT() *MockClientMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // GetFlags mocks base method | ||||
| func (m *MockClient) GetFlags(flags map[string]string) map[string]string { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetFlags", flags) | ||||
| 	ret0, _ := ret[0].(map[string]string) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // GetFlags indicates an expected call of GetFlags | ||||
| func (mr *MockClientMockRecorder) GetFlags(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlags", reflect.TypeOf((*MockClient)(nil).GetFlags), flags) | ||||
| } | ||||
|  | ||||
| // GetServiceInstances mocks base method | ||||
| func (m *MockClient) GetServiceInstances() (map[string]unstructured.Unstructured, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetServiceInstances") | ||||
| 	ret0, _ := ret[0].(map[string]unstructured.Unstructured) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetServiceInstances indicates an expected call of GetServiceInstances | ||||
| func (mr *MockClientMockRecorder) GetServiceInstances() *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceInstances", reflect.TypeOf((*MockClient)(nil).GetServiceInstances)) | ||||
| } | ||||
|  | ||||
| // GetBindingsFromDevfile mocks base method | ||||
| func (m *MockClient) GetBindingsFromDevfile(devfileObj parser.DevfileObj, context string) ([]api.ServiceBinding, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetBindingsFromDevfile", devfileObj, context) | ||||
| 	ret0, _ := ret[0].([]api.ServiceBinding) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetBindingsFromDevfile indicates an expected call of GetBindingsFromDevfile | ||||
| func (mr *MockClientMockRecorder) GetBindingsFromDevfile(devfileObj, context interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindingsFromDevfile", reflect.TypeOf((*MockClient)(nil).GetBindingsFromDevfile), devfileObj, context) | ||||
| } | ||||
|  | ||||
| // GetBindingFromCluster mocks base method | ||||
| func (m *MockClient) GetBindingFromCluster(name string) (api.ServiceBinding, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetBindingFromCluster", name) | ||||
| 	ret0, _ := ret[0].(api.ServiceBinding) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetBindingFromCluster indicates an expected call of GetBindingFromCluster | ||||
| func (mr *MockClientMockRecorder) GetBindingFromCluster(name interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindingFromCluster", reflect.TypeOf((*MockClient)(nil).GetBindingFromCluster), name) | ||||
| } | ||||
|  | ||||
| // ValidateAddBinding mocks base method | ||||
| func (m *MockClient) ValidateAddBinding(flags map[string]string) error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "ValidateAddBinding", flags) | ||||
| 	ret0, _ := ret[0].(error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // ValidateAddBinding indicates an expected call of ValidateAddBinding | ||||
| func (mr *MockClientMockRecorder) ValidateAddBinding(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAddBinding", reflect.TypeOf((*MockClient)(nil).ValidateAddBinding), flags) | ||||
| } | ||||
|  | ||||
| // SelectServiceInstance mocks base method | ||||
| func (m *MockClient) SelectServiceInstance(flags map[string]string, serviceMap map[string]unstructured.Unstructured) (string, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "SelectServiceInstance", flags, serviceMap) | ||||
| 	ret0, _ := ret[0].(string) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // SelectServiceInstance indicates an expected call of SelectServiceInstance | ||||
| func (mr *MockClientMockRecorder) SelectServiceInstance(flags, serviceMap interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectServiceInstance", reflect.TypeOf((*MockClient)(nil).SelectServiceInstance), flags, serviceMap) | ||||
| } | ||||
|  | ||||
| // AskBindingName mocks base method | ||||
| func (m *MockClient) AskBindingName(serviceName, componentName string, flags map[string]string) (string, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "AskBindingName", serviceName, componentName, flags) | ||||
| 	ret0, _ := ret[0].(string) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // AskBindingName indicates an expected call of AskBindingName | ||||
| func (mr *MockClientMockRecorder) AskBindingName(serviceName, componentName, flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskBindingName", reflect.TypeOf((*MockClient)(nil).AskBindingName), serviceName, componentName, flags) | ||||
| } | ||||
|  | ||||
| // AskBindAsFiles mocks base method | ||||
| func (m *MockClient) AskBindAsFiles(flags map[string]string) (bool, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "AskBindAsFiles", flags) | ||||
| 	ret0, _ := ret[0].(bool) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // AskBindAsFiles indicates an expected call of AskBindAsFiles | ||||
| func (mr *MockClientMockRecorder) AskBindAsFiles(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskBindAsFiles", reflect.TypeOf((*MockClient)(nil).AskBindAsFiles), flags) | ||||
| } | ||||
|  | ||||
| // AddBinding mocks base method | ||||
| // AddBinding mocks base method. | ||||
| func (m *MockClient) AddBinding(bindingName string, bindAsFiles bool, unstructuredService unstructured.Unstructured, obj parser.DevfileObj) (parser.DevfileObj, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "AddBinding", bindingName, bindAsFiles, unstructuredService, obj) | ||||
| @@ -162,27 +45,102 @@ func (m *MockClient) AddBinding(bindingName string, bindAsFiles bool, unstructur | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // AddBinding indicates an expected call of AddBinding | ||||
| // AddBinding indicates an expected call of AddBinding. | ||||
| func (mr *MockClientMockRecorder) AddBinding(bindingName, bindAsFiles, unstructuredService, obj interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBinding", reflect.TypeOf((*MockClient)(nil).AddBinding), bindingName, bindAsFiles, unstructuredService, obj) | ||||
| } | ||||
|  | ||||
| // ValidateRemoveBinding mocks base method | ||||
| func (m *MockClient) ValidateRemoveBinding(flags map[string]string) error { | ||||
| // AskBindAsFiles mocks base method. | ||||
| func (m *MockClient) AskBindAsFiles(flags map[string]string) (bool, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "ValidateRemoveBinding", flags) | ||||
| 	ret0, _ := ret[0].(error) | ||||
| 	ret := m.ctrl.Call(m, "AskBindAsFiles", flags) | ||||
| 	ret0, _ := ret[0].(bool) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // AskBindAsFiles indicates an expected call of AskBindAsFiles. | ||||
| func (mr *MockClientMockRecorder) AskBindAsFiles(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskBindAsFiles", reflect.TypeOf((*MockClient)(nil).AskBindAsFiles), flags) | ||||
| } | ||||
|  | ||||
| // AskBindingName mocks base method. | ||||
| func (m *MockClient) AskBindingName(serviceName, componentName string, flags map[string]string) (string, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "AskBindingName", serviceName, componentName, flags) | ||||
| 	ret0, _ := ret[0].(string) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // AskBindingName indicates an expected call of AskBindingName. | ||||
| func (mr *MockClientMockRecorder) AskBindingName(serviceName, componentName, flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AskBindingName", reflect.TypeOf((*MockClient)(nil).AskBindingName), serviceName, componentName, flags) | ||||
| } | ||||
|  | ||||
| // GetBindingFromCluster mocks base method. | ||||
| func (m *MockClient) GetBindingFromCluster(name string) (api.ServiceBinding, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetBindingFromCluster", name) | ||||
| 	ret0, _ := ret[0].(api.ServiceBinding) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetBindingFromCluster indicates an expected call of GetBindingFromCluster. | ||||
| func (mr *MockClientMockRecorder) GetBindingFromCluster(name interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindingFromCluster", reflect.TypeOf((*MockClient)(nil).GetBindingFromCluster), name) | ||||
| } | ||||
|  | ||||
| // GetBindingsFromDevfile mocks base method. | ||||
| func (m *MockClient) GetBindingsFromDevfile(devfileObj parser.DevfileObj, context string) ([]api.ServiceBinding, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetBindingsFromDevfile", devfileObj, context) | ||||
| 	ret0, _ := ret[0].([]api.ServiceBinding) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetBindingsFromDevfile indicates an expected call of GetBindingsFromDevfile. | ||||
| func (mr *MockClientMockRecorder) GetBindingsFromDevfile(devfileObj, context interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBindingsFromDevfile", reflect.TypeOf((*MockClient)(nil).GetBindingsFromDevfile), devfileObj, context) | ||||
| } | ||||
|  | ||||
| // GetFlags mocks base method. | ||||
| func (m *MockClient) GetFlags(flags map[string]string) map[string]string { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetFlags", flags) | ||||
| 	ret0, _ := ret[0].(map[string]string) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // ValidateRemoveBinding indicates an expected call of ValidateRemoveBinding | ||||
| func (mr *MockClientMockRecorder) ValidateRemoveBinding(flags interface{}) *gomock.Call { | ||||
| // GetFlags indicates an expected call of GetFlags. | ||||
| func (mr *MockClientMockRecorder) GetFlags(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRemoveBinding", reflect.TypeOf((*MockClient)(nil).ValidateRemoveBinding), flags) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlags", reflect.TypeOf((*MockClient)(nil).GetFlags), flags) | ||||
| } | ||||
|  | ||||
| // RemoveBinding mocks base method | ||||
| // GetServiceInstances mocks base method. | ||||
| func (m *MockClient) GetServiceInstances() (map[string]unstructured.Unstructured, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetServiceInstances") | ||||
| 	ret0, _ := ret[0].(map[string]unstructured.Unstructured) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetServiceInstances indicates an expected call of GetServiceInstances. | ||||
| func (mr *MockClientMockRecorder) GetServiceInstances() *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceInstances", reflect.TypeOf((*MockClient)(nil).GetServiceInstances)) | ||||
| } | ||||
|  | ||||
| // RemoveBinding mocks base method. | ||||
| func (m *MockClient) RemoveBinding(bindingName string, obj parser.DevfileObj) (parser.DevfileObj, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "RemoveBinding", bindingName, obj) | ||||
| @@ -191,8 +149,51 @@ func (m *MockClient) RemoveBinding(bindingName string, obj parser.DevfileObj) (p | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // RemoveBinding indicates an expected call of RemoveBinding | ||||
| // RemoveBinding indicates an expected call of RemoveBinding. | ||||
| func (mr *MockClientMockRecorder) RemoveBinding(bindingName, obj interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveBinding", reflect.TypeOf((*MockClient)(nil).RemoveBinding), bindingName, obj) | ||||
| } | ||||
|  | ||||
| // SelectServiceInstance mocks base method. | ||||
| func (m *MockClient) SelectServiceInstance(flags map[string]string, serviceMap map[string]unstructured.Unstructured) (string, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "SelectServiceInstance", flags, serviceMap) | ||||
| 	ret0, _ := ret[0].(string) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // SelectServiceInstance indicates an expected call of SelectServiceInstance. | ||||
| func (mr *MockClientMockRecorder) SelectServiceInstance(flags, serviceMap interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectServiceInstance", reflect.TypeOf((*MockClient)(nil).SelectServiceInstance), flags, serviceMap) | ||||
| } | ||||
|  | ||||
| // ValidateAddBinding mocks base method. | ||||
| func (m *MockClient) ValidateAddBinding(flags map[string]string) error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "ValidateAddBinding", flags) | ||||
| 	ret0, _ := ret[0].(error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // ValidateAddBinding indicates an expected call of ValidateAddBinding. | ||||
| func (mr *MockClientMockRecorder) ValidateAddBinding(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAddBinding", reflect.TypeOf((*MockClient)(nil).ValidateAddBinding), flags) | ||||
| } | ||||
|  | ||||
| // ValidateRemoveBinding mocks base method. | ||||
| func (m *MockClient) ValidateRemoveBinding(flags map[string]string) error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "ValidateRemoveBinding", flags) | ||||
| 	ret0, _ := ret[0].(error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // ValidateRemoveBinding indicates an expected call of ValidateRemoveBinding. | ||||
| func (mr *MockClientMockRecorder) ValidateRemoveBinding(flags interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRemoveBinding", reflect.TypeOf((*MockClient)(nil).ValidateRemoveBinding), flags) | ||||
| } | ||||
|   | ||||
| @@ -124,9 +124,6 @@ func Log(client kclient.ClientInterface, componentName string, appName string, f | ||||
| // We then return a list of "components" intended for listing / output purposes specifically for commands such as: | ||||
| // `odo list` | ||||
| // that are both odo and non-odo components. | ||||
| // | ||||
| // We then return a list of "components" intended for listing / output purposes specifically for commands such as: | ||||
| // `odo list` | ||||
| func ListAllClusterComponents(client kclient.ClientInterface, namespace string) ([]api.ComponentAbstract, error) { | ||||
|  | ||||
| 	// Get all the dynamic resources available | ||||
| @@ -155,8 +152,8 @@ func ListAllClusterComponents(client kclient.ClientInterface, namespace string) | ||||
| 		} | ||||
|  | ||||
| 		// Figure out the correct name to use | ||||
| 		// if there is no instance label, we SKIP the resource as | ||||
| 		// it is not a component essential for Kubernetes. | ||||
| 		// if there is no instance label (app.kubernetes.io/instance), | ||||
| 		// we SKIP the resource as it is not a component essential for Kubernetes. | ||||
| 		name := odolabels.GetComponentName(labels) | ||||
| 		if name == "" { | ||||
| 			continue | ||||
|   | ||||
| @@ -54,7 +54,7 @@ func (c *Client) GetBindableKinds() (bindingApi.BindableKinds, error) { | ||||
| 	return bindableKind, nil | ||||
| } | ||||
|  | ||||
| // GetBindableKindStatusRestMapping retuns a list of *meta.RESTMapping of all the bindable kind operator CRD | ||||
| // GetBindableKindStatusRestMapping returns a list of *meta.RESTMapping of all the bindable kind operator CRD | ||||
| func (c Client) GetBindableKindStatusRestMapping(bindableKindStatuses []bindingApi.BindableKindsStatus) ([]*meta.RESTMapping, error) { | ||||
| 	var result []*meta.RESTMapping | ||||
| 	for _, bks := range bindableKindStatuses { | ||||
|   | ||||
| @@ -29,6 +29,14 @@ type ClientInterface interface { | ||||
| 	// GetAllResourcesFromSelector returns all resources of any kind (including CRs) matching the given label selector | ||||
| 	GetAllResourcesFromSelector(selector string, ns string) ([]unstructured.Unstructured, error) | ||||
|  | ||||
| 	// binding.go | ||||
| 	IsServiceBindingSupported() (bool, error) | ||||
| 	GetBindableKinds() (bindingApi.BindableKinds, error) | ||||
| 	GetBindableKindStatusRestMapping(bindableKindStatuses []bindingApi.BindableKindsStatus) ([]*meta.RESTMapping, error) | ||||
| 	GetBindingServiceBinding(name string) (bindingApi.ServiceBinding, error) | ||||
| 	GetSpecServiceBinding(name string) (specApi.ServiceBinding, error) | ||||
| 	NewServiceBindingServiceObject(unstructuredService unstructured.Unstructured, bindingName string) (bindingApi.Service, error) | ||||
|  | ||||
| 	// deployment.go | ||||
| 	GetDeploymentByName(name string) (*appsv1.Deployment, error) | ||||
| 	GetOneDeployment(componentName, appName string) (*appsv1.Deployment, error) | ||||
| @@ -87,14 +95,6 @@ type ClientInterface interface { | ||||
| 	GetOperatorGVRList() ([]meta.RESTMapping, error) | ||||
| 	ConvertUnstructuredToResource(u unstructured.Unstructured, obj interface{}) error | ||||
|  | ||||
| 	// binding.go | ||||
| 	IsServiceBindingSupported() (bool, error) | ||||
| 	GetBindableKinds() (bindingApi.BindableKinds, error) | ||||
| 	GetBindableKindStatusRestMapping(bindableKindStatuses []bindingApi.BindableKindsStatus) ([]*meta.RESTMapping, error) | ||||
| 	GetBindingServiceBinding(name string) (bindingApi.ServiceBinding, error) | ||||
| 	GetSpecServiceBinding(name string) (specApi.ServiceBinding, error) | ||||
| 	NewServiceBindingServiceObject(unstructuredService unstructured.Unstructured, bindingName string) (bindingApi.Service, error) | ||||
|  | ||||
| 	// owner_reference.go | ||||
| 	TryWithBlockOwnerDeletion(ownerReference metav1.OwnerReference, exec func(ownerReference metav1.OwnerReference) error) error | ||||
|  | ||||
| @@ -105,6 +105,7 @@ type ClientInterface interface { | ||||
| 	GetPodUsingComponentName(componentName string) (*corev1.Pod, error) | ||||
| 	GetOnePodFromSelector(selector string) (*corev1.Pod, error) | ||||
| 	GetPodLogs(podName, containerName string, followLog bool) (io.ReadCloser, error) | ||||
| 	GetAllPodsInNamespace() (*corev1.PodList, error) | ||||
|  | ||||
| 	// port_forwarding.go | ||||
| 	// SetupPortForwarding creates port-forwarding for the pod on the port pairs provided in the | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -242,3 +242,7 @@ func (c *Client) GetPodLogs(podName, containerName string, followLog bool) (io.R | ||||
|  | ||||
| 	return rd, err | ||||
| } | ||||
|  | ||||
| func (c *Client) GetAllPodsInNamespace() (*corev1.PodList, error) { | ||||
| 	return c.KubeClient.CoreV1().Pods(c.Namespace).List(context.TODO(), metav1.ListOptions{}) | ||||
| } | ||||
|   | ||||
| @@ -43,6 +43,8 @@ const suffixSpacing = "  " | ||||
| const prefixSpacing = " " | ||||
|  | ||||
| var mu sync.Mutex | ||||
| var colors = []color.Attribute{color.FgRed, color.FgGreen, color.FgYellow, color.FgBlue, color.FgMagenta, color.FgCyan, color.FgWhite} | ||||
| var colorCounter = 0 | ||||
|  | ||||
| // Status is used to track ongoing status in a CLI, with a nice loading spinner | ||||
| // when attached to a terminal | ||||
| @@ -517,3 +519,10 @@ func getSpacingString() string { | ||||
| 	} | ||||
| 	return "•" | ||||
| } | ||||
|  | ||||
| // ColorPicker picks a color from colors slice defined at the starting of this file | ||||
| // It increments the colorCounter variable so that next iteration returns a different color | ||||
| func ColorPicker() color.Attribute { | ||||
| 	colorCounter++ | ||||
| 	return colors[(colorCounter)%len(colors)] | ||||
| } | ||||
|   | ||||
							
								
								
									
										9
									
								
								pkg/logs/interface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pkg/logs/interface.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| package logs | ||||
|  | ||||
| import "io" | ||||
|  | ||||
| type Client interface { | ||||
| 	// DevModeLogs gets logs for the provided component name and namespace. A component could have multiple pods and | ||||
| 	// containers running on the cluster. It returns a slice of maps where container name is the key and its logs are the value | ||||
| 	DevModeLogs(componentName string, namespace string) ([]map[string]io.ReadCloser, error) | ||||
| } | ||||
							
								
								
									
										108
									
								
								pkg/logs/logs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								pkg/logs/logs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| package logs | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/kclient" | ||||
| 	odolabels "github.com/redhat-developer/odo/pkg/labels" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| ) | ||||
|  | ||||
| type LogsClient struct { | ||||
| 	kubernetesClient kclient.ClientInterface | ||||
| } | ||||
|  | ||||
| func NewLogsClient(kubernetesClient kclient.ClientInterface) *LogsClient { | ||||
| 	return &LogsClient{ | ||||
| 		kubernetesClient: kubernetesClient, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var _ Client = (*LogsClient)(nil) | ||||
|  | ||||
| func (o *LogsClient) DevModeLogs(componentName string, namespace string) ([]map[string]io.ReadCloser, error) { | ||||
| 	// get all resources in the namespace which are running in Dev mode | ||||
| 	selector := odolabels.Builder().WithComponentName(componentName).WithMode(odolabels.ComponentDevMode).Selector() | ||||
| 	resources, err := o.kubernetesClient.GetAllResourcesFromSelector(selector, namespace) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// get all pods in the namespace | ||||
| 	podList, err := o.kubernetesClient.GetAllPodsInNamespace() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// match pod ownerReference (if any) with resources running in Dev mode | ||||
| 	var pods []corev1.Pod | ||||
| 	for _, pod := range podList.Items { | ||||
| 		for _, owner := range pod.GetOwnerReferences() { | ||||
| 			match, err := o.matchOwnerReferenceWithResources(owner, resources) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} else if match { | ||||
| 				pods = append(pods, pod) | ||||
| 				break // because we don't need to check other owner references of the pod anymore | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// get all containers from the pods of interest | ||||
| 	podContainersMap := map[string][]corev1.Container{} | ||||
| 	for _, pod := range pods { | ||||
| 		for _, container := range pod.Spec.Containers { | ||||
| 			if _, ok := podContainersMap[pod.Name]; !ok { | ||||
| 				podContainersMap[pod.Name] = []corev1.Container{container} | ||||
| 			} else { | ||||
| 				podContainersMap[pod.Name] = append(podContainersMap[pod.Name], container) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// get logs of all containers | ||||
| 	logs := []map[string]io.ReadCloser{} | ||||
|  | ||||
| 	for pod, containers := range podContainersMap { | ||||
| 		for _, container := range containers { | ||||
| 			containerLogs, err := o.kubernetesClient.GetPodLogs(pod, container.Name, false) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			logs = append(logs, map[string]io.ReadCloser{container.Name: containerLogs}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return logs, nil | ||||
| } | ||||
|  | ||||
| // matchOwnerReferenceWithResources recursively checks if the owner reference passed to it matches any of the resources | ||||
| // This is useful when trying to find if a pod is owned by any of the ReplicaSet or Deployment in the cluster. | ||||
| func (o *LogsClient) matchOwnerReferenceWithResources(owner metav1.OwnerReference, resources []unstructured.Unstructured) (bool, error) { | ||||
| 	// first, check if ownerReference belongs to any of the resources | ||||
| 	for _, resource := range resources { | ||||
| 		if resource.GetUID() != "" && owner.UID != "" && resource.GetUID() == owner.UID { | ||||
| 			return true, nil | ||||
| 		} | ||||
| 	} | ||||
| 	// second, get the resource indicated by ownerReference and check its ownerReferences field | ||||
| 	restMapping, err := o.kubernetesClient.GetRestMappingFromGVK(schema.FromAPIVersionAndKind(owner.APIVersion, owner.Kind)) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	resource, err := o.kubernetesClient.GetDynamicResource(restMapping.Resource, owner.Name) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	ownerReferences := resource.GetOwnerReferences() | ||||
| 	// recursively check if ownerReference matches any of the resources' UID | ||||
| 	for _, ownerReference := range ownerReferences { | ||||
| 		return o.matchOwnerReferenceWithResources(ownerReference, resources) | ||||
| 	} | ||||
| 	return false, nil | ||||
| } | ||||
							
								
								
									
										205
									
								
								pkg/logs/logs_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								pkg/logs/logs_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| package logs | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/api/meta" | ||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/kclient" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
| ) | ||||
|  | ||||
| func fakePod(name string) unstructured.Unstructured { | ||||
| 	return unstructured.Unstructured{Object: map[string]interface{}{ | ||||
| 		"apiVersion": "v1", | ||||
| 		"kind":       "Pod", | ||||
| 		"metadata": map[string]interface{}{ | ||||
| 			"name": fmt.Sprintf("pod-%s", name), | ||||
| 			"uid":  fmt.Sprintf("pod-%s", name), | ||||
| 		}, | ||||
| 		"spec": map[string]interface{}{ | ||||
| 			"containers": map[string]interface{}{ | ||||
| 				"name":  fmt.Sprintf("%s-1", name), | ||||
| 				"image": "image", | ||||
| 			}, | ||||
| 		}, | ||||
| 	}} | ||||
| } | ||||
|  | ||||
| func fakeDeployment(name string) unstructured.Unstructured { | ||||
| 	return unstructured.Unstructured{ | ||||
| 		Object: map[string]interface{}{ | ||||
| 			"apiVersion": "apps/v1", | ||||
| 			"kind":       "Deployment", | ||||
| 			"metadata": map[string]interface{}{ | ||||
| 				"name": fmt.Sprintf("deployment-%s", name), | ||||
| 				"uid":  fmt.Sprintf("deployment-%s", name), | ||||
| 			}, | ||||
| 			"spec": map[string]interface{}{ | ||||
| 				"selector": map[string]interface{}{ | ||||
| 					"matchLabels": map[string]interface{}{ | ||||
| 						"app": "test", | ||||
| 					}, | ||||
| 				}, | ||||
| 				"template": map[string]interface{}{ | ||||
| 					"metadata": map[string]interface{}{ | ||||
| 						"labels": map[string]interface{}{ | ||||
| 							"app": "test", | ||||
| 						}, | ||||
| 					}, | ||||
| 					"spec": fakePod(name), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func generateOwnerRefernce(object unstructured.Unstructured) metav1.OwnerReference { | ||||
| 	return metav1.OwnerReference{ | ||||
| 		APIVersion: object.GetAPIVersion(), | ||||
| 		Kind:       object.GetKind(), | ||||
| 		Name:       object.GetName(), | ||||
| 		UID:        object.GetUID(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestLogsClient_matchOwnerReferenceWithResources_PodsWithOwnerInResources(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		resources func() []unstructured.Unstructured | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    bool | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "Case 1: pod owned by a deployment", | ||||
| 			args: args{ | ||||
| 				resources: func() []unstructured.Unstructured { | ||||
| 					pod := fakePod("pod") | ||||
| 					deployment := fakeDeployment("deployment") | ||||
| 					deployOwnerRef := generateOwnerRefernce(deployment) | ||||
| 					pod.SetOwnerReferences([]metav1.OwnerReference{deployOwnerRef}) | ||||
| 					return []unstructured.Unstructured{pod, deployment} | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    true, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			ctrl := gomock.NewController(t) | ||||
| 			kubernetesClient := kclient.NewMockClientInterface(ctrl) | ||||
| 			o := &LogsClient{ | ||||
| 				kubernetesClient: kubernetesClient, | ||||
| 			} | ||||
|  | ||||
| 			got, err := o.matchOwnerReferenceWithResources(tt.args.resources()[0].GetOwnerReferences()[0], tt.args.resources()) | ||||
| 			if (err != nil) != tt.wantErr { | ||||
| 				t.Errorf("matchOwnerReferenceWithResources() error = %v, wantErr %v", err, tt.wantErr) | ||||
| 				return | ||||
| 			} | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("matchOwnerReferenceWithResources() got = %v, want %v", got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestLogsClient_matchOwnerReferenceWithResources_PodsWithNoOwnerInResources(t *testing.T) { | ||||
| 	// pod and deployment that are not a part of args.resources | ||||
| 	independentDeploy := fakeDeployment("independent-deploy") | ||||
| 	independentPod := fakePod("independent-pod") | ||||
| 	independentPod.SetOwnerReferences([]metav1.OwnerReference{generateOwnerRefernce(independentDeploy)}) | ||||
|  | ||||
| 	type args struct { | ||||
| 		resources func() []unstructured.Unstructured | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		args     args | ||||
| 		gvk      *meta.RESTMapping | ||||
| 		resource *unstructured.Unstructured | ||||
| 		want     bool | ||||
| 		wantErr  bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "Case 1: Pod not owned by anything in `resources` slice", | ||||
| 			args: args{ | ||||
| 				resources: func() []unstructured.Unstructured { | ||||
| 					pod := fakePod("pod") | ||||
| 					deployment := fakeDeployment("deployment") | ||||
| 					deployOwnerRef := generateOwnerRefernce(deployment) | ||||
| 					pod.SetOwnerReferences([]metav1.OwnerReference{deployOwnerRef}) | ||||
| 					return []unstructured.Unstructured{pod, deployment} | ||||
| 				}, | ||||
| 			}, | ||||
| 			gvk: &meta.RESTMapping{ | ||||
| 				Resource: schema.GroupVersionResource{ | ||||
| 					Group:    "apps", | ||||
| 					Version:  "v1", | ||||
| 					Resource: "deployments", | ||||
| 				}, | ||||
| 				GroupVersionKind: schema.GroupVersionKind{ | ||||
| 					Group:   "apps", | ||||
| 					Version: "v1", | ||||
| 					Kind:    "Deployment", | ||||
| 				}, | ||||
| 			}, | ||||
| 			resource: &unstructured.Unstructured{Object: map[string]interface{}{ | ||||
| 				"apiVersion": "apps/v1", | ||||
| 				"kind":       "Deployment", | ||||
| 				"metadata": map[string]interface{}{ | ||||
| 					"namespace": independentDeploy.GetNamespace(), | ||||
| 					"name":      independentDeploy.GetName(), | ||||
| 					"uid":       independentDeploy.GetUID(), | ||||
| 				}, | ||||
| 				"spec": map[string]interface{}{ | ||||
| 					"selector": map[string]interface{}{ | ||||
| 						"matchLabels": map[string]interface{}{ | ||||
| 							"app": "test", | ||||
| 						}, | ||||
| 					}, | ||||
| 					"template": map[string]interface{}{ | ||||
| 						"metadata": map[string]interface{}{ | ||||
| 							"labels": map[string]interface{}{ | ||||
| 								"app": "test", | ||||
| 							}, | ||||
| 						}, | ||||
| 						"spec": fakePod(independentDeploy.GetName()), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}}, | ||||
| 			want:    false, | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			ctrl := gomock.NewController(t) | ||||
| 			kubernetesClient := kclient.NewMockClientInterface(ctrl) | ||||
| 			kubernetesClient.EXPECT().GetRestMappingFromGVK( | ||||
| 				schema.FromAPIVersionAndKind(independentDeploy.GetAPIVersion(), independentDeploy.GetKind())).Return(tt.gvk, nil).AnyTimes() | ||||
| 			kubernetesClient.EXPECT().GetDynamicResource(tt.gvk.Resource, independentDeploy.GetName()).Return(tt.resource, nil).AnyTimes() | ||||
|  | ||||
| 			o := &LogsClient{ | ||||
| 				kubernetesClient: kubernetesClient, | ||||
| 			} | ||||
| 			got, err := o.matchOwnerReferenceWithResources(independentPod.GetOwnerReferences()[0], tt.args.resources()) | ||||
| 			if (err != nil) != tt.wantErr { | ||||
| 				t.Errorf("matchOwnerReferenceWithResources() error = %v, wantErr %v", err, tt.wantErr) | ||||
| 				return | ||||
| 			} | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("matchOwnerReferenceWithResources() got = %v, want %v", got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -8,6 +8,8 @@ import ( | ||||
| 	"strings" | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/cli/logs" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/cli/add" | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/cli/alizer" | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/cli/build_images" | ||||
| @@ -182,6 +184,7 @@ func odoRootCmd(name, fullName string) *cobra.Command { | ||||
| 		registry.NewCmdRegistry(registry.RecommendedCommandName, util.GetFullName(fullName, registry.RecommendedCommandName)), | ||||
| 		create.NewCmdCreate(create.RecommendedCommandName, util.GetFullName(fullName, create.RecommendedCommandName)), | ||||
| 		set.NewCmdSet(set.RecommendedCommandName, util.GetFullName(fullName, set.RecommendedCommandName)), | ||||
| 		logs.NewCmdLogs(logs.RecommendedCommandName, util.GetFullName(fullName, logs.RecommendedCommandName)), | ||||
| 	) | ||||
|  | ||||
| 	// Add all subcommands to base commands | ||||
|   | ||||
							
								
								
									
										158
									
								
								pkg/odo/cli/logs/logs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								pkg/odo/cli/logs/logs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| package logs | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/fatih/color" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/log" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/devfile/location" | ||||
| 	odoutil "github.com/redhat-developer/odo/pkg/odo/util" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/cmdline" | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/genericclioptions" | ||||
| 	"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	ktemplates "k8s.io/kubectl/pkg/util/templates" | ||||
| ) | ||||
|  | ||||
| const RecommendedCommandName = "logs" | ||||
|  | ||||
| type LogsOptions struct { | ||||
| 	// context | ||||
| 	Context *genericclioptions.Context | ||||
| 	// clients | ||||
| 	clientset *clientset.Clientset | ||||
|  | ||||
| 	// variables | ||||
| 	componentName string | ||||
| 	contextDir    string | ||||
| 	out           io.Writer | ||||
| } | ||||
|  | ||||
| func NewLogsOptions() *LogsOptions { | ||||
| 	return &LogsOptions{ | ||||
| 		out: log.GetStdout(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var logsExample = ktemplates.Examples(` | ||||
| 	# Show logs of all containers | ||||
| 	%[1]s | ||||
| `) | ||||
|  | ||||
| func (o *LogsOptions) SetClientset(clientset *clientset.Clientset) { | ||||
| 	o.clientset = clientset | ||||
| } | ||||
|  | ||||
| func (o *LogsOptions) Complete(cmdline cmdline.Cmdline, args []string) error { | ||||
| 	var err error | ||||
| 	o.contextDir, err = os.Getwd() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	isEmptyDir, err := location.DirIsEmpty(o.clientset.FS, o.contextDir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if isEmptyDir { | ||||
| 		return errors.New("this command cannot run in an empty directory, run the command in a directory containing source code or initialize using 'odo init'") | ||||
| 	} | ||||
|  | ||||
| 	o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline).NeedDevfile("")) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("unable to create context: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	o.componentName = o.Context.EnvSpecificInfo.GetDevfileObj().GetMetadataName() | ||||
|  | ||||
| 	o.clientset.KubernetesClient.SetNamespace(o.Context.GetProject()) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (o *LogsOptions) Validate() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (o *LogsOptions) Run(ctx context.Context) error { | ||||
| 	containersLogs, err := o.clientset.LogsClient.DevModeLogs(o.componentName, o.Context.GetProject()) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	uniqueContainerNames := map[string]struct{}{} | ||||
| 	for _, entry := range containersLogs { | ||||
| 		for container, logs := range entry { | ||||
| 			uniqueName := getUniqueContainerName(container, uniqueContainerNames) | ||||
| 			uniqueContainerNames[uniqueName] = struct{}{} | ||||
| 			err = printLogs(uniqueName, logs, o.out) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func getUniqueContainerName(name string, uniqueNames map[string]struct{}) string { | ||||
| 	if _, ok := uniqueNames[name]; ok { | ||||
| 		// name already present in uniqueNames; find another name | ||||
| 		// first check if last character in name is a number; if so increment it, else append name with 1 | ||||
| 		last, err := strconv.Atoi(string(name[len(name)-1])) | ||||
| 		if err == nil { | ||||
| 			last++ | ||||
| 			name = fmt.Sprintf("%s[%d]", name[:len(name)-1], last) | ||||
| 		} else { | ||||
| 			last = 1 | ||||
| 			name = fmt.Sprintf("%s[%d]", name, last) | ||||
| 		} | ||||
| 		return getUniqueContainerName(name, uniqueNames) | ||||
| 	} | ||||
| 	return name | ||||
| } | ||||
|  | ||||
| // printLogs prints the logs of the containers with container name prefixed to the log message | ||||
| func printLogs(containerName string, rd io.ReadCloser, out io.Writer) error { | ||||
| 	color.Set(log.ColorPicker()) | ||||
| 	defer color.Unset() | ||||
| 	scanner := bufio.NewScanner(rd) | ||||
| 	scanner.Split(bufio.ScanLines) | ||||
|  | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Text() | ||||
| 		_, err := fmt.Fprintln(out, containerName+": "+line) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NewCmdLogs(name, fullname string) *cobra.Command { | ||||
| 	o := NewLogsOptions() | ||||
| 	logsCmd := &cobra.Command{ | ||||
| 		Use:   name, | ||||
| 		Short: "Show logs of all containers of the component", | ||||
| 		Long: `odo logs shows logs of all containers of the component running in the Dev mode. | ||||
| It prefixes each log message with the container name.`, | ||||
| 		Example: fmt.Sprintf(logsExample, fullname), | ||||
| 		Args:    cobra.MaximumNArgs(0), | ||||
| 		Run: func(cmd *cobra.Command, args []string) { | ||||
| 			genericclioptions.GenericRun(o, cmd, args) | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	clientset.Add(logsCmd, clientset.LOGS, clientset.FILESYSTEM) | ||||
| 	logsCmd.Annotations["command"] = "main" | ||||
| 	logsCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) | ||||
| 	return logsCmd | ||||
| } | ||||
| @@ -12,6 +12,7 @@ | ||||
| package clientset | ||||
|  | ||||
| import ( | ||||
| 	"github.com/redhat-developer/odo/pkg/logs" | ||||
| 	"github.com/spf13/cobra" | ||||
|  | ||||
| 	"github.com/redhat-developer/odo/pkg/alizer" | ||||
| @@ -33,6 +34,8 @@ import ( | ||||
| const ( | ||||
| 	// ALIZER instantiates client for pkg/alizer | ||||
| 	ALIZER = "DEP_ALIZER" | ||||
| 	// BINDING instantiates client for pkg/binding | ||||
| 	BINDING = "DEP_BINDING" | ||||
| 	// DELETE_COMPONENT instantiates client for pkg/component/delete | ||||
| 	DELETE_COMPONENT = "DEP_DELETE_COMPONENT" | ||||
| 	// DEPLOY instantiates client for pkg/deploy | ||||
| @@ -47,6 +50,8 @@ const ( | ||||
| 	KUBERNETES_NULLABLE = "DEP_KUBERNETES_NULLABLE" | ||||
| 	// KUBERNETES instantiates client for pkg/kclient | ||||
| 	KUBERNETES = "DEP_KUBERNETES" | ||||
| 	// LOGS instantiates client for pkg/logs | ||||
| 	LOGS = "DEP_LOGS" | ||||
| 	// PREFERENCE instantiates client for pkg/preference | ||||
| 	PREFERENCE = "DEP_PREFERENCE" | ||||
| 	// PROJECT instantiates client for pkg/project | ||||
| @@ -57,8 +62,6 @@ const ( | ||||
| 	STATE = "DEP_STATE" | ||||
| 	// WATCH instantiates client for pkg/watch | ||||
| 	WATCH = "DEP_WATCH" | ||||
| 	// BINDING instantiates client for pkg/binding | ||||
| 	BINDING = "DEP_BINDING" | ||||
| 	/* Add key for new package here */ | ||||
| ) | ||||
|  | ||||
| @@ -70,6 +73,7 @@ var subdeps map[string][]string = map[string][]string{ | ||||
| 	DEPLOY:           {KUBERNETES}, | ||||
| 	DEV:              {WATCH}, | ||||
| 	INIT:             {ALIZER, FILESYSTEM, PREFERENCE, REGISTRY}, | ||||
| 	LOGS:             {KUBERNETES}, | ||||
| 	PROJECT:          {KUBERNETES_NULLABLE}, | ||||
| 	REGISTRY:         {FILESYSTEM, PREFERENCE}, | ||||
| 	STATE:            {FILESYSTEM}, | ||||
| @@ -86,6 +90,7 @@ type Clientset struct { | ||||
| 	FS               filesystem.Filesystem | ||||
| 	InitClient       _init.Client | ||||
| 	KubernetesClient kclient.ClientInterface | ||||
| 	LogsClient       logs.Client | ||||
| 	PreferenceClient preference.Client | ||||
| 	ProjectClient    project.Client | ||||
| 	RegistryClient   registry.Client | ||||
| @@ -151,6 +156,9 @@ func Fetch(command *cobra.Command) (*Clientset, error) { | ||||
| 	if isDefined(command, INIT) { | ||||
| 		dep.InitClient = _init.NewInitClient(dep.FS, dep.PreferenceClient, dep.RegistryClient, dep.AlizerClient) | ||||
| 	} | ||||
| 	if isDefined(command, LOGS) { | ||||
| 		dep.LogsClient = logs.NewLogsClient(dep.KubernetesClient) | ||||
| 	} | ||||
| 	if isDefined(command, PROJECT) { | ||||
| 		dep.ProjectClient = project.NewClient(dep.KubernetesClient) | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										53
									
								
								tests/integration/devfile/cmd_logs_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								tests/integration/devfile/cmd_logs_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| package devfile | ||||
|  | ||||
| import ( | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	. "github.com/onsi/ginkgo" | ||||
| 	. "github.com/onsi/gomega" | ||||
| 	"github.com/onsi/gomega/gexec" | ||||
| 	"github.com/redhat-developer/odo/tests/helper" | ||||
| ) | ||||
|  | ||||
| var _ = Describe("odo logs command tests", func() { | ||||
| 	var componentName string | ||||
| 	var commonVar helper.CommonVar | ||||
|  | ||||
| 	var _ = BeforeEach(func() { | ||||
| 		commonVar = helper.CommonBeforeEach() | ||||
| 		componentName = helper.RandString(6) | ||||
| 		helper.Chdir(commonVar.Context) | ||||
| 		Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeFalse()) | ||||
| 	}) | ||||
|  | ||||
| 	var _ = AfterEach(func() { | ||||
| 		helper.CommonAfterEach(commonVar) | ||||
| 	}) | ||||
|  | ||||
| 	When("directory is empty", func() { | ||||
|  | ||||
| 		BeforeEach(func() { | ||||
| 			Expect(helper.ListFilesInDir(commonVar.Context)).To(HaveLen(0)) | ||||
| 		}) | ||||
|  | ||||
| 		It("should error", func() { | ||||
| 			output := helper.Cmd("odo", "logs").ShouldFail().Err() | ||||
| 			Expect(output).To(ContainSubstring("this command cannot run in an empty directory")) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	When("component is created and odo logs is executed", func() { | ||||
| 		BeforeEach(func() { | ||||
| 			helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) | ||||
| 			helper.Cmd("odo", "init", "--name", componentName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass() | ||||
| 			Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeFalse()) | ||||
| 		}) | ||||
| 		It("should successfully show logs of the running component", func() { | ||||
| 			err := helper.RunDevMode(func(session *gexec.Session, outContents []byte, errContents []byte, ports map[string]string) { | ||||
| 				out := helper.Cmd("odo", "logs").ShouldPass().Out() | ||||
| 				Expect(out).To(ContainSubstring("runtime: App started on PORT 3000")) | ||||
| 			}) | ||||
| 			Expect(err).ToNot(HaveOccurred()) | ||||
| 		}) | ||||
| 	}) | ||||
| }) | ||||
		Reference in New Issue
	
	Block a user
	 Dharmit Shah
					Dharmit Shah