Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
# Management Service API
./management-service/bin

# XP UI
./ui/build
57 changes: 28 additions & 29 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Build xp-management binary
FROM golang:1.16-alpine as api-builder
ARG API_BIN_NAME
ARG API_BIN_NAME=xp-management

ENV GOOS=linux GOARCH=amd64

Expand All @@ -12,41 +12,40 @@ RUN chmod +x ./bin/${API_BIN_NAME}

COPY ./api/*.yaml ./bin/

# Build optimized static xp-ui bundle
FROM node:16 as ui-builder

ARG REACT_APP_ENVIRONMENT
ARG REACT_APP_XP_API="/v1"
ARG REACT_APP_MLP_API
ARG REACT_APP_OAUTH_CLIENT_ID
ARG REACT_APP_SENTRY_DSN
ARG REACT_APP_USER_DOCS_URL

RUN mkdir -p /opt/xp_ui
COPY ./ui /opt/xp_ui
WORKDIR /opt/xp_ui

ENV NODE_OPTIONS "--max_old_space_size=4096"

RUN yarn install --network-concurrency 1
RUN yarn build

# Clean image with xp-management binary and production build of xp-ui
FROM alpine:latest

ARG API_BIN_NAME
ENV API_BIN_NAME ${API_BIN_NAME}
ENV XP_UI_APP_DIRECTORY "./xp-ui"
# Install bash
USER root
RUN apk add --no-cache bash

ARG API_BIN_NAME=xp-management
ARG XP_UI_DIST_PATH=ui/build

ENV XPUICONFIG_SERVINGDIRECTORY "/app/xp-ui"
ENV XP_PORT "8080"
ENV XP_USER "xp"
ENV XP_USER_GROUP "app"

EXPOSE ${XP_PORT}

RUN addgroup -S app && adduser -S app -G app
USER app
RUN addgroup -S ${XP_USER_GROUP} \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any particular reasons why creating user and group is required?

I would thought its for security reasons why the user and group are created to limit which user can execute the binaries in entrypoint.sh, or is this configure there via USER ${XP_USER} ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, containers run as root. A container running as root has full control of the host system. If a service can run without privileges, ideally, we should use a non-root user to restrict privileges. This works in our case, since we're essentially just launching an application service and don’t require root access.

USER does not create a non-root user for use, it simply switches from default ROOT user to the specified non-root user. Hence, we have to create it before using it.

&& adduser -S ${XP_USER} -G ${XP_USER_GROUP} -H \
&& mkdir /app \
&& chown -R ${XP_USER}:${XP_USER_GROUP} /app

COPY --chown=${XP_USER}:${XP_USER_GROUP} --from=api-builder /app/bin/* /app/
COPY --chown=${XP_USER}:${XP_USER_GROUP} --from=api-builder /app/database /app/database/

USER ${XP_USER}
WORKDIR /app

COPY --from=api-builder /app/bin/* ./
COPY --from=api-builder /app/database ./database
COPY --from=ui-builder /opt/xp_ui/build ${XP_UI_APP_DIRECTORY}/
# UI must be built outside docker
COPY --chown=${XP_USER}:${XP_USER_GROUP} ${XP_UI_DIST_PATH} ${XPUICONFIG_SERVINGDIRECTORY}/

COPY ./docker-entrypoint.sh ./

ENV XP_API_BIN "./${API_BIN_NAME}"
ENV XP_UI_DIST_DIR ${XPUICONFIG_SERVINGDIRECTORY}

ENTRYPOINT [ "./xp-management" ]
ENTRYPOINT [ "./docker-entrypoint.sh" ]
69 changes: 69 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/bash
set -e

CMD=()
XP_UI_CONFIG_PATH=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be captured as part of the Helm chart, in deployment.yaml. I'll direct you to this in the subsequent PR.

XP_UI_DIST_DIR=${XP_UI_DIST_DIR:-}
XP_UI_DIST_CONFIG_FILE=${XP_UI_DIST_DIR}/app.config.js
XP_API_BIN=${XP_API_BIN:?"ERROR: XP_API_BIN is not specified"}

show_help() {
cat <<EOF
Usage: $(basename "$0") <options> <...>
-ui-config JSON file containing configuration of XP UI
EOF
}

main(){
parse_command_line "$@"

if [[ -n "$XP_UI_CONFIG_PATH" ]]; then
echo "XP UI config found at ${XP_UI_CONFIG_PATH}..."
if [[ -n "$XP_UI_DIST_DIR" ]]; then
echo "Overriding UI config at $XP_UI_DIST_CONFIG_FILE"

echo "var xpConfig = $(cat $XP_UI_CONFIG_PATH);" > "$XP_UI_DIST_CONFIG_FILE"

echo "Done."
else
echo "XP_UI_DIST_DIR: XP UI static build directory not provided. Skipping."
fi
else
echo "XP UI config is not provided. Skipping."
fi
}

parse_command_line(){
while [[ $# -gt 0 ]]; do
case "$1" in
-ui-config)
if [[ -n "$2" ]]; then
XP_UI_CONFIG_PATH="$2"
shift
else
echo "ERROR: '-ui-config' cannot be empty." >&2
show_help
exit 1
fi
;;
*)
CMD+=("$1")
;;
esac

shift
done

if [[ -n "$XP_UI_CONFIG_PATH" ]]; then
if [ ! -f "$XP_UI_CONFIG_PATH" ]; then
echo "ERROR: config file $XP_UI_CONFIG_PATH does not exist." >&2
show_help
exit 1
fi
fi
}

main "$@"

echo "Launching xp-api server: " "$XP_API_BIN" "${CMD[@]}"
exec "$XP_API_BIN" "${CMD[@]}"
3 changes: 2 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"json-bigint": "1.0.0",
"lint-staged": "^11.1.2",
"moment": "^2.29.0",
"object-assign-deep": "^0.4.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"resize-observer-polyfill": "^1.5.1",
Expand Down Expand Up @@ -70,4 +71,4 @@
"last 1 safari version"
]
}
}
}
41 changes: 41 additions & 0 deletions ui/public/app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
var xpConfig = {
"apiConfig": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible we set default for all (less auth)? im assuming user need to overwrite this file

Copy link
Contributor Author

@terryyylim terryyylim Jun 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, user should override this file via configuring uiConfig helm value, and let entrypoint.sh script inject the values during runtime. The helm chart will be added in a follow-up PR.

I've left sensible default values that are commented out for values which are configurable, I think that should suffice? Unless there's certain values you think I should further include.

/*
* Timeout (in milliseconds) for requests to API
* "apiTimeout": 10000,
*
* Endpoint to XP API
* "xpApiUrl": "/api/xp/v1",
*
* Endpoint to MLP API
* "mlpApiUrl": "/api/v1"
*/
},

