Skip to content

Commit

Permalink
make IAP optional for click-deploy app (kubeflow#1927)
Browse files Browse the repository at this point in the history
* make IAP optional for click-deploy app

* more

* automate cloud shell

* frontend edit following review feedbacks

* add input check when user choose to setup IAP
  • Loading branch information
kunmingg authored and k8s-ci-robot committed Nov 20, 2018
1 parent 8e9ea86 commit ebca47d
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 22 deletions.
37 changes: 29 additions & 8 deletions bootstrap/cmd/bootstrap/app/ksServer.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,23 @@ import (
"io/ioutil"
"math/rand"
"github.com/cenkalti/backoff"
"bytes"
)

// The name of the prototype for Jupyter.
const JupyterPrototype = "jupyterhub"

// root dir of local cached VERSIONED REGISTRIES
const CachedRegistries = "/opt/versioned_registries"
const CloudShellTemplatePath = "/opt/registries/kubeflow/deployment/gke/cloud_shell_templates"

// key used for storing start time of a request to deploy in the request contexts
const StartTime = "StartTime"

const KubeflowRegName = "kubeflow"
const KubeflowFolder = "ks_app"
const DmFolder = "gcp_config"
const CloudShellFolder = "kf_util"

// KsService defines an interface for working with ksonnet.
type KsService interface {
Expand Down Expand Up @@ -329,12 +332,6 @@ func (s *CreateRequest) Validate() error {
if len(s.Project) == 0 {
missings = append(missings, "Project")
}
if len(s.ClientId) == 0 {
missings = append(missings, "Web App Client ID")
}
if len(s.ClientSecret) == 0 {
missings = append(missings, "Web App Client Secret")
}
if len(missings) == 0 {
return nil
}
Expand Down Expand Up @@ -501,8 +498,9 @@ func (s *ksServer) CreateApp(ctx context.Context, request CreateRequest, dmDeplo
}

if dmDeploy != nil {
s.UpdateDmConfig(repoDir, request.Project, request.Name, kfVersion, dmDeploy)
UpdateDmConfig(repoDir, request.Project, request.Name, kfVersion, dmDeploy)
}
UpdateCloudShellConfig(repoDir, request.Project, request.Name, kfVersion, request.Zone)
err = s.SaveAppToRepo(request.Project, request.Email, repoDir)
if err != nil {
log.Errorf("There was a problem saving config to cloud repo; %v", err)
Expand Down Expand Up @@ -840,7 +838,7 @@ func (s *ksServer) GetApp(project string, appName string, kfVersion string, toke

// Save ks app config local changes to project source repo.
// Not thread safe, be aware when call it.
func (s *ksServer) UpdateDmConfig(repoDir string, project string, appName string, kfVersion string, dmDeploy *deploymentmanager.Deployment) error {
func UpdateDmConfig(repoDir string, project string, appName string, kfVersion string, dmDeploy *deploymentmanager.Deployment) error {
confDir := path.Join(repoDir, GetRepoName(project), kfVersion, appName, DmFolder)
if err := os.RemoveAll(confDir); err != nil {
return err
Expand All @@ -859,6 +857,29 @@ func (s *ksServer) UpdateDmConfig(repoDir string, project string, appName string
return nil
}

// Save cloud shell config to project source repo.
func UpdateCloudShellConfig(repoDir string, project string, appName string, kfVersion string, zone string) error {
confDir := path.Join(repoDir, GetRepoName(project), kfVersion, appName, CloudShellFolder)
if err := os.RemoveAll(confDir); err != nil {
return err
}
if err := os.MkdirAll(confDir, os.ModePerm); err != nil {
return err
}
for _, filename := range([]string{"conn.sh", "conn.md"}) {
data, err := ioutil.ReadFile(path.Join(CloudShellTemplatePath, filename))
if err != nil {
return err
}
data = bytes.Replace(data, []byte("project_id_placeholder"), []byte(project), -1)
data = bytes.Replace(data, []byte("zone_placeholder"), []byte(zone), -1)
data = bytes.Replace(data, []byte("deploy_name_placeholder"), []byte(appName), -1)
if err := ioutil.WriteFile(path.Join(confDir, filename), data, os.ModePerm); err != nil {
return err
}
}
return nil
}

// Save ks app config local changes to project source repo.
// Not thread safe, be aware when call it.
Expand Down
27 changes: 24 additions & 3 deletions components/gcp-click-to-deploy/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,44 @@ class App extends React.Component<any, { signedIn: boolean }> {
<div>
To deploy Kubeflow on Google Cloud Platform:
</div>
<br />
<div>
<ul>
<li> Enter the Project ID of the GCP project you want to use </li>
<li> Pick a name for your deployment </li>
<li> Follow these
<li> (Optional / Recommended) Follow these
<a href="https://www.kubeflow.org/docs/started/getting-started-gke/#create-oauth-client-credentials"
style={{ color: 'inherit', marginLeft: 5 }}
target="_blank"
>
instructions</a> to create an OAuth client and
then enter as Web App Client ID and Secret</li>
then enter as IAP Oauth Client ID and Secret</li>
<li> (Optional) Choose GKE zone where you want Kubeflow to be deployed </li>
<li> (Optional) Choose Kubeflow version </li>
<li> Click Create Deployment </li>
</ul>
</div>
<div>
To connect to deployed Kubeflow cluster:
</div>
<div>
<ul>
<li> If you configured IAP Oauth Client ID and Secret: </li>
</ul>
<ul>
<ul>
<li> Click IAP Access (might need up to 20 minutes for domain and IAP to be setup) </li>
</ul>
</ul>
<ul>
<li> If you checked Skip IAP for your deployment: </li>
</ul>
<ul>
<ul>
<li> Click Cloud Shell; follow instructions on right side of the new tab.
</li>
</ul>
</ul>
</div>
<div>
Notice:
<ul>
Expand Down
107 changes: 96 additions & 11 deletions components/gcp-click-to-deploy/src/DeployForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface DeployFormState {
kfverison: string;
clientId: string;
clientSecret: string;
iap: boolean;
}

const Text = glamorous.div({
Expand All @@ -55,6 +56,13 @@ const logsContainerStyle = (show: boolean) => {
} as React.CSSProperties;
};

const IapElementStyle = (show: boolean) => {
return {
display: show ? 'inline' : 'none',
minHeight: 0,
} as React.CSSProperties;
};

const logsToggle: React.CSSProperties = {
color: '#fff',
fontWeight: 'bold',
Expand Down Expand Up @@ -110,6 +118,7 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
deploymentName: 'kubeflow',
dialogBody: '',
dialogTitle: '',
iap: true,
kfverison: 'v0.3.2',
project: '',
showLogs: false,
Expand Down Expand Up @@ -154,13 +163,17 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
<Row>
<Input name="deploymentName" label="Deployment name" spellCheck={false} value={this.state.deploymentName} 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 style={{ minHeight: 20}}>
<input style={{ fontSize: '1.1em', margin: '0% 1% 0% 11%' }} type="checkbox" onChange={this._handleCheck.bind(this)} />
<label style={{ minHeight: 20 }} >Skip IAP</label>
</Row>
<Row>
<Input name="clientSecret" label="Web App Client Secret" spellCheck={false} value={this.state.clientSecret} onChange={this._handleChange.bind(this)} />
<Row style={{ minHeight: 0 }}>
<Input style={IapElementStyle(this.state.iap)} name="clientId" label="IAP Oauth Client ID" spellCheck={false} value={this.state.clientId} onChange={this._handleChange.bind(this)} />
</Row>
<Row>
<Row style={{ minHeight: 0 }}>
<Input style={IapElementStyle(this.state.iap)} name="clientSecret" label="IAP Oauth Client Secret" spellCheck={false} value={this.state.clientSecret} onChange={this._handleChange.bind(this)} />
</Row>
<Row style={{ minHeight: 20}}>
<Text style={{ fontSize: '1.1em', margin: '2% 11%' }}>GKE Zone: </Text>
<select name="zone" style={{ display: 'flex', fontSize: '1.1em', margin: '2% 10.5%',}} spellCheck={false} value={this.state.zone} onChange={this._handleChange.bind(this)} >
<option value="us-central1-a">us-central1-a</option>
Expand All @@ -174,7 +187,7 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
<option value="asia-east1-b">asia-east1-b</option>
</select>
</Row>
<Row>
<Row style={{ minHeight: 20}}>
<Text style={{ fontSize: '1.1em', margin: '2% 11%' }}>Kubeflow Version:</Text>
<select name="kfverison" style={{ display: 'flex', fontSize: '1.1em', margin: '2% 1%',}} spellCheck={false} value={this.state.kfverison} onChange={this._handleChange.bind(this)} >
<option value="v0.3.2">v0.3.2</option>
Expand All @@ -185,6 +198,14 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
Create Deployment
</DeployBtn>

<DeployBtn style={IapElementStyle(this.state.iap)} variant="contained" color="default" onClick={this._iapAddress.bind(this)}>
IAP Access
</DeployBtn>

<DeployBtn style={IapElementStyle(!this.state.iap)} variant="contained" color="default" onClick={this._cloudShell.bind(this)}>
Cloud Shell
</DeployBtn>

<YamlBtn variant="outlined" color="default" onClick={this._showYaml.bind(this)}>
View YAML
</YamlBtn>
Expand Down Expand Up @@ -249,8 +270,9 @@ export default class DeployForm extends React.Component<any, DeployFormState> {

const state = this.state;
const email = await Gapi.getSignedInEmail();

this._configSpec.defaultApp.parameters.forEach((p: any) => {
let iapIdx = 0;
for (let i = 0, len = this._configSpec.defaultApp.parameters.length; i < len; i ++){
const p = this._configSpec.defaultApp.parameters[i];
if (p.name === 'ipName') {
p.value = this.state.deploymentName + '-ip';
}
Expand All @@ -262,12 +284,51 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
if (p.name === 'acmeEmail') {
p.value = email;
}
});

if (p.name === 'jupyterHubAuthenticator') {
iapIdx = i;
}
}
if (this.state.clientId === '' || this.state.clientSecret === '') {
this._configSpec.defaultApp.parameters.splice(iapIdx, 1);
}
this._configSpec.defaultApp.registries[0].version = this.state.kfverison;

return this._configSpec;
}

private async _cloudShell() {
const key = 'project';
if (this.state[key] === '') {
this.setState({
dialogBody: 'project id is missing',
dialogTitle: 'Missing field',
});
return;
}
const cloudShellConfPath = this.state.kfverison + '/' + this.state.deploymentName + '/kf_util';
const cloudShellUrl = 'https://cloud.google.com/console/cloudshell/open?shellonly=true&git_repo=https://source.developers.google.com/p/' +
this.state.project + '/r/' + this.state.project + '-kubeflow-config&working_dir=' + cloudShellConfPath + '&tutorial=conn.md';
window.open(cloudShellUrl, '_blank');
}

private async _iapAddress() {
for (const prop of ['project', 'deploymentName']) {
if (this.state[prop] === '') {
this.setState({
dialogBody: 'Some required fields (project, deploymentName) are missing',
dialogTitle: 'Missing field',
});
return;
}
}
this.setState({
showLogs: true,
});
const dashboardUri = 'https://' + this.state.deploymentName + '.endpoints.' + this.state.project + '.cloud.goog/';
this._redirectToKFDashboard(dashboardUri);
}

// Create a Kubeflow deployment.
private async _createDeployment() {
for (const prop of ['project', 'zone', 'deploymentName']) {
Expand All @@ -279,6 +340,17 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
return;
}
}
if (this.state.iap) {
for (const prop of ['clientId', 'clientSecret']) {
if (this.state[prop] === '') {
this.setState({
dialogBody: 'Some required fields (IAP Oauth Client ID, IAP Oauth Client Secret) are missing',
dialogTitle: 'Missing field',
});
return;
}
}
}
const deploymentNameKey = 'deploymentName';
if (this.state[deploymentNameKey].length < 4 || this.state[deploymentNameKey].length > 20) {
this.setState({
Expand Down Expand Up @@ -471,8 +543,14 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
} else if (r.operation!.status! && r.operation!.status === 'DONE') {
const readyTime = new Date();
readyTime.setTime(readyTime.getTime() + (20 * 60 * 1000));
this._appendLine('Deployment is done, your kubeflow app url should be ready within 20 minutes (by '
+ readyTime.toLocaleTimeString() + '): ' + dashboardUri);
this._appendLine('Deployment initialized, configuring environment');
if (this.state.clientId === '' || this.state.clientSecret === '') {
this._appendLine('(IAP skipped), cluster should be ready within 5 minutes. To connect to cluster, click cloud shell and follow instruction');
} else {
this._appendLine('your kubeflow app url should be ready within 20 minutes (by '
+ readyTime.toLocaleTimeString() + '): https://'
+ this.state.deploymentName + '.endpoints.' + this.state.project + '.cloud.goog');
}
clearInterval(monitorInterval);
this._redirectToKFDashboard(dashboardUri);
} else {
Expand All @@ -491,6 +569,7 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
// an image served by the target site, the img load is a simple html
// request and not an AJAX request, thus bypassing the CORS in this
// case.
this._appendLine('Validating if IAP is up and running...');
const imgUri = dashboardUri + 'hub/logo';
const startTime = new Date().getTime() / 1000;
const img = document.createElement('img');
Expand Down Expand Up @@ -523,4 +602,10 @@ export default class DeployForm extends React.Component<any, DeployFormState> {
} as any);
}

private _handleCheck(event: Event) {
this.setState({
['iap']: !this.state.iap
});
}

}
5 changes: 5 additions & 0 deletions deployment/gke/cloud_shell_templates/conn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# To establish connection to your kubeflow cluster

## Run following command in your cloud shell terminal

**./conn.sh**
22 changes: 22 additions & 0 deletions deployment/gke/cloud_shell_templates/conn.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

# conn.sh - establish connection to kubeflow cluster

set -e # exit on error

PROJECT_ID=project_id_placeholder
ZONE=zone_placeholder
DEPLOY_NAME=deploy_name_placeholder

gcloud container clusters get-credentials ${DEPLOY_NAME} --zone ${ZONE} --project ${PROJECT_ID}

echo "Checking load balancing resource"
for i in $(seq 1 10)
do kubectl get pods -n kubeflow --selector=service=ambassador --field-selector=status.phase=Running | grep -q 'Running' && break || sleep 10
done

POD_NAME=$(kubectl get pods -n kubeflow --selector=service=ambassador --field-selector=status.phase=Running -o jsonpath="{.items[0].metadata.name}")

echo "Load balancing now ready"
echo "Your kubeflow service now accessible via: https://ssh.cloud.google.com/devshell/proxy?authuser=0&port=8081"
kubectl port-forward -n kubeflow ${POD_NAME} 8081:80

0 comments on commit ebca47d

Please sign in to comment.