Skip to content

Commit

Permalink
[gcp-deployer] Setup oauth2 id & secret; insert service account keys …
Browse files Browse the repository at this point in the history
…via backend service (kubeflow#1255)

* insert service account keys to GKE cluster

* handle review feedbacks;
add client id & secret fields;
manage k8s resource through deployment manager;

* Refactor:
From now on web app create k8s resources through backend service.

* rebase

* patch yaml spec missing pieces, make backend URL configurable through yaml per user's need
  • Loading branch information
kunmingg authored and k8s-ci-robot committed Aug 7, 2018
1 parent 3d268af commit db5d6cd
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 3 deletions.
72 changes: 72 additions & 0 deletions components/gcp-click-to-deploy/kf_app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# run kubectl create -f kf_app.yaml will deploy kubeflow management app's frontend and backend to your GKE cluster.
# to access web UI, do "kubectl proxy" and open
# "http://localhost:8001/api/v1/namespaces/default/services/kubeflow-controller:3000/proxy"
# Note: Do not use 127.0.0.1 to replace above localhost otherwise you cannot login with your account.

apiVersion: v1
data:
# Default config assume user access app via kubectl proxy (default at localhost:8001),
# otherwise change address below to your address pointing to controller-backend
app-config.yaml: |
appAddress: http://localhost:8001/api/v1/namespaces/default/services/kubeflow-controller:8080/proxy
kind: ConfigMap
metadata:
name: kubeflow-controller
namespace: default
---
apiVersion: v1
kind: Service
metadata:
name: kubeflow-controller
namespace: default
spec:
ports:
- name: web-app
port: 3000
- name: backend
port: 8080
selector:
app: kubeflow-controller
type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: kubeflow-controller
name: kubeflow-controller
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: kubeflow-controller
template:
metadata:
labels:
app: kubeflow-controller
namespace: default
spec:
containers:
- name: web-app
image: gcr.io/kubeflow-images-public/test-app:0806
ports:
- containerPort: 3000
volumeMounts:
- name: config-volume
mountPath: /app/src/user_config
- name: controller-backend
image: gcr.io/kubeflow-images-public/bootstrapper:latest
workingDir: /opt/bootstrap
command: [ "/opt/kubeflow/bootstrapper"]
args: [
"--in-cluster",
"--namespace=kubeflow",
"--app-dir=/opt/bootstrap/default"
]
ports:
- containerPort: 8080
volumes:
- name: config-volume
configMap:
name: kubeflow-controller
3 changes: 3 additions & 0 deletions components/gcp-click-to-deploy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@kubernetes/client-node": "^0.3.0",
"@material-ui/core": "^1.3.1",
"@types/request": "^2.47.1",
"glamor": "^2.20.40",
"glamorous": "^4.13.1",
"js-yaml": "^3.12.0",
Expand All @@ -22,6 +24,7 @@
"@types/gapi": "0.0.35",
"@types/gapi.auth2": "0.0.47",
"@types/gapi.client.deploymentmanager": "^2.0.0",
"@types/gapi.client.iam": "^1.0.0",
"@types/jest": "^23.0.0",
"@types/js-yaml": "^3.11.1",
"@types/node": "^10.3.2",
Expand Down
80 changes: 78 additions & 2 deletions components/gcp-click-to-deploy/src/DeployForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TextField from '@material-ui/core/TextField';
import glamorous from 'glamorous';
import * as jsYaml from 'js-yaml';
import * as React from 'react';
import * as request from 'request';
import Gapi from './Gapi';
import { flattenDeploymentOperationError, log, wait } from './Utils';

Expand All @@ -19,6 +20,7 @@ import { flattenDeploymentOperationError, log, wait } from './Utils';
// copy in the latest configs.
import clusterSpecPath from './configs/cluster-kubeflow.yaml';
import clusterJinjaPath from './configs/cluster.jinja';
import appConfigPath from './user_config/app-config.yaml';

// TODO(jlewi): For the FQDN we should have a drop down box to select custom
// domain or automatically provisioned domain. Based on the response if the user
Expand All @@ -34,6 +36,8 @@ interface DeployFormState {
project: string;
showLogs: boolean;
zone: string;
clientId: string;
clientSecret: string;
}

const Text = glamorous.div({
Expand Down Expand Up @@ -101,10 +105,13 @@ export default class DeployForm extends React.Component<any, DeployFormState> {

private _clusterJinja = '';
private _clusterSpec: any;
private _configSpec: any;

constructor(props: any) {
super(props);
this.state = {
clientId: '',
clientSecret: '',
deploymentName: 'kubeflow',
dialogBody: '',
dialogTitle: '',
Expand Down Expand Up @@ -152,6 +159,23 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
.catch((error) => {
log('Request failed', error);
});
fetch(appConfigPath, { mode: 'no-cors' })
.then((response) => {
log('Got response');
return response.text();
})
.then((text) => {
this._configSpec = jsYaml.safeLoad(text);
// log('Loaded clusterSpecPath successfully');
})
.catch((error) => {
log('Request failed', error);
this.setState({
dialogBody: 'Failed to load user config file.',
dialogTitle: 'Config loading Error',
});
return;
});

}

Expand All @@ -175,6 +199,12 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
<Row>
<Input name="hostName" label="Hostname" spellCheck={false} value={this.state.hostName} onChange={this._handleChange.bind(this)} />
</Row>
<Row>
<Input name="clientId" label="Web App Client Id" spellCheck={false} value={this.state.clientId} onChange={this._handleChange.bind(this)} />
</Row>
<Row>
<Input name="clientSecret" label="Web App Client Secret" spellCheck={false} value={this.state.clientSecret} onChange={this._handleChange.bind(this)} />
</Row>

<div style={{ display: 'flex', padding: '20px 60px 40px' }}>
<DeployBtn variant="contained" color="primary" onClick={this._createDeployment.bind(this)}>
Expand Down Expand Up @@ -247,6 +277,8 @@ export default class DeployForm extends React.Component<any, DeployFormState> {

kubeflow.name = this.state.deploymentName;
kubeflow.properties.zone = this.state.zone;
kubeflow.properties.clientId = btoa(this.state.clientId);
kubeflow.properties.clientSecret = btoa(this.state.clientSecret);

const config: any = jsYaml.safeLoad(kubeflow.properties.bootstrapperConfig);

Expand Down Expand Up @@ -391,7 +423,7 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
this._appendLine('Starting new deployment..');

const deploymentName = this.state.deploymentName;
Gapi.deploymentmanager.insert(project, resource)
await Gapi.deploymentmanager.insert(project, resource)
.then(res => {
this._appendLine('Result of create deployment operation:\n' + JSON.stringify(res));
this._monitorDeployment(project, deploymentName);
Expand All @@ -404,6 +436,49 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
});
});


// Step 4: In-cluster resources set up
let status = '';
let getAttempts = 0;
const getTimeout = 15000;
do {
getAttempts++;
const curStatus = await Gapi.deploymentmanager.get(this.state.project, deploymentName)
.catch(err => {
this._appendLine('Cluster endpoint not available yet.');
});
if (!curStatus) {
await wait(getTimeout);
continue;
}
status = curStatus.operation!.status!;
} while (status !== 'DONE' && getAttempts < 20);

const token = await Gapi.getToken();
await request(
{
body: JSON.stringify(
{
namespace: 'kubeflow',
project,
secretKey: 'admin-gcp-sa.json',
secretName: 'admin-gcp-sa',
serviceAccount: `${deploymentName}-admin@${project}.iam.gserviceaccount.com`,
token,
}
),
headers: { 'content-type': 'application/json' },
method: 'PUT',
uri: this._configSpec.appAddress + '/insertSaKey',
},
(error, response, body) => {
if (!error) {
this._appendLine('Service Account Key inserted.');
} else {
this._appendLine('error: ' + response.statusCode);
}
}
);
}

/**
Expand All @@ -414,6 +489,7 @@ export default class DeployForm extends React.Component<any, DeployFormState> {

const servicesToEnable = new Set([
'deploymentmanager.googleapis.com',
'container.googleapis.com',
'cloudresourcemanager.googleapis.com',
'endpoints.googleapis.com',
'iam.googleapis.com',
Expand All @@ -437,7 +513,7 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
'deployment failed with error:' + flattenDeploymentOperationError(r.operation!));
clearInterval(monitorInterval);
} else {
this._appendLine(r.operation!.status!);
this._appendLine(`Status of ${deploymentName}: ` + r.operation!.status!);
}
})
.catch(err => this._appendLine('deployment failed with error:' + err));
Expand Down
8 changes: 7 additions & 1 deletion components/gcp-click-to-deploy/src/Gapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,18 @@ export default class Gapi {
path: `https://cloudresourcemanager.googleapis.com/v1/projects/${projectId}`
}).then(response => (response.result as any).projectNumber as number,
badResult => {
throw new Error('Errors enabling service: ' + flattenDeploymentOperationError(badResult.result));
throw new Error('Error trying to get the project number: ' + JSON.stringify(badResult));
});
}

};

public static async getToken(): Promise<string | null> {
await this.load();
const user = await this._getCurrentUser();
return user ? user.getAuthResponse(true).access_token : null;
}

public static async signIn(doPrompt?: boolean): Promise<void> {
const rePromptOptions = 'login consent select_account';
const promptFlags = doPrompt ? rePromptOptions : '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ resources:
ipName: kubeflow-ip
# Name of the service account to use for k8s worker node pools
vmServiceAccountName: kubeflow-service-account
# For oauth2, will be automated soon.
clientId: clientId
clientSecret: clientSecret
# Provide the config for the bootstrapper. This should be a string
# containing the YAML spec for the bootstrapper.
#
Expand Down
27 changes: 27 additions & 0 deletions components/gcp-click-to-deploy/src/configs/cluster.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ limitations under the License.
{% set NAMESPACE_COLLECTION = '/api/v1/namespaces' %}
{% set CM_COLLECTION = COLLECTION_PREFIX + 'configmaps' %}
{% set RC_COLLECTION = COLLECTION_PREFIX + 'replicationcontrollers' %}
{% set SECRETS_COLLECTION = COLLECTION_PREFIX + 'secrets' %}
{% set SERVICE_COLLECTION = COLLECTION_PREFIX + 'services' %}
{% set PVC_COLLECTION = COLLECTION_PREFIX + 'persistentvolumeclaims' %}
{% set STATEFULSETS_COLLECTION = '/apis/apps/v1/namespaces/{namespace}/statefulsets' %}
Expand Down Expand Up @@ -318,6 +319,16 @@ the corresponding type provider.
name: kubeflow-admin
spec:

{# Namespace for kubeflow. #}
- name: kf-namespace
type: {{ env['project'] }}/$(ref.{{ TYPE_NAME }}.name):{{ NAMESPACE_COLLECTION }}
properties:
apiVersion: v1
kind: Namespace
metadata:
name: kubeflow
spec:

{# The deployment manager uses the cloudservices account. We need to create
a cluster role binding making the cloudservices account cluster admin
so that we can then create other cluster role bindings.
Expand Down Expand Up @@ -388,6 +399,22 @@ the corresponding type provider.
dependsOn:
- admin-namespace

- name: kubeflow-oauth-key
type: {{ env['project'] }}/$(ref.{{ TYPE_NAME }}.name):{{ SECRETS_COLLECTION }}
properties:
apiVersion: v1
kind: Secret
namespace: {{ properties["namespace"] }}
metadata:
name: kubeflow-oauth
type: Opaque
data:
CLIENT_ID: {{ properties["clientId"] }}
CLIENT_SECRET: {{ properties["clientSecret"] }}
metadata:
dependsOn:
- kf-namespace

{# ConfigMap for the bootstrapper #}
- name: bootstrap-configmap
type: {{ env['project'] }}/$(ref.{{ TYPE_NAME }}.name):{{ CM_COLLECTION }}
Expand Down

0 comments on commit db5d6cd

Please sign in to comment.