├── .bowerrc ├── .gitignore ├── .jshintrc ├── .openshift ├── action_hooks │ └── README.md ├── cron │ ├── README.cron │ ├── daily │ │ └── .gitignore │ ├── hourly │ │ └── .gitignore │ ├── minutely │ │ └── .gitignore │ ├── monthly │ │ └── .gitignore │ └── weekly │ │ ├── README │ │ ├── chrono.dat │ │ ├── chronograph │ │ ├── jobs.allow │ │ └── jobs.deny └── markers │ └── .gitkeep ├── .travis.yml ├── Gruntfile.js ├── Jenkinsfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── app ├── config.template ├── css │ ├── app.css │ └── cockpit.css ├── forms │ ├── delete-cluster.html │ ├── new-cluster.html │ └── scale-cluster.html ├── index.html ├── js │ ├── DataService.js │ ├── app.js │ ├── clusterops.js │ ├── controllers.js │ ├── dialog.js │ ├── directives.js │ ├── factories.js │ └── listing.js ├── login.html └── partials │ ├── cluster-body.html │ ├── cluster-detail.html │ ├── cluster-panel.html │ ├── clusters.html │ ├── detail-body.html │ └── login.html ├── bower.json ├── image.yaml ├── make-build-dir.sh ├── modules ├── app │ ├── added │ │ └── app │ │ │ ├── config.template │ │ │ ├── css │ │ │ ├── forms │ │ │ ├── index.html │ │ │ ├── js │ │ │ └── partials │ ├── install │ └── module.yaml ├── chown │ ├── install │ └── module.yaml ├── launch │ ├── added │ │ ├── launch.sh │ │ ├── server.js │ │ └── sparkimage.sh │ ├── install │ └── module.yaml ├── npm_bower │ ├── added │ │ ├── bower.json │ │ └── package.json │ ├── install │ └── module.yaml ├── oc │ ├── check_for_download │ ├── install │ └── module.yaml └── update_os │ ├── install │ └── module.yaml ├── oshinko-webui-build ├── Dockerfile ├── help.md ├── modules │ ├── app │ │ ├── added │ │ │ └── app │ │ │ │ ├── config.template │ │ │ │ ├── css │ │ │ │ ├── app.css │ │ │ │ └── cockpit.css │ │ │ │ ├── forms │ │ │ │ ├── delete-cluster.html │ │ │ │ ├── new-cluster.html │ │ │ │ └── scale-cluster.html │ │ │ │ ├── index.html │ │ │ │ ├── js │ │ │ │ ├── DataService.js │ │ │ │ ├── app.js │ │ │ │ ├── clusterops.js │ │ │ │ ├── controllers.js │ │ │ │ ├── dialog.js │ │ │ │ ├── directives.js │ │ │ │ ├── factories.js │ │ │ │ └── listing.js │ │ │ │ └── partials │ │ │ │ ├── cluster-body.html │ │ │ │ ├── cluster-detail.html │ │ │ │ ├── cluster-panel.html │ │ │ │ ├── clusters.html │ │ │ │ ├── detail-body.html │ │ │ │ └── login.html │ │ ├── install │ │ └── module.yaml │ ├── chown │ │ ├── install │ │ └── module.yaml │ ├── launch │ │ ├── added │ │ │ ├── launch.sh │ │ │ ├── server.js │ │ │ └── sparkimage.sh │ │ ├── install │ │ └── module.yaml │ ├── npm_bower │ │ ├── added │ │ │ ├── bower.json │ │ │ └── package.json │ │ ├── install │ │ └── module.yaml │ ├── oc │ │ ├── check_for_download │ │ ├── install │ │ └── module.yaml │ └── update_os │ │ ├── install │ │ └── module.yaml └── openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz ├── package.json ├── release-templates.sh ├── scripts ├── info.sh ├── launch-local.sh └── launch.sh ├── server.js ├── sparkimage.sh ├── test ├── .jshintrc ├── conf.js ├── e2e-setup.sh ├── e2e.sh ├── karma.conf.js ├── prepare.sh ├── spec │ ├── all-functionality-insecure.js │ └── all-functionality.js └── unit │ └── controllersSpec.js └── tools └── resources.yaml /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # PyInstaller 10 | # Usually these files are written by a python script from a template 11 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 12 | *.manifest 13 | *.spec 14 | 15 | # Installer logs 16 | pip-log.txt 17 | pip-delete-this-directory.txt 18 | 19 | # Unit test / coverage reports 20 | htmlcov/ 21 | .tox/ 22 | .coverage 23 | .coverage.* 24 | .cache 25 | nosetests.xml 26 | coverage.xml 27 | *,cover 28 | .hypothesis/ 29 | 30 | # Translations 31 | *.mo 32 | *.pot 33 | 34 | # Django stuff: 35 | *.log 36 | 37 | # Sphinx documentation 38 | docs/_build/ 39 | 40 | # PyBuilder 41 | target/ 42 | 43 | #Ipython Notebook 44 | .ipynb_checkpoints 45 | 46 | # Development related 47 | .idea/* 48 | *.iml 49 | node_modules/ 50 | debug 51 | test/localconf.js 52 | app/bower_components/ 53 | 54 | # generated configuration 55 | config.local.js 56 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "expr" : true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": false, 13 | "newcap": false, 14 | "noarg": true, 15 | "quotmark": false, 16 | "smarttabs": true, 17 | "strict": true, 18 | "sub" : true, 19 | "undef": true, 20 | "unused": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.openshift/action_hooks/README.md: -------------------------------------------------------------------------------- 1 | For information about action hooks supported by OpenShift, consult the documentation: 2 | 3 | http://openshift.github.io/documentation/oo_user_guide.html#the-openshift-directory 4 | -------------------------------------------------------------------------------- /.openshift/cron/README.cron: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a periodic basis 2 | ======================================= 3 | Any scripts or jobs added to the minutely, hourly, daily, weekly or monthly 4 | directories will be run on a scheduled basis (frequency is as indicated by the 5 | name of the directory) using run-parts. 6 | 7 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 8 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} 9 | 10 | The presence of two specially named files jobs.deny and jobs.allow controls 11 | how run-parts executes your scripts/jobs. 12 | jobs.deny ===> Prevents specific scripts or jobs from being executed. 13 | jobs.allow ===> Only execute the named scripts or jobs (all other/non-named 14 | scripts that exist in this directory are ignored). 15 | 16 | The principles of jobs.deny and jobs.allow are the same as those of cron.deny 17 | and cron.allow and are described in detail at: 18 | http://docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/ch-Automating_System_Tasks.html#s2-autotasks-cron-access 19 | 20 | See: man crontab or above link for more details and see the the weekly/ 21 | directory for an example. 22 | 23 | PLEASE NOTE: The Cron cartridge must be installed in order to run the configured jobs. 24 | -------------------------------------------------------------------------------- /.openshift/cron/daily/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radanalyticsio/oshinko-webui/cd9d5b9abcb01fd6fbd9909d12d9c8b6e5fd2402/.openshift/cron/daily/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/hourly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radanalyticsio/oshinko-webui/cd9d5b9abcb01fd6fbd9909d12d9c8b6e5fd2402/.openshift/cron/hourly/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/minutely/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radanalyticsio/oshinko-webui/cd9d5b9abcb01fd6fbd9909d12d9c8b6e5fd2402/.openshift/cron/minutely/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/monthly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radanalyticsio/oshinko-webui/cd9d5b9abcb01fd6fbd9909d12d9c8b6e5fd2402/.openshift/cron/monthly/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/weekly/README: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a weekly basis 2 | ===================================== 3 | Any scripts or jobs added to this directory will be run on a scheduled basis 4 | (weekly) using run-parts. 5 | 6 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 7 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} and handles 8 | the files named jobs.deny and jobs.allow specially. 9 | 10 | In this specific example, the chronograph script is the only script or job file 11 | executed on a weekly basis (due to white-listing it in jobs.allow). And the 12 | README and chrono.dat file are ignored either as a result of being black-listed 13 | in jobs.deny or because they are NOT white-listed in the jobs.allow file. 14 | 15 | For more details, please see ../README.cron file. 16 | 17 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/chrono.dat: -------------------------------------------------------------------------------- 1 | Time And Relative D...n In Execution (Open)Shift! 2 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/chronograph: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "`date`: `cat $(dirname \"$0\")/chrono.dat`" 4 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/jobs.allow: -------------------------------------------------------------------------------- 1 | # 2 | # Script or job files listed in here (one entry per line) will be 3 | # executed on a weekly-basis. 4 | # 5 | # Example: The chronograph script will be executed weekly but the README 6 | # and chrono.dat files in this directory will be ignored. 7 | # 8 | # The README file is actually ignored due to the entry in the 9 | # jobs.deny which is checked before jobs.allow (this file). 10 | # 11 | chronograph 12 | 13 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/jobs.deny: -------------------------------------------------------------------------------- 1 | # 2 | # Any script or job files listed in here (one entry per line) will NOT be 3 | # executed (read as ignored by run-parts). 4 | # 5 | 6 | README 7 | 8 | -------------------------------------------------------------------------------- /.openshift/markers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radanalyticsio/oshinko-webui/cd9d5b9abcb01fd6fbd9909d12d9c8b6e5fd2402/.openshift/markers/.gitkeep -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - "8" 6 | services: 7 | - docker 8 | addons: 9 | apt: 10 | packages: 11 | - net-tools 12 | chrome: stable 13 | matrix: 14 | include: 15 | - env: TO_TEST=secure OPENSHIFT_VERSION=v3.10 16 | - env: TO_TEST=standard OPENSHIFT_VERSION=v3.10 17 | - env: TO_TEST=secure OPENSHIFT_VERSION=v3.11 18 | - env: TO_TEST=standard OPENSHIFT_VERSION=v3.11 19 | fast_finish: true 20 | before_install: 21 | before_install: 22 | - sudo apt-get update -qq 23 | - sudo sed -i "s/\DOCKER_OPTS=\"/DOCKER_OPTS=\"--insecure-registry=172.30.0.0\/16 /g" /etc/default/docker 24 | - sudo cat /etc/default/docker 25 | - sudo service docker restart 26 | - ./test/prepare.sh 27 | install: 28 | - npm install -g protractor 29 | before_script: 30 | - export DISPLAY=:99.0 31 | - sh -e /etc/init.d/xvfb start 32 | - webdriver-manager update --gecko=false 33 | script: 34 | - | 35 | if [ "$TO_TEST" = "secure" ]; then 36 | WEBUI_START_XVFB=false make test-e2e-secure 37 | else 38 | WEBUI_START_XVFB=false make test-e2e 39 | fi 40 | notifications: 41 | email: 42 | on_success: never 43 | on_failure: never 44 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | // Used Jenkins plugins: 4 | // * Pipeline GitHub Notify Step Plugin 5 | // * Disable GitHub Multibranch Status Plugin 6 | // 7 | 8 | // This script expect following environment variables to be set: 9 | // 10 | // $OCP_HOSTNAME -- hostname of running Openshift cluster 11 | // $OCP_USER -- Openshift user 12 | // $OCP_PASSWORD -- Openshift user's password 13 | // 14 | // $EXTERNAL_DOCKER_REGISTRY -- address of a docker registry 15 | // $EXTERNAL_DOCKER_REGISTRY_USER -- username to use to authenticate to specified docker registry 16 | // $EXTERNAL_DOCKER_REGISTRY_PASSWORD -- password/token to use to authenticate to specified docker registry 17 | 18 | def prepareTests() { 19 | 20 | // wipeout workspace 21 | deleteDir() 22 | 23 | dir('oshinko-webui') { 24 | checkout scm 25 | sh('npm install') 26 | sh('bower install') 27 | } 28 | 29 | // check golang version 30 | sh('go version') 31 | 32 | // download oc client 33 | dir('client') { 34 | sh('docker cp $(docker create docker.io/openshift/origin:v3.9):/bin/oc .') 35 | } 36 | 37 | // login to openshift instance 38 | sh('oc login https://$OCP_HOSTNAME:8443 -u $OCP_USER -p $OCP_PASSWORD --insecure-skip-tls-verify=true') 39 | // let's start on a specific project, to prevent start on a random project which could be deleted in the meantime 40 | sh('oc project testsuite') 41 | 42 | // start xvfb 43 | sh('Xvfb -ac :99 -screen 0 1280x1024x16 &') 44 | sh('sudo webdriver-manager update --gecko=false') 45 | } 46 | 47 | 48 | def buildUrl 49 | def globalEnvVariables = ["WEBUI_TEST_EXTERNAL_REGISTRY=$EXTERNAL_DOCKER_REGISTRY", "WEBUI_TEST_EXTERNAL_USER=$EXTERNAL_DOCKER_REGISTRY_USER", "WEBUI_TEST_EXTERNAL_PASSWORD=$EXTERNAL_DOCKER_REGISTRY_PASSWORD", "DISPLAY=:99.0"] 50 | 51 | 52 | node('radanalytics-test') { 53 | stage('init') { 54 | // generate build url 55 | buildUrl = sh(script: 'curl https://url.corp.redhat.com/new?$BUILD_URL', returnStdout: true) 56 | try { 57 | githubNotify(context: 'jenkins-ci/oshinko-webui', description: 'This change is being built', status: 'PENDING', targetUrl: buildUrl) 58 | } catch (err) { 59 | echo("Wasn't able to notify Github: ${err}") 60 | } 61 | } 62 | } 63 | 64 | parallel testStandard: { 65 | node('radanalytics-test') { 66 | stage('Test standard') { 67 | withEnv(globalEnvVariables + ["GOPATH=$WORKSPACE", "KUBECONFIG=$WORKSPACE/client/kubeconfig", "PATH+OC_PATH=$WORKSPACE/client"]) { 68 | 69 | try { 70 | prepareTests() 71 | 72 | // run tests 73 | dir('oshinko-webui') { 74 | sh('make test-e2e | tee -a test-standard.log && exit ${PIPESTATUS[0]}') 75 | } 76 | } catch (err) { 77 | try { 78 | githubNotify(context: 'jenkins-ci/oshinko-webui', description: 'There are test failures', status: 'FAILURE', targetUrl: buildUrl) 79 | } catch (errNotify) { 80 | echo("Wasn't able to notify Github: ${errNotify}") 81 | } 82 | throw err 83 | } finally { 84 | dir('oshinko-webui') { 85 | archiveArtifacts(allowEmptyArchive: true, artifacts: 'test-standard.log') 86 | } 87 | } 88 | } 89 | } 90 | } 91 | }, testSecure: { 92 | node('radanalytics-test') { 93 | stage('Test secure') { 94 | withEnv(globalEnvVariables + ["GOPATH=$WORKSPACE", "KUBECONFIG=$WORKSPACE/client/kubeconfig", "PATH+OC_PATH=$WORKSPACE/client", "WEBUI_TEST_SECURE_USER=$OCP_USER", "WEBUI_TEST_SECURE_PASSWORD=$OCP_PASSWORD"]) { 95 | 96 | try { 97 | prepareTests() 98 | 99 | // run tests 100 | dir('oshinko-webui') { 101 | sh('make test-e2e-secure | tee -a test-secure.log && exit ${PIPESTATUS[0]}') 102 | } 103 | } catch (err) { 104 | try { 105 | githubNotify(context: 'jenkins-ci/oshinko-webui', description: 'There are test failures', status: 'FAILURE', targetUrl: buildUrl) 106 | } catch (errNotify) { 107 | echo("Wasn't able to notify Github: ${errNotify}") 108 | } 109 | throw err 110 | } finally { 111 | dir('oshinko-webui') { 112 | archiveArtifacts(allowEmptyArchive: true, artifacts: 'test-secure.log') 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | try { 121 | githubNotify(context: 'jenkins-ci/oshinko-webui', description: 'This change looks good', status: 'SUCCESS', targetUrl: buildUrl) 122 | } catch (err) { 123 | echo("Wasn't able to notify Github: ${err}") 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : image test-e2e test-e2e-secure 2 | 3 | DOCKERFILE_CONTEXT=oshinko-webui-build 4 | 5 | image: $(DOCKERFILE_CONTEXT) 6 | docker build -t oshinko-webui $(DOCKERFILE_CONTEXT) 7 | 8 | test-e2e: image 9 | test/e2e.sh 10 | 11 | test-e2e-secure: image 12 | WEBUI_TEST_SECURE=true test/e2e.sh 13 | 14 | clean-context: 15 | -rm -f $(DOCKERFILE_CONTEXT)/Dockerfile 16 | -rm -rf $(DOCKERFILE_CONTEXT)/modules 17 | -rm -rf $(DOCKERFILE_CONTEXT)/*.tar.gz 18 | 19 | context: clean-context 20 | cekit generate --descriptor=image.yaml 21 | cp -R target/image/* $(DOCKERFILE_CONTEXT) 22 | $(MAKE) zero-tarballs 23 | 24 | zero-tarballs: 25 | -truncate -s 0 $(DOCKERFILE_CONTEXT)/*.tar.gz 26 | 27 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://travis-ci.org/radanalyticsio/oshinko-webui.svg?branch=master)](https://travis-ci.org/radanalyticsio/oshinko-webui) 2 | [![Docker build](https://img.shields.io/docker/automated/radanalyticsio/oshinko-webui.svg)](https://hub.docker.com/r/radanalyticsio/oshinko-webui) 3 | [![Known Vulnerabilities](https://snyk.io/test/github/radanalyticsio/oshinko-webui/badge.svg)](https://snyk.io/test/github/radanalyticsio/oshinko-webui) 4 | 5 | # oshinko-webui 6 | 7 | This project provides a solution for deploying and managing Apache Spark 8 | clusters in an OpenShift environment. The oshinko-webui is deployed into a 9 | project within OpenShift, and then can create, update, and destroy Apache 10 | Spark clusters in that project. Once installed, it consists of a Node.JS 11 | application that is contained within a Pod and provides a web browser based 12 | user interface for controlling the lifecycle of Spark clusters. 13 | 14 | ## Installation 15 | 16 | In general, there are two main audiences for a discussion of installing the 17 | oshinko-webui: users and developers. If you are interested in running as a 18 | user, or to test drive the application, please see the 19 | [Step-by-step quickstart](https://github.com/radanalyticsio/oshinko-webui#step-by-step-quickstart) 20 | instructions. If you would like to get started hacking on oshinko-webui please 21 | see the 22 | [Developer instructions](https://github.com/radanalyticsio/oshinko-webui#running-the-app-during-development) 23 | section. 24 | 25 | ### Step-by-step quickstart 26 | 27 | These instructions assume that you have access to an OpenShift cluster and 28 | the `oc` command line tool. Although these instructions will help you to 29 | install the oshinko-webui into your OpenShift project, it is possible in 30 | some circumstances that you will not have enough privileges to run the 31 | installation. In the event that you are unable to create the necessary 32 | components to install the application, please consult with your OpenShift 33 | administrator. 34 | 35 | Before performing the following instructions, you must be logged in to 36 | your account and project using the `oc` tool. 37 | 38 | **Step 1. Create the service account and template** 39 | 40 | For oshinko-webui to interact with OpenShift and control the Spark resources 41 | you will create, it needs a service account in your project. The service account 42 | is created with edit permissions along with the oshinko-webui template by issuing 43 | the following command: 44 | 45 | oc create -f https://radanalytics.io/resources.yaml 46 | 47 | **Step 2. Run oshinko-webui** 48 | 49 | oc new-app --template=oshinko-webui 50 | 51 | ## Developer instructions 52 | 53 | If you are interested in developing the code for oshinko-webui or hacking on 54 | its internals, the following instructions will help you to deploy, run, and 55 | test the code. 56 | 57 | Before getting started you will need to have access to an OpenShift cluster, 58 | the `oc` command line application, and the 59 | [oshinko-cli](https://github.com/radanalyticsio/oshinko-cli) command line 60 | application. 61 | 62 | ### Running the app during development 63 | 64 | You'll need to have a node environment installed (developed using NodeJS v6.3.1). 65 | You might prefer to use nvm (https://github.com/creationix/nvm) 66 | to manage your node environment. 67 | Once that is set up, you can run the following: 68 | 69 | $ npm install 70 | $ npm install -g bower 71 | $ bower install 72 | 73 | Now you're ready to run the oshinko-webui server. 74 | 75 | Note, a working local "oc" binary is expected. 76 | To run locally, you'll need a proxy to the api server running. 77 | The following will run a basic proxy, see oc proxy --help if you require 78 | something more specific. 79 | 80 | $ oc proxy --disable-filter=true --api-prefix=/proxy & 81 | 82 | Edit the exports in scripts/launch-local.sh to match your environment. 83 | 84 | Change to the scripts directory and run 85 | 86 | $ ./launch-local.sh 87 | 88 | 89 | You can pick one of these options: 90 | 91 | * install node.js and run `node server.js` 92 | 93 | Then navigate your browser to `http://localhost:` to see the app running in 94 | your browser. 95 | 96 | 97 | ### Running unit tests 98 | To run the unit tests: 99 | 100 | $ npm install -g karma-cli 101 | $ karma start test/karma.conf.js 102 | 103 | 104 | ### End to end testing 105 | 106 | The end to end tests can be run using the `test/e2e.sh` script. 107 | This script assumes a current login to an OpenShift instance 108 | and it runs the test in the current project (it's recommended 109 | to create a fresh project for the test run). It also assumes 110 | that the local oshinko-webui repository has been setup (ie all 111 | the dependencies have been installed and the webui components 112 | have been installed with `npm` and `bower` as noted above). 113 | 114 | The `test/e2e.sh` script will create a serviceaccount, 115 | templates, a configmap, etc in the current project as part 116 | of the test. 117 | 118 | As a convenience, the *test-e2e* and *test-e2e-secure* make 119 | targets can be used to run the test. These targets will 120 | first create a new OpenShift project with prefix *webui-*, 121 | build a local image and then run the test with defaults. 122 | For example: 123 | 124 | ```sh 125 | $ make test-e2e 126 | ... 127 | $ 128 | $ make test-e2e-secure 129 | ... 130 | ``` 131 | 132 | The environment variables below can be set for the call 133 | to make, for example: 134 | 135 | ```sh 136 | $ WEBUI_START_XVFB=false make test-e2e 137 | ``` 138 | 139 | #### Environment variables for test configuration 140 | 141 | There are several enviroment variables that you can set 142 | to configure the tests: 143 | 144 | ``WEBUI_START_XVFB`` (default is true) 145 | 146 | This causes the test to start an Xvfb server running 147 | for display 99 if it's not already running (required). 148 | 149 | ``WEBUI_TEST_IMAGE`` (default is oshinko-webui:latest if WEBUI_TEST_LOCAL_IMAGE is true or docker.io/radanalyticsio/oshinko-webui otherwise) 150 | 151 | The image to use for testing. The defaults are set up to 152 | reference an image from the local docker host (ie, one that has just been 153 | built) but this setting can be used to reference an image from an arbitrary 154 | docker registry. 155 | 156 | ``WEBUI_TEST_LOCAL_IMAGE`` (default is true) 157 | 158 | This indicates that the s2i images to be tested are local, that is they 159 | are available from the local docker daemon but not in an external registry 160 | like docker hub. 161 | 162 | If this is set to "false", the test image is assumed to be in an external 163 | registy. **WEBUI_TEST_INTEGRATED_REGISTRY** and **WEBUI_TEST_EXTERNAL_REGISTRY** 164 | will be ignored because there will be no need to push local images to 165 | a registry. 166 | 167 | ``WEBUI_TEST_INTEGRATED_REGISTRY`` 168 | 169 | This is the IP address of the integrated registry. Use this setting when: 170 | * running the test using local images 171 | * running the test on a host where the integrated registry is reachable (like the OpenShift master) 172 | * using an OpenShift instance that was not created with `oc cluster up` 173 | 174 | ```sh 175 | $ WEBUI_TEST_INTEGRATED_REGISTRY=172.123.456.89:5000 test/e2e.sh 176 | ``` 177 | 178 | ``WEBUI_TEST_EXTERNAL_REGISTRY`` 179 | 180 | This is the IP address of a docker registry. If this is set then 181 | **WEBUI_TEST_EXTERNAL_USER** and **WEBUI_TEST_EXTERNAL_PASSWORD** must also 182 | be set so that the tests can log in to the registry. 183 | Use this setting when: 184 | * running the test using local images 185 | * running the test from a host where the integrated registry is not reachable 186 | * using an OpenShift instance that was not created with `oc cluster up` 187 | 188 | ``WEBUI_TEST_SECURE`` (default is false) 189 | 190 | Use the template for a secure webui. If this is set to true 191 | and the OpenShift instance was not created with `oc cluster up`, 192 | then **WEBUI_TEST_SECURE_USER** and **WEBUI_TEST_SECURE_PASSWORD** 193 | should be used to set login credentials for the webui. 194 | 195 | ``WEBUI_TEST_SECURE_USER`` (default is "developer") 196 | 197 | Username to use for a secure webui test 198 | 199 | ``WEBUI_TEST_SECURE_PASSWORD`` (default is "deverloperpass") 200 | 201 | Password to use for a secure webui test 202 | 203 | ``WEBUI_TEST_RESOURCES`` (default is local tools/resources.yaml) 204 | 205 | The resources.yaml file used to set up test resources. The value 206 | may be a file path, or it may be a url such as 207 | https://radanalytics.io/resources.yaml. 208 | 209 | #### Dependencies for end to end tests 210 | 211 | The end to end tests require a number of dependencies. 212 | The `test/e2e-setup.sh` script has been provided to install 213 | the dependencies and setup an oshinko-webui repository 214 | for testing. This is especially helpful when setting up 215 | a clean machine. 216 | 217 | Note, `test/e2e-setup.sh` assumes passwordless sudo. 218 | 219 | You can look through the script and see what it installs 220 | and make sure those things are installed yourself or you 221 | can do this: 222 | 223 | ```bash 224 | $ test/e2e-setup.sh # from the oshinko-webui main directory 225 | ``` 226 | 227 | The script should be idempotent. 228 | -------------------------------------------------------------------------------- /app/config.template: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | // This is the template configuration for the webui 5 | // A generated version of this config is created at run-time when running 6 | // from the oshinko-webui template. To run locally, you should edit 7 | // this file replacing all with an appropriate value. 8 | var proxyHost = ; 9 | window.OPENSHIFT_CONFIG = { 10 | apis: { 11 | hostPort: proxyHost, 12 | prefix: "/proxy/apis" 13 | }, 14 | api: { 15 | openshift: { 16 | hostPort: proxyHost, 17 | prefix: "/proxy/oapi" 18 | }, 19 | k8s: { 20 | hostPort: proxyHost, 21 | prefix: "/proxy/api" 22 | } 23 | } 24 | }; 25 | 26 | window.__env = {}; 27 | window.__env.oc_proxy_location = proxyHost; 28 | window.__env.namespace = ; 29 | window.__env.refresh_interval = ; 30 | window.__env.spark_image = 'SPARK_DEFAULT'; 31 | 32 | })(); 33 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * 6 | */ 7 | 8 | 9 | /* app css stylesheet */ 10 | 11 | .login-pf { 12 | background-color: #1a1a1a; 13 | color: #ffffff; 14 | position: absolute; 15 | top: 40px; 16 | width: 100%; 17 | padding-top:30px; 18 | } 19 | 20 | .login-pf .details { 21 | border-left: 1px solid #474747; 22 | padding-left: 40px; 23 | } 24 | 25 | .newbutton { 26 | padding-right: 20px; 27 | } 28 | 29 | .capitalize { 30 | text-transform: capitalize; 31 | } 32 | 33 | .nav-tabs { 34 | margin-bottom:20px !important; 35 | } 36 | 37 | /* login page css */ 38 | .login-pf { 39 | height: 100%; 40 | } 41 | .login-pf .container { 42 | background-color: #181818; 43 | background-color: rgba(255, 255, 255, 0.055); 44 | clear: right; 45 | padding-bottom: 40px; 46 | padding-top: 20px; 47 | width: auto; 48 | } 49 | 50 | .login-pf .container .details p:first-child { 51 | border-top: 1px solid #474747; 52 | padding-top: 25px; 53 | margin-top: 25px; 54 | } 55 | 56 | .login-pf .container .details p { 57 | margin-bottom: 2px; 58 | } 59 | .login-pf .container .form-horizontal .control-label { 60 | font-size: 13px; 61 | font-weight: 400; 62 | text-align: left; 63 | } 64 | .login-pf .container .form-horizontal .form-group:last-child, 65 | .login-pf .container .form-horizontal .form-group:last-child .help-block:last-child { 66 | margin-bottom: 0; 67 | } 68 | .login-pf .container .help-block { 69 | color: #fff; 70 | } 71 | .login-button-container{ 72 | float: right; 73 | padding-left: 20px; 74 | padding-right: 20px; 75 | width: 25%; 76 | } 77 | /* login page css */ 78 | 79 | 80 | 81 | /* landing page css */ 82 | 83 | .container-cards-pf { 84 | display: flex; 85 | margin-top: 10px; 86 | } 87 | 88 | .card-pf { 89 | flex-grow: 1; 90 | margin: 10px; 91 | display: flex; 92 | flex-direction: column; 93 | flex-basis: 25%; 94 | } 95 | 96 | .card-pf-aggregate-status .card-pf-body { 97 | margin-bottom: 0px; 98 | } 99 | 100 | .card-pf-wide { 101 | flex-basis: ~"calc(75% + 20px)"; 102 | } 103 | 104 | .card-pf-double { 105 | flex-basis: ~"calc(66% - 76px)"; 106 | } 107 | 108 | .dashboard-cards { 109 | width: 100%; 110 | max-width: 1024px; 111 | padding-right: 7px; 112 | padding-left: 20px; 113 | } 114 | 115 | .card-pf-aggregate-status-notification .spinner { 116 | display: inline-block; 117 | margin-right: 7px; 118 | } 119 | 120 | .card-pf-aggregate-status-text { 121 | font-size: 13px; 122 | vertical-align: 3px; 123 | } 124 | 125 | .card-pf-heading { 126 | margin-bottom: 0px; 127 | 128 | button { 129 | margin: 15px 0px; 130 | } 131 | 132 | i { 133 | font-size: 24px; 134 | line-height: 16px; 135 | vertical-align: top; 136 | padding-right: 5px; 137 | } 138 | } 139 | 140 | .card-pf-footer { 141 | min-height: 5em; 142 | } 143 | 144 | .card-pf-body { 145 | margin-top: 10px; 146 | margin-bottom: auto; 147 | } 148 | 149 | .card-pf-body.blank-slate-pf { 150 | background: transparent; 151 | padding-top: 20px; 152 | } 153 | 154 | .card-pf-body table.listing-ct { 155 | margin-top: 0px; 156 | width: 100%; 157 | } 158 | 159 | .card-pf-body table.listing-ct thead th { 160 | border-top: none; 161 | } 162 | 163 | .card-pf-body table.listing-ct tbody:last-child { 164 | border-bottom: none; 165 | } 166 | 167 | .card-pf-body table.listing-ct tbody:last-child tr:last-child { 168 | border-bottom: none; 169 | } 170 | 171 | 172 | /* cluster-panel css */ 173 | .cluster-panel { 174 | padding-right: 25px; 175 | padding-top: 5px; 176 | } 177 | 178 | .cluster-panel-actions { 179 | padding-right: 5px; 180 | padding-top: 5px; 181 | } 182 | 183 | table.cluster-body { 184 | margin-top: 0px; 185 | 186 | thead.th { 187 | border-top:0px; 188 | font-size: 12px; 189 | } 190 | 191 | tr { 192 | display: table-row; 193 | vertical-align: inherit; 194 | border-color: inherit; 195 | } 196 | 197 | tr.listing-ct-item { 198 | border-top: 1px solid color("#eee"); 199 | border-bottom: 1px solid color("#eee"); 200 | cursor: pointer; 201 | } 202 | } 203 | table.cluster-listing { 204 | max-width: 2000px; 205 | min-width: 85%; 206 | 207 | thead { 208 | th { 209 | border-top: 0px; 210 | } 211 | } 212 | 213 | tr { 214 | display: table-row; 215 | vertical-align: inherit; 216 | border-color: inherit; 217 | } 218 | 219 | .details-listing { 220 | min-width: 70% !important; 221 | } 222 | 223 | tr.listing-ct-item { 224 | border-top: 1px solid color("#eee"); 225 | border-bottom: 1px solid color("#eee"); 226 | cursor: pointer; 227 | } 228 | 229 | tr.inner-project-listing { 230 | display: table-row; 231 | } 232 | 233 | tbody.first { 234 | tr.tag-item td { 235 | padding-top: 20px; 236 | } 237 | } 238 | 239 | tbody.last { 240 | tr.listing-ct-item.tag-item td:first-child { 241 | background-size: 100% 20px; 242 | background-repeat: no-repeat; 243 | } 244 | } 245 | 246 | tr.tag-item td { 247 | padding-bottom: 0px; 248 | } 249 | 250 | tr.listing-ct-item tt { 251 | color: @metadata-color; 252 | } 253 | } 254 | 255 | .dl-horizontal dt{ 256 | text-align: left !important; 257 | width: auto; 258 | padding-right: 1em; 259 | } 260 | 261 | .dl-horizontal dd{ 262 | margin-left: 0; 263 | margin-bottom: 0; 264 | } 265 | 266 | .close-icon { 267 | color:#000000; 268 | } 269 | 270 | /* Making table appearance similar to console */ 271 | .table.table-bordered > tbody > tr td, .table.table-bordered > tbody > tr th, .table.table-bordered > thead > tr td, .table.table-bordered > thead > tr th { 272 | border-left: 0; 273 | border-right: 0; 274 | padding-bottom: 8px; 275 | padding-top: 8px; 276 | vertical-align: middle 277 | } 278 | 279 | /* Removing outline when details tabs are focused */ 280 | .nav-tabs > li > a:focus { 281 | outline: 0; 282 | } -------------------------------------------------------------------------------- /app/forms/delete-cluster.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /app/forms/new-cluster.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 67 | 71 | 72 | -------------------------------------------------------------------------------- /app/forms/scale-cluster.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | Spark Cluster Management 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * 6 | */ 7 | 8 | 'use strict'; 9 | 10 | 11 | var app = angular.module('Oshinko', [ 12 | 'ngRoute', 13 | 'Oshinko.controllers', 14 | 'Oshinko.factories', 15 | 'ab-base64' 16 | ]); 17 | 18 | app.constant("API_CFG", _.get(window.OPENSHIFT_CONFIG, "api", {})); 19 | app.constant("APIS_CFG", _.get(window.OPENSHIFT_CONFIG, "apis", {})); 20 | app.config(['$locationProvider', function($locationProvider) { 21 | 22 | $locationProvider.html5Mode(false); 23 | 24 | }]); 25 | 26 | app.config(['$routeProvider', function ($routeProvider) { 27 | $routeProvider.when('/clusters/:Id?', 28 | { 29 | templateUrl: function (params) { 30 | if (!params.Id) { 31 | return 'webui/partials/clusters.html'; 32 | } 33 | else { 34 | return 'webui/partials/cluster-detail.html'; 35 | } 36 | }, 37 | controller: 'ClusterCtrl', 38 | activetab: 'clusters' 39 | }); 40 | $routeProvider.when('/login', 41 | { 42 | templateUrl: 'partials/login.html', 43 | controller: 'LoginController', 44 | activetab: '' 45 | }); 46 | $routeProvider.otherwise({redirectTo: '/clusters'}); 47 | }]); 48 | 49 | hawtioPluginLoader.addModule('Oshinko'); 50 | hawtioPluginLoader.registerPreBootstrapTask(function(next) { 51 | // Skips api discovery, needed to run spec tests 52 | if ( _.get(window, "OPENSHIFT_CONFIG.api.k8s.resources") ) { 53 | next(); 54 | return; 55 | } 56 | 57 | var api = { 58 | k8s: {}, 59 | openshift: {} 60 | }; 61 | var apis = {}; 62 | var API_DISCOVERY_ERRORS = []; 63 | var protocol = window.location.protocol + "//"; 64 | 65 | // Fetch /api/v1 for legacy k8s resources, we will never bump the version of these legacy apis so fetch version immediately 66 | var k8sBaseURL = protocol + window.OPENSHIFT_CONFIG.api.k8s.hostPort + window.OPENSHIFT_CONFIG.api.k8s.prefix; 67 | var k8sDeferred = $.get(k8sBaseURL + "/v1") 68 | .done(function(data) { 69 | api.k8s.v1 = _.keyBy(data.resources, 'name'); 70 | }) 71 | .fail(function(data, textStatus, jqXHR) { 72 | API_DISCOVERY_ERRORS.push({ 73 | data: data, 74 | textStatus: textStatus, 75 | xhr: jqXHR 76 | }); 77 | }); 78 | 79 | // Fetch /oapi/v1 for legacy openshift resources, we will never bump the version of these legacy apis so fetch version immediately 80 | var osBaseURL = protocol + window.OPENSHIFT_CONFIG.api.openshift.hostPort + window.OPENSHIFT_CONFIG.api.openshift.prefix; 81 | var osDeferred = $.get(osBaseURL + "/v1") 82 | .done(function(data) { 83 | api.openshift.v1 = _.keyBy(data.resources, 'name'); 84 | }) 85 | .fail(function(data, textStatus, jqXHR) { 86 | API_DISCOVERY_ERRORS.push({ 87 | data: data, 88 | textStatus: textStatus, 89 | xhr: jqXHR 90 | }); 91 | }); 92 | 93 | // Fetch /apis to get the list of groups and versions, then fetch each group/ 94 | // Because the api discovery doc returns arrays and we want maps, this creates a structure like: 95 | // { 96 | // extensions: { 97 | // name: "extensions", 98 | // preferredVersion: "v1beta1", 99 | // versions: { 100 | // v1beta1: { 101 | // version: "v1beta1", 102 | // groupVersion: "extensions/v1beta1" 103 | // resources: { 104 | // daemonsets: { 105 | // /* resource returned from discovery API */ 106 | // } 107 | // } 108 | // } 109 | // } 110 | // } 111 | // } 112 | var apisBaseURL = protocol + window.OPENSHIFT_CONFIG.apis.hostPort + window.OPENSHIFT_CONFIG.apis.prefix; 113 | var getGroups = function(baseURL, hostPrefix, data) { 114 | var apisDeferredVersions = []; 115 | _.each(data.groups, function(apiGroup) { 116 | var group = { 117 | name: apiGroup.name, 118 | preferredVersion: apiGroup.preferredVersion.version, 119 | versions: {}, 120 | hostPrefix: hostPrefix 121 | }; 122 | apis[group.name] = group; 123 | _.each(apiGroup.versions, function(apiVersion) { 124 | var versionStr = apiVersion.version; 125 | group.versions[versionStr] = { 126 | version: versionStr, 127 | groupVersion: apiVersion.groupVersion 128 | }; 129 | apisDeferredVersions.push($.get(baseURL + "/" + apiVersion.groupVersion) 130 | .done(function(data) { 131 | group.versions[versionStr].resources = _.keyBy(data.resources, 'name'); 132 | }) 133 | .fail(function(data, textStatus, jqXHR) { 134 | API_DISCOVERY_ERRORS.push({ 135 | data: data, 136 | textStatus: textStatus, 137 | xhr: jqXHR 138 | }); 139 | })); 140 | }); 141 | }); 142 | return $.when.apply(this, apisDeferredVersions); 143 | }; 144 | var apisDeferred = $.get(apisBaseURL) 145 | .then(_.partial(getGroups, apisBaseURL, null), function(data, textStatus, jqXHR) { 146 | API_DISCOVERY_ERRORS.push({ 147 | data: data, 148 | textStatus: textStatus, 149 | xhr: jqXHR 150 | }); 151 | }); 152 | 153 | // Additional servers can be defined for debugging and prototyping against new servers not yet served by the aggregator 154 | // There can not be any conflicts in the groups/resources from these API servers. 155 | var additionalDeferreds = []; 156 | _.each(window.OPENSHIFT_CONFIG.additionalServers, function(server) { 157 | var baseURL = (server.protocol ? (server.protocol + "://") : protocol) + server.hostPort + server.prefix; 158 | additionalDeferreds.push($.get(baseURL) 159 | .then(_.partial(getGroups, baseURL, server), function(data, textStatus, jqXHR) { 160 | if (server.required !== false) { 161 | API_DISCOVERY_ERRORS.push({ 162 | data: data, 163 | textStatus: textStatus, 164 | xhr: jqXHR 165 | }); 166 | } 167 | })); 168 | }); 169 | 170 | // Will be called on success or failure 171 | var discoveryFinished = function() { 172 | window.OPENSHIFT_CONFIG.api.k8s.resources = api.k8s; 173 | window.OPENSHIFT_CONFIG.api.openshift.resources = api.openshift; 174 | window.OPENSHIFT_CONFIG.apis.groups = apis; 175 | if (API_DISCOVERY_ERRORS.length) { 176 | window.OPENSHIFT_CONFIG.apis.API_DISCOVERY_ERRORS = API_DISCOVERY_ERRORS; 177 | } 178 | next(); 179 | }; 180 | var allDeferreds = [ 181 | k8sDeferred, 182 | osDeferred, 183 | apisDeferred 184 | ]; 185 | allDeferreds = allDeferreds.concat(additionalDeferreds); 186 | $.when.apply(this, allDeferreds).always(discoveryFinished); 187 | }); 188 | 189 | -------------------------------------------------------------------------------- /app/js/dialog.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Cockpit. 3 | * 4 | * Copyright (C) 2015 Red Hat, Inc. 5 | * 6 | * Cockpit is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation; either version 2.1 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Cockpit is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with Cockpit; If not, see . 18 | */ 19 | /* jshint ignore:start */ 20 | (function() { 21 | "use strict"; 22 | 23 | angular.module('ui.cockpit', [ 24 | 'ui.bootstrap' 25 | ]) 26 | 27 | /* 28 | * Implements a directive that works with ui-bootstrap's 29 | * $modal service. Implements Cockpit dialog HIG behavior. 30 | * 31 | * This dialog treats a button with .btn-cancel class as a cancel 32 | * button. Clicking it will dismiss the dialog or (see below) cancel 33 | * a completion promise. 34 | * 35 | * From inside the dialog, you can invoke the following methods on 36 | * the scope: 37 | * 38 | * failure(ex) 39 | * failure([ex1, ex2]) 40 | * 41 | * Displays errors either globally or on fields. The ex.message or 42 | * ex.toString() is displayed as the failure message. If ex.target 43 | * is a valid CSS selector, then the failure message will be displayed 44 | * under the selected field. 45 | * 46 | * failure() 47 | * failure(null) 48 | * 49 | * Clears all failures from display. 50 | * 51 | * complete(promise) 52 | * complete(data) 53 | * 54 | * Complete the dialog. If a promise is passed, then the dialog will 55 | * enter into a wait state until the promise completes. If promise resolves 56 | * then the dialog will be closed with the resolve value. If the promise 57 | * rejects, then failures will be displayed by invoking failure() above. 58 | * 59 | * While the promise is completing, all .form-control and .btn will 60 | * be disabled. If promise.cancel() is a method, then the .btn-cancel 61 | * will remain clickable, and clicking it will cancel the promise, and 62 | * when the promise completes, will dismiss the dialog. 63 | */ 64 | .directive('modalDialog', [ 65 | "$q", 66 | function($q) { 67 | return { 68 | restrict: 'E', 69 | transclude: true, 70 | template: '', 71 | link: function(scope, element, attrs) { 72 | var state = null; 73 | 74 | function detach() { 75 | if (state) 76 | state.detach(); 77 | state = null; 78 | } 79 | 80 | scope.complete = function(thing) { 81 | detach(); 82 | if (!thing || !thing.then) 83 | thing = $q(thing); 84 | state = new DialogState(element, thing, scope); 85 | }; 86 | 87 | scope.failure = function(/* ... */) { 88 | var errors; 89 | var n = arguments.length; 90 | if (n === 0) { 91 | errors = null; 92 | } else if (n === 1) { 93 | errors = arguments[0]; 94 | } else { 95 | errors = []; 96 | errors.push.apply(errors, arguments); 97 | } 98 | 99 | if (!errors) { 100 | detach(); 101 | return; 102 | } 103 | 104 | var defer = $q.defer(); 105 | defer.reject(errors); 106 | scope.complete(defer.promise); 107 | }; 108 | 109 | /* Dialog cancellation before promises kick in */ 110 | function dismissDialog() { 111 | scope.$dismiss(); 112 | } 113 | 114 | var cancel = queryFirst(element, ".btn-cancel"); 115 | cancel.on("click", dismissDialog); 116 | scope.$on("$routeChangeStart", dismissDialog); 117 | 118 | scope.$on("$destroy", function() { 119 | cancel.off("click", dismissDialog); 120 | detach(); 121 | }); 122 | 123 | /* 124 | * Allow us to submit via the enter button. 125 | * The subFunc argument is the name of a function 126 | * that exists in the scope of the modal controller. 127 | */ 128 | scope.submitForm = function mySubmit(event, subFunc, arg) { 129 | if (event.keyCode === 13) { 130 | scope.complete(eval("scope." + subFunc)(arg)); 131 | } 132 | }; 133 | } 134 | }; 135 | } 136 | ]); 137 | 138 | function queryAll(element, selector) { 139 | var list, result = []; 140 | var j, i, jlen, len = element.length; 141 | for (i = 0; i < len; i++) { 142 | list = element[i].querySelectorAll(selector); 143 | if (list) { 144 | for (j = 0, jlen = list.length; j < jlen; j++) 145 | result.push(list[i]); 146 | } 147 | } 148 | return angular.element(result); 149 | } 150 | 151 | function queryFirst(element, selector) { 152 | var result = null; 153 | var i, len = element.length; 154 | for (i = 0; !result && i < len; i++) 155 | result = element[i].querySelector(selector); 156 | return angular.element(result); 157 | } 158 | 159 | /* 160 | * This state object handles one of three different states. 161 | * This does not exist in the case "before" these states are relevant. 162 | * 163 | * state = null: pending 164 | * state = true: succeeded 165 | * state = false: failed 166 | */ 167 | function DialogState(element, promise, scope) { 168 | var state = null; 169 | var result = null; 170 | 171 | /* Set to true when cancel was requested */ 172 | var cancelled = false; 173 | var detached = false; 174 | 175 | /* The wait field elements */ 176 | var disabled = []; 177 | var wait = angular.element("
"); 178 | wait.append(angular.element("
")); 179 | var notify = angular.element(""); 180 | wait.append(notify); 181 | 182 | this.detach = detachState; 183 | 184 | if (!promise) { 185 | detachState(); 186 | return; 187 | } 188 | 189 | promise.then(function(data) { 190 | result = data; 191 | if (promise) 192 | changeState(true); 193 | }, function(data) { 194 | result = data; 195 | if (promise) 196 | changeState(false); 197 | }, function(data) { 198 | if (promise) 199 | notifyWait(data); 200 | }); 201 | 202 | window.setTimeout(function() { 203 | if (promise && scope && state === null) { 204 | changeState(null); 205 | scope.$digest(); 206 | } 207 | }, 0); 208 | 209 | function changeState(value) { 210 | if (detached) 211 | return; 212 | state = value; 213 | if (cancelled) { 214 | scope.$dismiss(); 215 | return; 216 | } else if (state === null) { 217 | clearErrors(); 218 | displayWait(); 219 | } else if (state === true) { 220 | clearErrors(); 221 | scope.$close(result); /* Close dialog */ 222 | } else if (state === false) { 223 | clearWait(); 224 | displayErrors(result); 225 | } else { 226 | console.warn("invalid dialog state", state); 227 | } 228 | } 229 | 230 | function detachState() { 231 | scope = null; 232 | promise = null; 233 | clearErrors(); 234 | clearWait(); 235 | } 236 | 237 | function displayErrors(errors) { 238 | clearErrors(); 239 | 240 | if (!angular.isArray(errors)) 241 | errors = [errors]; 242 | errors.forEach(function(error) { 243 | var target = null; 244 | /* Each error can have a target field */ 245 | if (error.target) 246 | target = queryFirst(element, error.target); 247 | if (target && target[0]) 248 | fieldError(target, error); 249 | else 250 | globalError(error); 251 | }); 252 | } 253 | 254 | function globalError(error) { 255 | var alert = angular.element("
"); 256 | alert.text(error.message || error.toString()); 257 | alert.prepend(angular.element("")); 258 | 259 | var wrapper = queryFirst(element, ".modal-footer"); 260 | if (wrapper.length) 261 | wrapper.prepend(alert); 262 | else 263 | element.append(alert); 264 | } 265 | 266 | function fieldError(target, error) { 267 | var message = angular.element("
"); 268 | message.text(error.message || error.toString()); 269 | var wrapper = target.parent(); 270 | wrapper.addClass("has-error"); 271 | target.after(message); 272 | wrapper.on("keypress change", handleClear); 273 | } 274 | 275 | function handleClear(ev) { 276 | var target = ev.target; 277 | while (target !== this) { 278 | clearError(angular.element(target)); 279 | target = target.parentNode; 280 | } 281 | } 282 | 283 | function clearError(target) { 284 | var wrapper = target.parent(); 285 | queryAll(wrapper, ".dialog-error").remove(); 286 | wrapper.removeClass("has-error"); 287 | wrapper.off("keypress change", handleClear); 288 | } 289 | 290 | function clearErrors() { 291 | var messages = queryAll(element, ".dialog-error"); 292 | angular.forEach(messages, function(message) { 293 | clearError(angular.element(message)); 294 | }); 295 | } 296 | 297 | function handleCancel(ev) { 298 | if (promise.cancel) 299 | promise.cancel(); 300 | cancelled = true; 301 | ev.stopPropagation(); 302 | ev.preventDefault(); 303 | return false; 304 | } 305 | 306 | function notifyWait(data) { 307 | var message = data.message || data; 308 | if (typeof message === "string" || typeof message === "number") 309 | notify.text(message); 310 | else if (!message) 311 | notify.text(""); 312 | } 313 | 314 | function clearWait() { 315 | var control; 316 | while (true) { 317 | control = disabled.pop(); 318 | if (!control) 319 | break; 320 | control.removeAttr("disabled"); 321 | } 322 | wait.remove(); 323 | queryFirst(element, ".btn-cancel").off("click", handleCancel); 324 | } 325 | 326 | function displayWait() { 327 | clearWait(); 328 | 329 | /* Insert the wait area */ 330 | queryFirst(element, ".modal-footer").prepend(wait); 331 | 332 | /* Disable everything and stash previous disabled state */ 333 | function disable(el) { 334 | var control = angular.element(el); 335 | if (control.attr("disabled") || 336 | promise.cancel && control.hasClass("btn-cancel")) 337 | return; 338 | disabled.push(control); 339 | control.attr("disabled", "disabled"); 340 | } 341 | 342 | angular.forEach(queryAll(element, ".form-control"), disable); 343 | angular.forEach(queryAll(element, ".btn"), disable); 344 | 345 | queryFirst(element, ".btn-cancel").on("click", handleCancel); 346 | } 347 | } 348 | 349 | }()); 350 | /* jshint ignore:end */ 351 | -------------------------------------------------------------------------------- /app/js/directives.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2017 Red Hat, Inc. 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | /* jshint -W098 */ 10 | var module = angular.module('Oshinko.directives', []); 11 | module.directive('appVersion', 12 | ['version', 13 | function (version) { 14 | return function (scope, elm, attrs) { 15 | elm.text(version); 16 | }; 17 | } 18 | ] 19 | ); 20 | module.directive('clusterPanel', [ 21 | '$q', 22 | 'sendNotifications', 23 | '$interval', 24 | function ($q) { 25 | return { 26 | restrict: 'A', 27 | scope: true, 28 | link: function (scope, element, attrs) { 29 | 30 | function queryFirst(element, selector) { 31 | var result = null; 32 | var i, len = element.length; 33 | for (i = 0; !result && i < len; i++) { 34 | result = element[i].querySelector(selector); 35 | } 36 | return angular.element(result); 37 | } 38 | 39 | var wait = angular.element("
"); 40 | var notify = angular.element(""); 41 | 42 | function appendSpinner() { 43 | wait.append(angular.element("
")); 44 | wait.append(notify); 45 | queryFirst(element, ".table-footer").prepend(wait); 46 | } 47 | 48 | function appendSpinnerPromise() { 49 | var defer = $q.defer(); 50 | appendSpinner(); 51 | defer.resolve(); 52 | return defer.promise; 53 | } 54 | 55 | var clusterId = scope.clusterId; 56 | var setClusterDetails = function (clusterName) { 57 | try { 58 | scope.cluster_details = scope.oshinkoClusters[clusterName]; 59 | scope.cluster_details['name'] = scope.cluster_details.master.svc[Object.keys(scope.cluster_details.master.svc)[0]].metadata.labels['oshinko-cluster']; 60 | scope.cluster_details['workerCount'] = Object.keys(scope.cluster_details.worker.pod).length; 61 | scope.cluster_details['masterCount'] = Object.keys(scope.cluster_details.master.pod).length; 62 | scope.cluster_details['allPods'] = Object.values(scope.cluster_details.worker.pod); 63 | scope.cluster_details['allPods'].push(Object.values(scope.cluster_details.master.pod)[0]); 64 | scope.cluster_details['containers'] = clusterName + "-m|" + clusterName + "-w"; 65 | var masterPodName = Object.keys(scope.cluster_details.master.pod)[0]; 66 | var clusterMetrics = scope.cluster_details.master.pod[masterPodName].metadata.labels["oshinko-metrics-enabled"] && scope.cluster_details.master.pod[masterPodName].metadata.labels["oshinko-metrics-enabled"] === "true"; 67 | scope.metricsAvailable = clusterMetrics; 68 | } catch (e) { 69 | // most likely recently deleted 70 | scope.cluster_details = null; 71 | } 72 | }; 73 | setClusterDetails(clusterId); 74 | 75 | 76 | var tab = 'main'; 77 | var REFRESH_SECONDS = 10; 78 | scope.tab = function (name, ev) { 79 | if (ev) { 80 | tab = name; 81 | ev.stopPropagation(); 82 | } 83 | return tab === name; 84 | }; 85 | 86 | }, 87 | templateUrl: "webui/partials/cluster-panel.html" 88 | }; 89 | } 90 | ]); 91 | module.directive('clusterBody', [ 92 | function () { 93 | return { 94 | restrict: 'A', 95 | templateUrl: 'webui/partials/cluster-body.html', 96 | link: function (scope, element, attrs) { 97 | } 98 | }; 99 | } 100 | ]); 101 | /* jshint +W098 */ -------------------------------------------------------------------------------- /app/js/factories.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | var module = angular.module('Oshinko.factories', ['ui.bootstrap', 'patternfly.notification']); 10 | 11 | module.factory('clusterActions', [ 12 | '$uibModal', 13 | function($uibModal) { 14 | function deleteCluster(clusterName) { 15 | return $uibModal.open({ 16 | animation: true, 17 | controller: 'ClusterDeleteCtrl', 18 | templateUrl: '/webui/forms/' + 'delete-cluster.html', 19 | resolve: { 20 | dialogData: function() { 21 | return { clusterName: clusterName }; 22 | } 23 | } 24 | }).result; 25 | } 26 | function newCluster() { 27 | return $uibModal.open({ 28 | animation: true, 29 | controller: 'ClusterNewCtrl', 30 | templateUrl: '/webui/forms/' + 'new-cluster.html', 31 | resolve: { 32 | dialogData: function() { 33 | return { }; 34 | } 35 | } 36 | }).result; 37 | } 38 | function scaleCluster(clusterName, workerCount, masterCount) { 39 | return $uibModal.open({ 40 | animation: true, 41 | controller: 'ClusterDeleteCtrl', 42 | templateUrl: '/webui/forms/' + 'scale-cluster.html', 43 | resolve: { 44 | dialogData: function() { 45 | return { clusterName: clusterName, 46 | workerCount: workerCount, 47 | masterCount: masterCount 48 | }; 49 | } 50 | } 51 | }).result; 52 | } 53 | return { 54 | deleteCluster: deleteCluster, 55 | newCluster: newCluster, 56 | scaleCluster: scaleCluster 57 | }; 58 | } 59 | ]); 60 | 61 | module.factory('sendNotifications', function(Notifications) { 62 | var notificationFactory = {}; 63 | var typeMap = { 64 | 'Info': Notifications.info, 65 | 'Success': Notifications.success, 66 | 'Warning': Notifications.warn, 67 | 'Error': Notifications.error 68 | }; 69 | 70 | notificationFactory.notify = function(type, message) { 71 | typeMap[type](message); 72 | }; 73 | return notificationFactory; 74 | }); 75 | 76 | /* Error handling factory. Since our server will return 77 | * a successful status code, even on things where an operation 78 | * was not successful, we need to take a closer look at 79 | * the response itself to determine if we should report an 80 | * error to the user. This factory is mean to be the one stop shop 81 | * for error handling and can be extended to handle all sorts 82 | * of things. 83 | */ 84 | module.factory('errorHandling', function(sendNotifications) { 85 | var errorHandlingFactory= {}; 86 | 87 | errorHandlingFactory.handle = function(response, error, defer, successMsg) { 88 | if (response && response.data && response.data.errors) { 89 | response.data.errors.forEach(function (singleError) { 90 | console.error(singleError['title'] + "\nStatus Code: " + singleError.status + "\n" + singleError.details); 91 | }); 92 | if (defer) { 93 | defer.reject(response.data.errors[0].details); 94 | } 95 | } else if (error) { 96 | console.error("Problem communicating with server. Error code: " + error.status); 97 | if (defer) { 98 | defer.reject(error.data); 99 | } 100 | } else { 101 | sendNotifications.notify("Success", successMsg); 102 | if(defer) { 103 | defer.resolve(successMsg); 104 | } 105 | } 106 | }; 107 | return errorHandlingFactory; 108 | }); 109 | -------------------------------------------------------------------------------- /app/js/listing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Cockpit. 3 | * 4 | * Copyright (C) 2015 Red Hat, Inc. 5 | * 6 | * Cockpit is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation; either version 2.1 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Cockpit is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with Cockpit; If not, see . 18 | */ 19 | /* jshint ignore:start */ 20 | (function() { 21 | "use strict"; 22 | 23 | function inClassOrTag(el, cls, tag) { 24 | return (el && el.classList && el.classList.contains(cls)) || 25 | (el && el.tagName === tag) || 26 | (el && inClassOrTag(el.parentNode, cls, tag)); 27 | } 28 | 29 | angular.module('ui.listing', []) 30 | 31 | .directive('listingTable', [ 32 | function() { 33 | return { 34 | restrict: 'A', 35 | link: function(scope, element, attrs) { 36 | } 37 | }; 38 | } 39 | ]) 40 | 41 | .factory('ListingState', [ 42 | function() { 43 | return function ListingState(scope) { 44 | var self = this; 45 | var data = { }; 46 | 47 | self.selected = { }; 48 | self.enableActions = false; 49 | 50 | /* Check that either .btn or li were not clicked */ 51 | function checkBrowserEvent(ev) { 52 | return !(ev && inClassOrTag(ev.target, "btn", "li")); 53 | } 54 | 55 | self.hasSelected = function hasSelected(id) { 56 | return !angular.equals({}, self.selected); 57 | }; 58 | 59 | self.expanded = function expanded(id) { 60 | if (angular.isUndefined(id)) { 61 | for (id in data) 62 | return true; 63 | return false; 64 | } else { 65 | return id in data; 66 | } 67 | }; 68 | 69 | self.toggle = function toggle(id, ev) { 70 | var value; 71 | if (self.enableActions) { 72 | ev.stopPropagation(); 73 | return; 74 | } 75 | 76 | if (id) { 77 | value = !(id in data); 78 | if (value) 79 | self.expand(id, ev); 80 | else 81 | self.collapse(id, ev); 82 | } 83 | }; 84 | 85 | self.expand = function expand(id, ev) { 86 | data[id] = true; 87 | if (ev) 88 | ev.stopPropagation(); 89 | }; 90 | 91 | self.activate = function expand(id, ev) { 92 | var emitted; 93 | if (checkBrowserEvent(ev)) 94 | emitted = scope.$emit("activate", id); 95 | }; 96 | 97 | self.collapse = function collapse(id, ev) { 98 | if (id) { 99 | delete data[id]; 100 | } else { 101 | Object.keys(data).forEach(function(old) { 102 | delete data[old]; 103 | }); 104 | } 105 | if (ev) 106 | ev.stopPropagation(); 107 | }; 108 | }; 109 | } 110 | ]) 111 | 112 | .directive('listingPanel', [ 113 | function() { 114 | return { 115 | restrict: 'A', 116 | scope: true, 117 | link: function(scope, element, attrs) { 118 | var tab = 'main'; 119 | scope.tab = function(name, ev) { 120 | if (ev) { 121 | tab = name; 122 | ev.stopPropagation(); 123 | } 124 | return tab === name; 125 | }; 126 | }, 127 | templateUrl: function(element, attrs) { 128 | var kind = attrs.kind; 129 | return "webui/partials/" + kind.toLowerCase() + "-panel.html"; 130 | } 131 | }; 132 | } 133 | ]); 134 | }()); 135 | /* jshint ignore:end */ 136 | -------------------------------------------------------------------------------- /app/login.html: -------------------------------------------------------------------------------- 1 | 9 | 48 | -------------------------------------------------------------------------------- /app/partials/cluster-body.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 23 | 29 | 30 | 31 |
PodsTypeStatus
13 |
14 | 15 | {{pod.status.podIP}} 16 |
17 |
19 |
20 | {{pod.metadata.labels["oshinko-type"]}} 21 |
22 |
24 | {{pod.status.phase}} 25 | {{pod.status.phase}} 26 | {{pod.status.phase}} 27 | {{pod.status.phase}} 28 |
32 |
-------------------------------------------------------------------------------- /app/partials/cluster-detail.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 |
12 | 23 |