"authConfig": {
/*
* OAuth2 Client ID
* "oauthClientId": "CLIENT_ID"
*/
},

"appConfig": {
/*
* Environment name
* "environment": "dev",
*
* Default page for documentation
* "docsUrl": "https://github.com/gojek/xp"
*/
},

"sentryConfig": {
/*
* DSN of Sentry project
* "dsn": "SENTRY_DSN",
*
* Sentry environment (if it's different from appConfig.environment)
* "environment": "dev"
*/
}
};
5 changes: 5 additions & 0 deletions ui/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<meta name="theme-color" content="#000000" />
<meta name="description" content="Machine Learning Platform" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon-32x32.png" />
<!--
app.config.js contains user-provided configuration to customize default
application settings
-->
<script type="text/javascript" src="%PUBLIC_URL%/app.config.js"></script>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
Expand Down
79 changes: 41 additions & 38 deletions ui/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,50 @@ import {
} from "@gojek/mlp-ui";
import { Redirect, Router } from "@reach/router";

import { apiConfig, appConfig, authConfig } from "config";
import { useConfig } from "config";
import ExperimentsLandingPage from "experiments/ExperimentsLandingPage";
import Home from "Home";
import { PrivateLayout } from "PrivateLayout";

const App = () => (
<ErrorBoundary>
<MlpApiContextProvider
mlpApiUrl={apiConfig.mlpApiUrl}
timeout={apiConfig.apiTimeout}>
<AuthProvider clientId={authConfig.oauthClientId}>
<Router role="group">
<Login path="/login" />

<Redirect from="/" to={appConfig.homepage} noThrow />

<Redirect
from={`${appConfig.homepage}/projects/:projectId`}
to={`${appConfig.homepage}/projects/:projectId/experiments`}
noThrow
/>

{/* HOME */}
<PrivateRoute
path={appConfig.homepage}
render={PrivateLayout(Home)}
/>

{/* EXPERIMENTS */}
<PrivateRoute
path={`${appConfig.homepage}/projects/:projectId/experiments/*`}
render={PrivateLayout(ExperimentsLandingPage)}
/>

{/* DEFAULT */}
<Empty default />
</Router>
<Toast />
</AuthProvider>
</MlpApiContextProvider>
</ErrorBoundary>
);
const App = () => {
const { apiConfig, appConfig, authConfig } = useConfig();
return (
<ErrorBoundary>
<MlpApiContextProvider
mlpApiUrl={apiConfig.mlpApiUrl}
timeout={apiConfig.apiTimeout}>
<AuthProvider clientId={authConfig.oauthClientId}>
<Router role="group">
<Login path="/login" />

<Redirect from="/" to={appConfig.homepage} noThrow />

<Redirect
from={`${appConfig.homepage}/projects/:projectId`}
to={`${appConfig.homepage}/projects/:projectId/experiments`}
noThrow
/>

{/* HOME */}
<PrivateRoute
path={appConfig.homepage}
render={PrivateLayout(Home)}
/>

{/* EXPERIMENTS */}
<PrivateRoute
path={`${appConfig.homepage}/projects/:projectId/experiments/*`}
render={PrivateLayout(ExperimentsLandingPage)}
/>

{/* DEFAULT */}
<Empty default />
</Router>
<Toast />
</AuthProvider>
</MlpApiContextProvider>
</ErrorBoundary>
);
};

