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:
Dharmit Shah
2022-06-13 10:27:30 +05:30
committed by GitHub
parent 4ba943986d
commit e3b3b8eb53
15 changed files with 2076 additions and 1321 deletions

View File

@@ -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
```

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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{})
}

View File

@@ -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
View 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
View 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
View 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)
}
})
}
}

View File

@@ -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
View 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
}

View File

@@ -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)
}

View 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())
})
})
})