Cluster Details

24 |
25 |
26 |
27 |
28 | 29 |
30 |

Details are unavailable

31 |

The cluster may have been deleted.

32 |
33 | 34 | 35 | Details 36 |
37 |
38 | 39 | Pods 40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /app/partials/cluster-panel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | -------------------------------------------------------------------------------- /app/partials/clusters.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 |
12 |
13 | 15 |
16 |

Spark Clusters

17 |
18 |
19 |
20 |
21 | 22 |
23 |

No Spark Clusters present

24 |

You can deploy a new spark cluster.

25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 | 71 | 72 | 73 |
NameStatusMasterMastersWorkersSpark UI LinkActions
{{ cluster }} 43 | 44 | 45 | 46 | 47 | 48 | {{ getClusterStatus(oshinkoClusters[cluster]) }} 49 | {{ getSparkMasterUrl(cluster) }}{{ countMasters(oshinkoClusters[cluster]) }}{{ countWorkers(oshinkoClusters[cluster]) }}Spark UIN/A 56 | 57 | 61 | 69 | 70 |
74 |
75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /app/partials/detail-body.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Name
4 |
{{cluster_id}}
5 |
Status
6 |
{{getClusterStatus(oshinkoClusters[cluster_id])}}
7 |
Master
8 |
{{getSparkMasterUrl(cluster_id)}}
9 |
Master count
10 |
{{countMasters(oshinkoClusters[cluster_id])}}
11 |
Worker count
12 |
{{countWorkers(oshinkoClusters[cluster_id])}}
13 |
Master Web UI
14 |
{{getSparkWebUi(oshinkoClusters[cluster_id])}}
15 |
N/A
16 |
Cluster configuration
17 |
{{getClusterConfig(oshinkoClusters[cluster_id])}}
18 |
19 |
20 | -------------------------------------------------------------------------------- /app/partials/login.html: -------------------------------------------------------------------------------- 1 | 9 | 47 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oshinko-web", 3 | "description": "A web ui for managing spark clusters on Openshift", 4 | "main": "server.js", 5 | "authors": [ 6 | "Chad Roberts " 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://github.com/crobby/angular-seed-openshift", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "1.5.11", 20 | "angular-animate": "1.5.11", 21 | "angular-touch": "1.5.11", 22 | "angular-route": "1.5.11", 23 | "patternfly": "3.16.0", 24 | "angular-patternfly": "3.16.0", 25 | "angular-bootstrap": "0.14.3", 26 | "angular-mocks": "1.5.11", 27 | "angular-notification-icons": "0.4.4", 28 | "angular-hint": "0.3.8", 29 | "uri.js": "1.18.0", 30 | "hawtio-core": "2.0.37", 31 | "hawtio-extension-service": "2.0.2", 32 | "angular-utf8-base64": "0.0.5", 33 | "kubernetes-label-selector": "2.0.0" 34 | }, 35 | "resolutions": { 36 | "bootstrap": "3.3.7", 37 | "angular": "1.5.11", 38 | "jquery": "2.1.4", 39 | "lodash": "4.17.11" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /image.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | version: 0.5.5 4 | release: community 5 | name: oshinko-webui-openshift 6 | from: centos:7 7 | ports: 8 | - value: 8080 9 | modules: 10 | install: 11 | - name: update_os 12 | - name: npm_bower 13 | - name: launch 14 | - name: oc 15 | - name: app 16 | - name: chown 17 | packages: 18 | repositories: 19 | - name: scl 20 | rpm: centos-release-scl 21 | install: 22 | - wget 23 | - git 24 | - bzip2 25 | - rh-nodejs8 26 | artifacts: 27 | - url: https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz 28 | md5: edc8566e6168bd645a7f46d2e7b48663 29 | run: 30 | user: 185 31 | cmd: 32 | - "/usr/src/app/launch.sh" 33 | workdir: /usr/src/app 34 | -------------------------------------------------------------------------------- /make-build-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Regenerate the build directory based on image.*.yaml 4 | make clean-context 5 | make context 6 | make zero-tarballs 7 | 8 | # Add any changes for a commit 9 | git add oshinko-webui-build 10 | -------------------------------------------------------------------------------- /modules/app/added/app/config.template: -------------------------------------------------------------------------------- 1 | ../../../../app/config.template -------------------------------------------------------------------------------- /modules/app/added/app/css: -------------------------------------------------------------------------------- 1 | ../../../../app/css -------------------------------------------------------------------------------- /modules/app/added/app/forms: -------------------------------------------------------------------------------- 1 | ../../../../app/forms -------------------------------------------------------------------------------- /modules/app/added/app/index.html: -------------------------------------------------------------------------------- 1 | ../../../../app/index.html -------------------------------------------------------------------------------- /modules/app/added/app/js: -------------------------------------------------------------------------------- 1 | ../../../../app/js -------------------------------------------------------------------------------- /modules/app/added/app/partials: -------------------------------------------------------------------------------- 1 | ../../../../app/partials -------------------------------------------------------------------------------- /modules/app/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | 5 | SRC=/tmp/scripts/app/added 6 | 7 | cd $SRC/app 8 | 9 | cp -a css /usr/src/app/app 10 | cp -a js /usr/src/app/app 11 | cp -a partials /usr/src/app/app 12 | cp -a forms /usr/src/app/app 13 | cp -a config.template /usr/src/app/app 14 | cp -a *.html /usr/src/app/app 15 | -------------------------------------------------------------------------------- /modules/app/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: app 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /modules/chown/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | 5 | mkdir -p /usr/src/app 6 | chmod a+rwX -R /usr/src/app 7 | -------------------------------------------------------------------------------- /modules/chown/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: chown 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /modules/launch/added/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #!/bin/bash 4 | 5 | source /usr/src/app/sparkimage.sh 6 | if [ -z "$SPARK_DEFAULT" ]; then 7 | SPARK_DEFAULT=$SPARK_IMAGE 8 | fi 9 | export SPARK_DEFAULT 10 | 11 | cp /usr/src/app/app/config.template /usr/src/app/app/config.local.js 12 | 13 | # Set the text label on advanced cluster create 14 | sed -i "s@SPARK_DEFAULT@$SPARK_DEFAULT@" app/forms/new-cluster.html 15 | sed -i "s@SPARK_DEFAULT@$SPARK_DEFAULT@" app/js/controllers.js 16 | sed -i "s##'$CURRENT_NAMESPACE'#" /usr/src/app/app/config.local.js 17 | sed -i "s##'$OSHINKO_REFRESH_INTERVAL'#" /usr/src/app/app/config.local.js 18 | sed -i "s#SPARK_DEFAULT#$SPARK_DEFAULT#" /usr/src/app/app/config.local.js 19 | if [ $INSECURE_WEBUI = "true" ]; then 20 | export OSHINKO_PROXY_LOCATION=`/usr/src/app/oc get routes $WEB_ROUTE_NAME --template={{.spec.host}}` 21 | echo "The oshinko proxy location is $OSHINKO_PROXY_LOCATION" 22 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" /usr/src/app/app/config.local.js 23 | /usr/src/app/oc expose service $WEB_ROUTE_NAME-proxy --path=/proxy --hostname=$OSHINKO_PROXY_LOCATION 24 | else 25 | export OSHINKO_PROXY_LOCATION=`/usr/src/app/oc get routes $WEB_ROUTE_NAME-oaproxy --template={{.spec.host}}` 26 | echo "The oshinko proxy location is $OSHINKO_PROXY_LOCATION" 27 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" /usr/src/app/app/config.local.js 28 | /usr/src/app/oc create route edge oc-proxy-route --service=$WEB_ROUTE_NAME-ocproxy --path=/proxy --insecure-policy=Allow --hostname=$OSHINKO_PROXY_LOCATION 29 | fi 30 | 31 | export OSHINKO_SA_TOKEN=`cat /var/run/secrets/kubernetes.io/serviceaccount/token` 32 | . /opt/rh/rh-nodejs8/enable 33 | npm start 34 | -------------------------------------------------------------------------------- /modules/launch/added/server.js: -------------------------------------------------------------------------------- 1 | ../../../server.js -------------------------------------------------------------------------------- /modules/launch/added/sparkimage.sh: -------------------------------------------------------------------------------- 1 | ../../../sparkimage.sh -------------------------------------------------------------------------------- /modules/launch/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | 5 | mkdir -p /usr/src/app 6 | cp /tmp/scripts/launch/added/launch.sh /usr/src/app 7 | cp /tmp/scripts/launch/added/sparkimage.sh /usr/src/app 8 | cp /tmp/scripts/launch/added/server.js /usr/src/app -------------------------------------------------------------------------------- /modules/launch/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: launch 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /modules/npm_bower/added/bower.json: -------------------------------------------------------------------------------- 1 | ../../../bower.json -------------------------------------------------------------------------------- /modules/npm_bower/added/package.json: -------------------------------------------------------------------------------- 1 | ../../../package.json -------------------------------------------------------------------------------- /modules/npm_bower/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | SRC=/tmp/scripts/npm_bower/added 6 | cd "$SRC" 7 | 8 | mkdir -p /usr/src/app 9 | 10 | cp -pt /usr/src/app \ 11 | "$SRC/package.json" \ 12 | "$SRC/bower.json" 13 | 14 | echo '{ "allow_root": true, "directory": "app/bower_components" }' > /usr/src/app/.bowerrc 15 | 16 | MANPATH= 17 | . /opt/rh/rh-nodejs8/enable 18 | 19 | cd /usr/src/app 20 | npm install 21 | npm install -g bower 22 | bower install --allow-root 23 | -------------------------------------------------------------------------------- /modules/npm_bower/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: npm_bower 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /modules/oc/check_for_download: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! [ -s "$1" ]; then 4 | filename=$(basename $1) 5 | version=$(echo $filename | cut -d '-' -f5) 6 | ref=$(echo $filename | cut -d '-' -f6) 7 | wget https://github.com/openshift/origin/releases/download/$version/openshift-origin-client-tools-$version-$ref-linux-64bit.tar.gz -O $1 8 | fi -------------------------------------------------------------------------------- /modules/oc/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | SCRIPT_DIR=$(dirname $0) 6 | ADDED_DIR=${SCRIPT_DIR}/added 7 | ARTIFACTS_DIR=/tmp/artifacts 8 | 9 | fullname=$(find $ARTIFACTS_DIR -name openshift-origin-client-tools-v[0-9.]*-linux-64bit\.tar\.gz) 10 | filename=$(basename $fullname) 11 | noextension="${filename%.*.*}" 12 | 13 | # If there is a zero-length oshinko-cli tarball, find the verison in the 14 | # name and download from github 15 | 16 | /bin/sh -x $SCRIPT_DIR/check_for_download $fullname 17 | 18 | # unpack the oc tool 19 | { 20 | cd /tmp/artifacts 21 | tar xzf "$filename" 22 | mv "$noextension"/oc /usr/src/app/oc 23 | } 24 | 25 | -------------------------------------------------------------------------------- /modules/oc/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: oc 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /modules/update_os/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | yum update -y && yum clean all && rm -rf /var/cache/yum 6 | 7 | -------------------------------------------------------------------------------- /modules/update_os/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: update_os 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Red Hat 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # ------------------------------------------------------------------------ 16 | # 17 | # This is a Dockerfile for the oshinko-webui-openshift:0.5.5 image. 18 | 19 | FROM centos:7 20 | 21 | USER root 22 | 23 | RUN yum install -y centos-release-scl \ 24 | && yum clean all && rm -rf /var/cache/yum 25 | 26 | 27 | 28 | # Install required RPMs and ensure that the packages were installed 29 | RUN yum install -y wget git bzip2 rh-nodejs8 \ 30 | && yum clean all && rm -rf /var/cache/yum \ 31 | && rpm -q wget git bzip2 rh-nodejs8 32 | 33 | 34 | # Add all artifacts to the /tmp/artifacts 35 | # directory 36 | COPY \ 37 | openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz \ 38 | /tmp/artifacts/ 39 | 40 | 41 | # Environment variables 42 | ENV \ 43 | JBOSS_IMAGE_NAME="oshinko-webui-openshift" \ 44 | JBOSS_IMAGE_VERSION="0.5.5" 45 | 46 | # Labels 47 | LABEL \ 48 | io.cekit.version="2.2.7" \ 49 | io.openshift.expose-services="8080/tcp:webcache" \ 50 | name="oshinko-webui-openshift" \ 51 | org.concrt.version="2.2.7" \ 52 | version="0.5.5" 53 | 54 | # Exposed ports 55 | EXPOSE 8080 56 | # Add scripts used to configure the image 57 | COPY modules /tmp/scripts 58 | 59 | # Custom scripts 60 | USER root 61 | RUN [ "bash", "-x", "/tmp/scripts/update_os/install" ] 62 | 63 | USER root 64 | RUN [ "bash", "-x", "/tmp/scripts/npm_bower/install" ] 65 | 66 | USER root 67 | RUN [ "bash", "-x", "/tmp/scripts/launch/install" ] 68 | 69 | USER root 70 | RUN [ "bash", "-x", "/tmp/scripts/oc/install" ] 71 | 72 | USER root 73 | RUN [ "bash", "-x", "/tmp/scripts/app/install" ] 74 | 75 | USER root 76 | RUN [ "bash", "-x", "/tmp/scripts/chown/install" ] 77 | 78 | USER root 79 | RUN rm -rf /tmp/scripts 80 | USER root 81 | RUN rm -rf /tmp/artifacts 82 | 83 | USER 185 84 | 85 | # Specify the working directory 86 | WORKDIR /usr/src/app 87 | 88 | 89 | CMD ["/usr/src/app/launch.sh"] 90 | 91 | -------------------------------------------------------------------------------- /oshinko-webui-build/help.md: -------------------------------------------------------------------------------- 1 | # oshinko-webui-openshift 2 | 3 | ## Description 4 | 5 | 6 | 7 | 8 | ## Environment variables 9 | 10 | ### Informational 11 | 12 | These environment variables are defined in the image. 13 | 14 | __JBOSS_IMAGE_NAME__ 15 | >"oshinko-webui-openshift" 16 | 17 | __JBOSS_IMAGE_VERSION__ 18 | >"0.5.5" 19 | 20 | 21 | ### Configuration 22 | 23 | The image can be configured by defining these environment variables 24 | when starting a container: 25 | 26 | 27 | 28 | ## Labels 29 | 30 | __io.cekit.version__ 31 | > 2.2.7 32 | 33 | __io.openshift.expose-services__ 34 | > 8080/tcp:webcache 35 | 36 | __name__ 37 | > oshinko-webui-openshift 38 | 39 | __org.concrt.version__ 40 | > 2.2.7 41 | 42 | __version__ 43 | > 0.5.5 44 | 45 | 46 | ## Security implications 47 | 48 | 49 | ### Published Ports 50 | 51 | * 8080 52 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/config.template: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | // This is the template configuration for the webui 5 | // A generated version of this config is created at run-time when running 6 | // from the oshinko-webui template. To run locally, you should edit 7 | // this file replacing all with an appropriate value. 8 | var proxyHost = ; 9 | window.OPENSHIFT_CONFIG = { 10 | apis: { 11 | hostPort: proxyHost, 12 | prefix: "/proxy/apis" 13 | }, 14 | api: { 15 | openshift: { 16 | hostPort: proxyHost, 17 | prefix: "/proxy/oapi" 18 | }, 19 | k8s: { 20 | hostPort: proxyHost, 21 | prefix: "/proxy/api" 22 | } 23 | } 24 | }; 25 | 26 | window.__env = {}; 27 | window.__env.oc_proxy_location = proxyHost; 28 | window.__env.namespace = ; 29 | window.__env.refresh_interval = ; 30 | window.__env.spark_image = 'SPARK_DEFAULT'; 31 | 32 | })(); 33 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * 6 | */ 7 | 8 | 9 | /* app css stylesheet */ 10 | 11 | .login-pf { 12 | background-color: #1a1a1a; 13 | color: #ffffff; 14 | position: absolute; 15 | top: 40px; 16 | width: 100%; 17 | padding-top:30px; 18 | } 19 | 20 | .login-pf .details { 21 | border-left: 1px solid #474747; 22 | padding-left: 40px; 23 | } 24 | 25 | .newbutton { 26 | padding-right: 20px; 27 | } 28 | 29 | .capitalize { 30 | text-transform: capitalize; 31 | } 32 | 33 | .nav-tabs { 34 | margin-bottom:20px !important; 35 | } 36 | 37 | /* login page css */ 38 | .login-pf { 39 | height: 100%; 40 | } 41 | .login-pf .container { 42 | background-color: #181818; 43 | background-color: rgba(255, 255, 255, 0.055); 44 | clear: right; 45 | padding-bottom: 40px; 46 | padding-top: 20px; 47 | width: auto; 48 | } 49 | 50 | .login-pf .container .details p:first-child { 51 | border-top: 1px solid #474747; 52 | padding-top: 25px; 53 | margin-top: 25px; 54 | } 55 | 56 | .login-pf .container .details p { 57 | margin-bottom: 2px; 58 | } 59 | .login-pf .container .form-horizontal .control-label { 60 | font-size: 13px; 61 | font-weight: 400; 62 | text-align: left; 63 | } 64 | .login-pf .container .form-horizontal .form-group:last-child, 65 | .login-pf .container .form-horizontal .form-group:last-child .help-block:last-child { 66 | margin-bottom: 0; 67 | } 68 | .login-pf .container .help-block { 69 | color: #fff; 70 | } 71 | .login-button-container{ 72 | float: right; 73 | padding-left: 20px; 74 | padding-right: 20px; 75 | width: 25%; 76 | } 77 | /* login page css */ 78 | 79 | 80 | 81 | /* landing page css */ 82 | 83 | .container-cards-pf { 84 | display: flex; 85 | margin-top: 10px; 86 | } 87 | 88 | .card-pf { 89 | flex-grow: 1; 90 | margin: 10px; 91 | display: flex; 92 | flex-direction: column; 93 | flex-basis: 25%; 94 | } 95 | 96 | .card-pf-aggregate-status .card-pf-body { 97 | margin-bottom: 0px; 98 | } 99 | 100 | .card-pf-wide { 101 | flex-basis: ~"calc(75% + 20px)"; 102 | } 103 | 104 | .card-pf-double { 105 | flex-basis: ~"calc(66% - 76px)"; 106 | } 107 | 108 | .dashboard-cards { 109 | width: 100%; 110 | max-width: 1024px; 111 | padding-right: 7px; 112 | padding-left: 20px; 113 | } 114 | 115 | .card-pf-aggregate-status-notification .spinner { 116 | display: inline-block; 117 | margin-right: 7px; 118 | } 119 | 120 | .card-pf-aggregate-status-text { 121 | font-size: 13px; 122 | vertical-align: 3px; 123 | } 124 | 125 | .card-pf-heading { 126 | margin-bottom: 0px; 127 | 128 | button { 129 | margin: 15px 0px; 130 | } 131 | 132 | i { 133 | font-size: 24px; 134 | line-height: 16px; 135 | vertical-align: top; 136 | padding-right: 5px; 137 | } 138 | } 139 | 140 | .card-pf-footer { 141 | min-height: 5em; 142 | } 143 | 144 | .card-pf-body { 145 | margin-top: 10px; 146 | margin-bottom: auto; 147 | } 148 | 149 | .card-pf-body.blank-slate-pf { 150 | background: transparent; 151 | padding-top: 20px; 152 | } 153 | 154 | .card-pf-body table.listing-ct { 155 | margin-top: 0px; 156 | width: 100%; 157 | } 158 | 159 | .card-pf-body table.listing-ct thead th { 160 | border-top: none; 161 | } 162 | 163 | .card-pf-body table.listing-ct tbody:last-child { 164 | border-bottom: none; 165 | } 166 | 167 | .card-pf-body table.listing-ct tbody:last-child tr:last-child { 168 | border-bottom: none; 169 | } 170 | 171 | 172 | /* cluster-panel css */ 173 | .cluster-panel { 174 | padding-right: 25px; 175 | padding-top: 5px; 176 | } 177 | 178 | .cluster-panel-actions { 179 | padding-right: 5px; 180 | padding-top: 5px; 181 | } 182 | 183 | table.cluster-body { 184 | margin-top: 0px; 185 | 186 | thead.th { 187 | border-top:0px; 188 | font-size: 12px; 189 | } 190 | 191 | tr { 192 | display: table-row; 193 | vertical-align: inherit; 194 | border-color: inherit; 195 | } 196 | 197 | tr.listing-ct-item { 198 | border-top: 1px solid color("#eee"); 199 | border-bottom: 1px solid color("#eee"); 200 | cursor: pointer; 201 | } 202 | } 203 | table.cluster-listing { 204 | max-width: 2000px; 205 | min-width: 85%; 206 | 207 | thead { 208 | th { 209 | border-top: 0px; 210 | } 211 | } 212 | 213 | tr { 214 | display: table-row; 215 | vertical-align: inherit; 216 | border-color: inherit; 217 | } 218 | 219 | .details-listing { 220 | min-width: 70% !important; 221 | } 222 | 223 | tr.listing-ct-item { 224 | border-top: 1px solid color("#eee"); 225 | border-bottom: 1px solid color("#eee"); 226 | cursor: pointer; 227 | } 228 | 229 | tr.inner-project-listing { 230 | display: table-row; 231 | } 232 | 233 | tbody.first { 234 | tr.tag-item td { 235 | padding-top: 20px; 236 | } 237 | } 238 | 239 | tbody.last { 240 | tr.listing-ct-item.tag-item td:first-child { 241 | background-size: 100% 20px; 242 | background-repeat: no-repeat; 243 | } 244 | } 245 | 246 | tr.tag-item td { 247 | padding-bottom: 0px; 248 | } 249 | 250 | tr.listing-ct-item tt { 251 | color: @metadata-color; 252 | } 253 | } 254 | 255 | .dl-horizontal dt{ 256 | text-align: left !important; 257 | width: auto; 258 | padding-right: 1em; 259 | } 260 | 261 | .dl-horizontal dd{ 262 | margin-left: 0; 263 | margin-bottom: 0; 264 | } 265 | 266 | .close-icon { 267 | color:#000000; 268 | } 269 | 270 | /* Making table appearance similar to console */ 271 | .table.table-bordered > tbody > tr td, .table.table-bordered > tbody > tr th, .table.table-bordered > thead > tr td, .table.table-bordered > thead > tr th { 272 | border-left: 0; 273 | border-right: 0; 274 | padding-bottom: 8px; 275 | padding-top: 8px; 276 | vertical-align: middle 277 | } 278 | 279 | /* Removing outline when details tabs are focused */ 280 | .nav-tabs > li > a:focus { 281 | outline: 0; 282 | } -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/forms/delete-cluster.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/forms/new-cluster.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 67 | 71 | 72 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/forms/scale-cluster.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/index.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | Spark Cluster Management 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * 6 | */ 7 | 8 | 'use strict'; 9 | 10 | 11 | var app = angular.module('Oshinko', [ 12 | 'ngRoute', 13 | 'Oshinko.controllers', 14 | 'Oshinko.factories', 15 | 'ab-base64' 16 | ]); 17 | 18 | app.constant("API_CFG", _.get(window.OPENSHIFT_CONFIG, "api", {})); 19 | app.constant("APIS_CFG", _.get(window.OPENSHIFT_CONFIG, "apis", {})); 20 | app.config(['$locationProvider', function($locationProvider) { 21 | 22 | $locationProvider.html5Mode(false); 23 | 24 | }]); 25 | 26 | app.config(['$routeProvider', function ($routeProvider) { 27 | $routeProvider.when('/clusters/:Id?', 28 | { 29 | templateUrl: function (params) { 30 | if (!params.Id) { 31 | return 'webui/partials/clusters.html'; 32 | } 33 | else { 34 | return 'webui/partials/cluster-detail.html'; 35 | } 36 | }, 37 | controller: 'ClusterCtrl', 38 | activetab: 'clusters' 39 | }); 40 | $routeProvider.when('/login', 41 | { 42 | templateUrl: 'partials/login.html', 43 | controller: 'LoginController', 44 | activetab: '' 45 | }); 46 | $routeProvider.otherwise({redirectTo: '/clusters'}); 47 | }]); 48 | 49 | hawtioPluginLoader.addModule('Oshinko'); 50 | hawtioPluginLoader.registerPreBootstrapTask(function(next) { 51 | // Skips api discovery, needed to run spec tests 52 | if ( _.get(window, "OPENSHIFT_CONFIG.api.k8s.resources") ) { 53 | next(); 54 | return; 55 | } 56 | 57 | var api = { 58 | k8s: {}, 59 | openshift: {} 60 | }; 61 | var apis = {}; 62 | var API_DISCOVERY_ERRORS = []; 63 | var protocol = window.location.protocol + "//"; 64 | 65 | // Fetch /api/v1 for legacy k8s resources, we will never bump the version of these legacy apis so fetch version immediately 66 | var k8sBaseURL = protocol + window.OPENSHIFT_CONFIG.api.k8s.hostPort + window.OPENSHIFT_CONFIG.api.k8s.prefix; 67 | var k8sDeferred = $.get(k8sBaseURL + "/v1") 68 | .done(function(data) { 69 | api.k8s.v1 = _.keyBy(data.resources, 'name'); 70 | }) 71 | .fail(function(data, textStatus, jqXHR) { 72 | API_DISCOVERY_ERRORS.push({ 73 | data: data, 74 | textStatus: textStatus, 75 | xhr: jqXHR 76 | }); 77 | }); 78 | 79 | // Fetch /oapi/v1 for legacy openshift resources, we will never bump the version of these legacy apis so fetch version immediately 80 | var osBaseURL = protocol + window.OPENSHIFT_CONFIG.api.openshift.hostPort + window.OPENSHIFT_CONFIG.api.openshift.prefix; 81 | var osDeferred = $.get(osBaseURL + "/v1") 82 | .done(function(data) { 83 | api.openshift.v1 = _.keyBy(data.resources, 'name'); 84 | }) 85 | .fail(function(data, textStatus, jqXHR) { 86 | API_DISCOVERY_ERRORS.push({ 87 | data: data, 88 | textStatus: textStatus, 89 | xhr: jqXHR 90 | }); 91 | }); 92 | 93 | // Fetch /apis to get the list of groups and versions, then fetch each group/ 94 | // Because the api discovery doc returns arrays and we want maps, this creates a structure like: 95 | // { 96 | // extensions: { 97 | // name: "extensions", 98 | // preferredVersion: "v1beta1", 99 | // versions: { 100 | // v1beta1: { 101 | // version: "v1beta1", 102 | // groupVersion: "extensions/v1beta1" 103 | // resources: { 104 | // daemonsets: { 105 | // /* resource returned from discovery API */ 106 | // } 107 | // } 108 | // } 109 | // } 110 | // } 111 | // } 112 | var apisBaseURL = protocol + window.OPENSHIFT_CONFIG.apis.hostPort + window.OPENSHIFT_CONFIG.apis.prefix; 113 | var getGroups = function(baseURL, hostPrefix, data) { 114 | var apisDeferredVersions = []; 115 | _.each(data.groups, function(apiGroup) { 116 | var group = { 117 | name: apiGroup.name, 118 | preferredVersion: apiGroup.preferredVersion.version, 119 | versions: {}, 120 | hostPrefix: hostPrefix 121 | }; 122 | apis[group.name] = group; 123 | _.each(apiGroup.versions, function(apiVersion) { 124 | var versionStr = apiVersion.version; 125 | group.versions[versionStr] = { 126 | version: versionStr, 127 | groupVersion: apiVersion.groupVersion 128 | }; 129 | apisDeferredVersions.push($.get(baseURL + "/" + apiVersion.groupVersion) 130 | .done(function(data) { 131 | group.versions[versionStr].resources = _.keyBy(data.resources, 'name'); 132 | }) 133 | .fail(function(data, textStatus, jqXHR) { 134 | API_DISCOVERY_ERRORS.push({ 135 | data: data, 136 | textStatus: textStatus, 137 | xhr: jqXHR 138 | }); 139 | })); 140 | }); 141 | }); 142 | return $.when.apply(this, apisDeferredVersions); 143 | }; 144 | var apisDeferred = $.get(apisBaseURL) 145 | .then(_.partial(getGroups, apisBaseURL, null), function(data, textStatus, jqXHR) { 146 | API_DISCOVERY_ERRORS.push({ 147 | data: data, 148 | textStatus: textStatus, 149 | xhr: jqXHR 150 | }); 151 | }); 152 | 153 | // Additional servers can be defined for debugging and prototyping against new servers not yet served by the aggregator 154 | // There can not be any conflicts in the groups/resources from these API servers. 155 | var additionalDeferreds = []; 156 | _.each(window.OPENSHIFT_CONFIG.additionalServers, function(server) { 157 | var baseURL = (server.protocol ? (server.protocol + "://") : protocol) + server.hostPort + server.prefix; 158 | additionalDeferreds.push($.get(baseURL) 159 | .then(_.partial(getGroups, baseURL, server), function(data, textStatus, jqXHR) { 160 | if (server.required !== false) { 161 | API_DISCOVERY_ERRORS.push({ 162 | data: data, 163 | textStatus: textStatus, 164 | xhr: jqXHR 165 | }); 166 | } 167 | })); 168 | }); 169 | 170 | // Will be called on success or failure 171 | var discoveryFinished = function() { 172 | window.OPENSHIFT_CONFIG.api.k8s.resources = api.k8s; 173 | window.OPENSHIFT_CONFIG.api.openshift.resources = api.openshift; 174 | window.OPENSHIFT_CONFIG.apis.groups = apis; 175 | if (API_DISCOVERY_ERRORS.length) { 176 | window.OPENSHIFT_CONFIG.apis.API_DISCOVERY_ERRORS = API_DISCOVERY_ERRORS; 177 | } 178 | next(); 179 | }; 180 | var allDeferreds = [ 181 | k8sDeferred, 182 | osDeferred, 183 | apisDeferred 184 | ]; 185 | allDeferreds = allDeferreds.concat(additionalDeferreds); 186 | $.when.apply(this, allDeferreds).always(discoveryFinished); 187 | }); 188 | 189 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/js/dialog.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Cockpit. 3 | * 4 | * Copyright (C) 2015 Red Hat, Inc. 5 | * 6 | * Cockpit is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation; either version 2.1 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Cockpit is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with Cockpit; If not, see . 18 | */ 19 | /* jshint ignore:start */ 20 | (function() { 21 | "use strict"; 22 | 23 | angular.module('ui.cockpit', [ 24 | 'ui.bootstrap' 25 | ]) 26 | 27 | /* 28 | * Implements a directive that works with ui-bootstrap's 29 | * $modal service. Implements Cockpit dialog HIG behavior. 30 | * 31 | * This dialog treats a button with .btn-cancel class as a cancel 32 | * button. Clicking it will dismiss the dialog or (see below) cancel 33 | * a completion promise. 34 | * 35 | * From inside the dialog, you can invoke the following methods on 36 | * the scope: 37 | * 38 | * failure(ex) 39 | * failure([ex1, ex2]) 40 | * 41 | * Displays errors either globally or on fields. The ex.message or 42 | * ex.toString() is displayed as the failure message. If ex.target 43 | * is a valid CSS selector, then the failure message will be displayed 44 | * under the selected field. 45 | * 46 | * failure() 47 | * failure(null) 48 | * 49 | * Clears all failures from display. 50 | * 51 | * complete(promise) 52 | * complete(data) 53 | * 54 | * Complete the dialog. If a promise is passed, then the dialog will 55 | * enter into a wait state until the promise completes. If promise resolves 56 | * then the dialog will be closed with the resolve value. If the promise 57 | * rejects, then failures will be displayed by invoking failure() above. 58 | * 59 | * While the promise is completing, all .form-control and .btn will 60 | * be disabled. If promise.cancel() is a method, then the .btn-cancel 61 | * will remain clickable, and clicking it will cancel the promise, and 62 | * when the promise completes, will dismiss the dialog. 63 | */ 64 | .directive('modalDialog', [ 65 | "$q", 66 | function($q) { 67 | return { 68 | restrict: 'E', 69 | transclude: true, 70 | template: '', 71 | link: function(scope, element, attrs) { 72 | var state = null; 73 | 74 | function detach() { 75 | if (state) 76 | state.detach(); 77 | state = null; 78 | } 79 | 80 | scope.complete = function(thing) { 81 | detach(); 82 | if (!thing || !thing.then) 83 | thing = $q(thing); 84 | state = new DialogState(element, thing, scope); 85 | }; 86 | 87 | scope.failure = function(/* ... */) { 88 | var errors; 89 | var n = arguments.length; 90 | if (n === 0) { 91 | errors = null; 92 | } else if (n === 1) { 93 | errors = arguments[0]; 94 | } else { 95 | errors = []; 96 | errors.push.apply(errors, arguments); 97 | } 98 | 99 | if (!errors) { 100 | detach(); 101 | return; 102 | } 103 | 104 | var defer = $q.defer(); 105 | defer.reject(errors); 106 | scope.complete(defer.promise); 107 | }; 108 | 109 | /* Dialog cancellation before promises kick in */ 110 | function dismissDialog() { 111 | scope.$dismiss(); 112 | } 113 | 114 | var cancel = queryFirst(element, ".btn-cancel"); 115 | cancel.on("click", dismissDialog); 116 | scope.$on("$routeChangeStart", dismissDialog); 117 | 118 | scope.$on("$destroy", function() { 119 | cancel.off("click", dismissDialog); 120 | detach(); 121 | }); 122 | 123 | /* 124 | * Allow us to submit via the enter button. 125 | * The subFunc argument is the name of a function 126 | * that exists in the scope of the modal controller. 127 | */ 128 | scope.submitForm = function mySubmit(event, subFunc, arg) { 129 | if (event.keyCode === 13) { 130 | scope.complete(eval("scope." + subFunc)(arg)); 131 | } 132 | }; 133 | } 134 | }; 135 | } 136 | ]); 137 | 138 | function queryAll(element, selector) { 139 | var list, result = []; 140 | var j, i, jlen, len = element.length; 141 | for (i = 0; i < len; i++) { 142 | list = element[i].querySelectorAll(selector); 143 | if (list) { 144 | for (j = 0, jlen = list.length; j < jlen; j++) 145 | result.push(list[i]); 146 | } 147 | } 148 | return angular.element(result); 149 | } 150 | 151 | function queryFirst(element, selector) { 152 | var result = null; 153 | var i, len = element.length; 154 | for (i = 0; !result && i < len; i++) 155 | result = element[i].querySelector(selector); 156 | return angular.element(result); 157 | } 158 | 159 | /* 160 | * This state object handles one of three different states. 161 | * This does not exist in the case "before" these states are relevant. 162 | * 163 | * state = null: pending 164 | * state = true: succeeded 165 | * state = false: failed 166 | */ 167 | function DialogState(element, promise, scope) { 168 | var state = null; 169 | var result = null; 170 | 171 | /* Set to true when cancel was requested */ 172 | var cancelled = false; 173 | var detached = false; 174 | 175 | /* The wait field elements */ 176 | var disabled = []; 177 | var wait = angular.element("
"); 178 | wait.append(angular.element("
")); 179 | var notify = angular.element(""); 180 | wait.append(notify); 181 | 182 | this.detach = detachState; 183 | 184 | if (!promise) { 185 | detachState(); 186 | return; 187 | } 188 | 189 | promise.then(function(data) { 190 | result = data; 191 | if (promise) 192 | changeState(true); 193 | }, function(data) { 194 | result = data; 195 | if (promise) 196 | changeState(false); 197 | }, function(data) { 198 | if (promise) 199 | notifyWait(data); 200 | }); 201 | 202 | window.setTimeout(function() { 203 | if (promise && scope && state === null) { 204 | changeState(null); 205 | scope.$digest(); 206 | } 207 | }, 0); 208 | 209 | function changeState(value) { 210 | if (detached) 211 | return; 212 | state = value; 213 | if (cancelled) { 214 | scope.$dismiss(); 215 | return; 216 | } else if (state === null) { 217 | clearErrors(); 218 | displayWait(); 219 | } else if (state === true) { 220 | clearErrors(); 221 | scope.$close(result); /* Close dialog */ 222 | } else if (state === false) { 223 | clearWait(); 224 | displayErrors(result); 225 | } else { 226 | console.warn("invalid dialog state", state); 227 | } 228 | } 229 | 230 | function detachState() { 231 | scope = null; 232 | promise = null; 233 | clearErrors(); 234 | clearWait(); 235 | } 236 | 237 | function displayErrors(errors) { 238 | clearErrors(); 239 | 240 | if (!angular.isArray(errors)) 241 | errors = [errors]; 242 | errors.forEach(function(error) { 243 | var target = null; 244 | /* Each error can have a target field */ 245 | if (error.target) 246 | target = queryFirst(element, error.target); 247 | if (target && target[0]) 248 | fieldError(target, error); 249 | else 250 | globalError(error); 251 | }); 252 | } 253 | 254 | function globalError(error) { 255 | var alert = angular.element("
"); 256 | alert.text(error.message || error.toString()); 257 | alert.prepend(angular.element("")); 258 | 259 | var wrapper = queryFirst(element, ".modal-footer"); 260 | if (wrapper.length) 261 | wrapper.prepend(alert); 262 | else 263 | element.append(alert); 264 | } 265 | 266 | function fieldError(target, error) { 267 | var message = angular.element("
"); 268 | message.text(error.message || error.toString()); 269 | var wrapper = target.parent(); 270 | wrapper.addClass("has-error"); 271 | target.after(message); 272 | wrapper.on("keypress change", handleClear); 273 | } 274 | 275 | function handleClear(ev) { 276 | var target = ev.target; 277 | while (target !== this) { 278 | clearError(angular.element(target)); 279 | target = target.parentNode; 280 | } 281 | } 282 | 283 | function clearError(target) { 284 | var wrapper = target.parent(); 285 | queryAll(wrapper, ".dialog-error").remove(); 286 | wrapper.removeClass("has-error"); 287 | wrapper.off("keypress change", handleClear); 288 | } 289 | 290 | function clearErrors() { 291 | var messages = queryAll(element, ".dialog-error"); 292 | angular.forEach(messages, function(message) { 293 | clearError(angular.element(message)); 294 | }); 295 | } 296 | 297 | function handleCancel(ev) { 298 | if (promise.cancel) 299 | promise.cancel(); 300 | cancelled = true; 301 | ev.stopPropagation(); 302 | ev.preventDefault(); 303 | return false; 304 | } 305 | 306 | function notifyWait(data) { 307 | var message = data.message || data; 308 | if (typeof message === "string" || typeof message === "number") 309 | notify.text(message); 310 | else if (!message) 311 | notify.text(""); 312 | } 313 | 314 | function clearWait() { 315 | var control; 316 | while (true) { 317 | control = disabled.pop(); 318 | if (!control) 319 | break; 320 | control.removeAttr("disabled"); 321 | } 322 | wait.remove(); 323 | queryFirst(element, ".btn-cancel").off("click", handleCancel); 324 | } 325 | 326 | function displayWait() { 327 | clearWait(); 328 | 329 | /* Insert the wait area */ 330 | queryFirst(element, ".modal-footer").prepend(wait); 331 | 332 | /* Disable everything and stash previous disabled state */ 333 | function disable(el) { 334 | var control = angular.element(el); 335 | if (control.attr("disabled") || 336 | promise.cancel && control.hasClass("btn-cancel")) 337 | return; 338 | disabled.push(control); 339 | control.attr("disabled", "disabled"); 340 | } 341 | 342 | angular.forEach(queryAll(element, ".form-control"), disable); 343 | angular.forEach(queryAll(element, ".btn"), disable); 344 | 345 | queryFirst(element, ".btn-cancel").on("click", handleCancel); 346 | } 347 | } 348 | 349 | }()); 350 | /* jshint ignore:end */ 351 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/js/directives.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2017 Red Hat, Inc. 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | /* jshint -W098 */ 10 | var module = angular.module('Oshinko.directives', []); 11 | module.directive('appVersion', 12 | ['version', 13 | function (version) { 14 | return function (scope, elm, attrs) { 15 | elm.text(version); 16 | }; 17 | } 18 | ] 19 | ); 20 | module.directive('clusterPanel', [ 21 | '$q', 22 | 'sendNotifications', 23 | '$interval', 24 | function ($q) { 25 | return { 26 | restrict: 'A', 27 | scope: true, 28 | link: function (scope, element, attrs) { 29 | 30 | function queryFirst(element, selector) { 31 | var result = null; 32 | var i, len = element.length; 33 | for (i = 0; !result && i < len; i++) { 34 | result = element[i].querySelector(selector); 35 | } 36 | return angular.element(result); 37 | } 38 | 39 | var wait = angular.element("
"); 40 | var notify = angular.element(""); 41 | 42 | function appendSpinner() { 43 | wait.append(angular.element("
")); 44 | wait.append(notify); 45 | queryFirst(element, ".table-footer").prepend(wait); 46 | } 47 | 48 | function appendSpinnerPromise() { 49 | var defer = $q.defer(); 50 | appendSpinner(); 51 | defer.resolve(); 52 | return defer.promise; 53 | } 54 | 55 | var clusterId = scope.clusterId; 56 | var setClusterDetails = function (clusterName) { 57 | try { 58 | scope.cluster_details = scope.oshinkoClusters[clusterName]; 59 | scope.cluster_details['name'] = scope.cluster_details.master.svc[Object.keys(scope.cluster_details.master.svc)[0]].metadata.labels['oshinko-cluster']; 60 | scope.cluster_details['workerCount'] = Object.keys(scope.cluster_details.worker.pod).length; 61 | scope.cluster_details['masterCount'] = Object.keys(scope.cluster_details.master.pod).length; 62 | scope.cluster_details['allPods'] = Object.values(scope.cluster_details.worker.pod); 63 | scope.cluster_details['allPods'].push(Object.values(scope.cluster_details.master.pod)[0]); 64 | scope.cluster_details['containers'] = clusterName + "-m|" + clusterName + "-w"; 65 | var masterPodName = Object.keys(scope.cluster_details.master.pod)[0]; 66 | var clusterMetrics = scope.cluster_details.master.pod[masterPodName].metadata.labels["oshinko-metrics-enabled"] && scope.cluster_details.master.pod[masterPodName].metadata.labels["oshinko-metrics-enabled"] === "true"; 67 | scope.metricsAvailable = clusterMetrics; 68 | } catch (e) { 69 | // most likely recently deleted 70 | scope.cluster_details = null; 71 | } 72 | }; 73 | setClusterDetails(clusterId); 74 | 75 | 76 | var tab = 'main'; 77 | var REFRESH_SECONDS = 10; 78 | scope.tab = function (name, ev) { 79 | if (ev) { 80 | tab = name; 81 | ev.stopPropagation(); 82 | } 83 | return tab === name; 84 | }; 85 | 86 | }, 87 | templateUrl: "webui/partials/cluster-panel.html" 88 | }; 89 | } 90 | ]); 91 | module.directive('clusterBody', [ 92 | function () { 93 | return { 94 | restrict: 'A', 95 | templateUrl: 'webui/partials/cluster-body.html', 96 | link: function (scope, element, attrs) { 97 | } 98 | }; 99 | } 100 | ]); 101 | /* jshint +W098 */ -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/js/factories.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | var module = angular.module('Oshinko.factories', ['ui.bootstrap', 'patternfly.notification']); 10 | 11 | module.factory('clusterActions', [ 12 | '$uibModal', 13 | function($uibModal) { 14 | function deleteCluster(clusterName) { 15 | return $uibModal.open({ 16 | animation: true, 17 | controller: 'ClusterDeleteCtrl', 18 | templateUrl: '/webui/forms/' + 'delete-cluster.html', 19 | resolve: { 20 | dialogData: function() { 21 | return { clusterName: clusterName }; 22 | } 23 | } 24 | }).result; 25 | } 26 | function newCluster() { 27 | return $uibModal.open({ 28 | animation: true, 29 | controller: 'ClusterNewCtrl', 30 | templateUrl: '/webui/forms/' + 'new-cluster.html', 31 | resolve: { 32 | dialogData: function() { 33 | return { }; 34 | } 35 | } 36 | }).result; 37 | } 38 | function scaleCluster(clusterName, workerCount, masterCount) { 39 | return $uibModal.open({ 40 | animation: true, 41 | controller: 'ClusterDeleteCtrl', 42 | templateUrl: '/webui/forms/' + 'scale-cluster.html', 43 | resolve: { 44 | dialogData: function() { 45 | return { clusterName: clusterName, 46 | workerCount: workerCount, 47 | masterCount: masterCount 48 | }; 49 | } 50 | } 51 | }).result; 52 | } 53 | return { 54 | deleteCluster: deleteCluster, 55 | newCluster: newCluster, 56 | scaleCluster: scaleCluster 57 | }; 58 | } 59 | ]); 60 | 61 | module.factory('sendNotifications', function(Notifications) { 62 | var notificationFactory = {}; 63 | var typeMap = { 64 | 'Info': Notifications.info, 65 | 'Success': Notifications.success, 66 | 'Warning': Notifications.warn, 67 | 'Error': Notifications.error 68 | }; 69 | 70 | notificationFactory.notify = function(type, message) { 71 | typeMap[type](message); 72 | }; 73 | return notificationFactory; 74 | }); 75 | 76 | /* Error handling factory. Since our server will return 77 | * a successful status code, even on things where an operation 78 | * was not successful, we need to take a closer look at 79 | * the response itself to determine if we should report an 80 | * error to the user. This factory is mean to be the one stop shop 81 | * for error handling and can be extended to handle all sorts 82 | * of things. 83 | */ 84 | module.factory('errorHandling', function(sendNotifications) { 85 | var errorHandlingFactory= {}; 86 | 87 | errorHandlingFactory.handle = function(response, error, defer, successMsg) { 88 | if (response && response.data && response.data.errors) { 89 | response.data.errors.forEach(function (singleError) { 90 | console.error(singleError['title'] + "\nStatus Code: " + singleError.status + "\n" + singleError.details); 91 | }); 92 | if (defer) { 93 | defer.reject(response.data.errors[0].details); 94 | } 95 | } else if (error) { 96 | console.error("Problem communicating with server. Error code: " + error.status); 97 | if (defer) { 98 | defer.reject(error.data); 99 | } 100 | } else { 101 | sendNotifications.notify("Success", successMsg); 102 | if(defer) { 103 | defer.resolve(successMsg); 104 | } 105 | } 106 | }; 107 | return errorHandlingFactory; 108 | }); 109 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/js/listing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Cockpit. 3 | * 4 | * Copyright (C) 2015 Red Hat, Inc. 5 | * 6 | * Cockpit is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU Lesser General Public License as published by 8 | * the Free Software Foundation; either version 2.1 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Cockpit is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with Cockpit; If not, see . 18 | */ 19 | /* jshint ignore:start */ 20 | (function() { 21 | "use strict"; 22 | 23 | function inClassOrTag(el, cls, tag) { 24 | return (el && el.classList && el.classList.contains(cls)) || 25 | (el && el.tagName === tag) || 26 | (el && inClassOrTag(el.parentNode, cls, tag)); 27 | } 28 | 29 | angular.module('ui.listing', []) 30 | 31 | .directive('listingTable', [ 32 | function() { 33 | return { 34 | restrict: 'A', 35 | link: function(scope, element, attrs) { 36 | } 37 | }; 38 | } 39 | ]) 40 | 41 | .factory('ListingState', [ 42 | function() { 43 | return function ListingState(scope) { 44 | var self = this; 45 | var data = { }; 46 | 47 | self.selected = { }; 48 | self.enableActions = false; 49 | 50 | /* Check that either .btn or li were not clicked */ 51 | function checkBrowserEvent(ev) { 52 | return !(ev && inClassOrTag(ev.target, "btn", "li")); 53 | } 54 | 55 | self.hasSelected = function hasSelected(id) { 56 | return !angular.equals({}, self.selected); 57 | }; 58 | 59 | self.expanded = function expanded(id) { 60 | if (angular.isUndefined(id)) { 61 | for (id in data) 62 | return true; 63 | return false; 64 | } else { 65 | return id in data; 66 | } 67 | }; 68 | 69 | self.toggle = function toggle(id, ev) { 70 | var value; 71 | if (self.enableActions) { 72 | ev.stopPropagation(); 73 | return; 74 | } 75 | 76 | if (id) { 77 | value = !(id in data); 78 | if (value) 79 | self.expand(id, ev); 80 | else 81 | self.collapse(id, ev); 82 | } 83 | }; 84 | 85 | self.expand = function expand(id, ev) { 86 | data[id] = true; 87 | if (ev) 88 | ev.stopPropagation(); 89 | }; 90 | 91 | self.activate = function expand(id, ev) { 92 | var emitted; 93 | if (checkBrowserEvent(ev)) 94 | emitted = scope.$emit("activate", id); 95 | }; 96 | 97 | self.collapse = function collapse(id, ev) { 98 | if (id) { 99 | delete data[id]; 100 | } else { 101 | Object.keys(data).forEach(function(old) { 102 | delete data[old]; 103 | }); 104 | } 105 | if (ev) 106 | ev.stopPropagation(); 107 | }; 108 | }; 109 | } 110 | ]) 111 | 112 | .directive('listingPanel', [ 113 | function() { 114 | return { 115 | restrict: 'A', 116 | scope: true, 117 | link: function(scope, element, attrs) { 118 | var tab = 'main'; 119 | scope.tab = function(name, ev) { 120 | if (ev) { 121 | tab = name; 122 | ev.stopPropagation(); 123 | } 124 | return tab === name; 125 | }; 126 | }, 127 | templateUrl: function(element, attrs) { 128 | var kind = attrs.kind; 129 | return "webui/partials/" + kind.toLowerCase() + "-panel.html"; 130 | } 131 | }; 132 | } 133 | ]); 134 | }()); 135 | /* jshint ignore:end */ 136 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/partials/cluster-body.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 23 | 29 | 30 | 31 |
PodsTypeStatus
13 |
14 | 15 | {{pod.status.podIP}} 16 |
17 |
19 |
20 | {{pod.metadata.labels["oshinko-type"]}} 21 |
22 |
24 | {{pod.status.phase}} 25 | {{pod.status.phase}} 26 | {{pod.status.phase}} 27 | {{pod.status.phase}} 28 |
32 |
-------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/partials/cluster-detail.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 |
12 | 23 |

Cluster Details

24 |
25 |
26 |
27 |
28 | 29 |
30 |

Details are unavailable

31 |

The cluster may have been deleted.

32 |
33 | 34 | 35 | Details 36 |
37 |
38 | 39 | Pods 40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/partials/cluster-panel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/partials/clusters.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 |
12 |
13 | 15 |
16 |

Spark Clusters

17 |
18 |
19 |
20 |
21 | 22 |
23 |

No Spark Clusters present

24 |

You can deploy a new spark cluster.

25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 | 71 | 72 | 73 |
NameStatusMasterMastersWorkersSpark UI LinkActions
{{ cluster }} 43 | 44 | 45 | 46 | 47 | 48 | {{ getClusterStatus(oshinkoClusters[cluster]) }} 49 | {{ getSparkMasterUrl(cluster) }}{{ countMasters(oshinkoClusters[cluster]) }}{{ countWorkers(oshinkoClusters[cluster]) }}Spark UIN/A 56 | 57 | 61 | 69 | 70 |
74 |
75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/partials/detail-body.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Name
4 |
{{cluster_id}}
5 |
Status
6 |
{{getClusterStatus(oshinkoClusters[cluster_id])}}
7 |
Master
8 |
{{getSparkMasterUrl(cluster_id)}}
9 |
Master count
10 |
{{countMasters(oshinkoClusters[cluster_id])}}
11 |
Worker count
12 |
{{countWorkers(oshinkoClusters[cluster_id])}}
13 |
Master Web UI
14 |
{{getSparkWebUi(oshinkoClusters[cluster_id])}}
15 |
N/A
16 |
Cluster configuration
17 |
{{getClusterConfig(oshinkoClusters[cluster_id])}}
18 |
19 |
20 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/added/app/partials/login.html: -------------------------------------------------------------------------------- 1 | 9 | 47 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | 5 | SRC=/tmp/scripts/app/added 6 | 7 | cd $SRC/app 8 | 9 | cp -a css /usr/src/app/app 10 | cp -a js /usr/src/app/app 11 | cp -a partials /usr/src/app/app 12 | cp -a forms /usr/src/app/app 13 | cp -a config.template /usr/src/app/app 14 | cp -a *.html /usr/src/app/app 15 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/app/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: app 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/chown/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | 5 | mkdir -p /usr/src/app 6 | chmod a+rwX -R /usr/src/app 7 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/chown/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: chown 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/launch/added/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #!/bin/bash 4 | 5 | source /usr/src/app/sparkimage.sh 6 | if [ -z "$SPARK_DEFAULT" ]; then 7 | SPARK_DEFAULT=$SPARK_IMAGE 8 | fi 9 | export SPARK_DEFAULT 10 | 11 | cp /usr/src/app/app/config.template /usr/src/app/app/config.local.js 12 | 13 | # Set the text label on advanced cluster create 14 | sed -i "s@SPARK_DEFAULT@$SPARK_DEFAULT@" app/forms/new-cluster.html 15 | sed -i "s@SPARK_DEFAULT@$SPARK_DEFAULT@" app/js/controllers.js 16 | sed -i "s##'$CURRENT_NAMESPACE'#" /usr/src/app/app/config.local.js 17 | sed -i "s##'$OSHINKO_REFRESH_INTERVAL'#" /usr/src/app/app/config.local.js 18 | sed -i "s#SPARK_DEFAULT#$SPARK_DEFAULT#" /usr/src/app/app/config.local.js 19 | if [ $INSECURE_WEBUI = "true" ]; then 20 | export OSHINKO_PROXY_LOCATION=`/usr/src/app/oc get routes $WEB_ROUTE_NAME --template={{.spec.host}}` 21 | echo "The oshinko proxy location is $OSHINKO_PROXY_LOCATION" 22 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" /usr/src/app/app/config.local.js 23 | /usr/src/app/oc expose service $WEB_ROUTE_NAME-proxy --path=/proxy --hostname=$OSHINKO_PROXY_LOCATION 24 | else 25 | export OSHINKO_PROXY_LOCATION=`/usr/src/app/oc get routes $WEB_ROUTE_NAME-oaproxy --template={{.spec.host}}` 26 | echo "The oshinko proxy location is $OSHINKO_PROXY_LOCATION" 27 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" /usr/src/app/app/config.local.js 28 | /usr/src/app/oc create route edge oc-proxy-route --service=$WEB_ROUTE_NAME-ocproxy --path=/proxy --insecure-policy=Allow --hostname=$OSHINKO_PROXY_LOCATION 29 | fi 30 | 31 | export OSHINKO_SA_TOKEN=`cat /var/run/secrets/kubernetes.io/serviceaccount/token` 32 | . /opt/rh/rh-nodejs8/enable 33 | npm start 34 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/launch/added/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2017 Red Hat, Inc. 5 | * 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var express = require("express"); 11 | var bodyParser = require("body-parser"); 12 | var app = express(); 13 | 14 | var oshinko_proxy_location = process.env.OSHINKO_PROXY_LOCATION || ""; 15 | var oshinko_current_namespace = process.env.CURRENT_NAMESPACE || ""; 16 | var spark_default = process.env.SPARK_DEFAULT || ""; 17 | 18 | app.set('views', __dirname + '/app'); 19 | app.use(bodyParser.json()); 20 | app.use(express.static(__dirname + '/app')); 21 | app.use('/webui/bower_components', express.static(__dirname + '/app/bower_components')); 22 | app.use('/webui/css', express.static(__dirname + '/app/css')); 23 | app.use('/webui/js', express.static(__dirname + '/app/js')); 24 | app.use('/webui/partials', express.static(__dirname + '/app/partials')); 25 | app.use('/webui/forms', express.static(__dirname + '/app/forms')); 26 | app.use('/webui', express.static(__dirname + '/app')); 27 | app.engine('html', require('ejs').renderFile); 28 | 29 | app.get('*', function (request, response) { 30 | response.render('index.html'); 31 | }); 32 | 33 | var port = process.env.OPENSHIFT_NODEJS_PORT || 8080; 34 | app.listen(port, function () { 35 | console.log("Listening on " + port); 36 | console.log("Proxy location: " + oshinko_proxy_location + "/proxy"); 37 | console.log("Current namespace is: " + oshinko_current_namespace); 38 | console.log("Spark default image if not overridden is " + spark_default); 39 | }); 40 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/launch/added/sparkimage.sh: -------------------------------------------------------------------------------- 1 | SPARK_IMAGE="radanalyticsio/openshift-spark:2.4-latest" 2 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/launch/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | 5 | mkdir -p /usr/src/app 6 | cp /tmp/scripts/launch/added/launch.sh /usr/src/app 7 | cp /tmp/scripts/launch/added/sparkimage.sh /usr/src/app 8 | cp /tmp/scripts/launch/added/server.js /usr/src/app -------------------------------------------------------------------------------- /oshinko-webui-build/modules/launch/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: launch 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/npm_bower/added/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oshinko-web", 3 | "description": "A web ui for managing spark clusters on Openshift", 4 | "main": "server.js", 5 | "authors": [ 6 | "Chad Roberts " 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://github.com/crobby/angular-seed-openshift", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "1.5.11", 20 | "angular-animate": "1.5.11", 21 | "angular-touch": "1.5.11", 22 | "angular-route": "1.5.11", 23 | "patternfly": "3.16.0", 24 | "angular-patternfly": "3.16.0", 25 | "angular-bootstrap": "0.14.3", 26 | "angular-mocks": "1.5.11", 27 | "angular-notification-icons": "0.4.4", 28 | "angular-hint": "0.3.8", 29 | "uri.js": "1.18.0", 30 | "hawtio-core": "2.0.37", 31 | "hawtio-extension-service": "2.0.2", 32 | "angular-utf8-base64": "0.0.5", 33 | "kubernetes-label-selector": "2.0.0" 34 | }, 35 | "resolutions": { 36 | "bootstrap": "3.3.7", 37 | "angular": "1.5.11", 38 | "jquery": "2.1.4", 39 | "lodash": "4.17.11" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/npm_bower/added/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oshinko", 3 | "private": true, 4 | "dependencies": { 5 | "ejs": "2.6.1", 6 | "bower": "1.8.8", 7 | "express": "4.16.0" 8 | }, 9 | "devDependencies": { 10 | "autoprefixer-core": "5.2.1", 11 | "jasmine": "2.5.2", 12 | "jshint-stylish": "^1.0.2", 13 | "requirejs": "2.3.5", 14 | "phantomjs-prebuilt": "2.1.16" 15 | }, 16 | "engines": { 17 | "node": "8.9.4" 18 | }, 19 | "main": "server.js", 20 | "scripts": { 21 | "start": "node server.js" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/npm_bower/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | SRC=/tmp/scripts/npm_bower/added 6 | cd "$SRC" 7 | 8 | mkdir -p /usr/src/app 9 | 10 | cp -pt /usr/src/app \ 11 | "$SRC/package.json" \ 12 | "$SRC/bower.json" 13 | 14 | echo '{ "allow_root": true, "directory": "app/bower_components" }' > /usr/src/app/.bowerrc 15 | 16 | MANPATH= 17 | . /opt/rh/rh-nodejs8/enable 18 | 19 | cd /usr/src/app 20 | npm install 21 | npm install -g bower 22 | bower install --allow-root 23 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/npm_bower/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: npm_bower 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/oc/check_for_download: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! [ -s "$1" ]; then 4 | filename=$(basename $1) 5 | version=$(echo $filename | cut -d '-' -f5) 6 | ref=$(echo $filename | cut -d '-' -f6) 7 | wget https://github.com/openshift/origin/releases/download/$version/openshift-origin-client-tools-$version-$ref-linux-64bit.tar.gz -O $1 8 | fi -------------------------------------------------------------------------------- /oshinko-webui-build/modules/oc/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | SCRIPT_DIR=$(dirname $0) 6 | ADDED_DIR=${SCRIPT_DIR}/added 7 | ARTIFACTS_DIR=/tmp/artifacts 8 | 9 | fullname=$(find $ARTIFACTS_DIR -name openshift-origin-client-tools-v[0-9.]*-linux-64bit\.tar\.gz) 10 | filename=$(basename $fullname) 11 | noextension="${filename%.*.*}" 12 | 13 | # If there is a zero-length oshinko-cli tarball, find the verison in the 14 | # name and download from github 15 | 16 | /bin/sh -x $SCRIPT_DIR/check_for_download $fullname 17 | 18 | # unpack the oc tool 19 | { 20 | cd /tmp/artifacts 21 | tar xzf "$filename" 22 | mv "$noextension"/oc /usr/src/app/oc 23 | } 24 | 25 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/oc/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: oc 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/update_os/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | yum update -y && yum clean all && rm -rf /var/cache/yum 6 | 7 | -------------------------------------------------------------------------------- /oshinko-webui-build/modules/update_os/module.yaml: -------------------------------------------------------------------------------- 1 | schema_version: 1 2 | 3 | name: update_os 4 | execute: 5 | - script: install 6 | -------------------------------------------------------------------------------- /oshinko-webui-build/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radanalyticsio/oshinko-webui/cd9d5b9abcb01fd6fbd9909d12d9c8b6e5fd2402/oshinko-webui-build/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oshinko", 3 | "private": true, 4 | "dependencies": { 5 | "ejs": "2.6.1", 6 | "bower": "1.8.8", 7 | "express": "4.16.0" 8 | }, 9 | "devDependencies": { 10 | "autoprefixer-core": "5.2.1", 11 | "jasmine": "2.5.2", 12 | "jshint-stylish": "^1.0.2", 13 | "requirejs": "2.3.5", 14 | "phantomjs-prebuilt": "2.1.16" 15 | }, 16 | "engines": { 17 | "node": "8.9.4" 18 | }, 19 | "main": "server.js", 20 | "scripts": { 21 | "start": "node server.js" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /release-templates.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Usage: release-templates.sh VERSION-TAG" 5 | exit 1 6 | fi 7 | 8 | TOP_DIR=$(readlink -f `dirname "$0"` | grep -o '.*/oshinko-webui') 9 | mkdir -p $TOP_DIR/release_templates 10 | rm -rf $TOP_DIR/release_templates/* 11 | 12 | cp $TOP_DIR/tools/*.yaml $TOP_DIR/release_templates 13 | 14 | sed -r -i "s@(radanalyticsio/oshinko-webui)@\1:$1@" $TOP_DIR/release_templates/* 15 | 16 | echo "Successfully wrote templates to release_templates/ with version tag $1" 17 | echo 18 | echo "grep radanalyticsio/oshinko-webui:$1 *" 19 | echo 20 | cd $TOP_DIR/; grep radanalyticsio/oshinko-webui:$1 release_templates/* 21 | -------------------------------------------------------------------------------- /scripts/info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | $(dirname $0)/oshinko version 3 | -------------------------------------------------------------------------------- /scripts/launch-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | export CURRENT_NAMESPACE=`oc project -q` 5 | export OSHINKO_REFRESH_INTERVAL=5 6 | export SPARK_DEFAULT='radanalyticsio/openshift-spark' 7 | export OSHINKO_PROXY_LOCATION='127.0.0.1:8001' #location of oc proxy running 8 | 9 | export BASEDIR=`git rev-parse --show-toplevel` 10 | cp $BASEDIR/app/config.template $BASEDIR/app/config.local.js 11 | 12 | sed -i "s#SPARK_DEFAULT#$SPARK_DEFAULT#" $BASEDIR/app/forms/new-cluster.html 13 | sed -i "s##'$CURRENT_NAMESPACE'#" $BASEDIR/app/config.local.js 14 | sed -i "s##'$OSHINKO_REFRESH_INTERVAL'#" $BASEDIR/app/config.local.js 15 | sed -i "s#SPARK_DEFAULT#$SPARK_DEFAULT#" $BASEDIR/app/config.local.js 16 | sed -i "s#SPARK_DEFAULT#$SPARK_DEFAULT#" $BASEDIR/app/js/controllers.js 17 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" $BASEDIR/app/config.local.js 18 | npm start -------------------------------------------------------------------------------- /scripts/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /usr/src/app/sparkimage.sh 4 | if [ -z "$SPARK_DEFAULT" ]; then 5 | SPARK_DEFAULT=$SPARK_IMAGE 6 | fi 7 | export SPARK_DEFAULT 8 | 9 | cp /usr/src/app/app/config.template /usr/src/app/app/config.local.js 10 | 11 | # Set the text label on advanced cluster create 12 | sed -i "s@SPARK_DEFAULT@$SPARK_DEFAULT@" app/forms/new-cluster.html 13 | sed -i "s@SPARK_DEFAULT@$SPARK_DEFAULT@" app/js/controllers.js 14 | sed -i "s##'$CURRENT_NAMESPACE'#" /usr/src/app/app/config.local.js 15 | sed -i "s##'$OSHINKO_REFRESH_INTERVAL'#" /usr/src/app/app/config.local.js 16 | sed -i "s#SPARK_DEFAULT#$SPARK_DEFAULT#" /usr/src/app/app/config.local.js 17 | if [ $INSECURE_WEBUI = "true" ]; then 18 | export OSHINKO_PROXY_LOCATION=`/usr/src/app/oc get routes $WEB_ROUTE_NAME --template={{.spec.host}}` 19 | echo "The oshinko proxy location is $OSHINKO_PROXY_LOCATION" 20 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" /usr/src/app/app/config.local.js 21 | /usr/src/app/oc expose service $WEB_ROUTE_NAME-proxy --path=/proxy --hostname=$OSHINKO_PROXY_LOCATION 22 | else 23 | export OSHINKO_PROXY_LOCATION=`/usr/src/app/oc get routes $WEB_ROUTE_NAME-oaproxy --template={{.spec.host}}` 24 | echo "The oshinko proxy location is $OSHINKO_PROXY_LOCATION" 25 | sed -i "s//'$OSHINKO_PROXY_LOCATION'/" /usr/src/app/app/config.local.js 26 | /usr/src/app/oc create route edge oc-proxy-route --service=$WEB_ROUTE_NAME-ocproxy --path=/proxy --insecure-policy=Allow --hostname=$OSHINKO_PROXY_LOCATION 27 | fi 28 | 29 | npm start 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Oshinko. 3 | * 4 | * Copyright (C) 2017 Red Hat, Inc. 5 | * 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var express = require("express"); 11 | var bodyParser = require("body-parser"); 12 | var app = express(); 13 | 14 | var oshinko_proxy_location = process.env.OSHINKO_PROXY_LOCATION || ""; 15 | var oshinko_current_namespace = process.env.CURRENT_NAMESPACE || ""; 16 | var spark_default = process.env.SPARK_DEFAULT || ""; 17 | 18 | app.set('views', __dirname + '/app'); 19 | app.use(bodyParser.json()); 20 | app.use(express.static(__dirname + '/app')); 21 | app.use('/webui/bower_components', express.static(__dirname + '/app/bower_components')); 22 | app.use('/webui/css', express.static(__dirname + '/app/css')); 23 | app.use('/webui/js', express.static(__dirname + '/app/js')); 24 | app.use('/webui/partials', express.static(__dirname + '/app/partials')); 25 | app.use('/webui/forms', express.static(__dirname + '/app/forms')); 26 | app.use('/webui', express.static(__dirname + '/app')); 27 | app.engine('html', require('ejs').renderFile); 28 | 29 | app.get('*', function (request, response) { 30 | response.render('index.html'); 31 | }); 32 | 33 | var port = process.env.OPENSHIFT_NODEJS_PORT || 8080; 34 | app.listen(port, function () { 35 | console.log("Listening on " + port); 36 | console.log("Proxy location: " + oshinko_proxy_location + "/proxy"); 37 | console.log("Current namespace is: " + oshinko_current_namespace); 38 | console.log("Spark default image if not overridden is " + spark_default); 39 | }); 40 | -------------------------------------------------------------------------------- /sparkimage.sh: -------------------------------------------------------------------------------- 1 | SPARK_IMAGE="radanalyticsio/openshift-spark:2.4-latest" 2 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "esnext": true, 7 | "jasmine": true, 8 | "latedef": true, 9 | "noarg": true, 10 | "node": true, 11 | "strict": true, 12 | "undef": true, 13 | "unused": true, 14 | "globals": { 15 | "angular": false, 16 | "inject": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | framework: 'jasmine', 3 | specs: ['spec/all-functionality-insecure.js'], 4 | baseUrl: 'http://localhost:8080', 5 | getPageTimeout: 120000, 6 | allScriptsTimeout: 120000, 7 | jasmineNodeOpts: { 8 | defaultTimeoutInterval: 240000 9 | }, 10 | params: { 11 | securelogin: { 12 | name: 'developer', 13 | password: 'developerpass' 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /test/e2e-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | which dnf 4 | if [ "$?" -eq 0 ]; then 5 | INSTALL=dnf 6 | else 7 | INSTALL=yum 8 | fi 9 | 10 | sudo $INSTALL install -y which npm tar bzip2 wget xorg-x11-server-Xvfb 11 | 12 | which bower 13 | if [ "$?" -ne 0 ]; then 14 | sudo npm install -g bower 15 | fi 16 | 17 | which protractor 18 | if [ "$?" -ne 0 ]; then 19 | sudo npm install -g protractor 20 | fi 21 | 22 | which karma 23 | if [ "$?" -ne 0 ]; then 24 | sudo npm install -g karma-cli 25 | fi 26 | 27 | which java 28 | if [ "$?" -ne 0 ]; then 29 | sudo $INSTALL install -y java-1.8.0-openjdk 30 | fi 31 | 32 | which google-chrome-stable 33 | if [ "$?" -ne 0 ]; then 34 | sudo bash -c 'cat << EOF > /etc/yum.repos.d/google-chrome.repo 35 | [google-chrome] 36 | name=google-chrome - \$basearch 37 | baseurl=http://dl.google.com/linux/chrome/rpm/stable/\$basearch 38 | enabled=1 39 | gpgcheck=1 40 | gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub 41 | EOF' 42 | 43 | sudo $INSTALL install -y google-chrome-stable 44 | fi 45 | 46 | nodeversion=$(node --version | cut -d '.' -f1) 47 | nodeversion="$((${nodeversion#v}))" 48 | if [ "$nodeversion" -lt 6 ]; then 49 | echo 50 | echo '****************************************************************************************************************************************' 51 | echo Looks like your node version is less than 6 which is required for the test 52 | echo You can follow these instructions to upgrade it https://nodejs.org/en/download/package-manager/#enterprise-linux-and-fedora 53 | echo '****************************************************************************************************************************************' 54 | echo 55 | exit 1 56 | fi 57 | 58 | echo "Updating webdriver" 59 | sudo webdriver-manager update --gecko=false 60 | 61 | which docker 62 | if [ "$?" -ne 0 ]; then 63 | echo *** Docker is not installed, it will be necessary to run the tests. 64 | fi 65 | 66 | 67 | which oc 68 | if [ "$?" -ne 0 ]; then 69 | echo *** The 'oc' client is not installed, it will be necessary to run the tests 70 | fi 71 | -------------------------------------------------------------------------------- /test/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOP_DIR=$(readlink -f `dirname "${BASH_SOURCE[0]}"` | grep -o '.*/oshinko-webui') 4 | PROJECT=$(oc project -q) 5 | 6 | WEBUI_START_XVFB=${WEBUI_START_XVFB:-true} 7 | WEBUI_TEST_IMAGE=${WEBUI_TEST_IMAGE:-} 8 | WEBUI_TEST_SECURE=${WEBUI_TEST_SECURE:-false} 9 | WEBUI_TEST_LOCAL_IMAGE=${WEBUI_TEST_LOCAL_IMAGE:-true} 10 | 11 | # If you're doing a test of the secure webui, default user/pass is "developer/developerpass" 12 | # If you need to change it you can set these 13 | WEBUI_TEST_SECURE_USER=${WEBUI_TEST_SECURE_USER:-} 14 | WEBUI_TEST_SECURE_PASSWORD=${WEBUI_TEST_SECURE_PASSWORD:-} 15 | 16 | # This is all for dealing with registries. External registry requires creds other than the current login 17 | WEBUI_TEST_INTEGRATED_REGISTRY=${WEBUI_TEST_INTEGRATED_REGISTRY:-} 18 | WEBUI_TEST_EXTERNAL_REGISTRY=${WEBUI_TEST_EXTERNAL_REGISTRY:-} 19 | WEBUI_TEST_EXTERNAL_USER=${WEBUI_TEST_EXTERNAL_USER:-} 20 | WEBUI_TEST_EXTERNAL_PASSWORD=${WEBUI_TEST_EXTERNAL_PASSWORD:-} 21 | 22 | WEBUI_TEST_RESOURCES=${WEBUI_TEST_RESOURCES:-$TOP_DIR/tools/resources.yaml} 23 | if [ -z "$WEBUI_TEST_IMAGE" ]; then 24 | if [ "$WEBUI_TEST_LOCAL_IMAGE" == true ]; then 25 | WEBUI_TEST_IMAGE=oshinko-webui:latest 26 | else 27 | WEBUI_TEST_IMAGE=docker.io/radanalyticsio/oshinko-webui 28 | fi 29 | fi 30 | 31 | function print_test_env { 32 | echo Using image $WEBUI_TEST_IMAGE 33 | echo Using resources.yaml from $WEBUI_TEST_RESOURCES 34 | 35 | if [ "$WEBUI_TEST_LOCAL_IMAGE" != true ]; then 36 | echo WEBUI_TEST_LOCAL_IMAGE = $WEBUI_TEST_LOCAL_IMAGE, webui image is external, ignoring registry env vars 37 | elif [ -n "$WEBUI_TEST_EXTERNAL_REGISTRY" ]; then 38 | echo Using external registry $WEBUI_TEST_EXTERNAL_REGISTRY 39 | if [ -z "$WEBUI_TEST_EXTERNAL_USER" ]; then 40 | echo "Error: WEBUI_TEST_EXTERNAL_USER not set!" 41 | exit 1 42 | else 43 | echo Using external registry user $WEBUI_TEST_EXTERNAL_USER 44 | fi 45 | if [ -z "$WEBUI_TEST_EXTERNAL_PASSWORD" ]; then 46 | echo "WEBUI_TEST_EXTERNAL_PASSWORD not set, assuming current docker login" 47 | else 48 | echo External registry password set 49 | fi 50 | elif [ -n "$WEBUI_TEST_INTEGRATED_REGISTRY" ]; then 51 | echo Using integrated registry $WEBUI_TEST_INTEGRATED_REGISTRY 52 | else 53 | echo Not using external or integrated registry 54 | fi 55 | echo Start xvfb = $WEBUI_START_XVFB 56 | echo Using secure oshinko webui $WEBUI_TEST_SECURE 57 | if [ "$WEBUI_TEST_SECURE" == true ]; then 58 | if [ -n "$WEBUI_TEST_SECURE_USER" ]; then 59 | echo Using oshinko webui secure user $WEBUI_TEST_SECURE_USER 60 | else 61 | echo Using default oshinko webui secure user set in conf.js 62 | fi 63 | if [ -n "$WEBUI_TEST_SECURE_PASSWORD" ]; then 64 | echo Oshinko webui secure password has been set 65 | else 66 | echo Using default oshinko webui secure password set in conf.js 67 | fi 68 | fi 69 | } 70 | 71 | function push_image { 72 | # The ip address of an internal/external registry may be set to support running against 73 | # an openshift that is not "oc cluster up" when using images that have been built locally. 74 | # In the case of "oc cluster up", the docker on the host is available from openshift so 75 | # no special pushes of images have to be done. 76 | # In the case of a "normal" openshift cluster, a local image we'll use for build has to be 77 | # available from the designated registry. 78 | # If we're using an image already in an external registry, openshift can pull it from 79 | # there and we don't have to do anything. 80 | local user= 81 | local password= 82 | local pushproj= 83 | local pushimage= 84 | local registry= 85 | if [ "$WEBUI_TEST_LOCAL_IMAGE" == true ]; then 86 | if [ -n "$WEBUI_TEST_EXTERNAL_REGISTRY" ]; then 87 | user=$WEBUI_TEST_EXTERNAL_USER 88 | password=$WEBUI_TEST_EXTERNAL_PASSWORD 89 | pushproj=$user 90 | pushimage=scratch-oshinko-webui 91 | registry=$WEBUI_TEST_EXTERNAL_REGISTRY 92 | elif [ -n "$WEBUI_TEST_INTEGRATED_REGISTRY" ]; then 93 | user=$(oc whoami) 94 | password=$(oc whoami -t) 95 | pushproj=$PROJECT 96 | pushimage=oshinko-webui 97 | registry=$WEBUI_TEST_INTEGRATED_REGISTRY 98 | fi 99 | fi 100 | if [ -n "$registry" ]; then 101 | set +e 102 | docker login --help | grep email &> /dev/null 103 | res=$? 104 | set -e 105 | if [ -n "$password" ] && [ -n "$user" ]; then 106 | if [ "$res" -eq 0 ]; then 107 | docker login -u ${user} -e jack@jack.com -p ${password} ${registry} 108 | else 109 | docker login -u ${user} -p ${password} ${registry} 110 | fi 111 | fi 112 | docker tag ${WEBUI_TEST_IMAGE} ${registry}/${pushproj}/${pushimage} 113 | docker push ${registry}/${pushproj}/${pushimage} 114 | OSHINKO_WEB_IMAGE=${registry}/${pushproj}/${pushimage} 115 | else 116 | OSHINKO_WEB_IMAGE=$WEBUI_TEST_IMAGE 117 | fi 118 | } 119 | 120 | function tweak_template { 121 | if [ "$WEBUI_TEST_LOCAL_IMAGE" == true ] && [ -z "$WEBUI_TEST_INTEGRATED_REGISTRY" ] && [ -z "$WEBUI_TEST_EXTERNAL_REGISTRY" ]; then 122 | echo Updating $WEBUI_TEST_RESOURCES to pull local webui image 123 | mkdir -p `pwd`/test/scratch/ 124 | if [ -f "$WEBUI_TEST_RESOURCES" ]; then 125 | cp "$WEBUI_TEST_RESOURCES" `pwd`/test/scratch/resources_mod.yaml 126 | else 127 | wget "$WEBUI_TEST_RESOURCES" -O `pwd`/test/scratch/resources_mod.yaml 128 | fi 129 | # This sed command finds the line with image: ${OSHINKO_WEB_IMAGE} 130 | # The next line sets the pull policy in the standard template (this should be maintained) 131 | # !b;n;s throws away the current processing, reads the next line, and starts a new substitution 132 | # Ultimately, this sets the pull policy for the web image to IfNotPresent so that an 'oc cluster up' 133 | # case can just reference the image from the host 134 | sed -i -r '/image.*OSHINKO_WEB_IMAGE/!b;n;s/(.*imagePullPolicy: *)Always/\1IfNotPresent/' `pwd`/test/scratch/resources_mod.yaml 135 | WEBUI_TEST_RESOURCES=`pwd`/test/scratch/resources_mod.yaml 136 | fi 137 | } 138 | 139 | function try_until_success { 140 | # This is a really simple case that just tests for success. 141 | # If more complicated waits are needed, we can use the oc commandline testsuite 142 | echo $1 143 | while true; do 144 | set +e 145 | eval $1 146 | res=$? 147 | set -e 148 | if [ "$res" = 0 ]; then 149 | break 150 | fi 151 | sleep 20s 152 | done 153 | } 154 | 155 | function create_resources { 156 | set +e 157 | oc create -f $WEBUI_TEST_RESOURCES 158 | if [ "$WEBUI_TEST_SECURE" == true ]; then 159 | oc new-app --template=oshinko-webui-secure -p OSHINKO_WEB_IMAGE=$OSHINKO_WEB_IMAGE 160 | else 161 | oc new-app --template=oshinko-webui -p OSHINKO_WEB_IMAGE=$OSHINKO_WEB_IMAGE 162 | fi 163 | oc create configmap storedconfig --from-literal=mastercount=1 --from-literal=workercount=4 164 | set -e 165 | } 166 | 167 | function wait_for_webui { 168 | local command= 169 | 170 | try_until_success "oc get pods -l app=oshinko-webui" 171 | IMAGETESTED=$(oc get pods -l app=oshinko-webui --template="{{range .items}}{{range .spec.containers}}{{.image}}{{end}}{{end}}") 172 | echo "Testing image $IMAGETESTED" 173 | 174 | if [ "$WEBUI_TEST_SECURE" == true ]; then 175 | TESTROUTE=$(oc get route oshinko-web-oaproxy --template='{{.spec.host}}') 176 | command="wget --no-check-certificate https://$TESTROUTE/proxy/api" 177 | else 178 | TESTROUTE=$(oc get route oshinko-web --template='{{.spec.host}}') 179 | command="wget http://$TESTROUTE/proxy/api" 180 | fi 181 | echo "Waiting for proxy to come up" 182 | try_until_success "$command" 183 | cat api && rm api 184 | 185 | echo "Make sure that webui is up" 186 | if [ "$WEBUI_TEST_SECURE" == true ]; then 187 | command="curl --insecure https://$TESTROUTE/webui" 188 | else 189 | command="curl http://$TESTROUTE/webui" 190 | fi 191 | try_until_success "$command" 192 | } 193 | 194 | function dump_env { 195 | echo "Other environment details" 196 | echo "ENVIRONMENT IS:" 197 | env 198 | echo "ROUTES" 199 | oc get routes 200 | echo "SERVICES" 201 | oc get services 202 | } 203 | 204 | function check_for_xvfb { 205 | if [ "$WEBUI_START_XVFB" == true ]; then 206 | if ! [ -f /tmp/.X99-lock ]; then 207 | echo Starting xvfb 208 | Xvfb -ac :99 -screen 0 1280x1024x16 & 209 | else 210 | echo xvfb is already running 211 | fi 212 | export DISPLAY=:99 213 | fi 214 | } 215 | 216 | orig_project=$(oc project -q) 217 | 218 | # Create the project here 219 | proj_name_prefix="webui" 220 | set +e # For some reason the result here from head is not 0 even though we get the desired result 221 | namespace=${proj_name_prefix}-$(date -Ins | md5sum | tr -dc 'a-z0-9' | fold -w 6 | head -n 1) 222 | set -e 223 | oc new-project $namespace &> /dev/null 224 | echo Using project $namespace 225 | 226 | 227 | # Modify the template if we're using a local image with no registry, i.e. we're in an oc cluster up case 228 | # In this case we don't need a push at all, but we need to have a pullpolicy of IfNotPresent 229 | print_test_env 230 | check_for_xvfb 231 | tweak_template 232 | push_image 233 | create_resources 234 | wait_for_webui 235 | dump_env 236 | 237 | echo "Running integration tests via protracor" 238 | echo "Protractor version is:" 239 | protractor --version 240 | if [ "$WEBUI_TEST_SECURE" == true ]; then 241 | user="" 242 | password="" 243 | if [ -n "$WEBUI_TEST_SECURE_USER" ]; then 244 | name="--params.securelogin.name=$WEBUI_TEST_SECURE_USER " 245 | fi 246 | if [ -n "$WEBUI_TEST_SECURE_PASSWORD" ]; then 247 | passw="--params.securelogin.password=$WEBUI_TEST_SECURE_PASSWORD " 248 | fi 249 | protractor test/conf.js $name$passw --baseUrl="https://$TESTROUTE/webui" --specs=test/spec/all-functionality.js 250 | else 251 | protractor test/conf.js --baseUrl="http://$TESTROUTE/webui" --specs=test/spec/all-functionality-insecure.js 252 | fi 253 | 254 | # Cleanup 255 | oc project $orig_project 256 | oc delete project $namespace -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue May 24 2016 13:28:14 GMT-0400 (EDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // frameworks to use 8 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 9 | frameworks: ['jasmine'], 10 | 11 | basePath : '../', 12 | 13 | plugins: [ 14 | 'karma-firefox-launcher', 15 | 'karma-jasmine', 16 | 'karma-requirejs' 17 | ], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | 'app/bower_components/jquery/dist/jquery.js', 22 | 'app/bower_components/patternfly/dist/js/patternfly.js', 23 | 'app/bower_components/angular/angular.js', 24 | 'node_modules/requirejs/require.js', 25 | 'node_modules/karma-requirejs/lib/adapter.js', 26 | 'app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js', 27 | 'app/bower_components/angular-patternfly/dist/angular-patternfly.js', 28 | 'app/bower_components/angular-route/angular-route.js', 29 | 'node_modules/jasmine/lib/jasmine.js', 30 | 'app/bower_components/angular-mocks/angular-mocks.js', 31 | 'app/bower_components/angular-animate/angular-animate.js', 32 | 'app/bower_components/lodash/lodash.js', 33 | 'app/bower_components/js-logger/src/logger.js', 34 | 'app/bower_components/hawtio-core/dist/hawtio-core.js', 35 | 'app/bower_components/hawtio-extension-service/dist/hawtio-extension-service.js', 36 | 'app/bower_components/angular-utf8-base64/angular-utf8-base64.js', 37 | 'app/js/app.js', 38 | 'app/js/clusterops.js', 39 | 'app/js/DataService.js', 40 | 'app/js/dialog.js', 41 | 'app/js/directives.js', 42 | 'app/js/factories.js', 43 | 'app/js/listing.js', 44 | 'test/unit/controllersSpec.js' 45 | ], 46 | 47 | 48 | // list of files to exclude 49 | exclude: [ 50 | ], 51 | 52 | 53 | // preprocess matching files before serving them to the browser 54 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 55 | preprocessors: { 56 | }, 57 | 58 | 59 | // test results reporter to use 60 | // possible values: 'dots', 'progress' 61 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 62 | reporters: ['progress'], 63 | 64 | 65 | // web server port 66 | port: 9876, 67 | 68 | 69 | // enable / disable colors in the output (reporters and logs) 70 | colors: true, 71 | 72 | 73 | // level of logging 74 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 75 | logLevel: config.LOG_INFO, 76 | 77 | 78 | // enable / disable watching file and executing tests whenever any file changes 79 | autoWatch: true, 80 | 81 | 82 | // start these browsers 83 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 84 | browsers: ['Firefox'], 85 | 86 | 87 | // Continuous Integration mode 88 | // if true, Karma captures browsers, runs the tests and exits 89 | singleRun: true, 90 | 91 | // Concurrency level 92 | // how many browser should be started simultaneous 93 | concurrency: Infinity 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /test/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function prepare() { 4 | ip addr show eth0 5 | export HOST_IP_ADDRESS="$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1)" 6 | echo "Host IP is $HOST_IP_ADDRESS" 7 | IMAGEFOROC="quay.io/openshift/origin-cli:$OPENSHIFT_VERSION" 8 | if [ "$OPENSHIFT_VERSION" == "v3.10" ]; then 9 | IMAGEFOROC="docker.io/openshift/origin:$OPENSHIFT_VERSION" 10 | fi 11 | sudo docker cp $(docker create $IMAGEFOROC):/bin/oc /usr/local/bin/oc 12 | oc cluster up --public-hostname=$HOST_IP_ADDRESS 13 | oc login -u system:admin 14 | export REGISTRY_URL=$(oc get svc -n default docker-registry -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}') 15 | oc login -u developer -p developer 16 | docker pull radanalyticsio/openshift-spark:2.4-latest 17 | } 18 | 19 | prepare 20 | -------------------------------------------------------------------------------- /test/spec/all-functionality-insecure.js: -------------------------------------------------------------------------------- 1 | // uncomment if you need to take some screenshots for debugging purposes 2 | // var fs = require('fs'); 3 | // 4 | // function writeScreenShot(data, filename) { 5 | // var dir = "screenshots"; 6 | // if (!fs.existsSync(dir)){ 7 | // fs.mkdirSync(dir); 8 | // } 9 | // var stream = fs.createWriteStream(dir + "/" + filename); 10 | // stream.write(new Buffer(data, 'base64')); 11 | // stream.end(); 12 | // } 13 | 14 | describe('Initial page functionality', function () { 15 | 16 | it('should login and display the clusters page', function () { 17 | browser.get('/webui'); 18 | expect(element(by.tagName('h2')).getText()).toEqual("Spark Clusters"); 19 | }); 20 | }); 21 | 22 | describe('Cluster page functionality', function () { 23 | it('should create, scale, and delete a cluster', function () { 24 | var EC = protractor.ExpectedConditions; 25 | // Create a cluster 26 | element(by.id('startbutton')).click(); 27 | element(by.id('cluster-new-name')).sendKeys('testcluster'); 28 | element(by.id('createbutton')).click(); 29 | browser.wait(EC.visibilityOf(element(by.id('testcluster-actions')))); 30 | 31 | //Scale 32 | element(by.id('testcluster-actions')).click(); 33 | element(by.id('testcluster-scalebutton')).click(); 34 | element(by.name('numworkers')).sendKeys(protractor.Key.CONTROL, "a", protractor.Key.NULL, "3"); 35 | element(by.id('scalebutton')).click(); 36 | browser.wait(EC.textToBePresentInElement(element(by.name('workercount-testcluster')), "3")); 37 | 38 | // Scale down 39 | element(by.id('testcluster-actions')).click(); 40 | element(by.id('testcluster-scalebutton')).click(); 41 | element(by.name('numworkers')).sendKeys(protractor.Key.CONTROL, "a", protractor.Key.NULL, "2"); 42 | element(by.id('scalebutton')).click(); 43 | browser.wait(EC.textToBePresentInElement(element(by.name('workercount-testcluster')), "2")); 44 | 45 | // Delete 46 | element(by.id('testcluster-actions')).click(); 47 | element(by.id('testcluster-deletebutton')).click(); 48 | element(by.id('deletebutton')).click(); 49 | browser.wait(EC.invisibilityOf(element(by.id('testcluster-actions')))); 50 | }); 51 | }); 52 | 53 | describe('Cluster page functionality, with additional cancel clicks', function () { 54 | it('should create, scale, and delete a cluster', function () { 55 | var EC = protractor.ExpectedConditions; 56 | // Create a cluster 57 | browser.wait(EC.elementToBeClickable(element(by.id('startbutton')))); 58 | element(by.id('startbutton')).click(); 59 | browser.wait(EC.elementToBeClickable(element(by.id('cancelbutton')))); 60 | element(by.id('cancelbutton')).click(); 61 | browser.wait(EC.elementToBeClickable(element(by.id('startbutton')))); 62 | element(by.id('startbutton')).click(); 63 | element(by.id('cluster-new-name')).sendKeys('secondcluster'); 64 | element(by.id('cancelbutton')).click(); 65 | browser.wait(EC.elementToBeClickable(element(by.id('startbutton')))); 66 | element(by.id('startbutton')).click(); 67 | element(by.id('cluster-new-name')).sendKeys('secondcluster'); 68 | element(by.id('createbutton')).click(); 69 | browser.wait(EC.visibilityOf(element(by.id('secondcluster-actions')))); 70 | 71 | //Scale 72 | element(by.id('secondcluster-actions')).click(); 73 | element(by.id('secondcluster-scalebutton')).click(); 74 | element(by.id('cancelbutton')).click(); 75 | browser.wait(EC.elementToBeClickable(element(by.id('secondcluster-actions')))); 76 | element(by.id('secondcluster-actions')).click(); 77 | element(by.id('secondcluster-scalebutton')).click(); 78 | element(by.name('numworkers')).sendKeys(protractor.Key.CONTROL, "a", protractor.Key.NULL, "3"); 79 | element(by.id('scalebutton')).click(); 80 | browser.wait(EC.textToBePresentInElement(element(by.name('workercount-secondcluster')), "3")); 81 | 82 | // Delete 83 | element(by.id('secondcluster-actions')).click(); 84 | element(by.id('secondcluster-deletebutton')).click(); 85 | element(by.id('cancelbutton')).click(); 86 | browser.wait(EC.elementToBeClickable(element(by.id('secondcluster-actions')))); 87 | element(by.id('secondcluster-actions')).click(); 88 | element(by.id('secondcluster-deletebutton')).click(); 89 | element(by.id('deletebutton')).click(); 90 | browser.wait(EC.invisibilityOf(element(by.id('secondcluster-actions')))); 91 | }); 92 | }); 93 | 94 | describe('Test advanced create functionality', function () { 95 | it('should create and delete a cluster', function () { 96 | var EC = protractor.ExpectedConditions; 97 | // Create a cluster 98 | element(by.id('startbutton')).click(); 99 | element(by.id('toggle-adv')).click(); 100 | element(by.id('cluster-new-name')).sendKeys('advcluster'); 101 | element(by.id('createbutton')).click(); 102 | browser.wait(EC.visibilityOf(element(by.id('advcluster-actions')))); 103 | 104 | // Delete 105 | element(by.id('advcluster-actions')).click(); 106 | element(by.id('advcluster-deletebutton')).click(); 107 | element(by.id('deletebutton')).click(); 108 | browser.wait(EC.invisibilityOf(element(by.id('advcluster-actions')))); 109 | }); 110 | }); 111 | 112 | describe('Test advanced create functionality', function () { 113 | it('should create and delete a cluster with a stored config', function () { 114 | var EC = protractor.ExpectedConditions; 115 | // Create a cluster 116 | element(by.id('startbutton')).click(); 117 | element(by.id('toggle-adv')).click(); 118 | element(by.id('cluster-new-name')).sendKeys('storedcfgcluster'); 119 | element(by.id('cluster-config-name')).sendKeys('storedconfig'); 120 | element(by.id('createbutton')).click(); 121 | browser.wait(EC.visibilityOf(element(by.id('storedcfgcluster-actions')))); 122 | 123 | // Delete 124 | element(by.id('storedcfgcluster-actions')).click(); 125 | element(by.id('storedcfgcluster-deletebutton')).click(); 126 | element(by.id('deletebutton')).click(); 127 | browser.wait(EC.invisibilityOf(element(by.id('storedcfgcluster-actions')))); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/spec/all-functionality.js: -------------------------------------------------------------------------------- 1 | // uncomment if you need to take some screenshots for debugging purposes 2 | // var fs = require('fs'); 3 | // 4 | // function writeScreenShot(data, filename) { 5 | // var dir = "screenshots"; 6 | // if (!fs.existsSync(dir)){ 7 | // fs.mkdirSync(dir); 8 | // } 9 | // var stream = fs.createWriteStream(dir + "/" + filename); 10 | // stream.write(new Buffer(data, 'base64')); 11 | // stream.end(); 12 | // } 13 | 14 | describe('Initial page functionality', function () { 15 | 16 | it('should login and display the clusters page', function () { 17 | browser.waitForAngularEnabled(false); 18 | browser.get('/webui'); 19 | element(by.name('username')).sendKeys(browser.params.securelogin.name); 20 | element(by.name('password')).sendKeys(browser.params.securelogin.password); 21 | element(by.tagName("button")).click(); 22 | element(by.name("approve")).click(); 23 | browser.waitForAngularEnabled(true); 24 | browser.get('/webui'); 25 | expect(element(by.tagName('h2')).getText()).toEqual("Spark Clusters"); 26 | }); 27 | }); 28 | 29 | describe('Cluster page functionality', function () { 30 | it('should create, scale, and delete a cluster', function () { 31 | var EC = protractor.ExpectedConditions; 32 | // Create a cluster 33 | element(by.id('startbutton')).click(); 34 | element(by.id('cluster-new-name')).sendKeys('testcluster'); 35 | element(by.id('createbutton')).click(); 36 | browser.wait(EC.visibilityOf(element(by.id('testcluster-actions')))); 37 | 38 | //Scale 39 | element(by.id('testcluster-actions')).click(); 40 | element(by.id('testcluster-scalebutton')).click(); 41 | element(by.name('numworkers')).sendKeys(protractor.Key.CONTROL, "a", protractor.Key.NULL, "3"); 42 | element(by.id('scalebutton')).click(); 43 | browser.wait(EC.textToBePresentInElement(element(by.name('workercount-testcluster')), "3")); 44 | 45 | // Scale down 46 | element(by.id('testcluster-actions')).click(); 47 | element(by.id('testcluster-scalebutton')).click(); 48 | element(by.name('numworkers')).sendKeys(protractor.Key.CONTROL, "a", protractor.Key.NULL, "2"); 49 | element(by.id('scalebutton')).click(); 50 | browser.wait(EC.textToBePresentInElement(element(by.name('workercount-testcluster')), "2")); 51 | 52 | // Delete 53 | element(by.id('testcluster-actions')).click(); 54 | element(by.id('testcluster-deletebutton')).click(); 55 | element(by.id('deletebutton')).click(); 56 | browser.wait(EC.invisibilityOf(element(by.id('testcluster-actions')))); 57 | }); 58 | }); 59 | 60 | describe('Cluster page functionality, with additional cancel clicks', function () { 61 | it('should create, scale, and delete a cluster', function () { 62 | var EC = protractor.ExpectedConditions; 63 | // Create a cluster 64 | browser.wait(EC.elementToBeClickable(element(by.id('startbutton')))); 65 | element(by.id('startbutton')).click(); 66 | browser.wait(EC.elementToBeClickable(element(by.id('cancelbutton')))); 67 | element(by.id('cancelbutton')).click(); 68 | browser.wait(EC.elementToBeClickable(element(by.id('startbutton')))); 69 | element(by.id('startbutton')).click(); 70 | element(by.id('cluster-new-name')).sendKeys('secondcluster'); 71 | element(by.id('cancelbutton')).click(); 72 | browser.wait(EC.elementToBeClickable(element(by.id('startbutton')))); 73 | element(by.id('startbutton')).click(); 74 | element(by.id('cluster-new-name')).sendKeys('secondcluster'); 75 | element(by.id('createbutton')).click(); 76 | browser.wait(EC.visibilityOf(element(by.id('secondcluster-actions')))); 77 | 78 | //Scale 79 | element(by.id('secondcluster-actions')).click(); 80 | element(by.id('secondcluster-scalebutton')).click(); 81 | element(by.id('cancelbutton')).click(); 82 | browser.wait(EC.elementToBeClickable(element(by.id('secondcluster-actions')))); 83 | element(by.id('secondcluster-actions')).click(); 84 | element(by.id('secondcluster-scalebutton')).click(); 85 | element(by.name('numworkers')).sendKeys(protractor.Key.CONTROL, "a", protractor.Key.NULL, "3"); 86 | element(by.id('scalebutton')).click(); 87 | browser.wait(EC.textToBePresentInElement(element(by.name('workercount-secondcluster')), "3")); 88 | 89 | // Delete 90 | element(by.id('secondcluster-actions')).click(); 91 | element(by.id('secondcluster-deletebutton')).click(); 92 | element(by.id('cancelbutton')).click(); 93 | browser.wait(EC.elementToBeClickable(element(by.id('secondcluster-actions')))); 94 | element(by.id('secondcluster-actions')).click(); 95 | element(by.id('secondcluster-deletebutton')).click(); 96 | element(by.id('deletebutton')).click(); 97 | browser.wait(EC.invisibilityOf(element(by.id('secondcluster-actions')))); 98 | }); 99 | }); 100 | 101 | describe('Test advanced create functionality', function () { 102 | it('should create and delete a cluster', function () { 103 | var EC = protractor.ExpectedConditions; 104 | // Create a cluster 105 | element(by.id('startbutton')).click(); 106 | element(by.id('toggle-adv')).click(); 107 | element(by.id('cluster-new-name')).sendKeys('advcluster'); 108 | element(by.id('createbutton')).click(); 109 | browser.wait(EC.visibilityOf(element(by.id('advcluster-actions')))); 110 | 111 | // Delete 112 | element(by.id('advcluster-actions')).click(); 113 | element(by.id('advcluster-deletebutton')).click(); 114 | element(by.id('deletebutton')).click(); 115 | browser.wait(EC.invisibilityOf(element(by.id('advcluster-actions')))); 116 | }); 117 | }); 118 | 119 | describe('Test advanced create functionality', function () { 120 | it('should create and delete a cluster with a stored config', function () { 121 | var EC = protractor.ExpectedConditions; 122 | // Create a cluster 123 | element(by.id('startbutton')).click(); 124 | element(by.id('toggle-adv')).click(); 125 | element(by.id('cluster-new-name')).sendKeys('storedcfgcluster'); 126 | element(by.id('cluster-config-name')).sendKeys('storedconfig'); 127 | element(by.id('createbutton')).click(); 128 | browser.wait(EC.visibilityOf(element(by.id('storedcfgcluster-actions')))); 129 | 130 | // Delete 131 | element(by.id('storedcfgcluster-actions')).click(); 132 | element(by.id('storedcfgcluster-deletebutton')).click(); 133 | element(by.id('deletebutton')).click(); 134 | browser.wait(EC.invisibilityOf(element(by.id('storedcfgcluster-actions')))); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/unit/controllersSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('New cluster functionality', function(){ 4 | var controller; 5 | var $scope = {}; 6 | 7 | beforeEach(module('Oshinko')); 8 | 9 | beforeEach(function() { 10 | inject(function(_$controller_, $q) { 11 | controller = _$controller_("ClusterNewCtrl", { 12 | $scope: $scope, 13 | $q: $q, 14 | dialogData: {}, 15 | clusterData: {}, 16 | sendNotifications: {}, 17 | errorHandling: {} 18 | }); 19 | }); 20 | }); 21 | 22 | it('should have an empty name', function() { 23 | expect($scope.fields.name).toEqual(''); 24 | }); 25 | it('should contain a default worker count of 1', function() { 26 | expect($scope.fields.workers).toEqual(1); 27 | }); 28 | it('should allow a cluster name of goodname', function() { 29 | expect($scope.NAME_RE.test("goodname")).toBeTruthy(); 30 | }); 31 | it('should not allow a cluster name of bad name', function() { 32 | expect($scope.NAME_RE.test("bad name")).toBeFalsy(); 33 | }); 34 | it('should not allow a cluster name that contains a dollar sign', function() { 35 | expect($scope.NAME_RE.test("$(oc whoami -t)")).toBeFalsy(); 36 | }); 37 | it('should not allow a cluster name that is empty', function() { 38 | expect($scope.NAME_RE.test("")).toBeFalsy(); 39 | }); 40 | it('should not allow a non number as the worker count', function() { 41 | expect($scope.NUMBER_RE.test("abcd")).toBeFalsy(); 42 | }); 43 | it('should allow a worker count of 4', function() { 44 | expect($scope.NUMBER_RE.test(4)).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe('Delete cluster functionality', function(){ 49 | var controller; 50 | var $scope = {}; 51 | 52 | beforeEach(module('Oshinko')); 53 | 54 | beforeEach(function() { 55 | inject(function(_$controller_, $q) { 56 | controller = _$controller_("ClusterDeleteCtrl", { 57 | $scope: $scope, 58 | $q: $q, 59 | dialogData: {}, 60 | clusterData: {}, 61 | sendNotifications: {}, 62 | errorHandling: {} 63 | }); 64 | }); 65 | }); 66 | 67 | it('should have the controller defined ', function() { 68 | expect(controller).toBeDefined(); 69 | expect($scope.clusterName).toBe(''); 70 | expect($scope.workerCount).toBeDefined(); 71 | }); 72 | }); 73 | 74 | describe('Main controller functionality', function(){ 75 | var controller; 76 | var scope = {}; 77 | var $route = { 78 | current: { 79 | params: { 80 | Id: "" 81 | } 82 | } 83 | }; 84 | 85 | beforeEach(module('Oshinko')); 86 | beforeEach(function() { 87 | inject(function(_$controller_, $q, $interval, $location, ListingState, $rootScope) { 88 | controller = _$controller_; 89 | scope = $rootScope.$new(); 90 | controller = ("ClusterCtrl", { 91 | $scope: scope, 92 | $q: $q, 93 | $route: $route, 94 | $interval: $interval, 95 | $location: $location, 96 | clusterDataFactory: {}, 97 | sendNotifications: {}, 98 | errorHandling: {} 99 | }); 100 | }); 101 | }); 102 | 103 | it('should have the controller defined ', function() { 104 | expect(controller).toBeDefined(); 105 | }); 106 | }); 107 | 108 | 109 | --------------------------------------------------------------------------------