export default App;
28 changes: 16 additions & 12 deletions ui/src/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import React, { Fragment } from "react";

import { EuiEmptyPrompt } from "@elastic/eui";

import { appConfig } from "config";
import { useConfig } from "config";

const Home = () => (
<EuiEmptyPrompt
iconType={appConfig.appIcon}
title={<h2>XP: Experimentation Platform</h2>}
body={
<Fragment>
<p>To start off, please select a project from the dropdown.</p>
</Fragment>
}
/>
);
const Home = () => {
const { appConfig } = useConfig();

return (
<EuiEmptyPrompt
iconType={appConfig.appIcon}
title={<h2>XP: Experimentation Platform</h2>}
body={
<Fragment>
<p>To start off, please select a project from the dropdown.</p>
</Fragment>
}
/>
);
};

export default Home;
3 changes: 2 additions & 1 deletion ui/src/PrivateLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
} from "@gojek/mlp-ui";
import { navigate } from "@reach/router";

import { appConfig } from "config";
import { useConfig } from "config";

export const PrivateLayout = (Component) => {
const { appConfig } = useConfig();
return (props) => (
<ApplicationsContextProvider>
<ProjectsContextProvider>
Expand Down
26 changes: 21 additions & 5 deletions ui/src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,30 @@ import ReactDOM from "react-dom";
import * as Sentry from "@sentry/browser";

import App from "App";
import { sentryConfig } from "config";
import * as serviceWorker from "serviceWorker";

Sentry.init(sentryConfig);
// Set custom tag 'app', for filtering
Sentry.setTag("app", "xp-ui");
import { ConfigProvider, useConfig } from "./config";

ReactDOM.render(<App />, document.getElementById("root"));
const SentryApp = ({ children }) => {
const {
sentryConfig: { dsn, environment },
} = useConfig();

Sentry.init({ dsn, environment });
Sentry.setTag("app", "xp-ui");

return children;
};

const XPUI = () => (
<ConfigProvider>
<SentryApp>
<App />
</SentryApp>
</ConfigProvider>
);

ReactDOM.render(XPUI(), document.getElementById("root"));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
Expand Down
Loading