From 130193f23562d93ee503b97f187a9241d193066d Mon Sep 17 00:00:00 2001 From: William Denniss Date: Mon, 1 Oct 2018 20:56:06 -0700 Subject: [PATCH] Initial release of kubehost By William Denniss, Van Tu and Cong Liu. --- AUTHORS | 8 + CONTRIBUTING.md | 28 +++ CONTRIBUTORS | 15 ++ LICENSE | 202 ++++++++++++++++++++++ README.md | 137 +++++++++++++++ kubehost | 450 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 840 insertions(+) create mode 100644 AUTHORS create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS create mode 100644 LICENSE create mode 100644 README.md create mode 100755 kubehost diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..192d139 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of the Kubehost authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. +# Names should be added to this file as: +# Name or Organization +# The email address is not required for organizations. + +Google LLC diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..939e534 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google.com/conduct/). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..4b8839c --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,15 @@ +# People who have agreed to one of the CLAs and can contribute patches. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# Names should be added to this file as: +# Name + +William Denniss +Van Tu +Cong Liu + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a5732d --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# kubehost + +Kubehost helps you expose services directly on nodes of your +Google Kubernetes Engine (GKE) cluster. + +The common way to expose a service and get an external IP is +`kubectl expose --type=LoadBalancer"`, which will expose +your deployment on a production-grade Google Cloud Load Balancer. +Sometimes you just want to expose a service on your VM directly, like +during development where uptime and reliability are not as important. +That's where Kubehost comes in. + +Kubehost uses existing features of GKE to expose your service directly +onto one of the VMs in your cluster, by creating a Pod that runs on +the VM's network and forwards traffic to your in-cluster (ClusterIP) +service, and creating firewall rules to permit external traffic. +While you could do this manually, Kubehost takes the toil out of +managing this configuration by automating the necessary actions. + +
+ +> ### :warning: For development use only +> kubehost is NOT designed for production use! Nodes in GKE +> are designed to be redundant, meaning they can fail. +> When the node on which your service is exposed via kubehost fails or +> is upgraded, your service will experience several minutes of downtime. +> By comparison, if you use a production-grade Google Cloud Load +> Balancer (and you have enough replicas of your Pod spread over +> multiple nodes with properly implemented health and readiness checks) +> then a node can fail with only minimal impact to the availability of +> your service. At any time you can upgrade to a Google Cloud Load +> Balancer with the `kubehost upgrade` command. + +## Installation + +`kubehost` is a bash script. To install, clone this repository and add +it to your `$PATH`, or copy `kubehost` to your `/usr/local/bin/`. + +You may need to set the executable permission, i.e. `chmod +x kubehost`. + +## Configuration + +Before using `kubehost`, you need to ensure both `gcloud` and `kubectl` +are configured with your desired project & cluster. + +1. run `gcloud init` to select your account, project and region + containing the GKE cluster. +2. run +(get-credentials)[https://cloud.google.com/sdk/gcloud/reference/container/clusters/get-credentials] + to configure `kubectl`. + +## Exposing a Deployment with kubehost + +1. Create your deployment like normal. +2. Create a ClusterIP service for your deployment (this is the default + service type, so no need to specify any type), **on your desired + external port**. +3. Run `kubehost bind ${SERVICE}`, where `${SERVICE}` is the name of + the Kubernetes service you created at step 2. + +What this does is create some "glue" in the form of a hostPort +deployment so that your service is bound to port you specified in the +service on your node's external IP (read "under the hood" for a longer +technical description). It also opens the necessary GCP firewall rules. + +To undo, `kubehost unbind ${SERVICE}` + +Complete example: + +```bash +kubectl run hello --image gcr.io/google-samples/hello-app:1.0 --port 8080 +kubectl expose deployment hello --port 80 --target-port 8080 --name hello-service +kubehost bind hello-service +``` + +Cleanup: +```bash +kubehost unbind hello-service +kubectl delete deployment hello +kubectl delete service hello-service +``` + +## Switching between hostPort and a Load Balancer + +### Upgrading to a Load Balancer from hostPort + +Is your app ready for prime time? Remove the hostPort Pod "glue", and +convert your Service into one backed by a +[Google Cloud Load Balancer](https://cloud.google.com/load-balancing/) +with one simple command: + +```bash +kubehost upgrade ${SERVICE} +``` + +Where `${SERVICE}` is the name of your Cluster IP service. + +### Downgrading a Load Balancer to hostPort + +Did you already expose your service with a Load Balancer and found it's +more than you needed? Convert it to an internal ClusterIP service, +and expose it on a host in one command with: + +```bash +kubehost downgrade ${SERVICE} +``` + +Where `${SERVICE}` is the name of your Kubernetes service of type +LoadBalancer. + +## Limitations + +* Kubehost currently works with services that have a single port. If you + need to expose two ports, create two ClusterIP services. +* Kubehost is not designed for production usage, see the note above. +* Kubehost doesn't give you a static IP. The IP address of node may + change which will affect your service. You can create a static IP + and use the [kubeIP](https://github.com/doitintl/kubeIP) operator to + keep it assigned through node maintenance events. + +## Under the Hood + +What Kubehost is doing when you call `bind` is creating +a Kubernetes Deployment with a single replica of a Pod that uses +hostPort to bind onto the host's network interface. The container in +this Pod forwards traffic to your ClusterIP service. + +While you could instead change your deployment to use hostPort directly + we think this approach is superior, as: + +1. It's closer to the production Kubernetes experience where deployments + have a matching service to receive traffic. +2. It's easier to switch between this and a production setup by + changing the Service type to LoadBalancer, and removing the hostPort + deployment (and vice-versa) – no need to modify your application + deployment. +3. Your deployment's replica count isn't limited by available ports. diff --git a/kubehost b/kubehost new file mode 100755 index 0000000..5ffcb24 --- /dev/null +++ b/kubehost @@ -0,0 +1,450 @@ +#!/bin/bash +# kubehost: expose services using hostPort + +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[[ -n "${DEBUG}" ]] && set -x + +#set -o pipefail -o noclobber -o nounset #-o errexit + +# Trap exit 67 and cascade +set -E +trap '[ "$?" -ne 67 ] || exit 67' ERR + +usage() { + cat <<"EOF" +Usage: + bind : Create a hostPort deployment and corresponding + firewall rule for a service. + unbind : Delete the hostPort deployment and firewall rule + created with 'bind' for a service. + getip : Show the IP of a service created with 'bind'. + create-firewall : Create a GCP firewall rule for the hostPort + deployment created with 'bind'. + delete-firewall : Delete a GCP firewall rule for the hostPort + deployment created with 'bind'. + upgrade : Convert a service that had a hostPort deployment + exposed with 'bind' to a production loadBalancer + service. + downgrade : Convert a loadBalancer service into a hostPort + deployment. + demo : Create and bind an example deployment. + demo_cleanup : Delete an example deployment. + version : Display version information. + -h,--help : Show this message. +Options: + --skip-firewall, -s : For 'bind' and 'unbind', doesn't modify firewall + rules. + --firewall-node-only : By default, firewall rules are applied to all + nodes on the cluster due to the fact that the + deployment may move around. This flag will create + a specific firewall rule for the node only. +EOF + exit 1 +} + +# Logs to stderr. +# @param {string*} Text to echo to stderr +echoerr() { echo "$@" 1>&2; } + +# Validates if the Kubernetes service exists. +# @param {string} name of the Kubernetes service +# @return 67 if the service doesn't exist +validate_service_exists() { + declare -r service=$1 + kubectl get service ${service} + if [ $? -ne 0 ]; then + exit 67 + fi +} + +# Validates that the given Kubernetes service can be used with hostPort. +# @param {string} name of the Kubernetes service to test +# @return 67 if invalid, otherwise 0 +validate_service_for_bind() { + declare -r service=$1 + validate_service_exists "${service}" + declare -r service_type="$(kubectl get service ${service} -o=jsonpath='{.spec.type}')" + + if [[ "${service_type}" == "LoadBalancer" ]]; then + echoerr "Service '${service}' is of type LoadBalancer, you don't need" \ + "to bind it to the host." + exit 67 + fi + + if [[ "${service_type}" == "NodePort" ]]; then + echoerr "Service '${service}' is of type NodePort which is not" \ + "supported." + echoerr "Use ClusterIP with the port you wish to expose, as kubehost" \ + "will expose that same port via a hostPort deployment." + exit 67 + fi +} + +# Validates that the given Kubernetes service can be upgraded to type +# LoadBalancer. +# @param {string} name of the Kubernetes service to validate +# @return 67 if invalid, otherwise 0 +validate_service_for_upgrade() { + declare -r service=$1 + validate_service_exists "${service}" + declare -r service_type="$(kubectl get service ${service} -o=jsonpath='{.spec.type}')" + + if [[ "${service_type}" != "ClusterIP" ]]; then + echoerr "This command expects the service '${service}' to be of" \ + "ClusterIP type." + exit 67 + fi +} + +# Validates that the given Kubernetes service can be downgraded to type +# hostPort. +# @param {string} name of the Kubernetes service to validate +# @return 67 if invalid, otherwise 0 +validate_service_for_downgrade() { + declare -r service=$1 + validate_service_exists "${service}" + declare -r service_type="$(kubectl get service ${service} -o=jsonpath='{.spec.type}')" + + if [[ "${service_type}" != "LoadBalancer" ]]; then + echoerr "This command expects the service '${service}' to be of" \ + "LoadBalancer type." + exit 67 + fi +} + +# Generates the hostPort deployment name for a given service. +# @param {string} name of the Kubernetes service +# @print the generated hostport deployment name +generate_deployment_name_for_service() { + declare -r service=$1 + declare -r deployment="${service}-hostport" + echo "${deployment}" +} + +# Creates a deployment with a hostPort matching the service's port. Note only +# the first port is used if the service has multiple ports. +# @param {string} name of the service to expose +# @print the name of the deployment that was created +expose_service() { + declare -r service=$1 + declare -r deployment="$(generate_deployment_name_for_service ${service})" + declare -r service_port="$(kubectl get service ${service} -o=jsonpath='{.spec.ports[0].port}')" + declare -r service_protocol="$(kubectl get service ${service} -o=jsonpath='{.spec.ports[0].protocol}')" + echoerr "Creating hostPort deployment '${deployment}' for service" \ + "'${service}' (${service_protocol}:${service_port})." + declare -r overrides=" + { + \"spec\": { + \"template\":{ + \"spec\": { + \"containers\": [{ + \"name\":\"${deployment}\", + \"image\":\"gcr.io/google_containers/proxy-to-service:v2\", + \"args\":[ + \"${service_protocol}\", + \"${service_port}\", + \"${service}\"], + \"ports\":[{ + \"protocol\": \"${service_protocol}\", + \"containerPort\": "${service_port}", + \"hostPort\": "${service_port}" + }], + \"resources\": { + \"requests\": { + \"cpu\": \"10m\", + \"memory\": \"10Mi\" + } + } + }] + } + } + } + }" + + kubectl run "${deployment}" \ + --image=gcr.io/google_containers/proxy-to-service:v2 \ + --overrides="${overrides}" 1>&2 + + echo "${deployment}" +} + +# Waits until the given deployment has at least 1 replica available. +# @param {string} name of the deployment to wait for +# @return 67 if waiting timed out, otherwise 0 +wait_deployment_available() { + declare -r deployment=$1 + local available=-1 + local counter=0 + local -r delta=1 limit=120 + echoerr "Waiting for available replicas of deployment '${deployment}'" + while [[ "${available}" -lt 1 ]]; do + sleep "${delta}" + counter="$((counter + delta))" + available="$(kubectl get deployment ${deployment} -o=jsonpath='{.status.availableReplicas}')" + if [ "${counter}" -gt "${limit}" ]; then + echoerr "No replicas available for deployment '${deployment}'." \ + "Either it's really slow to deploy, or your hostPort" \ + "deployment can't be scheduled due to the lack of an" \ + "available port." + exit 67 + fi + done +} + +# Returns the external IP assigned to a service of type loadBalancer. +# @param {string} name of the service +# @print The external IP of the service. +# @return 67 if load balancer is not ready after certain timeout +get_external_ip() { + declare -r service=$1 + local ip="" + local counter=0 + local -r delta=1 limit=120 + echoerr "Waiting for load balancer to be configured for service '${service}'" + while [[ "${ip}" == "" ]]; do + sleep "${delta}" + counter="$((counter + delta))" + ip="$(kubectl get service ${service} -o=jsonpath='{.status.loadBalancer.ingress[0].ip}')" + if [ "${counter}" -gt "${limit}" ]; then + echoerr "Failed to get the external IP for service '${service}'." + exit 67 + fi + done + echo "${ip}" +} + +# Gets the node on which the given deployment was deployed to. +# @param {string} Name of the deployment to query +# @print The node on which the deployment was deployed. +# @return 67 if the deployment was not found or isn't present on any nodes +get_hostport_deployment_node() { + declare -r deployment=$1 + declare -r host_pod="$(kubectl get pods -o=jsonpath={.items[?\(@.metadata.labels.run==\"${deployment}\"\)].metadata.name})" + if [[ ! "${host_pod}" ]]; then + echoerr "Deployment '${deployment}' not found, exiting." + exit 67 + fi + declare -r node="$(kubectl get pod ${host_pod} -o=jsonpath={.spec.nodeName})" + if [[ ! "${node}" ]]; then + echoerr "Deployment '${deployment}' is not on any node." + exit 67 + fi + echo "${node}" +} + +# Returns the IP address of the node. +# @param {string} name of the deployment to query +# @return 67 if waiting timed out, otherwise 0 +get_hostport_deployment_ip() { + local -r deployment=$1 + local -r node="$(get_hostport_deployment_node ${deployment})" + local -r ip="$(kubectl get node ${node} -o=jsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}')" + echo "$ip" +} + +# Returns 0 if the node appears in the compute instances list. +# A non-0 result likely indicates the user's gcloud isn't setup correctly. +# @param {string} name of the node to lookup +# @return 0 if the node exists, otherwise 1 +validate_gcloud_node() { + declare -r node=$1 + declare -r result="$(gcloud --format='value(name)' compute instances list | grep ${node})" + if [[ "${result}" == "${node}" ]]; then + return 0 + fi + return 1 +} + +# Returns 1 if the given firewall rule already exists, otherwise 0 +# @param {string} Name of the firewall rule to lookup +# @return 1 if the given firewall rule already exists, otherwise 0 +firewall_rule_exists() { + declare -r rule=$1 + declare -r result="$(gcloud compute firewall-rules list --format=value\(name\) | grep ${rule})" + if [[ "${result}" == "${rule}" ]]; then + return 1 + fi + return 0 +} + +# Creates or destroys a firewall rule. +# @param {string} the action to be performed, either "create" or "delete" +# @param {string} the name of the service for which the rule is being created +# @param {boolean} target only the node of the deployment if 1, otherwise +# rule will target all nodes in the cluster +manage_firewall_rule() { + declare -r action=$1 + declare -r service=$2 + declare -r create_node_tag="${3:-0}" + declare -r deployment="$(generate_deployment_name_for_service ${service})" + + declare -r node="$(get_hostport_deployment_node ${deployment})" + validate_gcloud_node "${node}" + if [ $? -ne 0 ]; then + echoerr "Failed to create a firewall rule because the node ${node}" \ + "isn't in your gcloud instance list. Run 'gcloud init' and" \ + "select the project and zone containing the Kubernetes cluster." \ + "Then run 'kubehost create-firewall ${service}'." + return 0 + fi + + declare -r service_port="$(kubectl get service ${service} -o=jsonpath={.spec.ports[0].port})" + declare -r service_protocol="$(kubectl get service ${service} -o=jsonpath={.spec.ports[0].protocol})" + declare -r namespace="$(kubectl get service ${service} -o=jsonpath={.metadata.namespace})" + + # Get the GKE instance tag for this cluster + declare -r gke_tag="$(gcloud compute instances list --filter=name=\(\"${node}\"\) --flatten=tags.items --format=value\(tags.items\) | grep 'gke.*node')" + local tag="${gke_tag}" + if [ "${create_node_tag}" -ne 0 ]; then + tag="${namespace}-${service}" + fi + declare -r fwname="${namespace}-${service}-rule" + if [[ "${action}" == "create" ]]; then + firewall_rule_exists "${fwname}" + if [ $? -ne 0 ]; then + echoerr "Firewall rule ${fwname} already exists, not recreating." + return 1 + fi + echoerr "Creating ingress firewall rule from ${service_protocol}:${service_port} to instances with tag ${tag}." + if [ "${create_node_tag}" -ne 0 ]; then + gcloud compute instances add-tags "${node}" --tags "${tag}" + fi + gcloud compute firewall-rules create "${fwname}" --allow "${service_protocol}:${service_port}" --target-tags="${tag}" + else + echoerr "Deleting firewall rule." + if [ "${create_node_tag}" -ne 0 ]; then + gcloud compute instances remove-tags "${node}" --tags "${tag}" --quiet + fi + gcloud compute firewall-rules delete "${fwname}" --quiet + fi +} + +demo_cmd() { + declare -r cmd=$1 + local yellow darkbg normal + yellow=$(tput setaf 2) + darkbg=$(tput setab 0) + normal=$(tput sgr0) + cur_ctx_fg="${KUBECTX_CURRENT_FGCOLOR:-$yellow}" + cur_ctx_bg="${KUBECTX_CURRENT_BGCOLOR:-$darkbg}" + + # Prints the command + echo "${cur_ctx_bg}${cur_ctx_fg} ${cmd}${normal}" + + # Evaluates the command + ${cmd} +} + +demo() { + demo_cmd "kubectl run hello --image gcr.io/google-samples/hello-app:1.0 --port 8080" + demo_cmd "kubectl expose deployment hello --port 80 --target-port 8080 --name hello-service" + demo_cmd "$0 bind hello-service" +} + +demo_cleanup() { + demo_cmd "$0 unbind hello-service" + demo_cmd "kubectl delete service hello-service" + demo_cmd "kubectl delete deployment hello" +} + +function main() { + # Process optional arguments + local skipfirewall=0 + local firewallnodeonly=0 + args=() + while [ $# -gt 0 ]; do + case $1 in + -s|--skip-firewall) skipfirewall=1; shift 1 ;; + --firewall-node-only) firewallnodeonly=1; shift 1 ;; + -h|--help) usage; exit 1 ;; + -*) echo "unknown option: $1" >&2; exit 1 ;; + *) args+=($1); shift 1 ;; + esac + done + + # Compulsory arguments + action="${args[0]}" + service="${args[1]}" + + case "${action}" in + "demo") + demo + exit 0 + ;; + "demo_cleanup") + demo_cleanup + exit 0 + ;; + "version") + echo "kubehost version 1.0" + exit 0 + ;; + esac + + if [[ ! "${service}" ]]; then + usage + exit 1 + fi + + case "${action}" in + "bind") + validate_service_for_bind "${service}" + declare -r deployment="$(expose_service $service)" + sleep 1 + wait_deployment_available "${deployment}" + if [ "${skipfirewall}" -ne 1 ]; then + manage_firewall_rule "create" "${service}" "${firewallnodeonly}" + fi + declare -r ip="$(get_hostport_deployment_ip ${deployment})" + echo "Service exposed on ${ip}" + ;; + "create-firewall") + manage_firewall_rule "create" "${service}" + ;; + "delete-firewall") + manage_firewall_rule "delete" "${service}" + ;; + "unbind") + declare -r deployment="$(generate_deployment_name_for_service ${service})" + if [ "${skipfirewall}" -ne 1 ]; then + manage_firewall_rule "delete" "${service}" + fi + kubectl delete deployment "${deployment}" + ;; + "getip") + declare -r deployment="$(generate_deployment_name_for_service ${service})" + declare -r ip="$(get_hostport_deployment_ip ${deployment})" + echo "Service exposed on ${ip}" + ;; + "upgrade") + validate_service_for_upgrade "${service}" + kubectl patch services ${service} --type='json' -p='[{"op": "replace", "path": "/spec/type", "value":"LoadBalancer"}]' + declare -r ip="$(get_external_ip ${service})" + echo "Service converted to LoadBalancer type and exposed on ${ip}" + $0 unbind ${service} + ;; + "downgrade") + validate_service_for_downgrade "${service}" + kubectl patch services ${service} --type='json' -p='[{"op": "replace", "path": "/spec/type", "value":"ClusterIP"},{"op": "remove", "path": "/spec/ports/0/nodePort"}]' + $0 bind ${service} + ;; + *) + usage + ;; + esac +} + +main $@