Skip to content

Commit

Permalink
Support both polling and streaming logs
Browse files Browse the repository at this point in the history
  • Loading branch information
charles-edouard.breteche authored and tekton-robot committed Jul 13, 2020
1 parent 93e04f7 commit 2690114
Show file tree
Hide file tree
Showing 20 changed files with 248 additions and 38 deletions.
2 changes: 2 additions & 0 deletions cmd/dashboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var (
tenantNamespace = flag.String("namespace", "", "If set, limits the scope of resources watched to this namespace only")
logLevel = flag.String("log-level", "info", "Minimum log level output by the logger")
logFormat = flag.String("log-format", "json", "Format for log output (json or console)")
streamLogs = flag.Bool("stream-logs", false, "Enable log streaming instead of polling")
)

func getCSRFAuthKey() []byte {
Expand Down Expand Up @@ -139,6 +140,7 @@ func main() {
ReadOnly: *readOnly,
IsOpenShift: *isOpenshift,
LogoutURL: *logoutUrl,
StreamLogs: *streamLogs,
}

resource := endpoints.Resource{
Expand Down
5 changes: 5 additions & 0 deletions config_frontend/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ limitations under the License.

import 'react-testing-library/cleanup-after-each';
import fetchMock from 'fetch-mock';
import { TextDecoder, TextEncoder } from 'util';
import { ReadableStream } from 'web-streams-polyfill/es6';

fetchMock.catch();
fetchMock.config.overwriteRoutes = true;

window.HTMLElement.prototype.scrollIntoView = function scrollIntoViewTestStub() {};
window.TextDecoder = TextDecoder;
window.TextEncoder = TextEncoder;
window.ReadableStream = ReadableStream;
1 change: 1 addition & 0 deletions docs/dev/installer.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Accepted options:
[--tenant-namespace <namespace>] Will limit the visibility to the specified namespace only
[--ingress-url <url>] Will create an additional ingress with the specified url
[--ingress-secret <secret>] Will add ssl support to the ingress
[--stream-logs] Will enable log streaming instead of polling
[--output <file>] Will output built manifests in the file instead of in the console
```

Expand Down
4 changes: 4 additions & 0 deletions overlays/patches/installer/deployment-patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@
path: /spec/template/spec/containers/0/args/-
value:
--openshift=--openshift
- op: add
path: /spec/template/spec/containers/0/args/-
value:
--stream-logs=--stream-logs
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"sass-loader": "^8.0.2",
"storybook-react-router": "^1.0.8",
"style-loader": "^0.23.1",
"web-streams-polyfill": "^2.1.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0",
Expand Down
61 changes: 47 additions & 14 deletions packages/components/src/components/Log/Log.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ export class LogContainer extends Component {

componentDidMount() {
this.loadLog();
this.initPolling();
}

componentWillUnmount() {
clearInterval(this.timer);
this.canceled = true;
}

getLogList = () => {
Expand Down Expand Up @@ -92,27 +92,57 @@ export class LogContainer extends Component {
}
};

/* istanbul ignore next */
initPolling = () => {
const { stepStatus, pollingInterval } = this.props;
if (!this.timer && stepStatus && !stepStatus.terminated) {
this.timer = setInterval(() => this.loadLog(), pollingInterval);
readChunks = ({ done, value }, decoder, text = '') => {
if (this.canceled) {
this.reader.cancel();
return undefined;
}
if (this.timer && stepStatus && stepStatus.terminated) {
clearInterval(this.timer);
let logs = text;
if (value) {
logs += decoder.decode(value, { stream: !done });
this.setState({
loading: false,
logs: logs.split('\n')
});
} else {
this.setState(prevState => ({
loading: false,
logs: prevState.logs
}));
}
if (done) {
return undefined;
}
return this.reader
.read()
.then(result => this.readChunks(result, decoder, logs));
};

loadLog = async () => {
const { fetchLogs, intl } = this.props;
const { fetchLogs, intl, stepStatus, pollingInterval } = this.props;
if (fetchLogs) {
try {
const logs = await fetchLogs();
this.setState({
loading: false,
logs: logs ? logs.split('\n') : undefined
});
} catch {
if (logs instanceof ReadableStream) {
const decoder = new TextDecoder();
this.reader = logs.getReader();
await this.reader
.read()
.then(result => this.readChunks(result, decoder))
.catch(error => {
throw error;
});
} else {
this.setState({
loading: false,
logs: logs ? logs.split('\n') : undefined
});
if (stepStatus && !stepStatus.terminated) {
this.timer = setTimeout(() => this.loadLog(), pollingInterval);
}
}
} catch (error) {
console.error(error); // eslint-disable-line no-console
this.setState({
loading: false,
logs: [
Expand All @@ -122,6 +152,9 @@ export class LogContainer extends Component {
})
]
});
if (stepStatus && !stepStatus.terminated) {
this.timer = setTimeout(this.loadLog, pollingInterval);
}
}
}
};
Expand Down
54 changes: 54 additions & 0 deletions packages/components/src/components/Log/Log.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,58 @@ describe('Log', () => {

await waitForElement(() => getByText(/Line 1/i));
});

it('renders the provided content when streaming logs', async () => {
const { getByText } = renderWithIntl(
<Log
stepStatus={{ terminated: { reason: 'Completed' } }}
fetchLogs={() =>
Promise.resolve(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('testing'));
}
})
)
}
/>
);
await waitForElement(() => getByText(/testing/i));
});

it('renders trailer when streaming logs', async () => {
const { getByText } = renderWithIntl(
<Log
stepStatus={{ terminated: { reason: 'Completed' } }}
fetchLogs={() =>
Promise.resolve(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('testing'));
}
})
)
}
/>
);
await waitForElement(() => getByText(/step completed/i));
});

it('renders error trailer when streaming logs', async () => {
const { getByText } = renderWithIntl(
<Log
stepStatus={{ terminated: { reason: 'Error' } }}
fetchLogs={() =>
Promise.resolve(
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('testing'));
}
})
)
}
/>
);
await waitForElement(() => getByText(/step failed/i));
});
});
2 changes: 2 additions & 0 deletions pkg/endpoints/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Properties struct {
ReadOnly bool `json:"ReadOnly"`
LogoutURL string `json:"LogoutURL,omitempty"`
TenantNamespace string `json:"TenantNamespace,omitempty"`
StreamLogs bool `json:"StreamLogs"`
}

const (
Expand Down Expand Up @@ -205,6 +206,7 @@ func (r Resource) GetProperties(request *restful.Request, response *restful.Resp
ReadOnly: r.Options.ReadOnly,
LogoutURL: r.Options.LogoutURL,
TenantNamespace: r.Options.TenantNamespace,
StreamLogs: r.Options.StreamLogs,
}

isTriggersInstalled := isTriggersInstalled(r, triggersNamespace)
Expand Down
1 change: 1 addition & 0 deletions pkg/endpoints/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Options struct {
ReadOnly bool
IsOpenShift bool
LogoutURL string
StreamLogs bool
}

// GetPipelinesNamespace returns the PipelinesNamespace property if set
Expand Down
6 changes: 6 additions & 0 deletions scripts/installer
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ CSRF_SECURE_COOKIE="false"
LOG_LEVEL="info"
LOG_FORMAT="json"
TENANT_NAMESPACE=""
STREAM_LOGS="false"
BASE_RELEASE_URL="https://storage.googleapis.com/tekton-releases/dashboard/previous"

# additional options passed to ko resolve
Expand Down Expand Up @@ -172,6 +173,7 @@ patch() {
replace "--read-only=--read-only" "--read-only=$READONLY"
replace "--namespace=--tenant-namespace" "--namespace=$TENANT_NAMESPACE"
replace "--openshift=--openshift" "--openshift=$OPENSHIFT"
replace "--stream-logs=--stream-logs" "--stream-logs=$STREAM_LOGS"
replace "namespace: tekton-dashboard" "namespace: $INSTALL_NAMESPACE"

if [ "$OPENSHIFT" == "true" ] && [ "$IMAGE_STREAM" == "true" ]; then
Expand Down Expand Up @@ -482,6 +484,7 @@ help () {
echo -e "\t[--tenant-namespace <namespace>]\tWill limit the visibility to the specified namespace only"
echo -e "\t[--ingress-url <url>]\t\t\tWill create an additional ingress with the specified url"
echo -e "\t[--ingress-secret <secret>]\t\tWill add ssl support to the ingress"
echo -e "\t[--stream-logs]\t\t\t\tWill enable log streaming instead of polling"
echo -e "\t[--output <file>]\t\t\tWill output built manifests in the file instead of in the console"
}

Expand Down Expand Up @@ -546,6 +549,9 @@ while [[ $# -gt 0 ]]; do
'--read-only')
READONLY="true"
;;
'--stream-logs')
STREAM_LOGS="true"
;;
'--csrf-secure-cookie')
CSRF_SECURE_COOKIE="true"
;;
Expand Down
29 changes: 18 additions & 11 deletions src/api/comms.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export function getPatchHeaders(headers = {}) {
};
}

function parseBody(response) {
function parseBody(response, stream = false) {
if (stream) {
return response.body;
}
const contentLength = response.headers.get('content-length');
if (contentLength === '0') {
return null;
Expand All @@ -59,18 +62,18 @@ function parseBody(response) {
return response.json();
}

export function checkStatus(response = {}) {
export function checkStatus(response = {}, stream = false) {
if (response.ok) {
switch (response.status) {
case 201:
return {
headers: response.headers,
body: parseBody(response)
body: parseBody(response, stream)
};
case 204:
return {};
default:
return parseBody(response);
return parseBody(response, stream);
}
}

Expand All @@ -88,7 +91,7 @@ function getToken() {
}).then(response => response.headers.get(CSRF_HEADER));
}

export async function request(uri, options = defaultOptions) {
export async function request(uri, options = defaultOptions, stream) {
let token;
if (!CSRF_SAFE_METHODS.includes(options.method)) {
token = await getToken();
Expand All @@ -103,14 +106,18 @@ export async function request(uri, options = defaultOptions) {
...defaultOptions,
...options,
headers
}).then(checkStatus);
}).then(response => checkStatus(response, stream));
}

export function get(uri, headers) {
return request(uri, {
method: 'GET',
headers: getHeaders(headers)
});
export function get(uri, headers, options = {}) {
return request(
uri,
{
method: 'GET',
headers: getHeaders(headers)
},
options.stream
);
}

export function post(uri, body) {
Expand Down
20 changes: 11 additions & 9 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export function getKubeAPI(
'/',
encodeURIComponent(name),
subResource ? `/${subResource}` : '',
queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''
queryParams && Object.keys(queryParams).length > 0
? `?${new URLSearchParams(queryParams).toString()}`
: ''
].join('');
}

Expand Down Expand Up @@ -305,11 +307,11 @@ export function getCondition({ name, namespace }) {
return get(uri);
}

export function getPodLogURL({ container, name, namespace }) {
let queryParams;
if (container) {
queryParams = { container };
}
export function getPodLogURL({ container, name, namespace, follow }) {
const queryParams = {
...(container && { container }),
...(follow && { follow })
};
const uri = `${getKubeAPI(
'pods',
{ name, namespace, subResource: 'log' },
Expand All @@ -318,9 +320,9 @@ export function getPodLogURL({ container, name, namespace }) {
return uri;
}

export function getPodLog({ container, name, namespace }) {
const uri = getPodLogURL({ container, name, namespace });
return get(uri, { Accept: 'text/plain' });
export function getPodLog({ container, name, namespace, stream }) {
const uri = getPodLogURL({ container, name, namespace, follow: stream });
return get(uri, { Accept: 'text/plain' }, { stream });
}

export function rerunPipelineRun(namespace, payload) {
Expand Down
Loading

0 comments on commit 2690114

Please sign in to comment.