├── .gitignore ├── .travis.yml ├── Dockerfile ├── Jenkinsfile ├── LICENSE.md ├── Makefile ├── ReadMe.md ├── cmd ├── completion.go ├── create.go ├── create_flow.go ├── debug.go ├── delete.go ├── edit.go ├── get.go ├── install.go ├── logs.go ├── operate.go ├── operator │ └── main.go ├── root.go ├── safe_string_test.go ├── update.go ├── url.go └── version.go ├── examples ├── blog-example │ ├── ReadMe.md │ ├── blogcount.js │ ├── blogendpoint-svc.yml │ ├── sampleBlog.json │ ├── source.js │ └── split.js ├── envvar.js ├── flow-twitter.yml ├── flow │ ├── ReadMe.md │ ├── hello.js │ └── sample.flow.yml ├── flow1.yml ├── function1.yml ├── hello.js └── source.js ├── glide.lock ├── glide.yaml ├── pkg ├── analytics │ └── analytics.go ├── config │ └── config.go ├── constants │ └── constants.go ├── funktion │ ├── client.go │ ├── connector_schema.go │ ├── connector_schema_strings_test.go │ ├── connector_schema_test.go │ ├── constants.go │ ├── deployment.go │ └── operator.go ├── k8sutil │ ├── k8sutil.go │ ├── kubectl_helpers.go │ └── pod_watch.go ├── queue │ └── queue.go ├── spec │ └── spec.go ├── update │ └── update.go └── version │ └── version.go ├── scripts └── check_license.sh └── version └── VERSION /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # IDE stuff 30 | .idea 31 | .vscode 32 | 33 | # external packages folder 34 | vendor/ 35 | 36 | /funktion* 37 | out/ 38 | release/ 39 | .check_license 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7.1 4 | install: 5 | - echo "Don't run anything." 6 | script: 7 | - make bootstrap 8 | - make test 9 | - make cross -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | ADD ./out/funktion-linux-amd64 /bin/operator 4 | 5 | ENTRYPOINT ["/bin/operator"] 6 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/groovy 2 | @Library('github.com/fabric8io/fabric8-pipeline-library@master') 3 | def dummy 4 | goNode{ 5 | dockerNode{ 6 | def v = goRelease{ 7 | githubOrganisation = 'funktionio' 8 | dockerOrganisation = 'funktion' 9 | project = 'funktion' 10 | } 11 | 12 | updateDownstreamDependencies(v) 13 | } 14 | } 15 | 16 | def updateDownstreamDependencies(v) { 17 | pushPomPropertyChangePR { 18 | propertyName = 'funktion.version' 19 | projects = [ 20 | 'funktionio/funktion-platform' 21 | ] 22 | version = v 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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 | 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | build: check funktion 3 | 4 | VERSION ?= $(shell cat version/VERSION) 5 | REPO = funktionio/funktion 6 | TAG = latest 7 | GO := GO15VENDOREXPERIMENT=1 go 8 | BUILD_DIR ?= ./out 9 | NAME = funktion 10 | 11 | LDFLAGS := -X github.com/$(REPO)/pkg/version.version=$(VERSION) -s -w -extldflags '-static' 12 | 13 | BUILDFLAGS := -ldflags="$(LDFLAGS)" 14 | 15 | funktion: $(shell find . -type f -name '*.go') 16 | go build -o funktion $(BUILDFLAGS) github.com/funktionio/funktion/cmd/operator 17 | 18 | funktion-linux-static: $(shell find . -type f -name '*.go') 19 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ 20 | go build -o ./out/funktion-linux-amd64 \ 21 | $(BUILDFLAGS) -a -installsuffix cgo \ 22 | github.com/funktionio/funktion/cmd/operator 23 | 24 | check: .check_license 25 | 26 | .check_license: $(shell find . -type f -name '*.go' ! -path './vendor/*') 27 | ./scripts/check_license.sh 28 | touch .check_license 29 | 30 | image: check funktion-linux-static 31 | docker build -t $(REPO):$(TAG) . 32 | 33 | .PHONY: check-gofmt 34 | check-gofmt: 35 | diff -u <(echo -n) <(gofmt -d `find . -type f -name '*.go' -not -path "./vendor/*"`) 36 | 37 | .PHONY: gofmt 38 | gofmt: 39 | gofmt -w `find . -type f -name '*.go' -not -path "./vendor/*"` 40 | 41 | test: 42 | CGO_ENABLED=0 $(GO) test github.com/funktionio/funktion/cmd github.com/funktionio/funktion/pkg/funktion 43 | 44 | e2e: 45 | go test -v ./test/e2e/ --kubeconfig "$(HOME)/.kube/config" --operator-image=funktion/funktion 46 | 47 | clean: 48 | rm -rf funktion funktion-linux-static .check_license release $(BUILD_DIR) 49 | 50 | clean-e2e: 51 | kubectl delete namespace funktion-e2e-tests 52 | 53 | bootstrap: 54 | $(GO) get -u github.com/Masterminds/glide 55 | GO15VENDOREXPERIMENT=1 glide install --strip-vendor --strip-vcs --update-vendored 56 | 57 | .PHONY: build check container e2e clean-e2e clean 58 | 59 | out/$(NAME): out/$(NAME)-$(GOOS)-$(GOARCH) 60 | cp $(BUILD_DIR)/$(NAME)-$(GOOS)-$(GOARCH) $(BUILD_DIR)/$(NAME) 61 | 62 | out/$(NAME)-darwin-amd64: gopath $(shell $(GOFILES)) version/VERSION 63 | CGO_ENABLED=0 GOARCH=amd64 GOOS=darwin go build $(BUILDFLAGS) -o $(BUILD_DIR)/$(NAME)-darwin-amd64 github.com/funktionio/funktion/cmd/operator 64 | 65 | out/$(NAME)-linux-amd64: gopath $(shell $(GOFILES)) version/VERSION 66 | CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build $(BUILDFLAGS) -o $(BUILD_DIR)/$(NAME)-linux-amd64 github.com/funktionio/funktion/cmd/operator 67 | 68 | out/$(NAME)-windows-amd64.exe: gopath $(shell $(GOFILES)) version/VERSION 69 | CGO_ENABLED=0 GOARCH=amd64 GOOS=windows go build $(BUILDFLAGS) -o $(BUILD_DIR)/$(NAME)-windows-amd64.exe github.com/funktionio/funktion/cmd/operator 70 | 71 | out/$(NAME)-linux-arm: gopath $(shell $(GOFILES)) version/VERSION 72 | CGO_ENABLED=0 GOARCH=arm GOOS=linux go build $(BUILDFLAGS) -o $(BUILD_DIR)/$(NAME)-linux-arm github.com/funktionio/funktion/cmd/operator 73 | 74 | .PHONY: release 75 | release: clean bootstrap test cross 76 | mkdir -p release 77 | cp out/$(NAME)-*-amd64* release 78 | cp out/$(NAME)-*-arm* release 79 | gh-release checksums sha256 80 | gh-release create funktionio/$(NAME) $(VERSION) master v$(VERSION) 81 | 82 | .PHONY: cross 83 | cross: out/$(NAME)-linux-amd64 out/$(NAME)-darwin-amd64 out/$(NAME)-windows-amd64.exe out/$(NAME)-linux-arm 84 | 85 | .PHONY: gopath 86 | gopath: $(GOPATH)/src/$(ORG) -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # NOTE this open source project is now sandboxed 2 | 3 | Red Hat has stopped funding this OSS project! Feel free to fork it and maintain it if you like. 4 | 5 | If you're looking for a kubernetes based FaaS we recommend you try use one of these open source alternatives: 6 | 7 | * [kubeless](http://kubeless.io/) 8 | * [openwhisk](https://openwhisk.apache.org/) 9 | 10 | #   Funktion 11 | 12 | **Funktion** is an open source event driven lambda style programming model on top of [Kubernetes](http://kubernetes.io). This project provides a command line tool for working with `Funktion` 13 | 14 | Funktion supports hundreds of different [trigger endpoint URLs](http://camel.apache.org/components.html) including most network protocols, transports, databases, messaging systems, social networks, cloud services and SaaS offerings. 15 | 16 | In a sense funktion is a [serverless](https://www.quora.com/What-is-Serverless-Computing) approach to event driven microservices as you focus on just writing _funktions_ and Kubernetes takes care of the rest. Its not that there's no servers; its more that you as the funktion developer don't have to worry about managing them. 17 | 18 |

19 | 20 | funktion logo 21 | 22 |

23 | 24 | 25 | ### Getting Started 26 | 27 | Please [Install Funktion](https://funktion.fabric8.io/docs/#install) then follow the [Getting Started Guide](https://funktion.fabric8.io/docs/#get-started) 28 | 29 | ### Documentation 30 | 31 | Please see [the website](https://funktion.fabric8.io/) and the [User Guide](https://funktion.fabric8.io/docs/) 32 | 33 | 34 | ### License 35 | 36 | This project is [Apache Licensed](license.md) 37 | 38 | ### Building 39 | 40 | You will need a recent install of `go` along with `glide`. 41 | 42 | Then the first time you want to build you need to do this: 43 | 44 | ``` 45 | mkdir -p $GOHOME/src/github.com/funktionio 46 | cd $GOHOME/src/github.com/funktionio 47 | git clone https://github.com/funktionio/funktion.git 48 | cd funktion 49 | make bootstrap 50 | ``` 51 | 52 | Now whenever you want to do build you can just type 53 | 54 | ``` 55 | make 56 | ``` 57 | 58 | and you'll get a `./funktion` binary you can play with 59 | 60 | #### Running locally outside of docker 61 | 62 | If you want to hack on the `operator` its often easier to just run it locally on your laptop using your local build via 63 | 64 | ``` 65 | ./funktion operate 66 | ``` 67 | 68 | And scale down/delete the `funktion-operator` thats running inside kubernetes. 69 | 70 | Provided your machine can talk to your kubernetes cluster via: 71 | 72 | ``` 73 | kubectl get pod 74 | kubectl get node 75 | ``` 76 | 77 | then the `funktion` binary should be able to monitor and operate all your flows and functions. 78 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "bytes" 19 | "github.com/spf13/cobra" 20 | "io" 21 | "os" 22 | ) 23 | 24 | func init() { 25 | RootCmd.AddCommand(newCompletionCmd()) 26 | } 27 | 28 | //Most of the code below has been taken from https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/completion.go 29 | //and aligned to fit funktion needs. 30 | const header = ` 31 | # Copyright 2016 Red Hat, Inc. 32 | # 33 | # Licensed under the Apache License, Version 2.0 (the "License"); 34 | # you may not use this file except in compliance with the License. 35 | # You may obtain a copy of the License at 36 | # 37 | # http://www.apache.org/licenses/LICENSE-2.0 38 | # 39 | # Unless required by applicable law or agreed to in writing, software 40 | # distributed under the License is distributed on an "AS IS" BASIS, 41 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 42 | # See the License for the specific language governing permissions and 43 | # limitations under the License. 44 | ` 45 | 46 | var ( 47 | completion_shells = map[string]func(out io.Writer, cmd *cobra.Command) error{ 48 | "bash": runCompletionBash, 49 | "zsh": runCompletionZsh, 50 | } 51 | ) 52 | 53 | func newCompletionCmd() *cobra.Command { 54 | shells := []string{} 55 | for s := range completion_shells { 56 | shells = append(shells, s) 57 | } 58 | 59 | cmd := &cobra.Command{ 60 | Use: "completion SHELL", 61 | Short: "Output shell completion code for the specified shell (bash or zsh)", 62 | Run: func(cmd *cobra.Command, args []string) { 63 | err := RunCompletion(os.Stdout, cmd, args) 64 | if err != nil { 65 | handleError(err) 66 | return 67 | } 68 | }, 69 | ValidArgs: shells, 70 | } 71 | 72 | return cmd 73 | } 74 | 75 | func RunCompletion(out io.Writer, cmd *cobra.Command, args []string) error { 76 | if len(args) == 0 { 77 | return usageError(cmd, "Shell not specified.") 78 | } 79 | if len(args) > 1 { 80 | return usageError(cmd, "Too many arguments. Expected only the shell type.") 81 | } 82 | run, found := completion_shells[args[0]] 83 | if !found { 84 | return usageError(cmd, "Unsupported shell type %q.", args[0]) 85 | } 86 | 87 | return run(out, cmd.Parent()) 88 | } 89 | 90 | func runCompletionBash(out io.Writer, funktion *cobra.Command) error { 91 | _, err := out.Write([]byte(header)) 92 | if err != nil { 93 | return err 94 | } 95 | return funktion.GenBashCompletion(out) 96 | } 97 | 98 | func runCompletionZsh(out io.Writer, funktion *cobra.Command) error { 99 | _, err := out.Write([]byte(header)) 100 | if err != nil { 101 | return err 102 | } 103 | zsh_initialization := ` 104 | __funktion_bash_source() { 105 | alias shopt=':' 106 | alias _expand=_bash_expand 107 | alias _complete=_bash_comp 108 | emulate -L sh 109 | setopt kshglob noshglob braceexpand 110 | source "$@" 111 | } 112 | __funktion_type() { 113 | # -t is not supported by zsh 114 | if [ "$1" == "-t" ]; then 115 | shift 116 | # fake Bash 4 to disable "complete -o nospace". Instead 117 | # "compopt +-o nospace" is used in the code to toggle trailing 118 | # spaces. We don't support that, but leave trailing spaces on 119 | # all the time 120 | if [ "$1" = "__funktion_compopt" ]; then 121 | echo builtin 122 | return 0 123 | fi 124 | fi 125 | type "$@" 126 | } 127 | __funktion_compgen() { 128 | local completions w 129 | completions=( $(compgen "$@") ) || return $? 130 | # filter by given word as prefix 131 | while [[ "$1" = -* && "$1" != -- ]]; do 132 | shift 133 | shift 134 | done 135 | if [[ "$1" == -- ]]; then 136 | shift 137 | fi 138 | for w in "${completions[@]}"; do 139 | if [[ "${w}" = "$1"* ]]; then 140 | echo "${w}" 141 | fi 142 | done 143 | } 144 | __funktion_compopt() { 145 | true # don't do anything. Not supported by bashcompinit in zsh 146 | } 147 | __funktion_declare() { 148 | if [ "$1" == "-F" ]; then 149 | whence -w "$@" 150 | else 151 | builtin declare "$@" 152 | fi 153 | } 154 | __funktion_ltrim_colon_completions() 155 | { 156 | if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then 157 | # Remove colon-word prefix from COMPREPLY items 158 | local colon_word=${1%${1##*:}} 159 | local i=${#COMPREPLY[*]} 160 | while [[ $((--i)) -ge 0 ]]; do 161 | COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"} 162 | done 163 | fi 164 | } 165 | __funktion_get_comp_words_by_ref() { 166 | cur="${COMP_WORDS[COMP_CWORD]}" 167 | prev="${COMP_WORDS[${COMP_CWORD}-1]}" 168 | words=("${COMP_WORDS[@]}") 169 | cword=("${COMP_CWORD[@]}") 170 | } 171 | __funktion_filedir() { 172 | local RET OLD_IFS w qw 173 | __debug "_filedir $@ cur=$cur" 174 | if [[ "$1" = \~* ]]; then 175 | # somehow does not work. Maybe, zsh does not call this at all 176 | eval echo "$1" 177 | return 0 178 | fi 179 | OLD_IFS="$IFS" 180 | IFS=$'\n' 181 | if [ "$1" = "-d" ]; then 182 | shift 183 | RET=( $(compgen -d) ) 184 | else 185 | RET=( $(compgen -f) ) 186 | fi 187 | IFS="$OLD_IFS" 188 | IFS="," __debug "RET=${RET[@]} len=${#RET[@]}" 189 | for w in ${RET[@]}; do 190 | if [[ ! "${w}" = "${cur}"* ]]; then 191 | continue 192 | fi 193 | if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then 194 | qw="$(__funktion_quote "${w}")" 195 | if [ -d "${w}" ]; then 196 | COMPREPLY+=("${qw}/") 197 | else 198 | COMPREPLY+=("${qw}") 199 | fi 200 | fi 201 | done 202 | } 203 | __funktion_quote() { 204 | if [[ $1 == \'* || $1 == \"* ]]; then 205 | # Leave out first character 206 | printf %q "${1:1}" 207 | else 208 | printf %q "$1" 209 | fi 210 | } 211 | autoload -U +X bashcompinit && bashcompinit 212 | # use word boundary patterns for BSD or GNU sed 213 | LWORD='[[:<:]]' 214 | RWORD='[[:>:]]' 215 | if sed --help 2>&1 | grep -q GNU; then 216 | LWORD='\<' 217 | RWORD='\>' 218 | fi 219 | __funktion_convert_bash_to_zsh() { 220 | sed \ 221 | -e 's/declare -F/whence -w/' \ 222 | -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \ 223 | -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \ 224 | -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \ 225 | -e "s/${LWORD}_filedir${RWORD}/__funktion_filedir/g" \ 226 | -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__funktion_get_comp_words_by_ref/g" \ 227 | -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__funktion_ltrim_colon_completions/g" \ 228 | -e "s/${LWORD}compgen${RWORD}/__funktion_compgen/g" \ 229 | -e "s/${LWORD}compopt${RWORD}/__funktion_compopt/g" \ 230 | -e "s/${LWORD}declare${RWORD}/__funktion_declare/g" \ 231 | -e "s/\\\$(type${RWORD}/\$(__funktion_type/g" \ 232 | <<'BASH_COMPLETION_EOF' 233 | ` 234 | out.Write([]byte(zsh_initialization)) 235 | 236 | buf := new(bytes.Buffer) 237 | funktion.GenBashCompletion(buf) 238 | out.Write(buf.Bytes()) 239 | 240 | zsh_tail := ` 241 | BASH_COMPLETION_EOF 242 | } 243 | __funktion_bash_source <(__funktion_convert_bash_to_zsh) 244 | ` 245 | out.Write([]byte(zsh_tail)) 246 | return nil 247 | } 248 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "log" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | "github.com/fsnotify/fsnotify" 27 | "github.com/funktionio/funktion/pkg/funktion" 28 | "github.com/spf13/cobra" 29 | "github.com/spf13/pflag" 30 | 31 | "k8s.io/client-go/1.5/pkg/api/v1" 32 | ) 33 | 34 | const ( 35 | flowExtension = ".flow.yml" 36 | ) 37 | 38 | type createFunctionCmd struct { 39 | createCmdCommon 40 | 41 | name string 42 | runtime string 43 | source string 44 | file string 45 | watch bool 46 | debug bool 47 | apply bool 48 | functionsOnly bool 49 | 50 | envVars []string 51 | 52 | configMaps map[string]*v1.ConfigMap 53 | } 54 | 55 | func init() { 56 | RootCmd.AddCommand(newApplyCmd()) 57 | RootCmd.AddCommand(newCreateCmd()) 58 | } 59 | 60 | func newApplyCmd() *cobra.Command { 61 | p := &createFunctionCmd{ 62 | apply: true, 63 | } 64 | cmd := &cobra.Command{ 65 | Use: "apply -f FILENAME", 66 | Short: "applies one or more resources from a file, directory or URL", 67 | Long: `This command will apply (create or update) one or more resources from a file, directory or URL`, 68 | Run: func(cmd *cobra.Command, args []string) { 69 | p.cmd = cmd 70 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 71 | if err != nil { 72 | handleError(err) 73 | return 74 | } 75 | handleError(p.createFromFile()) 76 | }, 77 | } 78 | 79 | cmd.AddCommand(newCreateFunctionCmd()) 80 | cmd.AddCommand(newCreateFlowCmd()) 81 | 82 | f := cmd.Flags() 83 | p.setupCommonFlags(f) 84 | return cmd 85 | } 86 | 87 | func newCreateCmd() *cobra.Command { 88 | p := &createFunctionCmd{} 89 | cmd := &cobra.Command{ 90 | Use: "create -f FILENAME", 91 | Short: "creates one or more resources from the command line, a file, directory or URL", 92 | Long: `This command will create one or more resources from the command one, a file, directory or URL`, 93 | Run: func(cmd *cobra.Command, args []string) { 94 | p.cmd = cmd 95 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 96 | if err != nil { 97 | handleError(err) 98 | return 99 | } 100 | handleError(p.createFromFile()) 101 | }, 102 | } 103 | 104 | cmd.AddCommand(newCreateFunctionCmd()) 105 | cmd.AddCommand(newCreateFlowCmd()) 106 | 107 | f := cmd.Flags() 108 | p.setupCommonFlags(f) 109 | return cmd 110 | } 111 | 112 | func newCreateFunctionCmd() *cobra.Command { 113 | p := &createFunctionCmd{} 114 | cmd := &cobra.Command{ 115 | Use: "fn [flags]", 116 | Short: "creates a new function", 117 | Long: `This command will create a new function resource`, 118 | Run: func(cmd *cobra.Command, args []string) { 119 | p.cmd = cmd 120 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 121 | if err != nil { 122 | handleError(err) 123 | return 124 | } 125 | handleError(p.createFunctionFromCLI()) 126 | }, 127 | } 128 | f := cmd.Flags() 129 | f.StringVarP(&p.name, "name", "n", "", "the name of the function to create") 130 | f.StringVarP(&p.source, "source", "s", "", "the source code of the function to create") 131 | f.StringVarP(&p.runtime, "runtime", "r", "nodejs", "the runtime to use. e.g. 'nodejs'") 132 | p.setupCommonFlags(f) 133 | return cmd 134 | } 135 | 136 | func (p *createFunctionCmd) createFromFile() error { 137 | file := p.file 138 | var err error 139 | if len(file) == 0 { 140 | return fmt.Errorf("No file argument specified!") 141 | } 142 | var matches []string 143 | if isExistingDir(file) { 144 | files, err := ioutil.ReadDir(file) 145 | if err != nil { 146 | return err 147 | } 148 | matches = []string{} 149 | for _, fi := range files { 150 | if !fi.IsDir() { 151 | matches = append(matches, filepath.Join(file, fi.Name())) 152 | } 153 | } 154 | } else { 155 | matches, err = filepath.Glob(file) 156 | if err != nil { 157 | return fmt.Errorf("Could not parse pattern %s due to %v", file, err) 158 | } else if len(matches) == 0 { 159 | fmt.Printf("No files exist matching the name: %s\n", file) 160 | fmt.Println("Please specify a file name that exists or specify the directory containing functions") 161 | return fmt.Errorf("No suitable source file: %s", file) 162 | } 163 | } 164 | for _, file := range matches { 165 | err = p.applyFile(file) 166 | if err != nil { 167 | return err 168 | } 169 | } 170 | if err == nil && p.watch { 171 | p.watchFiles() 172 | } 173 | return err 174 | } 175 | 176 | func (p *createFunctionCmd) setupCommonFlags(f *pflag.FlagSet) { 177 | f.StringArrayVarP(&p.envVars, "env", "e", []string{}, "pass one or more environment variables using the form NAME=VALUE") 178 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 179 | f.StringVar(&p.namespace, "namespace", "", "the namespace to create the resource") 180 | f.BoolVarP(&p.watch, "watch", "w", false, "whether to keep watching the files for changes to the function source code") 181 | f.BoolVarP(&p.debug, "debug", "d", false, "enable debugging for the function?") 182 | f.StringVarP(&p.file, "file", "f", "", "the file name that contains the source code for the function to create") 183 | } 184 | 185 | func (p *createFunctionCmd) createFunctionFromCLI() error { 186 | p.functionsOnly = true 187 | listOpts, err := funktion.CreateFunctionListOptions() 188 | if err != nil { 189 | return err 190 | } 191 | file := p.file 192 | if len(file) > 0 { 193 | return p.createFromFile() 194 | } else { 195 | kubeclient := p.kubeclient 196 | cms := kubeclient.ConfigMaps(p.namespace) 197 | resources, err := cms.List(*listOpts) 198 | if err != nil { 199 | return err 200 | } 201 | p.configMaps = map[string]*v1.ConfigMap{} 202 | for _, resource := range resources.Items { 203 | p.configMaps[resource.Name] = &resource 204 | } 205 | 206 | name := nameFromFile(file, p.name) 207 | if len(name) == 0 { 208 | name, err = p.generateName() 209 | if err != nil { 210 | return err 211 | } 212 | } 213 | update := p.configMaps[name] != nil 214 | cm, err := p.createFunction(name) 215 | if err != nil { 216 | return err 217 | } 218 | message := "created" 219 | if update { 220 | _, err = cms.Update(cm) 221 | message = "updated" 222 | } else { 223 | _, err = cms.Create(cm) 224 | } 225 | if err == nil { 226 | fmt.Printf("Function %s %s\n", name, message) 227 | } 228 | } 229 | if err == nil && p.watch { 230 | p.watchFiles() 231 | } 232 | return err 233 | } 234 | 235 | func (p *createFunctionCmd) watchFiles() { 236 | files := p.file 237 | if len(files) == 0 { 238 | return 239 | } 240 | fmt.Println("Watching files: ", files) 241 | fmt.Println("Please press Ctrl-C to terminate") 242 | watcher, err := fsnotify.NewWatcher() 243 | if err != nil { 244 | log.Fatal(err) 245 | } 246 | defer watcher.Close() 247 | 248 | matches, err := filepath.Glob(files) 249 | if err == nil { 250 | if len(matches) == 0 { 251 | // TODO could we watch for folder? 252 | log.Fatal("No files match pattern ", files) 253 | return 254 | } 255 | 256 | } else { 257 | matches = []string{files} 258 | } 259 | for _, file := range matches { 260 | err = watcher.Add(file) 261 | if err != nil { 262 | log.Fatal(err) 263 | } 264 | } 265 | 266 | for { 267 | select { 268 | case event := <-watcher.Events: 269 | if event.Op&fsnotify.Rename == fsnotify.Rename { 270 | // if a file is renamed (e.g. IDE may do that) we no longer get any more events 271 | // so lets add the files again to be sure 272 | if isExistingFile(event.Name) { 273 | err = watcher.Add(event.Name) 274 | if err != nil { 275 | fmt.Printf("Failed to watch file %s due to %v\n", event.Name, err) 276 | } 277 | } 278 | } 279 | err = p.applyFile(event.Name) 280 | if err != nil { 281 | fmt.Printf("Failed to apply function file %s due to %v\n", event.Name, err) 282 | } 283 | 284 | case err := <-watcher.Errors: 285 | log.Println("error:", err) 286 | } 287 | } 288 | } 289 | 290 | func isExistingFile(name string) bool { 291 | s, err := os.Stat(name) 292 | if err != nil { 293 | return false 294 | } 295 | return s.Mode().IsRegular() 296 | } 297 | 298 | func isExistingDir(name string) bool { 299 | s, err := os.Stat(name) 300 | if err != nil { 301 | return false 302 | } 303 | return s.Mode().IsDir() 304 | } 305 | 306 | func (p *createFunctionCmd) applyFile(fileName string) error { 307 | if !isExistingFile(fileName) { 308 | return nil 309 | } 310 | source, err := loadFileSource(fileName) 311 | if err != nil || len(source) == 0 { 312 | // ignore errors or blank source 313 | return nil 314 | } 315 | if !p.functionsOnly { 316 | if strings.HasSuffix(fileName, flowExtension) { 317 | return p.applyFlow(fileName, source) 318 | } 319 | } 320 | runtime, err := p.findRuntimeFromFileName(fileName) 321 | if err != nil { 322 | fmt.Printf("Failed to find runtime for file %s due to %v", fileName, err) 323 | } 324 | if len(runtime) == 0 { 325 | return nil 326 | } 327 | listOpts, err := funktion.CreateFunctionListOptions() 328 | if err != nil { 329 | return err 330 | } 331 | name := nameFromFile(fileName, "") 332 | if len(name) == 0 { 333 | return fmt.Errorf("Could not generate a function name!") 334 | } 335 | 336 | kubeclient := p.kubeclient 337 | cms := kubeclient.ConfigMaps(p.namespace) 338 | resources, err := cms.List(*listOpts) 339 | if err != nil { 340 | return err 341 | } 342 | var old *v1.ConfigMap = nil 343 | for _, resource := range resources.Items { 344 | if resource.Name == name { 345 | old = &resource 346 | break 347 | } 348 | } 349 | defaultLabels := map[string]string{} 350 | abs, err := filepath.Abs(fileName) 351 | if err == nil && len(abs) > 0 { 352 | folderName := convertToSafeLabelValue(filepath.Base(filepath.Dir(abs))) 353 | if len(folderName) > 0 { 354 | defaultLabels[funktion.ProjectLabel] = folderName 355 | } 356 | } 357 | cm, err := p.createFunctionFromSource(name, source, runtime, defaultLabels) 358 | if err != nil { 359 | return err 360 | } 361 | message := "created" 362 | if old != nil { 363 | oldSource := old.Data[funktion.SourceProperty] 364 | if source == oldSource && cm.Data[funktion.EnvVarsProperty] == old.Data[funktion.EnvVarsProperty] { 365 | // source not changed so lets not update! 366 | return nil 367 | } 368 | _, err = cms.Update(cm) 369 | message = "updated" 370 | } else { 371 | _, err = cms.Create(cm) 372 | } 373 | if err == nil { 374 | log.Println("Function", name, message) 375 | } 376 | return err 377 | } 378 | 379 | // findRuntimeFromFileName returns the runtime to use for the given file name 380 | // or an empty string if the file does not map to a runtime function source file 381 | func (p *createFunctionCmd) findRuntimeFromFileName(fileName string) (string, error) { 382 | // TODO we may want to use a cache and watch the runtimes to minimise API churn here on runtimes... 383 | listOpts, err := funktion.CreateRuntimeListOptions() 384 | if err != nil { 385 | return "", err 386 | } 387 | kubeclient := p.kubeclient 388 | cms := kubeclient.ConfigMaps(p.namespace) 389 | resources, err := cms.List(*listOpts) 390 | if err != nil { 391 | return "", err 392 | } 393 | ext := strings.TrimPrefix(filepath.Ext(fileName), ".") 394 | for _, resource := range resources.Items { 395 | data := resource.Data 396 | if data != nil { 397 | extensions := data[funktion.FileExtensionsProperty] 398 | if len(extensions) > 0 { 399 | values := strings.Split(extensions, ",") 400 | for _, value := range values { 401 | if ext == value { 402 | return resource.Name, nil 403 | } 404 | } 405 | } 406 | } 407 | } 408 | return "", nil 409 | } 410 | 411 | // returns the name of the resource to use given the fileName and the configured name 412 | // if there is a configured name we will use that otherwise we will use the file name 413 | // without the extension 414 | func nameFromFile(fileName, configuredName string) string { 415 | if len(configuredName) != 0 { 416 | return convertToSafeResourceName(configuredName) 417 | } 418 | if len(fileName) == 0 { 419 | return "" 420 | } 421 | _, name := filepath.Split(fileName) 422 | ext := filepath.Ext(name) 423 | l := len(ext) 424 | if l > 0 { 425 | name = name[0 : len(name)-l] 426 | } 427 | return convertToSafeResourceName(name) 428 | } 429 | 430 | func (p *createFunctionCmd) generateName() (string, error) { 431 | prefix := "function" 432 | counter := 1 433 | for { 434 | name := prefix + strconv.Itoa(counter) 435 | if p.configMaps[name] == nil { 436 | return name, nil 437 | } 438 | counter++ 439 | } 440 | } 441 | 442 | func (p *createFunctionCmd) createFunction(name string) (*v1.ConfigMap, error) { 443 | source := p.source 444 | if len(source) == 0 { 445 | file := p.file 446 | if len(file) == 0 { 447 | return nil, fmt.Errorf("No function source code or file name supplied! You must specify either -s or -f flags") 448 | } 449 | var err error 450 | source, err = loadFileSource(file) 451 | if err != nil { 452 | return nil, err 453 | } 454 | } 455 | runtime := p.runtime 456 | if len(runtime) == 0 { 457 | return nil, fmt.Errorf("No runtime supplied! Please pass `-n nodejs` or some other valid runtime") 458 | } 459 | err := p.checkRuntimeExists(runtime) 460 | if err != nil { 461 | return nil, err 462 | } 463 | defaultLabels := map[string]string{} 464 | return p.createFunctionFromSource(name, source, runtime, defaultLabels) 465 | } 466 | 467 | func (p *createFunctionCmd) createFunctionFromSource(name, source, runtime string, extraLabels map[string]string) (*v1.ConfigMap, error) { 468 | labels := map[string]string{ 469 | funktion.KindLabel: funktion.FunctionKind, 470 | funktion.RuntimeLabel: runtime, 471 | } 472 | for k, v := range extraLabels { 473 | if labels[k] == "" { 474 | labels[k] = v 475 | } 476 | } 477 | data := map[string]string{ 478 | funktion.SourceProperty: source, 479 | } 480 | if p.debug { 481 | data[funktion.DebugProperty] = "true" 482 | } 483 | if len(p.envVars) > 0 { 484 | data[funktion.EnvVarsProperty] = strings.Join(p.envVars, "\n") 485 | } 486 | cm := &v1.ConfigMap{ 487 | ObjectMeta: v1.ObjectMeta{ 488 | Name: name, 489 | Labels: labels, 490 | }, 491 | Data: data, 492 | } 493 | return cm, nil 494 | } 495 | 496 | func loadFileSource(fileName string) (string, error) { 497 | data, err := ioutil.ReadFile(fileName) 498 | if err != nil { 499 | return "", err 500 | } 501 | source := string(data) 502 | return source, nil 503 | } 504 | 505 | func (p *createFunctionCmd) checkRuntimeExists(name string) error { 506 | listOpts, err := funktion.CreateRuntimeListOptions() 507 | if err != nil { 508 | return err 509 | } 510 | cms, err := p.kubeclient.ConfigMaps(p.namespace).List(*listOpts) 511 | if err != nil { 512 | return err 513 | } 514 | for _, resource := range cms.Items { 515 | if resource.Name == name { 516 | return nil 517 | } 518 | } 519 | return fmt.Errorf("No runtime exists called `%s`", name) 520 | 521 | } 522 | -------------------------------------------------------------------------------- /cmd/create_flow.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "log" 21 | "net/url" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | "github.com/ghodss/yaml" 27 | "github.com/spf13/cobra" 28 | 29 | "k8s.io/client-go/1.5/kubernetes" 30 | "k8s.io/client-go/1.5/pkg/api" 31 | "k8s.io/client-go/1.5/pkg/api/v1" 32 | 33 | "github.com/funktionio/funktion/pkg/funktion" 34 | "github.com/funktionio/funktion/pkg/spec" 35 | ) 36 | 37 | const ( 38 | functionArgPrefix = "fn:" 39 | setBodyArgPrefix = "setBody:" 40 | setHeadersArgPrefix = "setHeaders:" 41 | ) 42 | 43 | type createCmdCommon struct { 44 | kubeclient *kubernetes.Clientset 45 | kubeConfigPath string 46 | namespace string 47 | cmd *cobra.Command 48 | } 49 | 50 | type createFlowCmd struct { 51 | createCmdCommon 52 | flowName string 53 | connectorName string 54 | args []string 55 | trace bool 56 | logResult bool 57 | } 58 | 59 | func newCreateFlowCmd() *cobra.Command { 60 | p := &createFlowCmd{} 61 | cmd := &cobra.Command{ 62 | Use: "flow [flags] [endpointUrl] [fn:name] [setBody:content] [setHeaders:foo:bar,xyz:abc]", 63 | Short: "Creates a new flow which creates an event stream and then invokes a function or HTTP endpoint", 64 | Long: `This command will create a new Flow which receives input events and then invokes either a function or HTTP endpoint`, 65 | Run: func(cmd *cobra.Command, args []string) { 66 | p.cmd = cmd 67 | p.args = args 68 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 69 | if err != nil { 70 | handleError(err) 71 | return 72 | } 73 | handleError(p.run()) 74 | }, 75 | } 76 | f := cmd.Flags() 77 | f.StringVarP(&p.flowName, "name", "n", "", "name of the flow to create") 78 | f.StringVarP(&p.connectorName, "connector", "c", "", "the Connector name to use. If not specified uses the first URL scheme") 79 | f.BoolVar(&p.trace, "trace", false, "enable tracing on the flow") 80 | f.BoolVar(&p.logResult, "log-result", true, "whether to log the result of the subcription to the log of the subcription pod") 81 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 82 | f.StringVar(&p.namespace, "namespace", "", "the namespace to create the flow inside") 83 | return cmd 84 | } 85 | 86 | func (p *createFlowCmd) run() error { 87 | var err error 88 | args := p.args 89 | if len(args) == 0 { 90 | return fmt.Errorf("No arguments specified! A flow must have one or more arguments of the form: [endpointUrl] | [function:name] | [setBody:content] | [setHeaders:foo=bar,abc=123]") 91 | } 92 | steps, err := parseSteps(args) 93 | if err != nil { 94 | return err 95 | } 96 | name := p.flowName 97 | if len(name) == 0 { 98 | name, err = p.generateName(steps) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | connectorName := p.connectorName 104 | if len(connectorName) == 0 { 105 | for _, step := range steps { 106 | uri := step.URI 107 | if len(uri) > 0 { 108 | connectorName, err = urlScheme(uri) 109 | if err != nil { 110 | return err 111 | } 112 | if len(connectorName) == 0 { 113 | return fmt.Errorf("No scheme specified for from URI %s", uri) 114 | } 115 | } 116 | } 117 | } 118 | funktionConfig := spec.FunkionConfig{ 119 | Flows: []spec.FunktionFlow{ 120 | { 121 | Name: "default", 122 | LogResult: p.logResult, 123 | Trace: p.trace, 124 | Steps: steps, 125 | }, 126 | }, 127 | } 128 | funktionData, err := yaml.Marshal(&funktionConfig) 129 | if err != nil { 130 | return fmt.Errorf("Failed to marshal funktion %v due to marshalling error %v", &funktionConfig, err) 131 | } 132 | funktionYml := string(funktionData) 133 | 134 | message := stepsText(steps) 135 | return p.applyFlowWithConnector(name, funktionYml, connectorName, message) 136 | } 137 | 138 | func (p *createCmdCommon) applyFlow(fileName, source string) error { 139 | _, name := filepath.Split(fileName) 140 | name = convertToSafeResourceName(name[0 : len(name)-len(flowExtension)]) 141 | if len(name) == 0 { 142 | return fmt.Errorf("Could not generate a name of the flow from file %s", fileName) 143 | } 144 | message := fmt.Sprintf("from file %s", fileName) 145 | // TODO parse from the steps! 146 | connectorName := "timer" 147 | return p.applyFlowWithConnector(name, source, connectorName, message) 148 | } 149 | 150 | func (p *createCmdCommon) applyFlowWithConnector(name, funktionYml, connectorName, message string) error { 151 | connector, err := p.checkConnectorExists(connectorName) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | applicationProperties := "" 157 | if connector.Data != nil { 158 | applicationProperties = connector.Data[funktion.ApplicationPropertiesProperty] 159 | } 160 | if len(applicationProperties) == 0 { 161 | applicationProperties = "# put your spring boot configuration properties here..." 162 | } 163 | 164 | labels := map[string]string{ 165 | funktion.KindLabel: funktion.FlowKind, 166 | funktion.ConnectorLabel: connectorName, 167 | } 168 | data := map[string]string{ 169 | funktion.FunktionYmlProperty: funktionYml, 170 | funktion.ApplicationPropertiesProperty: applicationProperties, 171 | } 172 | cm := v1.ConfigMap{ 173 | ObjectMeta: v1.ObjectMeta{ 174 | Name: name, 175 | Namespace: p.namespace, 176 | Labels: labels, 177 | }, 178 | Data: data, 179 | } 180 | update := false 181 | old, err := p.kubeclient.ConfigMaps(p.namespace).Get(name) 182 | if err == nil { 183 | update = true 184 | } 185 | 186 | action := "created" 187 | if update { 188 | if old.Data != nil && old.Labels != nil && 189 | old.Data[funktion.FunktionYmlProperty] == cm.Data[funktion.FunktionYmlProperty] && 190 | old.Data[funktion.ApplicationPropertiesProperty] == cm.Data[funktion.ApplicationPropertiesProperty] && 191 | old.Labels[funktion.ConnectorLabel] == cm.Labels[funktion.ConnectorLabel] { 192 | // source not changed so lets not update! 193 | return nil 194 | } 195 | _, err = p.kubeclient.ConfigMaps(p.namespace).Update(&cm) 196 | action = "updated" 197 | } else { 198 | _, err = p.kubeclient.ConfigMaps(p.namespace).Create(&cm) 199 | } 200 | 201 | if err == nil { 202 | log.Println("Flow", name, action, message) 203 | } 204 | return err 205 | } 206 | 207 | // parseSteps parses a sequence of arguments as either endpoint URLs, function:name, 208 | // setBody:content, setHeaders:foo=bar,abc=def 209 | func parseSteps(args []string) ([]spec.FunktionStep, error) { 210 | steps := []spec.FunktionStep{} 211 | for _, arg := range args { 212 | var step *spec.FunktionStep 213 | if strings.HasPrefix(arg, functionArgPrefix) { 214 | name := strings.TrimPrefix(arg, functionArgPrefix) 215 | if len(name) == 0 { 216 | return steps, fmt.Errorf("Function name required after %s", functionArgPrefix) 217 | } 218 | step = &spec.FunktionStep{ 219 | Kind: spec.FunctionKind, 220 | Name: name, 221 | } 222 | } else if strings.HasPrefix(arg, setBodyArgPrefix) { 223 | body := strings.TrimPrefix(arg, setBodyArgPrefix) 224 | step = &spec.FunktionStep{ 225 | Kind: spec.SetBodyKind, 226 | Body: body, 227 | } 228 | } else if strings.HasPrefix(arg, setHeadersArgPrefix) { 229 | headersText := strings.TrimPrefix(arg, setHeadersArgPrefix) 230 | if len(headersText) == 0 { 231 | return steps, fmt.Errorf("Header name and values required after %s", setHeadersArgPrefix) 232 | } 233 | headers, err := parseHeaders(headersText) 234 | if err != nil { 235 | return steps, err 236 | } 237 | step = &spec.FunktionStep{ 238 | Kind: spec.SetHeadersKind, 239 | Headers: headers, 240 | } 241 | } else { 242 | step = &spec.FunktionStep{ 243 | Kind: spec.EndpointKind, 244 | URI: arg, 245 | } 246 | } 247 | if step != nil { 248 | steps = append(steps, *step) 249 | } 250 | } 251 | return steps, nil 252 | } 253 | 254 | func parseHeaders(text string) (map[string]string, error) { 255 | m := map[string]string{} 256 | kvs := strings.Split(text, ",") 257 | for _, kv := range kvs { 258 | v := strings.SplitN(kv, ":", 2) 259 | if len(v) != 2 { 260 | return m, fmt.Errorf("Missing ':' in header `%s`", kv) 261 | } 262 | m[v[0]] = v[1] 263 | } 264 | return m, nil 265 | } 266 | 267 | func (p *createCmdCommon) checkConnectorExists(name string) (*v1.ConfigMap, error) { 268 | listOpts, err := funktion.CreateConnectorListOptions() 269 | if err != nil { 270 | return nil, err 271 | } 272 | cms := p.kubeclient.ConfigMaps(p.namespace) 273 | resources, err := cms.List(*listOpts) 274 | if err != nil { 275 | return nil, err 276 | } 277 | for _, resource := range resources.Items { 278 | if resource.Name == name { 279 | return &resource, nil 280 | } 281 | } 282 | return nil, fmt.Errorf("Connector \"%s\" not found so cannot create this flow", name) 283 | } 284 | 285 | func (p *createFlowCmd) generateName(steps []spec.FunktionStep) (string, error) { 286 | configmaps := p.kubeclient.ConfigMaps(p.namespace) 287 | cms, err := configmaps.List(api.ListOptions{}) 288 | if err != nil { 289 | return "", err 290 | } 291 | names := make(map[string]bool) 292 | for _, item := range cms.Items { 293 | names[item.Name] = true 294 | } 295 | prefix := "flow" 296 | 297 | fromUri := "" 298 | for _, step := range steps { 299 | fromUri = step.URI 300 | if len(fromUri) > 0 { 301 | break 302 | } 303 | } 304 | 305 | // lets try generate a flow name from the scheme 306 | if len(fromUri) > 0 { 307 | u, err := url.Parse(fromUri) 308 | if err != nil { 309 | fmt.Printf("Warning: cannot parse the from URL %s as got %v\n", fromUri, err) 310 | } else { 311 | path := strings.Trim(u.Host, "/") 312 | prefix = u.Scheme 313 | if len(p.connectorName) == 0 { 314 | p.connectorName = u.Scheme 315 | } 316 | if len(path) > 0 { 317 | prefix = prefix + "-" + path 318 | } 319 | prefix = convertToSafeResourceName(prefix) 320 | } 321 | } 322 | counter := 1 323 | for { 324 | name := prefix + strconv.Itoa(counter) 325 | if !names[name] { 326 | return name, nil 327 | } 328 | counter++ 329 | } 330 | } 331 | 332 | func urlScheme(text string) (string, error) { 333 | u, err := url.Parse(text) 334 | if err != nil { 335 | return "", fmt.Errorf("Warning: cannot parse the from URL %s as got %v\n", text, err) 336 | } else { 337 | return u.Scheme, nil 338 | } 339 | } 340 | 341 | // convertToSafeResourceName converts the given text into a usable kubernetes name 342 | // converting to lower case and removing any dodgy characters 343 | func convertToSafeResourceName(text string) string { 344 | var buffer bytes.Buffer 345 | lower := strings.ToLower(text) 346 | lastCharValid := false 347 | for i := 0; i < len(lower); i++ { 348 | ch := lower[i] 349 | if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') { 350 | buffer.WriteString(string(ch)) 351 | lastCharValid = true 352 | } else { 353 | if lastCharValid { 354 | buffer.WriteString("-") 355 | } 356 | lastCharValid = false 357 | } 358 | } 359 | return buffer.String() 360 | } 361 | 362 | // convertToSafeLabelValue converts the given text into a usable kubernetes label value 363 | // removing any dodgy characters 364 | func convertToSafeLabelValue(text string) string { 365 | var buffer bytes.Buffer 366 | l := len(text) - 1 367 | lastCharValid := false 368 | for i := 0; i <= l; i++ { 369 | ch := text[i] 370 | valid := (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') 371 | if i > 0 && i < l { 372 | valid = valid || (ch == '-' || ch == '_' || ch == '.') 373 | } 374 | if valid { 375 | buffer.WriteString(string(ch)) 376 | lastCharValid = true 377 | } else { 378 | if lastCharValid && i < l { 379 | buffer.WriteString("-") 380 | } 381 | lastCharValid = false 382 | } 383 | } 384 | return buffer.String() 385 | } 386 | -------------------------------------------------------------------------------- /cmd/debug.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "bufio" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | "github.com/funktionio/funktion/pkg/funktion" 27 | "github.com/funktionio/funktion/pkg/k8sutil" 28 | "github.com/pkg/browser" 29 | "github.com/spf13/cobra" 30 | 31 | "k8s.io/client-go/1.5/kubernetes" 32 | "k8s.io/client-go/1.5/pkg/api" 33 | "k8s.io/client-go/1.5/pkg/api/v1" 34 | "k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1" 35 | ) 36 | 37 | const ( 38 | chromeDevToolsURLPrefix = "chrome-devtools:" 39 | ) 40 | 41 | type debugCmd struct { 42 | kubeclient *kubernetes.Clientset 43 | cmd *cobra.Command 44 | kubeConfigPath string 45 | 46 | namespace string 47 | kind string 48 | name string 49 | localPort int 50 | remotePort int 51 | supportsChromeDevTools bool 52 | chromeDevTools bool 53 | portText string 54 | 55 | podAction k8sutil.PodAction 56 | debugCmd *exec.Cmd 57 | } 58 | 59 | func init() { 60 | RootCmd.AddCommand(newDebugCmd()) 61 | } 62 | 63 | func newDebugCmd() *cobra.Command { 64 | p := &debugCmd{} 65 | p.podAction = k8sutil.PodAction{ 66 | OnPodChange: p.viewLog, 67 | } 68 | cmd := &cobra.Command{ 69 | Use: "debug KIND NAME [flags]", 70 | Short: "debugs the given function or flow", 71 | Long: `This command will debug the latest container implementing the function or flow`, 72 | Run: func(cmd *cobra.Command, args []string) { 73 | p.cmd = cmd 74 | if len(args) < 1 { 75 | handleError(fmt.Errorf("No resource kind argument supplied! Possible values ['fn', 'flow']")) 76 | return 77 | } 78 | p.kind = args[0] 79 | kind, _, err := listOptsForKind(p.kind) 80 | if err != nil { 81 | handleError(err) 82 | return 83 | } 84 | if len(args) < 2 { 85 | handleError(fmt.Errorf("No %s name specified!", kind)) 86 | return 87 | } 88 | p.name = args[1] 89 | err = createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 90 | if err != nil { 91 | handleError(err) 92 | return 93 | } 94 | handleError(p.run()) 95 | }, 96 | } 97 | f := cmd.Flags() 98 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 99 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 100 | f.StringVarP(&p.name, "name", "v", "latest", "the version of the connectors to install") 101 | f.IntVarP(&p.localPort, "local-port", "l", 0, "The localhost port to use for debugging or the container's debugging port is used") 102 | f.IntVarP(&p.remotePort, "remote-port", "r", 0, "The remote container port to use for debugging or the container's debugging port is used") 103 | //f.BoolVarP(&p.chromeDevTools, "chrome", "c", false, "For node based containers open the Chrome DevTools to debug") 104 | return cmd 105 | } 106 | 107 | func (p *debugCmd) run() error { 108 | name, err := nameForDeployment(p.kubeclient, p.namespace, p.kind, p.name) 109 | if err != nil { 110 | return err 111 | } 112 | portText, err := p.createPortText(p.kind, p.name) 113 | if err != nil { 114 | return err 115 | } 116 | p.portText = portText 117 | kubeclient := p.kubeclient 118 | ds, err := kubeclient.Deployments(p.namespace).List(api.ListOptions{}) 119 | if err != nil { 120 | return err 121 | } 122 | var deployment *v1beta1.Deployment 123 | for _, item := range ds.Items { 124 | if item.Name == name { 125 | deployment = &item 126 | break 127 | } 128 | } 129 | if deployment == nil { 130 | return fmt.Errorf("No Deployment found called `%s`", name) 131 | } 132 | selector := deployment.Spec.Selector 133 | if selector == nil { 134 | return fmt.Errorf("Deployment `%s` does not have a selector!", name) 135 | } 136 | if selector.MatchLabels == nil { 137 | return fmt.Errorf("Deployment `%s` selector does not have a matchLabels!", name) 138 | } 139 | listOpts, err := k8sutil.V1BetaSelectorToListOptions(selector) 140 | if err != nil { 141 | return err 142 | } 143 | p.podAction.WatchPods(p.kubeclient, p.namespace, listOpts) 144 | return p.podAction.WatchLoop() 145 | } 146 | 147 | func (p *debugCmd) createPortText(kindText, name string) (string, error) { 148 | kind, listOpts, err := listOptsForKind(kindText) 149 | if err != nil { 150 | return "", err 151 | } 152 | cms := p.kubeclient.ConfigMaps(p.namespace) 153 | resources, err := cms.List(*listOpts) 154 | if err != nil { 155 | return "", err 156 | } 157 | var found *v1.ConfigMap 158 | for _, resource := range resources.Items { 159 | if name == resource.Name { 160 | found = &resource 161 | } 162 | } 163 | if found == nil { 164 | return "", fmt.Errorf("No %s resource found for name %s", kind, name) 165 | } 166 | 167 | debugPort := 0 168 | if kind == functionKind { 169 | // ensure debug mode is enabled on the function 170 | if found.Data == nil { 171 | found.Data = map[string]string{} 172 | } 173 | data := found.Data 174 | debugMode := data[funktion.DebugProperty] 175 | if strings.ToLower(debugMode) != "true" { 176 | data[funktion.DebugProperty] = "true" 177 | _, err = cms.Update(found) 178 | if err != nil { 179 | return "", fmt.Errorf("Failed to update Function %s to enable debug mode %v", name, err) 180 | } 181 | fmt.Printf("Enabled debug mode for Function %s\n", name) 182 | } 183 | 184 | // lets use the debug port on the runtime 185 | runtime := "" 186 | labels := found.Labels 187 | if labels != nil { 188 | runtime = labels[funktion.RuntimeLabel] 189 | } 190 | if len(runtime) > 0 { 191 | return p.createPortText(runtimeKind, runtime) 192 | } 193 | } else if kind == runtimeKind || kind == connectorKind { 194 | data := found.Data 195 | if data != nil { 196 | portValue := data[funktion.DebugPortProperty] 197 | if len(portValue) > 0 { 198 | debugPort, err = strconv.Atoi(portValue) 199 | if err != nil { 200 | return "", fmt.Errorf("Failed to convert debug port `%s` to a number due to %v", portValue, err) 201 | } 202 | } 203 | } 204 | annotations := found.Annotations 205 | if kind == runtimeKind && annotations != nil { 206 | flag := annotations[funktion.ChromeDevToolsAnnotation] 207 | if flag == "true" { 208 | p.supportsChromeDevTools = true 209 | } else if len(flag) == 0 { 210 | // TODO handle older nodejs runtimes which don't have the annotation 211 | // remove after next funktion-connectors release! 212 | if found.Name == "nodejs" { 213 | p.supportsChromeDevTools = true 214 | } 215 | } 216 | } 217 | } else if kind == flowKind { 218 | connector := "" 219 | data := found.Labels 220 | if data != nil { 221 | connector = data[funktion.ConnectorLabel] 222 | } 223 | if len(connector) > 0 { 224 | return p.createPortText(runtimeKind, connector) 225 | } 226 | } 227 | if debugPort == 0 { 228 | if kind == connectorKind || kind == flowKind { 229 | // default to java debug port for flows and connectors if none specified 230 | debugPort = 5005 231 | } 232 | } 233 | if debugPort > 0 { 234 | if p.localPort == 0 { 235 | p.localPort = debugPort 236 | } 237 | if p.remotePort == 0 { 238 | p.remotePort = debugPort 239 | } 240 | } 241 | if p.remotePort == 0 { 242 | return "", fmt.Errorf("No remote debug port could be defaulted. Please specify one via the `-r` flag") 243 | } 244 | if p.localPort == 0 { 245 | p.localPort = p.remotePort 246 | } 247 | return fmt.Sprintf("%d:%d", p.localPort, p.remotePort), nil 248 | } 249 | 250 | func (p *debugCmd) viewLog(pod *v1.Pod) error { 251 | if pod != nil { 252 | binaryFile, err := k8sutil.ResolveKubectlBinary(p.kubeclient) 253 | if err != nil { 254 | return err 255 | } 256 | name := pod.Name 257 | if p.debugCmd != nil { 258 | process := p.debugCmd.Process 259 | if process != nil { 260 | process.Kill() 261 | } 262 | } 263 | args := []string{"port-forward", name, p.portText} 264 | 265 | fmt.Printf("\n%s %s\n\n", filepath.Base(binaryFile), strings.Join(args, " ")) 266 | cmd := exec.Command(binaryFile, args...) 267 | p.debugCmd = cmd 268 | cmd.Stdout = os.Stdout 269 | cmd.Stderr = os.Stderr 270 | err = cmd.Start() 271 | if err != nil { 272 | return err 273 | } 274 | 275 | if p.supportsChromeDevTools { 276 | return p.findChromeDevToolsURL(pod, binaryFile) 277 | } 278 | } 279 | return nil 280 | } 281 | 282 | func (p *debugCmd) findChromeDevToolsURL(pod *v1.Pod, binaryFile string) error { 283 | name := pod.Name 284 | 285 | args := []string{"logs", "-f", name} 286 | cmd := exec.Command(binaryFile, args...) 287 | 288 | cmdReader, err := cmd.StdoutPipe() 289 | if err != nil { 290 | return fmt.Errorf("failed to get stdout for command %s %s: %v", filepath.Base(binaryFile), strings.Join(args, " "), err) 291 | } 292 | 293 | scanner := bufio.NewScanner(cmdReader) 294 | line := 0 295 | go func() { 296 | for scanner.Scan() { 297 | if line++; line > 50 { 298 | fmt.Printf("No log line found starting with `%s` in the first %d lines. Maybe debug is not really enabled in this pod?\n", chromeDevToolsURLPrefix, line) 299 | cmdReader.Close() 300 | killCmd(cmd) 301 | } 302 | text := strings.TrimSpace(scanner.Text()) 303 | if strings.HasPrefix(text, chromeDevToolsURLPrefix) { 304 | fmt.Printf("\nTo Debug open: %s\n\n", text) 305 | cmdReader.Close() 306 | killCmd(cmd) 307 | if p.chromeDevTools { 308 | browser.OpenURL(text) 309 | } 310 | } 311 | } 312 | }() 313 | 314 | if err := cmd.Start(); err != nil { 315 | killCmd(cmd) 316 | return fmt.Errorf("failed to start command %s %s: %v", filepath.Base(binaryFile), strings.Join(args, " "), err) 317 | } 318 | 319 | err = cmd.Wait() 320 | if err != nil { 321 | killCmd(cmd) 322 | // ignore errors as we get an error if we kill it 323 | } 324 | return nil 325 | } 326 | 327 | func killCmd(cmd *exec.Cmd) { 328 | if cmd != nil { 329 | p := cmd.Process 330 | if p != nil { 331 | p.Kill() 332 | } 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /cmd/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | "k8s.io/client-go/1.5/kubernetes" 22 | "k8s.io/client-go/1.5/pkg/api" 23 | "k8s.io/client-go/1.5/pkg/api/v1" 24 | "strings" 25 | ) 26 | 27 | type deleteCmd struct { 28 | kubeclient *kubernetes.Clientset 29 | cmd *cobra.Command 30 | kubeConfigPath string 31 | 32 | kind string 33 | namespace string 34 | name string 35 | all bool 36 | } 37 | 38 | func init() { 39 | RootCmd.AddCommand(newDeleteCmd()) 40 | } 41 | 42 | func newDeleteCmd() *cobra.Command { 43 | p := &deleteCmd{} 44 | cmd := &cobra.Command{ 45 | Use: "delete KIND ([NAME] | --all) [flags]", 46 | Short: "delete resources", 47 | Long: `This command will delete one more resources`, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | p.cmd = cmd 50 | if len(args) == 0 { 51 | handleError(fmt.Errorf("No resource kind argument supplied!")) 52 | return 53 | } 54 | p.kind = args[0] 55 | if len(args) > 1 { 56 | p.name = args[1] 57 | } 58 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 59 | if err != nil { 60 | handleError(err) 61 | return 62 | } 63 | handleError(p.run()) 64 | }, 65 | } 66 | f := cmd.Flags() 67 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 68 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 69 | f.BoolVar(&p.all, "all", false, "whether to delete all resources") 70 | return cmd 71 | } 72 | 73 | func (p *deleteCmd) run() error { 74 | kind, listOpts, err := listOptsForKind(p.kind) 75 | if err != nil { 76 | return err 77 | } 78 | cms := p.kubeclient.ConfigMaps(p.namespace) 79 | resources, err := cms.List(*listOpts) 80 | if err != nil { 81 | return err 82 | } 83 | name := p.name 84 | if len(name) == 0 { 85 | if !p.all { 86 | return fmt.Errorf("No `name` specified or the `--all` flag specified so cannot delete a %s", kind) 87 | } 88 | count := 0 89 | for _, resource := range resources.Items { 90 | err = p.deleteResource(&resource) 91 | if err != nil { 92 | return err 93 | } 94 | count++ 95 | } 96 | fmt.Printf("Deleted %d %s resource(s)\n", count, kind) 97 | } else { 98 | for _, resource := range resources.Items { 99 | if resource.Name == name { 100 | err = p.deleteResource(&resource) 101 | if err == nil { 102 | fmt.Printf("Deleted %s \"%s\" resource\n", kind, name) 103 | } 104 | return err 105 | } 106 | } 107 | return fmt.Errorf("%s \"%s\" not found", kind, name) 108 | } 109 | return nil 110 | } 111 | 112 | func (p *deleteCmd) deleteResource(cm *v1.ConfigMap) error { 113 | kind := strings.ToLower(cm.Kind) 114 | name := cm.Name 115 | ns := cm.Namespace 116 | if len(ns) == 0 { 117 | ns = p.namespace 118 | } 119 | err := p.kubeclient.ConfigMaps(ns).Delete(name, &api.DeleteOptions{}) 120 | if err != nil { 121 | return fmt.Errorf("Failed to delete %s \"%s\" due to: %v", kind, name, err) 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/funktionio/funktion/pkg/funktion" 24 | "github.com/funktionio/funktion/pkg/spec" 25 | "github.com/magiconair/properties" 26 | "github.com/spf13/cobra" 27 | 28 | "k8s.io/client-go/1.5/kubernetes" 29 | "k8s.io/client-go/1.5/pkg/api/v1" 30 | "strconv" 31 | ) 32 | 33 | type editConnectorCmd struct { 34 | kubeclient *kubernetes.Clientset 35 | cmd *cobra.Command 36 | kubeConfigPath string 37 | 38 | namespace string 39 | name string 40 | listProperties bool 41 | 42 | configMaps map[string]*v1.ConfigMap 43 | schema *spec.ConnectorSchema 44 | applicationProperties *properties.Properties 45 | 46 | setProperties map[string]string 47 | } 48 | 49 | func init() { 50 | RootCmd.AddCommand(newEditCmd()) 51 | } 52 | 53 | func newEditCmd() *cobra.Command { 54 | cmd := &cobra.Command{ 55 | Use: "edit KIND [NAME] [flags]", 56 | Short: "edits a resources", 57 | Long: `This command will create edit a resource`, 58 | } 59 | 60 | cmd.AddCommand(newEditConnectorCmd()) 61 | return cmd 62 | } 63 | 64 | func newEditConnectorCmd() *cobra.Command { 65 | p := &editConnectorCmd{} 66 | cmd := &cobra.Command{ 67 | Use: "connector NAME [flags] [prop1=value1] [prop2=value2]", 68 | Short: "edits a connectors configuration", 69 | Long: `This command will edit a connector resource`, 70 | Run: func(cmd *cobra.Command, args []string) { 71 | p.cmd = cmd 72 | if len(args) == 0 { 73 | fmt.Printf("You must specify the name of the connector as an argument!") 74 | return 75 | } 76 | p.name = args[0] 77 | if len(args) > 1 { 78 | sp, err := parseProperties(args[1:]) 79 | if err != nil { 80 | handleError(err) 81 | return 82 | } 83 | p.setProperties = sp 84 | } 85 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 86 | if err != nil { 87 | handleError(err) 88 | return 89 | } 90 | handleError(p.run()) 91 | }, 92 | } 93 | f := cmd.Flags() 94 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 95 | f.StringVar(&p.namespace, "namespace", "", "the namespace to query") 96 | f.BoolVarP(&p.listProperties, "list", "l", false, "list the properties to edit") 97 | return cmd 98 | } 99 | 100 | func parseProperties(args []string) (map[string]string, error) { 101 | m := map[string]string{} 102 | for _, arg := range args { 103 | values := strings.SplitN(arg, "=", 2) 104 | if len(values) != 2 { 105 | return nil, fmt.Errorf("Argument does not contain `=` but was `%s`", arg) 106 | } 107 | m[values[0]] = values[1] 108 | } 109 | return m, nil 110 | } 111 | 112 | func (p *editConnectorCmd) run() error { 113 | listOpts, err := funktion.CreateConnectorListOptions() 114 | if err != nil { 115 | return err 116 | } 117 | kubeclient := p.kubeclient 118 | cms := kubeclient.ConfigMaps(p.namespace) 119 | resources, err := cms.List(*listOpts) 120 | if err != nil { 121 | return err 122 | } 123 | var connector *v1.ConfigMap 124 | for _, resource := range resources.Items { 125 | if resource.Name == p.name { 126 | connector = &resource 127 | break 128 | } 129 | } 130 | name := p.name 131 | if connector == nil { 132 | return fmt.Errorf("No Connector called `%s` exists in namespace %s", name, p.namespace) 133 | } 134 | err = p.loadConnectorSchema(name, connector) 135 | if err != nil { 136 | return err 137 | } 138 | if p.listProperties { 139 | err = p.listConnectorProperties(name, connector) 140 | 141 | } else { 142 | err = p.editConnector(name, connector) 143 | } 144 | if err != nil { 145 | return err 146 | } 147 | return err 148 | } 149 | 150 | func (p *editConnectorCmd) loadConnectorSchema(name string, connector *v1.ConfigMap) error { 151 | // lets load the connector model 152 | if connector.Data == nil { 153 | return fmt.Errorf("No data in the Connector %s", name) 154 | } 155 | schemaYaml := connector.Data[funktion.SchemaYmlProperty] 156 | if len(schemaYaml) == 0 { 157 | return fmt.Errorf("No YAML for data key %s in Connector %s", funktion.SchemaYmlProperty, name) 158 | } 159 | schema, err := funktion.LoadConnectorSchema([]byte(schemaYaml)) 160 | if err != nil { 161 | return err 162 | } 163 | p.schema = schema 164 | 165 | propertiesText := connector.Data[funktion.ApplicationPropertiesProperty] 166 | if len(propertiesText) > 0 { 167 | props, err := properties.LoadString(propertiesText) 168 | if err != nil { 169 | return err 170 | } 171 | p.applicationProperties = props 172 | } else { 173 | p.applicationProperties = properties.NewProperties() 174 | } 175 | return nil 176 | } 177 | 178 | func (p *editConnectorCmd) listConnectorProperties(name string, connector *v1.ConfigMap) error { 179 | compProps := p.schema.ComponentProperties 180 | if len(compProps) == 0 { 181 | fmt.Printf("The connector `%s` has no properties to configure!", name) 182 | return nil 183 | } 184 | 185 | maxLen := 1 186 | for k, _ := range compProps { 187 | l := len(k) 188 | if l > maxLen { 189 | maxLen = l 190 | } 191 | } 192 | colText := strconv.Itoa(maxLen) 193 | fmt.Printf(" %-"+colText+"s VALUE\n", "NAME") 194 | for k, cp := range compProps { 195 | propertyKey := "camel.component." + name + "." + funktion.ToSpringBootPropertyName(k) 196 | value := p.applicationProperties.GetString(propertyKey, "") 197 | prompt := "?" 198 | if cp.Required { 199 | prompt = "*" 200 | } 201 | fmt.Printf("%s %-"+colText+"s %s\n", prompt, k, value) 202 | } 203 | return nil 204 | } 205 | 206 | func (p *editConnectorCmd) editConnector(name string, connector *v1.ConfigMap) error { 207 | compProps := p.schema.ComponentProperties 208 | if len(compProps) == 0 { 209 | fmt.Printf("The connector `%s` has no properties to configure!", name) 210 | return nil 211 | } 212 | 213 | updated := false 214 | if len(p.setProperties) > 0 { 215 | for k, v := range p.setProperties { 216 | propertyKey := springPropertiesKey(name, k) 217 | p.applicationProperties.Set(propertyKey, v) 218 | } 219 | updated = true 220 | } else { 221 | for k, cp := range compProps { 222 | label := funktion.HumanizeString(k) 223 | propertyKey := springPropertiesKey(name, k) 224 | 225 | value := p.applicationProperties.GetString(propertyKey, "") 226 | valueText := "" 227 | boolType := cp.Type == "boolean" 228 | if len(value) > 0 { 229 | if boolType { 230 | if value == "true" { 231 | valueText = "[Y/n]" 232 | } else { 233 | valueText = "[y/N]" 234 | } 235 | 236 | } else { 237 | valueText = "[" + value + "]" 238 | } 239 | } 240 | prompt := "?" 241 | if cp.Required { 242 | prompt = "*" 243 | } 244 | fmt.Printf("%s %s%s: ", prompt, label, valueText) 245 | 246 | var input string 247 | fmt.Scanln(&input) 248 | if len(input) > 0 { 249 | // convert boolean to true/false values 250 | if boolType { 251 | lower := strings.TrimSpace(strings.ToLower(input)) 252 | if lower[0] == 't' { 253 | input = "true" 254 | } else { 255 | input = "false" 256 | } 257 | } 258 | 259 | p.applicationProperties.Set(propertyKey, input) 260 | updated = true 261 | } 262 | } 263 | } 264 | 265 | if updated { 266 | var b bytes.Buffer 267 | w := bufio.NewWriter(&b) 268 | p.applicationProperties.Write(w, properties.UTF8) 269 | w.Flush() 270 | propText := b.String() 271 | cms := p.kubeclient.ConfigMaps(p.namespace) 272 | latestCon, err := cms.Get(name) 273 | if err != nil { 274 | return err 275 | } 276 | latestCon.Data[funktion.ApplicationPropertiesProperty] = propText 277 | _, err = cms.Update(latestCon) 278 | if err == nil { 279 | fmt.Printf("Connector %s updated\n", name) 280 | } 281 | return err 282 | } 283 | return nil 284 | } 285 | 286 | func springPropertiesKey(name string, k string) string { 287 | return "camel.component." + name + "." + funktion.ToSpringBootPropertyName(k) 288 | } 289 | -------------------------------------------------------------------------------- /cmd/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "github.com/ghodss/yaml" 22 | "github.com/spf13/cobra" 23 | 24 | "k8s.io/client-go/1.5/kubernetes" 25 | "k8s.io/client-go/1.5/pkg/api" 26 | "k8s.io/client-go/1.5/pkg/api/v1" 27 | "k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1" 28 | 29 | "github.com/funktionio/funktion/pkg/funktion" 30 | "github.com/funktionio/funktion/pkg/spec" 31 | ) 32 | 33 | type getCmd struct { 34 | kubeclient *kubernetes.Clientset 35 | cmd *cobra.Command 36 | kubeConfigPath string 37 | 38 | kind string 39 | namespace string 40 | name string 41 | 42 | deployments map[string]*v1beta1.Deployment 43 | services map[string]*v1.Service 44 | } 45 | 46 | func init() { 47 | RootCmd.AddCommand(newGetCmd()) 48 | } 49 | 50 | func newGetCmd() *cobra.Command { 51 | p := &getCmd{} 52 | cmd := &cobra.Command{ 53 | Use: "get KIND [NAME] [flags]", 54 | Short: "gets a list of the resources", 55 | Long: `This command will list all of the resources of a given kind`, 56 | Run: func(cmd *cobra.Command, args []string) { 57 | p.cmd = cmd 58 | if len(args) == 0 { 59 | handleError(fmt.Errorf("No resource kind argument supplied! Possible values ['connector', 'flow', 'function', 'runtime']")) 60 | return 61 | } 62 | p.kind = args[0] 63 | if len(args) > 1 { 64 | p.name = args[1] 65 | } 66 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 67 | if err != nil { 68 | handleError(err) 69 | return 70 | } 71 | handleError(p.run()) 72 | }, 73 | } 74 | f := cmd.Flags() 75 | //f.StringVarP(&p.format, "output", "o", "", "The format of the output") 76 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 77 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 78 | return cmd 79 | } 80 | 81 | func (p *getCmd) run() error { 82 | kind, listOpts, err := listOptsForKind(p.kind) 83 | if err != nil { 84 | return err 85 | } 86 | kubeclient := p.kubeclient 87 | cms := kubeclient.ConfigMaps(p.namespace) 88 | resources, err := cms.List(*listOpts) 89 | if err != nil { 90 | return err 91 | } 92 | p.deployments = map[string]*v1beta1.Deployment{} 93 | p.services = map[string]*v1.Service{} 94 | ds, err := kubeclient.Deployments(p.namespace).List(api.ListOptions{}) 95 | if err != nil { 96 | return err 97 | } 98 | p.deployments = map[string]*v1beta1.Deployment{} 99 | for _, item := range ds.Items { 100 | // TODO lets assume the name of the Deployment is the name of the Flow 101 | // but we may want to use a label instead to link them? 102 | name := item.Name 103 | copy := v1beta1.Deployment{} 104 | copy = item 105 | p.deployments[name] = © 106 | } 107 | if kind == functionKind { 108 | ss, err := kubeclient.Services(p.namespace).List(api.ListOptions{}) 109 | if err != nil { 110 | return err 111 | } 112 | for _, item := range ss.Items { 113 | // TODO lets assume the name of the Service is the name of the Function 114 | // but we may want to use a label instead to link them? 115 | name := item.Name 116 | copy := v1.Service{} 117 | copy = item 118 | p.services[name] = © 119 | } 120 | } 121 | name := p.name 122 | if len(name) == 0 { 123 | p.printHeader(kind) 124 | for _, resource := range resources.Items { 125 | p.printResource(&resource, kind) 126 | } 127 | 128 | } else { 129 | found := false 130 | for _, resource := range resources.Items { 131 | if resource.Name == name { 132 | p.printHeader(kind) 133 | p.printResource(&resource, kind) 134 | found = true 135 | break 136 | } 137 | } 138 | if !found { 139 | return fmt.Errorf("%s \"%s\" not found", kind, name) 140 | } 141 | } 142 | return nil 143 | } 144 | 145 | func (p *getCmd) printHeader(kind string) { 146 | switch kind { 147 | case flowKind: 148 | printFlowRow("NAME", "PODS", "STEPS") 149 | case functionKind: 150 | printFunctionRow("NAME", "PODS", "URL") 151 | default: 152 | printRuntimeRow("NAME", "VERSION") 153 | } 154 | } 155 | 156 | func (p *getCmd) printResource(cm *v1.ConfigMap, kind string) { 157 | switch kind { 158 | case functionKind: 159 | printFunctionRow(cm.Name, p.podText(cm), p.functionURLText(cm)) 160 | case flowKind: 161 | printFlowRow(cm.Name, p.podText(cm), p.flowStepsText(cm)) 162 | default: 163 | printRuntimeRow(cm.Name, p.runtimeVersion(cm)) 164 | } 165 | } 166 | 167 | func printFunctionRow(name string, pod string, flow string) { 168 | fmt.Printf("%-32s %-9s %s\n", name, pod, flow) 169 | } 170 | 171 | func printFlowRow(name string, pod string, flow string) { 172 | fmt.Printf("%-32s %-9s %s\n", name, pod, flow) 173 | } 174 | 175 | func printRuntimeRow(name string, version string) { 176 | fmt.Printf("%-32s %s\n", name, version) 177 | } 178 | 179 | func (p *getCmd) podText(cm *v1.ConfigMap) string { 180 | name := cm.Name 181 | deployment := p.deployments[name] 182 | if deployment == nil { 183 | return "" 184 | } 185 | var status = deployment.Status 186 | return fmt.Sprintf("%d/%d", status.AvailableReplicas, status.Replicas) 187 | } 188 | 189 | func (p *getCmd) functionURLText(cm *v1.ConfigMap) string { 190 | name := cm.Name 191 | service := p.services[name] 192 | if service == nil || service.Annotations == nil { 193 | return "" 194 | } 195 | return service.Annotations[exposeURLAnnotation] 196 | } 197 | 198 | func (p *getCmd) runtimeVersion(cm *v1.ConfigMap) string { 199 | labels := cm.Labels 200 | if labels != nil { 201 | return labels[funktion.VersionLabel] 202 | } 203 | return "" 204 | } 205 | 206 | func (p *getCmd) flowStepsText(cm *v1.ConfigMap) string { 207 | yamlText := cm.Data[funktion.FunktionYmlProperty] 208 | if len(yamlText) == 0 { 209 | return fmt.Sprintf("No `%s` property specified", funktion.FunktionYmlProperty) 210 | } 211 | fc := spec.FunkionConfig{} 212 | err := yaml.Unmarshal([]byte(yamlText), &fc) 213 | if err != nil { 214 | return fmt.Sprintf("Failed to parse `%s` YAML: %v", funktion.FunktionYmlProperty, err) 215 | } 216 | if len(fc.Flows) == 0 { 217 | return "No funktion flows" 218 | } 219 | rule := fc.Flows[0] 220 | return stepsText(rule.Steps) 221 | } 222 | 223 | func stepsText(steps []spec.FunktionStep) string { 224 | actionMessage := "No steps!" 225 | if len(steps) > 0 { 226 | var buffer bytes.Buffer 227 | for i, step := range steps { 228 | if i > 0 { 229 | buffer.WriteString(" => ") 230 | } 231 | kind := step.Kind 232 | text := kind 233 | switch kind { 234 | case spec.EndpointKind: 235 | text = fmt.Sprintf("%s", step.URI) 236 | case spec.FunctionKind: 237 | text = fmt.Sprintf("function %s", step.Name) 238 | } 239 | buffer.WriteString(text) 240 | } 241 | actionMessage = buffer.String() 242 | } 243 | return actionMessage 244 | } 245 | -------------------------------------------------------------------------------- /cmd/install.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "encoding/xml" 19 | "fmt" 20 | "io/ioutil" 21 | "net/http" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/funktionio/funktion/pkg/funktion" 28 | "github.com/funktionio/funktion/pkg/k8sutil" 29 | 30 | "github.com/ghodss/yaml" 31 | "github.com/spf13/cobra" 32 | 33 | "k8s.io/client-go/1.5/dynamic" 34 | "k8s.io/client-go/1.5/kubernetes" 35 | "k8s.io/client-go/1.5/pkg/api/v1" 36 | "k8s.io/client-go/1.5/pkg/runtime" 37 | ) 38 | 39 | const ( 40 | connectorMetadataUrl = "io/fabric8/funktion/funktion-connectors/maven-metadata.xml" 41 | connectorPackageUrlPrefix = "io/fabric8/funktion/funktion-connectors/%[1]s/funktion-connectors-%[1]s-" 42 | runtimePackageUrlPrefix = "io/fabric8/funktion/funktion-runtimes/%[1]s/funktion-runtimes-%[1]s-" 43 | 44 | operatorMetadataUrl = "io/fabric8/funktion/apps/funktion-operator/maven-metadata.xml" 45 | operatorPackageUrlPrefix = "io/fabric8/funktion/apps/funktion-operator/%[1]s/funktion-operator-%[1]s-" 46 | 47 | platformMetadataUrl = "io/fabric8/funktion/packages/funktion-platform/maven-metadata.xml" 48 | platformPackageUrlPrefix = "io/fabric8/funktion/packages/funktion-platform/%[1]s/funktion-platform-%[1]s-" 49 | ) 50 | 51 | type installConnectorCmd struct { 52 | kubeclient *kubernetes.Clientset 53 | cmd *cobra.Command 54 | kubeConfigPath string 55 | 56 | namespace string 57 | names []string 58 | version string 59 | mavenRepo string 60 | replace bool 61 | list bool 62 | all bool 63 | } 64 | 65 | type installRuntimeCmd struct { 66 | kubeclient *kubernetes.Clientset 67 | cmd *cobra.Command 68 | kubeConfigPath string 69 | 70 | namespace string 71 | names []string 72 | version string 73 | mavenRepo string 74 | replace bool 75 | list bool 76 | all bool 77 | } 78 | 79 | type installPackageCmd struct { 80 | kubeclient *kubernetes.Clientset 81 | cmd *cobra.Command 82 | kubeConfigPath string 83 | dynamicClient *dynamic.Client 84 | 85 | namespace string 86 | version string 87 | mavenRepo string 88 | replace bool 89 | 90 | packageMetadataUrl string 91 | packageUrlPrefix string 92 | } 93 | 94 | func init() { 95 | RootCmd.AddCommand(newInstallCmd()) 96 | } 97 | 98 | func newInstallCmd() *cobra.Command { 99 | cmd := &cobra.Command{ 100 | Use: "install [kind]", 101 | Short: "installs the standard Connectors into the current namespace", 102 | Long: `This command will install the default Connectors into the current namespace`, 103 | } 104 | 105 | cmd.AddCommand(newInstallConnectorCmd()) 106 | cmd.AddCommand(newInstallRuntimeCmd()) 107 | cmd.AddCommand(newInstallOperatorCmd()) 108 | cmd.AddCommand(newInstallPlatformCmd()) 109 | return cmd 110 | } 111 | 112 | func newInstallConnectorCmd() *cobra.Command { 113 | p := &installConnectorCmd{} 114 | cmd := &cobra.Command{ 115 | Use: "connector [NAMES] [flags]", 116 | Short: "installs Connectors into the current namespace", 117 | Long: `This command will install the standard Connectors into the current namespace`, 118 | Run: func(cmd *cobra.Command, args []string) { 119 | p.cmd = cmd 120 | p.names = args 121 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 122 | if err != nil { 123 | handleError(err) 124 | return 125 | } 126 | handleError(p.run()) 127 | }, 128 | } 129 | f := cmd.Flags() 130 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 131 | f.StringVarP(&p.mavenRepo, "maven-repo", "m", "https://repo1.maven.org/maven2/", "the maven repository used to download the Connector releases") 132 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 133 | f.StringVarP(&p.version, "version", "v", "latest", "the version of the connectors to install") 134 | f.BoolVar(&p.replace, "replace", false, "if enabled we will replace exising Connectors with installed version") 135 | f.BoolVarP(&p.list, "list", "l", false, "list all the available Connectors but don't install them") 136 | f.BoolVarP(&p.all, "all", "a", false, "Install all the connectors") 137 | return cmd 138 | } 139 | 140 | func newInstallRuntimeCmd() *cobra.Command { 141 | p := &installRuntimeCmd{} 142 | cmd := &cobra.Command{ 143 | Use: "runtime [NAMES] [flags]", 144 | Short: "installs the function Runtimes into the current namespace", 145 | Long: `This command will install the standard Runtimes into the current namespace`, 146 | Run: func(cmd *cobra.Command, args []string) { 147 | p.cmd = cmd 148 | p.names = args 149 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 150 | if err != nil { 151 | handleError(err) 152 | return 153 | } 154 | handleError(p.run()) 155 | }, 156 | } 157 | f := cmd.Flags() 158 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 159 | f.StringVarP(&p.mavenRepo, "maven-repo", "m", "https://repo1.maven.org/maven2/", "the maven repository used to download the Connector releases") 160 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 161 | f.StringVarP(&p.version, "version", "v", "latest", "the version of the connectors to install") 162 | f.BoolVar(&p.replace, "replace", false, "if enabled we will replace exising Connectors with installed version") 163 | /* 164 | f.BoolVarP(&p.list, "list", "l", false, "list all the available Runtimes but don't install them") 165 | f.BoolVarP(&p.all, "all", "a", false, "Install all the runtimes") 166 | */ 167 | return cmd 168 | } 169 | 170 | func newInstallOperatorCmd() *cobra.Command { 171 | p := &installPackageCmd{ 172 | packageMetadataUrl: operatorMetadataUrl, 173 | packageUrlPrefix: operatorPackageUrlPrefix, 174 | } 175 | cmd := &cobra.Command{ 176 | Use: "operator [NAMES] [flags]", 177 | Short: "installs the Funktion Operator into the current namespace (when using fabric8)", 178 | Long: `This command will install the Funktion Operator into the current namespace 179 | 180 | NOTE this command assumes you are already using the fabric8 developer platform - otherwise you should to install the 'platform' package`, 181 | Run: func(cmd *cobra.Command, args []string) { 182 | p.cmd = cmd 183 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 184 | if err != nil { 185 | handleError(err) 186 | return 187 | } 188 | handleError(p.run()) 189 | }, 190 | } 191 | p.configureFlags(cmd) 192 | return cmd 193 | } 194 | 195 | func newInstallPlatformCmd() *cobra.Command { 196 | p := &installPackageCmd{ 197 | packageMetadataUrl: platformMetadataUrl, 198 | packageUrlPrefix: platformPackageUrlPrefix, 199 | } 200 | cmd := &cobra.Command{ 201 | Use: "platform [NAMES] [flags]", 202 | Short: "installs the Funktion Platform into the current namespace (when not using fabric8)", 203 | Long: `This command will install the Funktion Platform into the current namespace 204 | 205 | NOTE that if you are not already running the fabric8 developer platform - otherwise you only need to install the 'operator' package`, 206 | Run: func(cmd *cobra.Command, args []string) { 207 | p.cmd = cmd 208 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 209 | if err != nil { 210 | handleError(err) 211 | return 212 | } 213 | handleError(p.run()) 214 | }, 215 | } 216 | p.configureFlags(cmd) 217 | return cmd 218 | } 219 | 220 | func (p *installPackageCmd) configureFlags(cmd *cobra.Command) { 221 | f := cmd.Flags() 222 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 223 | f.StringVarP(&p.mavenRepo, "maven-repo", "m", "https://repo1.maven.org/maven2/", "the maven repository used to download the Connector releases") 224 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to use otherwise the current namespace will be used") 225 | f.StringVarP(&p.version, "version", "v", "latest", "the version of the connectors to install") 226 | f.BoolVar(&p.replace, "replace", false, "if enabled we will replace exising Connectors with installed version") 227 | 228 | } 229 | func (p *installConnectorCmd) run() error { 230 | mavenRepo := p.mavenRepo 231 | version, err := versionForUrl(p.version, urlJoin(mavenRepo, connectorMetadataUrl)) 232 | if err != nil { 233 | return err 234 | } 235 | uri := fmt.Sprintf(urlJoin(mavenRepo, connectorPackageUrlPrefix), version) + "kubernetes.yml" 236 | return p.installConnectors(uri, version) 237 | } 238 | 239 | func (p *installConnectorCmd) installConnectors(uri string, version string) error { 240 | list, err := loadList(uri) 241 | if err != nil { 242 | return err 243 | } 244 | listOpts, err := funktion.CreateConnectorListOptions() 245 | if err != nil { 246 | return err 247 | } 248 | cms := p.kubeclient.ConfigMaps(p.namespace) 249 | resources, err := cms.List(*listOpts) 250 | if err != nil { 251 | return err 252 | } 253 | existingNames := map[string]bool{} 254 | for _, resource := range resources.Items { 255 | existingNames[resource.Name] = true 256 | } 257 | onlyNames := map[string]bool{} 258 | for _, onlyName := range p.names { 259 | onlyNames[onlyName] = true 260 | } 261 | 262 | if p.list { 263 | fmt.Printf("Version %s has Connectors:\n", version) 264 | } else if !p.all && len(onlyNames) == 0 { 265 | fmt.Printf("No connector names listed so none have been installed.\nPlease specify the connector names to install, use `--all-connectors` to install then all or use `--list-connectors` to list them\n") 266 | return nil 267 | } 268 | 269 | count := 0 270 | ignored := 0 271 | for _, item := range list.Items { 272 | cm, err := toConfigMap(&item) 273 | if err != nil { 274 | return err 275 | } 276 | name := cm.Name 277 | if p.list { 278 | fmt.Println(name) 279 | } else { 280 | if len(onlyNames) > 0 { 281 | if !onlyNames[name] { 282 | continue 283 | } 284 | } 285 | update := false 286 | operation := "create" 287 | if existingNames[name] { 288 | if p.replace { 289 | update = true 290 | } else { 291 | ignored++ 292 | continue 293 | } 294 | } 295 | 296 | if update { 297 | operation = "update" 298 | _, err = cms.Update(cm) 299 | } else { 300 | _, err = cms.Create(cm) 301 | } 302 | if err != nil { 303 | return fmt.Errorf("Failed to %s Connector %s due to %v", operation, name, err) 304 | } 305 | } 306 | count++ 307 | } 308 | 309 | if p.list { 310 | return nil 311 | } 312 | 313 | ignoreMessage := "" 314 | if !p.replace && ignored > 0 { 315 | ignoreMessage = fmt.Sprintf(". Ignored %d Connectors as they are already installed. (Please use `--replace` to force replacing them)", ignored) 316 | } 317 | 318 | fmt.Printf("Installed %d Connectors from version: %s%s\n", count, version, ignoreMessage) 319 | return nil 320 | } 321 | 322 | func (p *installRuntimeCmd) run() error { 323 | mavenRepo := p.mavenRepo 324 | version, err := versionForUrl(p.version, urlJoin(mavenRepo, connectorMetadataUrl)) 325 | if err != nil { 326 | return err 327 | } 328 | uri := fmt.Sprintf(urlJoin(mavenRepo, runtimePackageUrlPrefix), version) + "kubernetes.yml" 329 | err = p.installRuntimes(uri, version) 330 | if err != nil { 331 | return err 332 | } 333 | return nil 334 | } 335 | 336 | func (p *installRuntimeCmd) installRuntimes(uri string, version string) error { 337 | list, err := loadList(uri) 338 | if err != nil { 339 | return err 340 | } 341 | listOpts, err := funktion.CreateRuntimeListOptions() 342 | if err != nil { 343 | return err 344 | } 345 | cms := p.kubeclient.ConfigMaps(p.namespace) 346 | resources, err := cms.List(*listOpts) 347 | if err != nil { 348 | return err 349 | } 350 | existingNames := map[string]bool{} 351 | for _, resource := range resources.Items { 352 | existingNames[resource.Name] = true 353 | } 354 | count := 0 355 | ignored := 0 356 | for _, item := range list.Items { 357 | cm, err := toConfigMap(&item) 358 | if err != nil { 359 | return err 360 | } 361 | name := cm.Name 362 | update := false 363 | operation := "create" 364 | if existingNames[name] { 365 | if p.replace { 366 | update = true 367 | } else { 368 | ignored++ 369 | continue 370 | } 371 | } 372 | 373 | if update { 374 | operation = "update" 375 | _, err = cms.Update(cm) 376 | } else { 377 | _, err = cms.Create(cm) 378 | } 379 | if err != nil { 380 | return fmt.Errorf("Failed to %s Runtime %s due to %v", operation, name, err) 381 | } 382 | count++ 383 | } 384 | 385 | ignoreMessage := "" 386 | if !p.replace && ignored > 0 { 387 | ignoreMessage = fmt.Sprintf(". Ignored %d Runtimes as they are already installed. (Please use `--replace` to force replacing them)", ignored) 388 | } 389 | 390 | fmt.Printf("Installed %d Runtimes from version: %s%s\n", count, version, ignoreMessage) 391 | return nil 392 | } 393 | 394 | func (p *installPackageCmd) run() error { 395 | mavenRepo := p.mavenRepo 396 | version, err := versionForUrl(p.version, urlJoin(mavenRepo, p.packageMetadataUrl)) 397 | if err != nil { 398 | return err 399 | } 400 | extension := "kubernetes.yml" 401 | openshift, err := p.isOpenShift() 402 | if err != nil { 403 | fmt.Printf("WARNING %v\n", err) 404 | } 405 | if openshift { 406 | extension = "openshift.yml" 407 | } 408 | uri := fmt.Sprintf(urlJoin(mavenRepo, p.packageUrlPrefix), version) + extension 409 | err = p.checkNamespaceExists() 410 | if err != nil { 411 | return err 412 | } 413 | err = p.installPackage(uri, version) 414 | if err != nil { 415 | return err 416 | } 417 | return nil 418 | } 419 | 420 | func (p *installPackageCmd) isOpenShift() (bool, error) { 421 | c := p.kubeclient 422 | _, err := c.Core().GetRESTClient().Get().AbsPath("/oapi").Do().Raw() 423 | if err != nil { 424 | // ignore errors for now 425 | return false, nil 426 | } 427 | return true, nil 428 | } 429 | 430 | 431 | func (p *installPackageCmd) checkNamespaceExists() error { 432 | name := p.namespace 433 | namespaces := p.kubeclient.Namespaces() 434 | ns, err := namespaces.Get(name) 435 | if err != nil { 436 | ns = &v1.Namespace{ 437 | ObjectMeta: v1.ObjectMeta{ 438 | Name: name, 439 | Labels: map[string]string{ 440 | "system": "funktion", 441 | }, 442 | }, 443 | } 444 | _, err = namespaces.Create(ns) 445 | if err == nil { 446 | fmt.Printf("created Namespace %s\n", name) 447 | } 448 | } 449 | return err 450 | } 451 | 452 | func (p *installPackageCmd) installPackage(uri string, version string) error { 453 | binaryFile, err := k8sutil.ResolveKubectlBinary(p.kubeclient) 454 | if err != nil { 455 | return err 456 | } 457 | args := []string{"apply", "--namespace", p.namespace, "-f", uri} 458 | namespace := p.namespace 459 | if len(namespace) > 0 { 460 | args = []string{"apply", "--namespace", p.namespace, "-f", uri} 461 | } 462 | fmt.Printf("%s %s\n\n", filepath.Base(binaryFile), strings.Join(args, " ")) 463 | cmd := exec.Command(binaryFile, args...) 464 | cmd.Stdout = os.Stdout 465 | cmd.Stderr = os.Stderr 466 | return cmd.Run() 467 | 468 | /* 469 | TODO try use the dynamic client 470 | 471 | list, err := loadList(uri) 472 | if err != nil { 473 | return err 474 | } 475 | resources, err := p.kubeclient.ServerResources() 476 | if err != nil { 477 | return err 478 | } 479 | resourceMap := map[string]*unversioned.APIResource{} 480 | for _, ra := range resources { 481 | for _, r := range ra.APIResources { 482 | resourceMap[r.Kind] = &r 483 | } 484 | } 485 | client := p.dynamicClient 486 | ns := p.namespace 487 | count := 0 488 | m := meta.NewAccessor() 489 | for _, item := range list.Items { 490 | u := runtime.Unknown{Raw: item.Raw} 491 | kind := u.Kind 492 | resource := resourceMap[kind] 493 | if resource != nil { 494 | _, err := client.Resource(resource, ns).Create() 495 | if err != nil { 496 | return err 497 | } 498 | count++ 499 | } else { 500 | fmt.Printf("Could not find resource for kind %s\n", kind) 501 | } 502 | } 503 | fmt.Printf("Installed %d resources from version: %s\n", count, version) 504 | */ 505 | } 506 | 507 | func loadList(uri string) (*v1.List, error) { 508 | resp, err := http.Get(uri) 509 | if err != nil { 510 | return nil, fmt.Errorf("Cannot load YAML package at %s got: %v", uri, err) 511 | } 512 | defer resp.Body.Close() 513 | data, err := ioutil.ReadAll(resp.Body) 514 | if err != nil { 515 | return nil, fmt.Errorf("Cannot load YAML from %s got: %v", uri, err) 516 | } 517 | list := v1.List{} 518 | err = yaml.Unmarshal(data, &list) 519 | if err != nil { 520 | return nil, fmt.Errorf("Cannot parse YAML from %s got: %v", uri, err) 521 | } 522 | return &list, nil 523 | } 524 | 525 | func toConfigMap(item *runtime.RawExtension) (*v1.ConfigMap, error) { 526 | obj := item.Object 527 | switch c := obj.(type) { 528 | case *v1.ConfigMap: 529 | return c, nil 530 | default: 531 | raw := item.Raw 532 | cm := v1.ConfigMap{} 533 | err := yaml.Unmarshal(raw, &cm) 534 | return &cm, err 535 | } 536 | } 537 | 538 | func versionForUrl(v string, metadataUrl string) (string, error) { 539 | resp, err := http.Get(metadataUrl) 540 | if err != nil { 541 | return "", fmt.Errorf("Cannot get version to deploy from url %s due to: %v", metadataUrl, err) 542 | } 543 | defer resp.Body.Close() 544 | // read xml http response 545 | xmlData, err := ioutil.ReadAll(resp.Body) 546 | if err != nil { 547 | return "", fmt.Errorf("Cannot read version metadata from url %s due to: %v", metadataUrl, err) 548 | } 549 | 550 | type Metadata struct { 551 | Release string `xml:"versioning>release"` 552 | Versions []string `xml:"versioning>versions>version"` 553 | } 554 | 555 | var m Metadata 556 | err = xml.Unmarshal(xmlData, &m) 557 | if err != nil { 558 | return "", fmt.Errorf("Cannot parse version XML from url %s due to: %v", metadataUrl, err) 559 | } 560 | 561 | if v == "latest" { 562 | return m.Release, nil 563 | } 564 | 565 | for _, version := range m.Versions { 566 | if v == version { 567 | return version, nil 568 | } 569 | } 570 | return "", fmt.Errorf("Unknown version %s from URL %s when had valid version %v", v, metadataUrl, append(m.Versions, "latest")) 571 | } 572 | 573 | // urlJoin joins the given URL paths so that there is a / separating them but not a double // 574 | func urlJoin(repo string, path string) string { 575 | return strings.TrimSuffix(repo, "/") + "/" + strings.TrimPrefix(path, "/") 576 | } 577 | -------------------------------------------------------------------------------- /cmd/logs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/exec" 21 | "path/filepath" 22 | "strings" 23 | 24 | "github.com/funktionio/funktion/pkg/k8sutil" 25 | "github.com/spf13/cobra" 26 | 27 | "k8s.io/client-go/1.5/kubernetes" 28 | "k8s.io/client-go/1.5/pkg/api" 29 | "k8s.io/client-go/1.5/pkg/api/v1" 30 | "k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1" 31 | ) 32 | 33 | type logCmd struct { 34 | kubeclient *kubernetes.Clientset 35 | cmd *cobra.Command 36 | kubeConfigPath string 37 | 38 | namespace string 39 | kind string 40 | name string 41 | follow bool 42 | 43 | podAction k8sutil.PodAction 44 | logCmd *exec.Cmd 45 | } 46 | 47 | func init() { 48 | RootCmd.AddCommand(newLogCmd()) 49 | } 50 | 51 | func newLogCmd() *cobra.Command { 52 | p := &logCmd{} 53 | p.podAction = k8sutil.PodAction{ 54 | OnPodChange: p.viewLog, 55 | } 56 | cmd := &cobra.Command{ 57 | Use: "logs KIND NAME [flags]", 58 | Short: "tails the log of the given function or flow", 59 | Long: `This command will tail the log of the latest container implementing the function or flow`, 60 | Run: func(cmd *cobra.Command, args []string) { 61 | p.cmd = cmd 62 | if len(args) < 1 { 63 | handleError(fmt.Errorf("No resource kind argument supplied! Possible values ['fn', 'flow']")) 64 | return 65 | } 66 | p.kind = args[0] 67 | kind, _, err := listOptsForKind(p.kind) 68 | if err != nil { 69 | handleError(err) 70 | return 71 | } 72 | if len(args) < 2 { 73 | handleError(fmt.Errorf("No %s name specified!", kind)) 74 | return 75 | } 76 | p.name = args[1] 77 | err = createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 78 | if err != nil { 79 | handleError(err) 80 | return 81 | } 82 | handleError(p.run()) 83 | }, 84 | } 85 | f := cmd.Flags() 86 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 87 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 88 | f.StringVarP(&p.name, "name", "v", "latest", "the version of the connectors to install") 89 | f.BoolVarP(&p.follow, "follow", "f", true, "Whether or not to follow the log") 90 | return cmd 91 | } 92 | 93 | func (p *logCmd) run() error { 94 | kubeclient := p.kubeclient 95 | name, err := nameForDeployment(p.kubeclient, p.namespace, p.kind, p.name) 96 | if err != nil { 97 | return err 98 | } 99 | ds, err := kubeclient.Deployments(p.namespace).List(api.ListOptions{}) 100 | if err != nil { 101 | return err 102 | } 103 | var deployment *v1beta1.Deployment 104 | for _, item := range ds.Items { 105 | if item.Name == name { 106 | deployment = &item 107 | break 108 | } 109 | } 110 | if deployment == nil { 111 | return fmt.Errorf("No Deployment found called `%s`", name) 112 | } 113 | selector := deployment.Spec.Selector 114 | if selector == nil { 115 | return fmt.Errorf("Deployment `%s` does not have a selector!", name) 116 | } 117 | if selector.MatchLabels == nil { 118 | return fmt.Errorf("Deployment `%s` selector does not have a matchLabels!", name) 119 | } 120 | listOpts, err := k8sutil.V1BetaSelectorToListOptions(selector) 121 | if err != nil { 122 | return err 123 | } 124 | p.podAction.WatchPods(p.kubeclient, p.namespace, listOpts) 125 | return p.podAction.WatchLoop() 126 | } 127 | 128 | func (p *logCmd) viewLog(pod *v1.Pod) error { 129 | if pod != nil { 130 | binaryFile, err := k8sutil.ResolveKubectlBinary(p.kubeclient) 131 | if err != nil { 132 | return err 133 | } 134 | name := pod.Name 135 | if p.logCmd != nil { 136 | process := p.logCmd.Process 137 | if process != nil { 138 | process.Kill() 139 | } 140 | } 141 | args := []string{"logs"} 142 | if p.follow { 143 | args = append(args, "-f") 144 | } 145 | args = append(args, name) 146 | 147 | fmt.Printf("\n%s %s\n\n", filepath.Base(binaryFile), strings.Join(args, " ")) 148 | cmd := exec.Command(binaryFile, args...) 149 | p.logCmd = cmd 150 | cmd.Stdout = os.Stdout 151 | cmd.Stderr = os.Stderr 152 | err = cmd.Start() 153 | if err != nil { 154 | return err 155 | } 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /cmd/operate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/signal" 21 | "sync" 22 | "syscall" 23 | 24 | "github.com/funktionio/funktion/pkg/funktion" 25 | "github.com/go-kit/kit/log" 26 | 27 | "github.com/spf13/cobra" 28 | flag "github.com/spf13/pflag" 29 | "k8s.io/client-go/1.5/pkg/api" 30 | "k8s.io/client-go/1.5/tools/clientcmd" 31 | ) 32 | 33 | func init() { 34 | RootCmd.AddCommand(newOperateCmd()) 35 | } 36 | 37 | type operateCmd struct { 38 | namespace string 39 | allNamespaces bool 40 | } 41 | 42 | func newOperateCmd() *cobra.Command { 43 | p := &operateCmd{} 44 | cmd := &cobra.Command{ 45 | Use: "operate", 46 | Short: "Runs the funktion operator", 47 | Long: `This command will startup the operator for funktion`, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | handleError(p.operate(cmd, args)) 50 | }, 51 | } 52 | 53 | f := cmd.Flags() 54 | f.StringVarP(&p.namespace, "namespace", "n", "", "the name of the namespace to watch for resources") 55 | f.BoolVarP(&p.allNamespaces, "all", "a", false, "if enabled all namespaces will be watched. This option typically requires a cluster administrator role") 56 | return cmd 57 | } 58 | 59 | func (p *operateCmd) operate(cmd *cobra.Command, args []string) error { 60 | logger := log.NewContext(log.NewLogfmtLogger(os.Stdout)). 61 | With("ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller). 62 | With("operator", "funktion") 63 | 64 | flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 65 | 66 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 67 | flagset.StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to the config file to use for CLI requests.") 68 | 69 | overrides := &clientcmd.ConfigOverrides{} 70 | overrideFlags := clientcmd.RecommendedConfigOverrideFlags("") 71 | clientcmd.BindOverrideFlags(overrides, flagset, overrideFlags) 72 | 73 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) 74 | 75 | flagset.Parse(os.Args[1:]) 76 | 77 | cfg, err := kubeConfig.ClientConfig() 78 | if err != nil { 79 | logger.Log("msg", "failed to create Kubernetes client config", "error", err) 80 | return err 81 | } 82 | 83 | namespace := p.namespace 84 | if p.allNamespaces { 85 | namespace = api.NamespaceAll 86 | fmt.Printf("Funktion operator is starting watching namespace: %s\n", namespace) 87 | } else { 88 | if len(namespace) == 0 { 89 | namespace = os.Getenv("KUBERNETES_NAMESPACE") 90 | if len(namespace) <= 0 { 91 | namespace, _, err := kubeConfig.Namespace() 92 | if err != nil { 93 | return fmt.Errorf("Could not detect namespace %v", err) 94 | } 95 | if len(namespace) <= 0 { 96 | return fmt.Errorf("No namespace argument or $KUBERNETES_NAMESPACE environment variable specified and we could not detect the current namespace!") 97 | } 98 | } 99 | } 100 | fmt.Printf("Funktion operator is starting watching namespace: '%s'\n", namespace) 101 | } 102 | ko, err := funktion.New(cfg, logger, namespace) 103 | if err != nil { 104 | logger.Log("error", err) 105 | return err 106 | } 107 | 108 | stopc := make(chan struct{}) 109 | errc := make(chan error) 110 | var wg sync.WaitGroup 111 | 112 | wg.Add(1) 113 | go func() { 114 | if err := ko.Run(stopc); err != nil { 115 | errc <- err 116 | } 117 | wg.Done() 118 | }() 119 | 120 | term := make(chan os.Signal) 121 | signal.Notify(term, os.Interrupt, syscall.SIGTERM) 122 | select { 123 | case <-term: 124 | fmt.Fprintln(os.Stderr) 125 | logger.Log("msg", "Received SIGTERM, exiting gracefully...") 126 | close(stopc) 127 | wg.Wait() 128 | case err := <-errc: 129 | logger.Log("msg", "Unexpected error received", "error", err) 130 | close(stopc) 131 | wg.Wait() 132 | return err 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /cmd/operator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package main 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/funktionio/funktion/cmd" 22 | ) 23 | 24 | func Main() int { 25 | if err := cmd.RootCmd.Execute(); err != nil { 26 | fmt.Println(err) 27 | os.Exit(-1) 28 | } 29 | return 0 30 | } 31 | 32 | func main() { 33 | os.Exit(Main()) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "k8s.io/client-go/1.5/dynamic" 22 | "k8s.io/client-go/1.5/kubernetes" 23 | "k8s.io/client-go/1.5/pkg/api" 24 | "k8s.io/client-go/1.5/tools/clientcmd" 25 | 26 | "github.com/funktionio/funktion/pkg/config" 27 | "github.com/funktionio/funktion/pkg/constants" 28 | "github.com/funktionio/funktion/pkg/funktion" 29 | 30 | "github.com/golang/glog" 31 | "github.com/spf13/cobra" 32 | "github.com/spf13/pflag" 33 | "github.com/spf13/viper" 34 | ) 35 | 36 | const ( 37 | flowKind = "flow" 38 | connectorKind = "connector" 39 | runtimeKind = "runtime" 40 | functionKind = "function" 41 | ) 42 | 43 | var RootCmd = &cobra.Command{ 44 | Use: "funktion", 45 | Short: "funktion is a Function as a Service (Lambda) style programming model for Kubernetes", 46 | Long: `Funktion lets you develop complex applications using Functions and then use Flows to bind those functions to any event source (over 200 event sources and connectors supported) and run and scale your functions on top of kubernetes. 47 | 48 | For more documentation please see: https://funktion.fabric8.io/`, 49 | Run: func(cmd *cobra.Command, args []string) { 50 | if len(args) == 0 { 51 | cmd.Help() 52 | } 53 | }, 54 | } 55 | 56 | func init() { 57 | viper.BindPFlags(RootCmd.PersistentFlags()) 58 | cobra.OnInitialize(initConfig) 59 | } 60 | 61 | func createKubernetesClient(cmd *cobra.Command, kubeConfigPath string, kubeclientHolder **kubernetes.Clientset, namespace *string) error { 62 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 63 | if len(kubeConfigPath) > 0 { 64 | loadingRules.ExplicitPath = kubeConfigPath 65 | } 66 | 67 | overrides := &clientcmd.ConfigOverrides{} 68 | //overrideFlags := clientcmd.RecommendedConfigOverrideFlags("") 69 | //clientcmd.BindOverrideFlags(overrides, cmd.Flags(), overrideFlags) 70 | 71 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) 72 | cfg, err := kubeConfig.ClientConfig() 73 | if err != nil { 74 | fmt.Printf("failed to create Kubernetes client config due to %v\n", err) 75 | return err 76 | } 77 | kubeclient, err := kubernetes.NewForConfig(cfg) 78 | if err != nil { 79 | return err 80 | } 81 | *kubeclientHolder = kubeclient 82 | if len(*namespace) == 0 { 83 | ns, _, err := kubeConfig.Namespace() 84 | if err != nil { 85 | return fmt.Errorf("Could not deduce default namespace due to: %v", err) 86 | } 87 | *namespace = ns 88 | } 89 | return nil 90 | } 91 | 92 | func createKubernetesDynamicClient(kubeConfigPath string) (*dynamic.Client, error) { 93 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 94 | if len(kubeConfigPath) > 0 { 95 | loadingRules.ExplicitPath = kubeConfigPath 96 | } 97 | 98 | overrides := &clientcmd.ConfigOverrides{} 99 | //overrideFlags := clientcmd.RecommendedConfigOverrideFlags("") 100 | //clientcmd.BindOverrideFlags(overrides, cmd.Flags(), overrideFlags) 101 | 102 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) 103 | cfg, err := kubeConfig.ClientConfig() 104 | if err != nil { 105 | fmt.Printf("failed to create Kubernetes client config due to %v\n", err) 106 | return nil, err 107 | } 108 | return dynamic.NewClient(cfg) 109 | } 110 | 111 | func handleError(err error) { 112 | if err != nil { 113 | fmt.Printf("Failed: %v\n", err) 114 | } 115 | } 116 | 117 | func usageError(cmd *cobra.Command, format string, args ...interface{}) error { 118 | msg := fmt.Sprintf(format, args...) 119 | return fmt.Errorf("%s\nSee '%s -h' for help and examples.", msg, cmd.CommandPath()) 120 | } 121 | 122 | func listOptsForKind(kind string) (string, *api.ListOptions, error) { 123 | switch kind { 124 | case "flow", "flows": 125 | listOpts, err := funktion.CreateFlowListOptions() 126 | return flowKind, listOpts, err 127 | case "conn", "connector", "connectors": 128 | listOpts, err := funktion.CreateConnectorListOptions() 129 | return connectorKind, listOpts, err 130 | case "r", "runtime", "runtimes": 131 | listOpts, err := funktion.CreateRuntimeListOptions() 132 | return runtimeKind, listOpts, err 133 | case "fn", "function", "functions", "funktion", "funktions": 134 | listOpts, err := funktion.CreateFunctionListOptions() 135 | return functionKind, listOpts, err 136 | default: 137 | return "", nil, fmt.Errorf("Unknown kind `%s` when known kinds are (`fn`, `flow`, `connector`, `runtime`)", kind) 138 | } 139 | } 140 | 141 | func nameForDeployment(kube *kubernetes.Clientset, namespace string, kind string, name string) (string, error) { 142 | // TODO we may need to map a function or flow to a different named resource if we have a naming clash 143 | // so we may need to look at a label or annotation on the function / flow 144 | return name, nil 145 | } 146 | func nameForService(kube *kubernetes.Clientset, namespace string, kind string, name string) (string, error) { 147 | // TODO we may need to map a function or flow to a different named resource if we have a naming clash 148 | // so we may need to look at a label or annotation on the function / flow 149 | return name, nil 150 | } 151 | 152 | // initConfig reads in config file and ENV variables if set. 153 | func initConfig() { 154 | configPath := constants.ConfigFile 155 | viper.SetConfigFile(configPath) 156 | viper.SetConfigType("json") 157 | err := viper.ReadInConfig() 158 | if err != nil { 159 | glog.Warningf("Error reading config file at %s: %s", configPath, err) 160 | } 161 | setupViper() 162 | } 163 | 164 | func setupViper() { 165 | viper.SetEnvPrefix("FUNKTION_") 166 | 167 | // Replaces '-' in flags with '_' in env variables 168 | // e.g. show-libmachine-logs => $ENVPREFIX_SHOW_LIBMACHINE_LOGS 169 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 170 | viper.AutomaticEnv() 171 | 172 | viper.SetDefault(config.WantUpdateNotification, true) 173 | viper.SetDefault(config.ReminderWaitPeriodInHours, 24) 174 | setFlagsUsingViper() 175 | } 176 | 177 | var viperWhiteList = []string{ 178 | "v", 179 | "alsologtostderr", 180 | "log_dir", 181 | } 182 | 183 | func setFlagsUsingViper() { 184 | for _, config := range viperWhiteList { 185 | var a = pflag.Lookup(config) 186 | if a == nil { 187 | continue 188 | } 189 | viper.SetDefault(a.Name, a.DefValue) 190 | // If the flag is set, override viper value 191 | if a.Changed { 192 | viper.Set(a.Name, a.Value.String()) 193 | } 194 | // Viper will give precedence first to calls to the Set command, 195 | // then to values from the config.yml 196 | a.Value.Set(viper.GetString(a.Name)) 197 | a.Changed = true 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /cmd/safe_string_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "runtime" 21 | "testing" 22 | ) 23 | 24 | func TestConvertToSafeResourceName(t *testing.T) { 25 | assertEquals(t, convertToSafeResourceName("Foo-Bar"), "foo-bar") 26 | assertEquals(t, convertToSafeResourceName("foo-bar"), "foo-bar") 27 | assertEquals(t, convertToSafeResourceName("foo/bar[]whatnot"), "foo-bar-whatnot") 28 | assertEquals(t, convertToSafeResourceName("-foo----bar-whatnot"), "foo-bar-whatnot") 29 | } 30 | 31 | func TestConvertToSafeLabelValue(t *testing.T) { 32 | assertEquals(t, convertToSafeLabelValue("Foo-Bar"), "Foo-Bar") 33 | assertEquals(t, convertToSafeLabelValue("foo-bar"), "foo-bar") 34 | assertEquals(t, convertToSafeLabelValue("foo bar"), "foo-bar") 35 | assertEquals(t, convertToSafeLabelValue(".foo."), "foo") 36 | assertEquals(t, convertToSafeLabelValue("foo/bar[]whatnot"), "foo-bar-whatnot") 37 | assertEquals(t, convertToSafeLabelValue("-foo----bar-whatnot"), "foo----bar-whatnot") 38 | } 39 | 40 | func assertEquals(t *testing.T, found, expected string) { 41 | if found != expected { 42 | logErr(t, found, expected) 43 | } 44 | } 45 | 46 | func logErr(t *testing.T, found, expected string) { 47 | out := new(bytes.Buffer) 48 | 49 | _, _, line, ok := runtime.Caller(2) 50 | if ok { 51 | fmt.Fprintf(out, "Line: %d ", line) 52 | } 53 | fmt.Fprintf(out, "Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found) 54 | t.Errorf(out.String()) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "github.com/funktionio/funktion/pkg/update" 19 | "os" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | type updateCmd struct { 25 | } 26 | 27 | func init() { 28 | RootCmd.AddCommand(newUpdateCmd()) 29 | } 30 | 31 | func newUpdateCmd() *cobra.Command { 32 | p := &updateCmd{} 33 | cmd := &cobra.Command{ 34 | Use: "update", 35 | Short: "updates this binary to the latest version from github", 36 | Long: `This command checks if there is a newer release of the funktion binary and if so downloads and replaces the file`, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | handleError(p.run()) 39 | }, 40 | } 41 | return cmd 42 | } 43 | 44 | func (p *updateCmd) run() error { 45 | update.MaybeUpdateFromGithub(os.Stdout) 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd/url.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os" 21 | "strings" 22 | "time" 23 | 24 | "github.com/pkg/browser" 25 | "github.com/spf13/cobra" 26 | 27 | "k8s.io/client-go/1.5/kubernetes" 28 | "k8s.io/client-go/1.5/pkg/api" 29 | "k8s.io/client-go/1.5/pkg/api/v1" 30 | ) 31 | 32 | const ( 33 | exposeURLAnnotation = "fabric8.io/exposeUrl" 34 | ) 35 | 36 | type locationCom struct { 37 | kubeclient *kubernetes.Clientset 38 | cmd *cobra.Command 39 | kubeConfigPath string 40 | 41 | namespace string 42 | kind string 43 | name string 44 | open bool 45 | retry bool 46 | } 47 | 48 | func init() { 49 | RootCmd.AddCommand(newLocationCom()) 50 | } 51 | 52 | func newLocationCom() *cobra.Command { 53 | p := &locationCom{} 54 | cmd := &cobra.Command{ 55 | Use: "url KIND NAME [flags]", 56 | Short: "views the external URL you can use to access a function or flow", 57 | Long: `This command will output the URL you can use to invoke a function of flow`, 58 | Run: func(cmd *cobra.Command, args []string) { 59 | p.cmd = cmd 60 | if len(args) < 1 { 61 | handleError(fmt.Errorf("No resource kind argument supplied! Possible values ['fn', 'flow']")) 62 | return 63 | } 64 | if len(args) < 2 { 65 | handleError(fmt.Errorf("No name specified!")) 66 | return 67 | } 68 | p.kind = args[0] 69 | p.name = args[1] 70 | err := createKubernetesClient(cmd, p.kubeConfigPath, &p.kubeclient, &p.namespace) 71 | if err != nil { 72 | handleError(err) 73 | return 74 | } 75 | handleError(p.run()) 76 | }, 77 | } 78 | f := cmd.Flags() 79 | f.StringVar(&p.kubeConfigPath, "kubeconfig", "", "the directory to look for the kubernetes configuration") 80 | f.StringVarP(&p.namespace, "namespace", "n", "", "the namespace to query") 81 | f.StringVarP(&p.name, "name", "v", "latest", "the version of the connectors to install") 82 | f.BoolVarP(&p.open, "open", "o", false, "Open the URL in a browser") 83 | f.BoolVarP(&p.retry, "retry", "r", true, "Whether to keep retrying if the endpoint is not yet available") 84 | return cmd 85 | } 86 | 87 | func (p *locationCom) run() error { 88 | name, err := nameForService(p.kubeclient, p.namespace, p.kind, p.name) 89 | if err != nil { 90 | return err 91 | } 92 | return p.openService(name) 93 | } 94 | 95 | func (p *locationCom) openService(serviceName string) error { 96 | c := p.kubeclient 97 | ns := p.namespace 98 | if p.retry { 99 | if err := RetryAfter(40, func() error { 100 | return CheckService(c, ns, serviceName) 101 | }, 10*time.Second); err != nil { 102 | fmt.Errorf("Could not find finalized endpoint being pointed to by %s: %v", serviceName, err) 103 | os.Exit(1) 104 | } 105 | } 106 | svcs, err := c.Services(ns).List(api.ListOptions{}) 107 | if err != nil { 108 | return fmt.Errorf("No services found %v\n", err) 109 | } 110 | for _, service := range svcs.Items { 111 | if serviceName == service.Name { 112 | url := service.ObjectMeta.Annotations[exposeURLAnnotation] 113 | if p.open { 114 | fmt.Printf("\nOpening URL %s\n", url) 115 | browser.OpenURL(url) 116 | } else { 117 | fmt.Printf("%s\n", url) 118 | } 119 | return nil 120 | } 121 | } 122 | return fmt.Errorf("No service %s in namespace %s\n", serviceName, ns) 123 | } 124 | 125 | // CheckService waits for the specified service to be ready by returning an error until the service is up 126 | // The check is done by polling the endpoint associated with the service and when the endpoint exists, returning no error->service-online 127 | // Credits: https://github.com/kubernetes/minikube/blob/v0.9.0/cmd/minikube/cmd/service.go#L89 128 | func CheckService(c *kubernetes.Clientset, ns string, serviceName string) error { 129 | svc, err := c.Services(ns).Get(serviceName) 130 | if err != nil { 131 | return err 132 | } 133 | url := svc.ObjectMeta.Annotations[exposeURLAnnotation] 134 | if url == "" { 135 | fmt.Print(".") 136 | return errors.New("") 137 | } 138 | endpoints := c.Endpoints(ns) 139 | if endpoints == nil { 140 | fmt.Errorf("No endpoints found in namespace %s\n", ns) 141 | } 142 | endpoint, err := endpoints.Get(serviceName) 143 | if err != nil { 144 | fmt.Errorf("No endpoints found for service %s\n", serviceName) 145 | return err 146 | } 147 | return CheckEndpointReady(endpoint) 148 | } 149 | 150 | //CheckEndpointReady checks that the kubernetes endpoint is ready 151 | // Credits: https://github.com/kubernetes/minikube/blob/v0.9.0/cmd/minikube/cmd/service.go#L101 152 | func CheckEndpointReady(endpoint *v1.Endpoints) error { 153 | if len(endpoint.Subsets) == 0 { 154 | fmt.Fprintf(os.Stderr, ".") 155 | return fmt.Errorf("Endpoint for service is not ready yet\n") 156 | } 157 | for _, subset := range endpoint.Subsets { 158 | if len(subset.NotReadyAddresses) != 0 { 159 | fmt.Fprintf(os.Stderr, "Waiting, endpoint for service is not ready yet...\n") 160 | return fmt.Errorf("Endpoint for service is not ready yet\n") 161 | } 162 | } 163 | return nil 164 | } 165 | 166 | func Retry(attempts int, callback func() error) (err error) { 167 | return RetryAfter(attempts, callback, 0) 168 | } 169 | 170 | func RetryAfter(attempts int, callback func() error, d time.Duration) (err error) { 171 | m := MultiError{} 172 | for i := 0; i < attempts; i++ { 173 | err = callback() 174 | if err == nil { 175 | return nil 176 | } 177 | m.Collect(err) 178 | time.Sleep(d) 179 | } 180 | return m.ToError() 181 | } 182 | 183 | type MultiError struct { 184 | Errors []error 185 | } 186 | 187 | func (m *MultiError) Collect(err error) { 188 | if err != nil { 189 | m.Errors = append(m.Errors, err) 190 | } 191 | } 192 | 193 | func (m MultiError) ToError() error { 194 | if len(m.Errors) == 0 { 195 | return nil 196 | } 197 | 198 | errStrings := []string{} 199 | for _, err := range m.Errors { 200 | errStrings = append(errStrings, err.Error()) 201 | } 202 | return fmt.Errorf(strings.Join(errStrings, "\n")) 203 | } 204 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | 22 | "github.com/funktionio/funktion/pkg/version" 23 | ) 24 | 25 | func init() { 26 | var versionCmd = &cobra.Command{ 27 | Use: "version", 28 | Short: "Print the version of funktion.", 29 | Long: `Print the version of funktion.`, 30 | Run: func(command *cobra.Command, args []string) { 31 | fmt.Println("funktion version:", version.GetVersion()) 32 | }, 33 | } 34 | RootCmd.AddCommand(versionCmd) 35 | } 36 | -------------------------------------------------------------------------------- /examples/blog-example/ReadMe.md: -------------------------------------------------------------------------------- 1 | ## Blog Example 2 | 3 | This example shows how to combine some functions and flows. 4 | 5 | Here's how to try it out. For more help on how to use the funktion CLI [check out the documentation](https://funktion.fabric8.io/docs/#cli) 6 | 7 | ```json 8 | 9 | echo "lets create the functions - pass -w to watch for file changes" 10 | funktion create fn -f examples/blog-example 11 | 12 | echo "lets create a flow" 13 | funktion create flow -n blogendpoint -c http4 http://localhost http://blogcount 14 | 15 | echo "lets invoke the funky flow" 16 | export SPLIT=`minikube service --url split -n funky` 17 | echo $SPLIT 18 | 19 | curl -X POST --header "Content-Type: application/json" -d ' 20 | [ 21 | { 22 | "userId": 1, 23 | "id": 1, 24 | "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", 25 | "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" 26 | }, 27 | { 28 | "userId": 1, 29 | "id": 2, 30 | "title": "qui est esse", 31 | "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" 32 | } 33 | ] 34 | ' $SPLIT 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /examples/blog-example/blogcount.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context, callback) { 2 | var body = context.request.body; 3 | body.titleWordCount = countWords(body.title); 4 | body.bodyWordCount = countWords(body.body); 5 | 6 | callback(200, JSON.stringify(body)); 7 | }; 8 | 9 | // 10 | function countWords(text) { 11 | var words = null; 12 | if (text) { 13 | words = text.split(" "); 14 | return words.length; 15 | } 16 | return 0; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /examples/blog-example/blogendpoint-svc.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: blogendpoint 5 | labels: 6 | expose: 'true' 7 | funktion.fabric8.io/kind: Function 8 | runtime: nodejs 9 | spec: 10 | ports: 11 | - protocol: TCP 12 | port: 80 13 | targetPort: 8080 14 | selector: 15 | name: blogendpoint 16 | -------------------------------------------------------------------------------- /examples/blog-example/sampleBlog.json: -------------------------------------------------------------------------------- 1 | { "userId": 1, "id": 0, "title": "funkion is awesome", "body": "this blog describes how lovely funktion is" } -------------------------------------------------------------------------------- /examples/blog-example/source.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context, callback) { 2 | var foo = "James"; 3 | callback(200, "Hello World " + foo); 4 | }; -------------------------------------------------------------------------------- /examples/blog-example/split.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, callback) { 2 | const http = require('http'); 3 | if (!http) { 4 | console.log("could not require http"); 5 | callback(400, "Could not require http"); 6 | return; 7 | } 8 | 9 | var body = context.request.body; 10 | if (body && body.constructor === Array) { 11 | var counter = 0; 12 | var result = { 13 | responses: [] 14 | }; 15 | var status = 200; 16 | var firstError = null; 17 | 18 | function returnError(e) { 19 | console.log("Caught error: " + e); 20 | result.error = e; 21 | status = 400; 22 | var resultJson = JSON.stringify(result); 23 | console.log("result: " + status + " " + resultJson); 24 | callback(status, resultJson); 25 | } 26 | 27 | try { 28 | var lastIndex = body.length - 1; 29 | body.forEach(function (item, idx) { 30 | var postData = JSON.stringify(item); 31 | var postOptions = { 32 | hostname: 'blogendpoint', 33 | port: '80', 34 | path: '/', 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | 'Cache-Control': 'no-cache', 39 | 'Content-Length': postData.length 40 | } 41 | }; 42 | 43 | req = http.request(postOptions, function (res) { 44 | result[idx] = { 45 | statusCode: res.statusCode 46 | }; 47 | res.setEncoding('utf8'); 48 | var data = ""; 49 | res.on('data', function (chunk) { 50 | data += chunk; 51 | }); 52 | res.on('end', function () { 53 | result[idx].response = data; 54 | if (idx === lastIndex) { 55 | result.count = counter; 56 | if (firstError) { 57 | console.log("Failed with error: " + firstError); 58 | results.error = firstError; 59 | status = 400; 60 | } 61 | var resultJson = JSON.stringify(result); 62 | callback(status, resultJson); 63 | } 64 | }); 65 | }); 66 | 67 | req.on('error', function (e) { 68 | console.log('problem with request: ' + e.message); 69 | if (idx === lastIndex) { 70 | returnError(e); 71 | } 72 | }); 73 | 74 | req.write(postData); 75 | req.end(); 76 | counter++; 77 | }); 78 | } catch (e) { 79 | returnError(e); 80 | } 81 | } else { 82 | callback(400, "No array is passed in. Was given: " + JSON.stringify(body)); 83 | } 84 | }; 85 | 86 | 87 | -------------------------------------------------------------------------------- /examples/envvar.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context, callback) { 2 | var name = process.env.NAME || "World"; 3 | callback(200, "Hello " + name + "!"); 4 | }; -------------------------------------------------------------------------------- /examples/flow-twitter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "v1" 3 | kind: "ConfigMap" 4 | metadata: 5 | name: "flow-twitter" 6 | labels: 7 | funktion.fabric8.io/kind: "Flow" 8 | connector: "twitter" 9 | data: 10 | application.properties: | 11 | # spring application properties file 12 | # NOTE these secrets are purely for demos 13 | # please replace with your access token secrets for your apps!!! 14 | camel.component.twitter.access-token=26693234-W0YjxL9cMJrC0VZZ4xdgFMymxIQ10LeL1K8YlbBY 15 | camel.component.twitter.access-token-secret=BZD51BgzbOdFstWZYsqB5p5dbuuDV12vrOdatzhY4E 16 | camel.component.twitter.consumer-key=NMqaca1bzXsOcZhP2XlwA 17 | camel.component.twitter.consumer-secret=VxNQiRLwwKVD0K9mmfxlTTbVdgRpriORypnUbHhxeQw 18 | funktion.yml: | 19 | --- 20 | flows: 21 | - logResult: true 22 | steps: 23 | - kind: endpoint 24 | uri: twitter://search?type=direct&keywords=fabric8 25 | -------------------------------------------------------------------------------- /examples/flow/ReadMe.md: -------------------------------------------------------------------------------- 1 | ## Flow example 2 | 3 | This directory contains a simple function and flow. 4 | 5 | To install them run: 6 | 7 | funktion apply -f examples/flow -w 8 | 9 | This will create a Function and a Flow resource and watch the files for changes and update them on the fly. 10 | 11 | The [sample.flow.yml](sample.flow.yml) defines a simple Flow of events which then invokes the [hello.js](hello.js) function -------------------------------------------------------------------------------- /examples/flow/hello.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context, callback) { 2 | var name = JSON.stringify(context.request.body) || "World"; 3 | callback(200, "Hello " + name + "!!"); 4 | }; -------------------------------------------------------------------------------- /examples/flow/sample.flow.yml: -------------------------------------------------------------------------------- 1 | --- 2 | flows: 3 | - logResult: true 4 | steps: 5 | - kind: "endpoint" 6 | uri: "timer://bar?period=8000" 7 | - kind: "endpoint" 8 | uri: "http://hello/" 9 | -------------------------------------------------------------------------------- /examples/flow1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "v1" 3 | kind: "ConfigMap" 4 | metadata: 5 | name: "flow1" 6 | labels: 7 | funktion.fabric8.io/kind: "Flow" 8 | connector: "timer" 9 | data: 10 | application.properties: | 11 | # spring application properties file 12 | foo = bar 13 | funktion.yml: | 14 | --- 15 | flows: 16 | - logResult: true 17 | steps: 18 | - kind: endpoint 19 | uri: timer://foo?fixedRate=true&period=5000 20 | - kind: endpoint 21 | uri: http://ip.jsontest.com/ 22 | -------------------------------------------------------------------------------- /examples/function1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "v1" 3 | kind: "ConfigMap" 4 | metadata: 5 | name: "function1" 6 | labels: 7 | funktion.fabric8.io/kind: "Function" 8 | runtime: "nodejs" 9 | data: 10 | source: | 11 | module.exports = function(context, callback) { 12 | callback(200, "Hello, world!\n"); 13 | } -------------------------------------------------------------------------------- /examples/hello.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context, callback) { 2 | var name = JSON.stringify(context.request.body) || "World"; 3 | callback(200, "Hello " + name + "!"); 4 | }; -------------------------------------------------------------------------------- /examples/source.js: -------------------------------------------------------------------------------- 1 | module.exports = function(context, callback) { 2 | var name = JSON.stringify(context.request.body) || "World"; 3 | callback(200, "Hello " + name + "!"); 4 | }; -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: ceea13887e90f9d7de130c9adb643387e800a6ca2b06003b85efdf7875961dea 2 | updated: 2017-01-09T15:13:07.883930244Z 3 | imports: 4 | - name: github.com/blang/semver 5 | version: 3a37c301dda64cbe17f16f661b4c976803c0e2d2 6 | - name: github.com/coreos/go-oidc 7 | version: 2b5d73091ea4b7ddb15e3ac00077f153120b5b61 8 | subpackages: 9 | - http 10 | - jose 11 | - key 12 | - oauth2 13 | - oidc 14 | - name: github.com/coreos/pkg 15 | version: 447b7ec906e523386d9c53be15b55a8ae86ea944 16 | subpackages: 17 | - health 18 | - httputil 19 | - timeutil 20 | - name: github.com/davecgh/go-spew 21 | version: 346938d642f2ec3594ed81d874461961cd0faa76 22 | subpackages: 23 | - spew 24 | - name: github.com/docker/distribution 25 | version: 559433598c7be9d30d6cfc5cad5b5dfdb686725c 26 | repo: https://github.com/openshift/docker-distribution.git 27 | vcs: git 28 | subpackages: 29 | - digest 30 | - reference 31 | - name: github.com/emicklei/go-restful 32 | version: 152183b11abcd2b07ee814c8da82296340949747 33 | repo: https://github.com/openshift/go-restful.git 34 | vcs: git 35 | subpackages: 36 | - log 37 | - swagger 38 | - name: github.com/fsnotify/fsnotify 39 | version: fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197 40 | - name: github.com/ghodss/yaml 41 | version: 04f313413ffd65ce25f2541bfd2b2ceec5c0908c 42 | - name: github.com/go-kit/kit 43 | version: f66b0e13579bfc5a48b9e2a94b1209c107ea1f41 44 | subpackages: 45 | - log 46 | - name: github.com/go-logfmt/logfmt 47 | version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 48 | - name: github.com/go-openapi/jsonpointer 49 | version: 779f45308c19820f1a69e9a4cd965f496e0da10f 50 | - name: github.com/go-openapi/jsonreference 51 | version: 36d33bfe519efae5632669801b180bf1a245da3b 52 | - name: github.com/go-openapi/spec 53 | version: 3a0434164aa36744c7ba29b822d36894a1e5ec96 54 | - name: github.com/go-openapi/swag 55 | version: 96d7b9ebd181a1735a1c9ac87914f2b32fbf56c9 56 | - name: github.com/go-stack/stack 57 | version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 58 | - name: github.com/gogo/protobuf 59 | version: 909568be09de550ed094403c2bf8a261b5bb730a 60 | subpackages: 61 | - proto 62 | - sortkeys 63 | - name: github.com/golang/glog 64 | version: 335da9dda11408a34b64344f82e9c03779b71673 65 | repo: https://github.com/openshift/glog.git 66 | vcs: git 67 | - name: github.com/golang/protobuf 68 | version: 8d92cf5fc15a4382f8964b08e1f42a75c0591aa3 69 | subpackages: 70 | - proto 71 | - name: github.com/google/go-github 72 | version: dcda0b96bd591fbeb40dcaddec3fb40a7fcaca34 73 | subpackages: 74 | - github 75 | - name: github.com/google/go-querystring 76 | version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 77 | subpackages: 78 | - query 79 | - name: github.com/google/gofuzz 80 | version: 44d81051d367757e1c7c6a5a86423ece9afcf63c 81 | - name: github.com/hashicorp/hcl 82 | version: eb6f65b2d77ed5078887f960ff570fbddbbeb49d 83 | subpackages: 84 | - hcl/ast 85 | - hcl/parser 86 | - hcl/scanner 87 | - hcl/strconv 88 | - hcl/token 89 | - json/parser 90 | - json/scanner 91 | - json/token 92 | - name: github.com/howeyc/gopass 93 | version: f5387c492211eb133053880d23dfae62aa14123d 94 | - name: github.com/imdario/mergo 95 | version: 50d4dbd4eb0e84778abe37cefef140271d96fade 96 | - name: github.com/inconshreveable/go-update 97 | version: 8152e7eb6ccf8679a64582a66b78519688d156ad 98 | subpackages: 99 | - internal/binarydist 100 | - internal/osext 101 | - name: github.com/inconshreveable/mousetrap 102 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 103 | - name: github.com/jonboulle/clockwork 104 | version: bcac9884e7502bb2b474c0339d889cb981a2f27f 105 | - name: github.com/jpillora/go-ogle-analytics 106 | version: 14b04e0594ef6a9fd943363b135656f0ec8c9d0e 107 | - name: github.com/juju/ratelimit 108 | version: 77ed1c8a01217656d2080ad51981f6e99adaa177 109 | - name: github.com/kardianos/osext 110 | version: c2c54e542fb797ad986b31721e1baedf214ca413 111 | - name: github.com/kr/logfmt 112 | version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 113 | - name: github.com/magiconair/properties 114 | version: 9c47895dc1ce54302908ab8a43385d1f5df2c11c 115 | - name: github.com/mailru/easyjson 116 | version: 9d6630dc8c577b56cb9687a9cf9e8578aca7298a 117 | subpackages: 118 | - buffer 119 | - jlexer 120 | - jwriter 121 | - name: github.com/mattn/go-runewidth 122 | version: 737072b4e32b7a5018b4a7125da8d12de90e8045 123 | - name: github.com/minishift/minishift 124 | version: 5e83488418289665426fa5a80d11a342a777e25b 125 | subpackages: 126 | - pkg/util/archive 127 | - pkg/util/github 128 | - pkg/util/os 129 | - name: github.com/mitchellh/mapstructure 130 | version: bfdb1a85537d60bc7e954e600c250219ea497417 131 | - name: github.com/pborman/uuid 132 | version: 5007efa264d92316c43112bc573e754bc889b7b1 133 | - name: github.com/pelletier/go-buffruneio 134 | version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d 135 | - name: github.com/pelletier/go-toml 136 | version: 439fbba1f887c286024370cb4f281ba815c4c7d7 137 | - name: github.com/pkg/browser 138 | version: 9302be274faad99162b9d48ec97b24306872ebb0 139 | - name: github.com/pkg/errors 140 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 141 | - name: github.com/PuerkitoBio/purell 142 | version: 0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4 143 | - name: github.com/PuerkitoBio/urlesc 144 | version: 5bd2802263f21d8788851d5305584c82a5c75d7e 145 | - name: github.com/spf13/afero 146 | version: 90dd71edc4d0a8b3511dc12ea15d617d03be09e0 147 | subpackages: 148 | - mem 149 | - name: github.com/spf13/cast 150 | version: 56a7ecbeb18dde53c6db4bd96b541fd9741b8d44 151 | - name: github.com/spf13/cobra 152 | version: 1dd5ff2e11b6dca62fdcb275eb804b94607d8b06 153 | - name: github.com/spf13/jwalterweatherman 154 | version: fa7ca7e836cf3a8bb4ebf799f472c12d7e903d66 155 | - name: github.com/spf13/pflag 156 | version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40 157 | - name: github.com/spf13/viper 158 | version: 5ed0fc31f7f453625df314d8e66b9791e8d13003 159 | - name: github.com/ugorji/go 160 | version: ded73eae5db7e7a0ef6f55aace87a2873c5d2b74 161 | subpackages: 162 | - codec 163 | - name: golang.org/x/crypto 164 | version: c3b1d0d6d8690eaebe3064711b026770cc37efa3 165 | subpackages: 166 | - ssh/terminal 167 | - name: golang.org/x/net 168 | version: 4876518f9e71663000c348837735820161a42df7 169 | subpackages: 170 | - context 171 | - http2 172 | - http2/hpack 173 | - idna 174 | - name: golang.org/x/oauth2 175 | version: 045497edb6234273d67dbc25da3f2ddbc4c4cacf 176 | subpackages: 177 | - google 178 | - internal 179 | - jws 180 | - jwt 181 | - name: golang.org/x/sys 182 | version: d75a52659825e75fff6158388dddc6a5b04f9ba5 183 | subpackages: 184 | - unix 185 | - name: golang.org/x/text 186 | version: 44f4f658a783b0cee41fe0a23b8fc91d9c120558 187 | subpackages: 188 | - transform 189 | - unicode/norm 190 | - width 191 | - name: google.golang.org/appengine 192 | version: 8758a385849434ba5eac8aeedcf5192c5a0f5f10 193 | subpackages: 194 | - internal 195 | - internal/app_identity 196 | - internal/base 197 | - internal/datastore 198 | - internal/log 199 | - internal/modules 200 | - internal/remote_api 201 | - internal/urlfetch 202 | - urlfetch 203 | - name: google.golang.org/cloud 204 | version: 975617b05ea8a58727e6c1a06b6161ff4185a9f2 205 | subpackages: 206 | - compute/metadata 207 | - internal 208 | - name: gopkg.in/cheggaaa/pb.v1 209 | version: d7e6ca3010b6f084d8056847f55d7f572f180678 210 | - name: gopkg.in/inf.v0 211 | version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 212 | - name: gopkg.in/yaml.v2 213 | version: a5b47d31c556af34a302ce5d659e6fea44d90de0 214 | - name: k8s.io/client-go 215 | version: 843f7c4f28b1f647f664f883697107d5c02c5acc 216 | subpackages: 217 | - 1.5/discovery 218 | - 1.5/dynamic 219 | - 1.5/kubernetes 220 | - 1.5/kubernetes/typed/apps/v1alpha1 221 | - 1.5/kubernetes/typed/authentication/v1beta1 222 | - 1.5/kubernetes/typed/authorization/v1beta1 223 | - 1.5/kubernetes/typed/autoscaling/v1 224 | - 1.5/kubernetes/typed/batch/v1 225 | - 1.5/kubernetes/typed/certificates/v1alpha1 226 | - 1.5/kubernetes/typed/core/v1 227 | - 1.5/kubernetes/typed/extensions/v1beta1 228 | - 1.5/kubernetes/typed/policy/v1alpha1 229 | - 1.5/kubernetes/typed/rbac/v1alpha1 230 | - 1.5/kubernetes/typed/storage/v1beta1 231 | - 1.5/pkg/api 232 | - 1.5/pkg/api/errors 233 | - 1.5/pkg/api/install 234 | - 1.5/pkg/api/meta 235 | - 1.5/pkg/api/meta/metatypes 236 | - 1.5/pkg/api/resource 237 | - 1.5/pkg/api/unversioned 238 | - 1.5/pkg/api/v1 239 | - 1.5/pkg/api/validation/path 240 | - 1.5/pkg/apimachinery 241 | - 1.5/pkg/apimachinery/announced 242 | - 1.5/pkg/apimachinery/registered 243 | - 1.5/pkg/apis/apps 244 | - 1.5/pkg/apis/apps/install 245 | - 1.5/pkg/apis/apps/v1alpha1 246 | - 1.5/pkg/apis/authentication 247 | - 1.5/pkg/apis/authentication/install 248 | - 1.5/pkg/apis/authentication/v1beta1 249 | - 1.5/pkg/apis/authorization 250 | - 1.5/pkg/apis/authorization/install 251 | - 1.5/pkg/apis/authorization/v1beta1 252 | - 1.5/pkg/apis/autoscaling 253 | - 1.5/pkg/apis/autoscaling/install 254 | - 1.5/pkg/apis/autoscaling/v1 255 | - 1.5/pkg/apis/batch 256 | - 1.5/pkg/apis/batch/install 257 | - 1.5/pkg/apis/batch/v1 258 | - 1.5/pkg/apis/batch/v2alpha1 259 | - 1.5/pkg/apis/certificates 260 | - 1.5/pkg/apis/certificates/install 261 | - 1.5/pkg/apis/certificates/v1alpha1 262 | - 1.5/pkg/apis/extensions 263 | - 1.5/pkg/apis/extensions/install 264 | - 1.5/pkg/apis/extensions/v1beta1 265 | - 1.5/pkg/apis/policy 266 | - 1.5/pkg/apis/policy/install 267 | - 1.5/pkg/apis/policy/v1alpha1 268 | - 1.5/pkg/apis/rbac 269 | - 1.5/pkg/apis/rbac/install 270 | - 1.5/pkg/apis/rbac/v1alpha1 271 | - 1.5/pkg/apis/storage 272 | - 1.5/pkg/apis/storage/install 273 | - 1.5/pkg/apis/storage/v1beta1 274 | - 1.5/pkg/auth/user 275 | - 1.5/pkg/conversion 276 | - 1.5/pkg/conversion/queryparams 277 | - 1.5/pkg/fields 278 | - 1.5/pkg/genericapiserver/openapi/common 279 | - 1.5/pkg/labels 280 | - 1.5/pkg/runtime 281 | - 1.5/pkg/runtime/serializer 282 | - 1.5/pkg/runtime/serializer/json 283 | - 1.5/pkg/runtime/serializer/protobuf 284 | - 1.5/pkg/runtime/serializer/recognizer 285 | - 1.5/pkg/runtime/serializer/streaming 286 | - 1.5/pkg/runtime/serializer/versioning 287 | - 1.5/pkg/selection 288 | - 1.5/pkg/third_party/forked/golang/reflect 289 | - 1.5/pkg/types 290 | - 1.5/pkg/util 291 | - 1.5/pkg/util/cert 292 | - 1.5/pkg/util/clock 293 | - 1.5/pkg/util/errors 294 | - 1.5/pkg/util/flowcontrol 295 | - 1.5/pkg/util/framer 296 | - 1.5/pkg/util/homedir 297 | - 1.5/pkg/util/integer 298 | - 1.5/pkg/util/intstr 299 | - 1.5/pkg/util/json 300 | - 1.5/pkg/util/labels 301 | - 1.5/pkg/util/net 302 | - 1.5/pkg/util/parsers 303 | - 1.5/pkg/util/rand 304 | - 1.5/pkg/util/runtime 305 | - 1.5/pkg/util/sets 306 | - 1.5/pkg/util/uuid 307 | - 1.5/pkg/util/validation 308 | - 1.5/pkg/util/validation/field 309 | - 1.5/pkg/util/wait 310 | - 1.5/pkg/util/yaml 311 | - 1.5/pkg/version 312 | - 1.5/pkg/watch 313 | - 1.5/pkg/watch/versioned 314 | - 1.5/plugin/pkg/client/auth 315 | - 1.5/plugin/pkg/client/auth/gcp 316 | - 1.5/plugin/pkg/client/auth/oidc 317 | - 1.5/rest 318 | - 1.5/tools/auth 319 | - 1.5/tools/cache 320 | - 1.5/tools/clientcmd 321 | - 1.5/tools/clientcmd/api 322 | - 1.5/tools/clientcmd/api/latest 323 | - 1.5/tools/clientcmd/api/v1 324 | - 1.5/tools/metrics 325 | - 1.5/transport 326 | testImports: [] 327 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/funktionio/funktion 2 | import: 3 | - package: github.com/go-kit/kit 4 | version: ^0.3.0 5 | subpackages: 6 | - log 7 | - package: k8s.io/client-go 8 | version: release-1.5 9 | subpackages: 10 | - 1.5/kubernetes 11 | - 1.5/rest 12 | - 1.5/tools/clientcmd 13 | - package: github.com/jpillora/go-ogle-analytics 14 | - package: github.com/gogo/protobuf 15 | version: ^0.3.0 16 | subpackages: 17 | - proto 18 | - sortkeys 19 | - package: github.com/spf13/cobra 20 | - package: github.com/spf13/pflag 21 | - package: github.com/mitchellh/mapstructure 22 | - package: github.com/magiconair/properties 23 | - package: github.com/fsnotify/fsnotify 24 | - package: github.com/kardianos/osext 25 | - package: github.com/pkg/browser 26 | - package: github.com/inconshreveable/go-update 27 | - package: github.com/blang/semver 28 | - package: github.com/golang/glog 29 | version: 335da9dda11408a34b64344f82e9c03779b71673 30 | repo: https://github.com/openshift/glog.git 31 | vcs: git 32 | - package: github.com/google/go-github 33 | - package: google.golang.org/cloud 34 | subpackages: 35 | - compute/metadata 36 | - package: golang.org/x/oauth2 37 | - package: gopkg.in/cheggaaa/pb.v1 38 | - package: github.com/spf13/viper 39 | - package: github.com/minishift/minishift 40 | subpackages: 41 | - pkg/util/github 42 | 43 | 44 | -------------------------------------------------------------------------------- /pkg/analytics/analytics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package analytics 16 | 17 | import ( 18 | "sync" 19 | 20 | ga "github.com/jpillora/go-ogle-analytics" 21 | ) 22 | 23 | const ( 24 | id = "UA-85532162-2" 25 | category = "funktion-operator" 26 | ) 27 | 28 | var ( 29 | client *ga.Client 30 | once sync.Once 31 | ) 32 | 33 | func send(e *ga.Event) { 34 | mustClient().Send(e) 35 | } 36 | 37 | func mustClient() *ga.Client { 38 | once.Do(func() { 39 | c, err := ga.NewClient(id) 40 | if err != nil { 41 | panic(err) 42 | } 43 | client = c 44 | }) 45 | return client 46 | } 47 | 48 | func FlowCreated() { 49 | send(ga.NewEvent(category, "flow_created")) 50 | } 51 | 52 | func FlowDeleted() { 53 | send(ga.NewEvent(category, "flow_deleted")) 54 | } 55 | 56 | func ConnectorCreated() { 57 | send(ga.NewEvent(category, "connector_created")) 58 | } 59 | 60 | func ConnectorDeleted() { 61 | send(ga.NewEvent(category, "connector_deleted")) 62 | } 63 | 64 | func FunctionCreated() { 65 | send(ga.NewEvent(category, "function_created")) 66 | } 67 | 68 | func FunctionDeleted() { 69 | send(ga.NewEvent(category, "function_deleted")) 70 | } 71 | 72 | func RuntimeCreated() { 73 | send(ga.NewEvent(category, "runtime_created")) 74 | } 75 | 76 | func RuntimeDeleted() { 77 | send(ga.NewEvent(category, "runtime_deleted")) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package config 16 | 17 | const ( 18 | WantUpdateNotification = "WantUpdateNotification" 19 | ReminderWaitPeriodInHours = "ReminderWaitPeriodInHours" 20 | ) 21 | -------------------------------------------------------------------------------- /pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package constants 15 | 16 | import ( 17 | "path/filepath" 18 | 19 | "k8s.io/client-go/1.5/pkg/util/homedir" 20 | ) 21 | 22 | var FunktionPath = filepath.Join(homedir.HomeDir(), ".funktion") 23 | 24 | // MakeFunktionPath is a utility to calculate a relative path to our directory. 25 | func MakeFunktionPath(fileName ...string) string { 26 | args := []string{FunktionPath} 27 | args = append(args, fileName...) 28 | return filepath.Join(args...) 29 | } 30 | 31 | var ConfigFile = MakeFunktionPath("config", "config.json") 32 | -------------------------------------------------------------------------------- /pkg/funktion/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package funktion 16 | 17 | import ( 18 | "k8s.io/client-go/1.5/kubernetes" 19 | "k8s.io/client-go/1.5/pkg/api" 20 | "k8s.io/client-go/1.5/pkg/labels" 21 | "k8s.io/client-go/1.5/pkg/runtime" 22 | "k8s.io/client-go/1.5/pkg/watch" 23 | "k8s.io/client-go/1.5/tools/cache" 24 | ) 25 | 26 | // NewConfigMapListWatch returns a new ListWatch for ConfigMaps with the given listOptions 27 | func NewConfigMapListWatch(client *kubernetes.Clientset, listOpts api.ListOptions, namespace string) *cache.ListWatch { 28 | configMaps := client.ConfigMaps(namespace) 29 | 30 | return &cache.ListWatch{ 31 | ListFunc: func(options api.ListOptions) (runtime.Object, error) { 32 | return configMaps.List(listOpts) 33 | }, 34 | WatchFunc: func(options api.ListOptions) (watch.Interface, error) { 35 | return configMaps.Watch(listOpts) 36 | }, 37 | } 38 | } 39 | 40 | // NewServiceListWatch creates a watch on services 41 | func NewServiceListWatch(client *kubernetes.Clientset, namespace string) *cache.ListWatch { 42 | listOpts := api.ListOptions{} 43 | services := client.Services(namespace) 44 | return &cache.ListWatch{ 45 | ListFunc: func(options api.ListOptions) (runtime.Object, error) { 46 | return services.List(listOpts) 47 | }, 48 | WatchFunc: func(options api.ListOptions) (watch.Interface, error) { 49 | return services.Watch(listOpts) 50 | }, 51 | } 52 | } 53 | 54 | // CreateFlowListOptions returns the default selector for Flow resources 55 | func CreateFlowListOptions() (*api.ListOptions, error) { 56 | return createKindListOptions(FlowKind) 57 | } 58 | 59 | // CreateConnectorListOptions returns the default selector for Connector resources 60 | func CreateConnectorListOptions() (*api.ListOptions, error) { 61 | return createKindListOptions(ConnectorKind) 62 | } 63 | 64 | // CreateRuntimeListOptions returns the default selector for Runtime resources 65 | func CreateRuntimeListOptions() (*api.ListOptions, error) { 66 | return createKindListOptions(RuntimeKind) 67 | } 68 | 69 | // CreateFunctionListOptions returns the default selector for Function resources 70 | func CreateFunctionListOptions() (*api.ListOptions, error) { 71 | return createKindListOptions(FunctionKind) 72 | } 73 | 74 | // CreateKindListOptions returns the selector for a given kind of resources 75 | func createKindListOptions(kind string) (*api.ListOptions, error) { 76 | selector, err := labels.Parse(KindLabel + "=" + kind) 77 | if err != nil { 78 | return nil, err 79 | } 80 | listOpts := api.ListOptions{ 81 | LabelSelector: selector, 82 | } 83 | return &listOpts, nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/funktion/connector_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package funktion 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "github.com/funktionio/funktion/pkg/spec" 22 | "github.com/ghodss/yaml" 23 | "strings" 24 | "unicode" 25 | ) 26 | 27 | func LoadConnectorSchema(yamlData []byte) (*spec.ConnectorSchema, error) { 28 | schema := spec.ConnectorSchema{} 29 | err := yaml.Unmarshal(yamlData, &schema) 30 | if err != nil { 31 | return nil, fmt.Errorf("Failed to parse schema YAML: %v", err) 32 | } 33 | return &schema, nil 34 | } 35 | 36 | // HumanizeString converts a camelCase text string into a textual label by capitalising 37 | // and separating camelcase words with a space 38 | func HumanizeString(text string) string { 39 | uncamel := UnCamelCaseString(text, " ") 40 | switch len(uncamel) { 41 | case 0: 42 | return "" 43 | case 1: 44 | return strings.ToUpper(uncamel) 45 | default: 46 | return strings.ToUpper(uncamel[0:1]) + uncamel[1:] 47 | } 48 | } 49 | 50 | // UnCamelCaseString converts a camelCase text string into a space separated string 51 | func UnCamelCaseString(text string, separator string) string { 52 | var buffer bytes.Buffer 53 | lastUpper := false 54 | for i, c := range text { 55 | if unicode.IsUpper(c) { 56 | if !lastUpper && i > 0 { 57 | buffer.WriteString(separator) 58 | } 59 | lastUpper = true 60 | } else { 61 | lastUpper = false 62 | } 63 | buffer.WriteRune(c) 64 | } 65 | return buffer.String() 66 | } 67 | 68 | func ToSpringBootPropertyName(text string) string { 69 | return strings.ToLower(UnCamelCaseString(text, "-")) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/funktion/connector_schema_strings_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package funktion 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestHumanizeStrings(t *testing.T) { 22 | assertEquals(t, HumanizeString("foo"), "Foo") 23 | assertEquals(t, HumanizeString("fooBarWhatnot"), "Foo Bar Whatnot") 24 | assertEquals(t, HumanizeString("goodBBQ"), "Good BBQ") 25 | } 26 | 27 | func TestToSpringBootPropertyName(t *testing.T) { 28 | assertEquals(t, ToSpringBootPropertyName("foobar"), "foobar") 29 | assertEquals(t, ToSpringBootPropertyName("fooBarWhatnot"), "foo-bar-whatnot") 30 | assertEquals(t, ToSpringBootPropertyName("goodBBQ"), "good-bbq") 31 | } 32 | -------------------------------------------------------------------------------- /pkg/funktion/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package funktion 16 | 17 | import "time" 18 | 19 | const ( 20 | // KindLabel is the label key used on ConfigMaps to indicate the kind of resource 21 | KindLabel = "funktion.fabric8.io/kind" 22 | 23 | // Flow 24 | 25 | // ConnectorLabel is the label key used on a ConfigMap to refer to a Connector 26 | ConnectorLabel = "connector" 27 | 28 | // Function 29 | 30 | // RuntimeLabel is the label key used on a ConfigMap to refer to a Runtime 31 | RuntimeLabel = "runtime" 32 | // ProjectLabel the name of the folder where the source comes from 33 | ProjectLabel = "project" 34 | 35 | // ConnectorKind is the value of a Connector fo the KindLabel 36 | ConnectorKind = "Connector" 37 | // FlowKind is the value of a Flow fo the KindLabel 38 | FlowKind = "Flow" 39 | // RuntimeKind is the value of a Runtime fo the KindLabel 40 | RuntimeKind = "Runtime" 41 | // FunctionKind is the value of a Function fo the KindLabel 42 | FunctionKind = "Function" 43 | // DeploymentKind is the value of a Deployment fo the KindLabel 44 | DeploymentKind = "Deployment" 45 | // ServiceKind is the value of a ConneServicector fo the KindLabel 46 | ServiceKind = "Service" 47 | 48 | // Runtime 49 | 50 | // ChromeDevToolsAnnotation boolean annotation to indicate chrome dev tools is enabled 51 | // and that the URL will appear in the pods log 52 | ChromeDevToolsAnnotation = "funktion.fabric8.io/chromeDevTools" 53 | 54 | // VersionLabel the version of the runtime 55 | VersionLabel = "version" 56 | 57 | // FileExtensionsProperty a comma separated list of file extensions (without the dot) which are handled by this runtime 58 | FileExtensionsProperty = "fileExtensions" 59 | 60 | // SourceMountPathProperty the path in the docker image where we should mount the source code 61 | SourceMountPathProperty = "sourceMountPath" 62 | 63 | resyncPeriod = 30 * time.Second 64 | ) 65 | -------------------------------------------------------------------------------- /pkg/funktion/deployment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package funktion 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/ghodss/yaml" 22 | "k8s.io/client-go/1.5/pkg/api/v1" 23 | "k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1" 24 | ) 25 | 26 | const ( 27 | // Connector 28 | 29 | // DeploymentYmlProperty data key for the deployment yaml file 30 | DeploymentYmlProperty = "deployment.yml" 31 | 32 | // SchemaYmlProperty data key for the schema (JSON schema as YAML) file 33 | SchemaYmlProperty = "schema.yml" 34 | 35 | // Flow 36 | 37 | // FunktionYmlProperty the data key for the funktion yaml file 38 | FunktionYmlProperty = "funktion.yml" 39 | 40 | // ApplicationPropertiesProperty is the data key for the spring boot application.properties file 41 | ApplicationPropertiesProperty = "application.properties" 42 | 43 | // ApplicationYmlProperty is the data key for the spring boot application.yml file 44 | ApplicationYmlProperty = "application.yml" 45 | 46 | // for Function 47 | 48 | // SourceProperty is the data key for the source code in a Function ConfigMap 49 | SourceProperty = "source" 50 | // DebugProperty is the data key for whether to enable debugging in a Function ConfigMap 51 | DebugProperty = "debug" 52 | // EnvVarsProperty represents a newline terminated list of NAME=VALUE expressions for environment variables 53 | EnvVarsProperty = "envVars" 54 | 55 | // ExposeLabel is the label key to expose services 56 | ExposeLabel = "expose" 57 | 58 | // for Runtime 59 | 60 | // DeploymentProperty is the data key for a Runtime's Deployment 61 | DeploymentProperty = "deployment" 62 | // DeploymentDebugProperty is the data key for a Runtime's Debug Deployment 63 | DeploymentDebugProperty = "deploymentDebug" 64 | // ServiceProperty is the data key for a Runtime's Service 65 | ServiceProperty = "service" 66 | // DebugPortProperty is the data key for a Runtime's debug port 67 | DebugPortProperty = "debugPort" 68 | 69 | // ConfigMapControllerAnnotation is the annotation for the configmapcontroller 70 | ConfigMapControllerAnnotation = "configmap.fabric8.io/update-on-change" 71 | 72 | // Deployment 73 | // NameLabel is the name label for Deployments 74 | NameLabel = "name" 75 | ) 76 | 77 | func makeFlowDeployment(flow *v1.ConfigMap, connector *v1.ConfigMap, old *v1beta1.Deployment) (*v1beta1.Deployment, error) { 78 | deployYaml := connector.Data[DeploymentYmlProperty] 79 | if len(deployYaml) == 0 { 80 | return nil, fmt.Errorf("No property `%s` on the Flow ConfigMap %s", DeploymentYmlProperty, flow.Name) 81 | } 82 | 83 | deployment := v1beta1.Deployment{} 84 | err := yaml.Unmarshal([]byte(deployYaml), &deployment) 85 | if err != nil { 86 | return nil, fmt.Errorf("Failed to parse Deployment YAML from property `%s` on the Flow ConfigMap %s. Error: %s", DeploymentYmlProperty, flow.Name, err) 87 | } 88 | 89 | name := flow.Name 90 | deployment.Name = name 91 | if deployment.Annotations == nil { 92 | deployment.Annotations = make(map[string]string) 93 | } 94 | if deployment.Labels == nil { 95 | deployment.Labels = make(map[string]string) 96 | } 97 | 98 | // lets copy across any old missing dependencies 99 | if old != nil { 100 | if old.Annotations != nil { 101 | for k, v := range old.Annotations { 102 | if len(deployment.Annotations[k]) == 0 { 103 | deployment.Annotations[k] = v 104 | } 105 | } 106 | } 107 | } 108 | if flow.Labels != nil { 109 | for k, v := range flow.Labels { 110 | if len(deployment.Labels[k]) == 0 { 111 | deployment.Labels[k] = v 112 | } 113 | } 114 | } 115 | if len(deployment.Annotations[ConfigMapControllerAnnotation]) == 0 { 116 | deployment.Annotations[ConfigMapControllerAnnotation] = flow.Name 117 | } 118 | 119 | volumeName := "config" 120 | items := []v1.KeyToPath{} 121 | 122 | // lets mount any files from the ConfigMap 123 | if len(flow.Data[FunktionYmlProperty]) > 0 { 124 | items = append(items, v1.KeyToPath{ 125 | Key: FunktionYmlProperty, 126 | Path: "funktion.yml", 127 | }) 128 | } 129 | if len(flow.Data[ApplicationPropertiesProperty]) > 0 { 130 | items = append(items, v1.KeyToPath{ 131 | Key: ApplicationPropertiesProperty, 132 | Path: "application.properties", 133 | }) 134 | } 135 | if len(flow.Data[ApplicationYmlProperty]) > 0 { 136 | items = append(items, v1.KeyToPath{ 137 | Key: ApplicationYmlProperty, 138 | Path: "application.yml", 139 | }) 140 | } 141 | if len(items) > 0 { 142 | podSpec := &deployment.Spec.Template.Spec 143 | podSpec.Volumes = append(podSpec.Volumes, v1.Volume{ 144 | Name: volumeName, 145 | VolumeSource: v1.VolumeSource{ 146 | ConfigMap: &v1.ConfigMapVolumeSource{ 147 | LocalObjectReference: v1.LocalObjectReference{ 148 | Name: flow.Name, 149 | }, 150 | Items: items, 151 | }, 152 | }, 153 | }) 154 | for i, container := range podSpec.Containers { 155 | podSpec.Containers[i].VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{ 156 | Name: volumeName, 157 | MountPath: "/deployments/config", 158 | ReadOnly: true, 159 | }) 160 | } 161 | } 162 | if len(deployment.Spec.Template.Spec.Containers[0].Name) == 0 { 163 | deployment.Spec.Template.Spec.Containers[0].Name = "connector" 164 | } 165 | setDeploymentLabel(&deployment, NameLabel, name) 166 | return &deployment, nil 167 | } 168 | 169 | func makeFunctionDeployment(function *v1.ConfigMap, runtime *v1.ConfigMap, old *v1beta1.Deployment) (*v1beta1.Deployment, error) { 170 | deployYaml := runtime.Data[DeploymentProperty] 171 | debugFlag := function.Data[DebugProperty] 172 | if strings.ToLower(debugFlag) == "true" { 173 | deployYaml = runtime.Data[DeploymentDebugProperty] 174 | if len(deployYaml) == 0 { 175 | return nil, fmt.Errorf("No property `%s` on the Runtime ConfigMap %s", DeploymentDebugProperty, runtime.Name) 176 | } 177 | } 178 | if len(deployYaml) == 0 { 179 | return nil, fmt.Errorf("No property `%s` on the Runtime ConfigMap %s", DeploymentProperty, runtime.Name) 180 | } 181 | 182 | deployment := v1beta1.Deployment{} 183 | err := yaml.Unmarshal([]byte(deployYaml), &deployment) 184 | if err != nil { 185 | return nil, fmt.Errorf("Failed to parse Deployment YAML from property `%s` on the Runtime ConfigMap %s. Error: %s", DeploymentYmlProperty, runtime.Name, err) 186 | } 187 | 188 | name := function.Name 189 | deployment.Name = name 190 | if deployment.Annotations == nil { 191 | deployment.Annotations = make(map[string]string) 192 | } 193 | if deployment.Labels == nil { 194 | deployment.Labels = make(map[string]string) 195 | } 196 | 197 | // lets copy across any old missing dependencies 198 | if old != nil { 199 | if old.Annotations != nil { 200 | for k, v := range old.Annotations { 201 | if len(deployment.Annotations[k]) == 0 { 202 | deployment.Annotations[k] = v 203 | } 204 | } 205 | } 206 | } 207 | if function.Labels != nil { 208 | for k, v := range function.Labels { 209 | if len(deployment.Labels[k]) == 0 { 210 | deployment.Labels[k] = v 211 | } 212 | } 213 | } 214 | if len(deployment.Annotations[ConfigMapControllerAnnotation]) == 0 { 215 | deployment.Annotations[ConfigMapControllerAnnotation] = function.Name 216 | } 217 | 218 | if len(function.Data[SourceProperty]) == 0 { 219 | return nil, fmt.Errorf("No property `%s` on the Function ConfigMap %s", SourceProperty, function.Name) 220 | } 221 | 222 | volumeName := "config" 223 | items := []v1.KeyToPath{ 224 | v1.KeyToPath{ 225 | Key: SourceProperty, 226 | Path: "source.js", 227 | }, 228 | } 229 | 230 | foundVolume := false 231 | podSpec := &deployment.Spec.Template.Spec 232 | for i, volume := range podSpec.Volumes { 233 | if volume.Name == "source" && volume.ConfigMap != nil { 234 | podSpec.Volumes[i].ConfigMap.Name = function.Name 235 | foundVolume = true 236 | } 237 | } 238 | if !foundVolume { 239 | podSpec.Volumes = append(podSpec.Volumes, v1.Volume{ 240 | Name: volumeName, 241 | VolumeSource: v1.VolumeSource{ 242 | ConfigMap: &v1.ConfigMapVolumeSource{ 243 | LocalObjectReference: v1.LocalObjectReference{ 244 | Name: function.Name, 245 | }, 246 | Items: items, 247 | }, 248 | }, 249 | }) 250 | } 251 | 252 | envVars := parseEnvVars(function.Data[EnvVarsProperty]) 253 | 254 | mountPath := runtime.Data[SourceMountPathProperty] 255 | if len(mountPath) == 0 { 256 | mountPath = "/funktion" 257 | } 258 | for i, container := range podSpec.Containers { 259 | foundVolumeMount := false 260 | for _, volumeMount := range container.VolumeMounts { 261 | if volumeMount.Name == "source" { 262 | foundVolumeMount = true 263 | } 264 | } 265 | if !foundVolumeMount { 266 | podSpec.Containers[i].VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{ 267 | Name: "source", 268 | MountPath: mountPath, 269 | ReadOnly: true, 270 | }) 271 | } 272 | if len(envVars) > 0 { 273 | applyEnvVars(&podSpec.Containers[i].Env, &envVars) 274 | } 275 | } 276 | if len(deployment.Spec.Template.Spec.Containers[0].Name) == 0 { 277 | deployment.Spec.Template.Spec.Containers[0].Name = "function" 278 | } 279 | setDeploymentLabel(&deployment, NameLabel, name) 280 | return &deployment, nil 281 | } 282 | 283 | func parseEnvVars(text string) []v1.EnvVar { 284 | answer := []v1.EnvVar{} 285 | if len(text) > 0 { 286 | lines := strings.Split(text, "\n") 287 | for _, line := range lines { 288 | l := strings.TrimSpace(line) 289 | pair := strings.SplitN(l, "=", 2) 290 | if len(pair) != 2 { 291 | fmt.Printf("Ignoring bad environment variable pair. Expecting `NAME=VALUE` but got: %s\n", l) 292 | continue 293 | } 294 | answer = append(answer, v1.EnvVar{ 295 | Name: pair[0], 296 | Value: pair[1], 297 | }) 298 | } 299 | return answer 300 | } 301 | return answer 302 | } 303 | 304 | func applyEnvVars(envVar *[]v1.EnvVar, overrides *[]v1.EnvVar) { 305 | if overrides == nil { 306 | return 307 | 308 | } 309 | if *envVar == nil { 310 | *envVar = []v1.EnvVar{} 311 | } 312 | for _, o := range *overrides { 313 | found := false 314 | for _, v := range *envVar { 315 | if v.Name == o.Name { 316 | v.Value = o.Value 317 | v.ValueFrom = nil 318 | found = true 319 | } 320 | } 321 | if !found { 322 | *envVar = append(*envVar, o) 323 | } 324 | } 325 | } 326 | 327 | func setDeploymentLabel(deployment *v1beta1.Deployment, key string, value string) { 328 | deployment.Labels[key] = value 329 | if deployment.Spec.Selector == nil { 330 | deployment.Spec.Selector = &v1beta1.LabelSelector{} 331 | } 332 | if deployment.Spec.Selector.MatchLabels == nil { 333 | deployment.Spec.Selector.MatchLabels = map[string]string{} 334 | } 335 | deployment.Spec.Selector.MatchLabels[key] = value 336 | if deployment.Spec.Template.ObjectMeta.Labels == nil { 337 | deployment.Spec.Template.ObjectMeta.Labels = map[string]string{} 338 | } 339 | deployment.Spec.Template.ObjectMeta.Labels[key] = value 340 | } 341 | func makeFunctionService(function *v1.ConfigMap, runtime *v1.ConfigMap, old *v1.Service, deployment *v1beta1.Deployment) (*v1.Service, error) { 342 | yamlText := runtime.Data[ServiceProperty] 343 | if len(yamlText) == 0 { 344 | return nil, fmt.Errorf("No property `%s` on the Runtime ConfigMap %s", ServiceProperty, runtime.Name) 345 | } 346 | 347 | svc := &v1.Service{} 348 | err := yaml.Unmarshal([]byte(yamlText), &svc) 349 | if err != nil { 350 | return nil, fmt.Errorf("Failed to parse Service YAML from property `%s` on the Runtime ConfigMap %s. Error: %s", DeploymentYmlProperty, runtime.Name, err) 351 | } 352 | 353 | svc.Name = function.Name 354 | if svc.Annotations == nil { 355 | svc.Annotations = make(map[string]string) 356 | } 357 | if svc.Labels == nil { 358 | svc.Labels = make(map[string]string) 359 | } 360 | 361 | svc.Spec.Selector = deployment.Spec.Selector.MatchLabels 362 | 363 | // lets copy across any old missing dependencies 364 | if old != nil { 365 | if old.Annotations != nil { 366 | for k, v := range old.Annotations { 367 | if len(svc.Annotations[k]) == 0 { 368 | svc.Annotations[k] = v 369 | } 370 | } 371 | } 372 | } 373 | if function.Labels != nil { 374 | for k, v := range function.Labels { 375 | if len(svc.Labels[k]) == 0 { 376 | svc.Labels[k] = v 377 | } 378 | } 379 | } 380 | if len(svc.Labels[ExposeLabel]) == 0 { 381 | svc.Labels[ExposeLabel] = "true" 382 | } 383 | return svc, nil 384 | } 385 | -------------------------------------------------------------------------------- /pkg/k8sutil/k8sutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package k8sutil 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | "time" 21 | 22 | "k8s.io/client-go/1.5/pkg/api/errors" 23 | "k8s.io/client-go/1.5/pkg/api/v1" 24 | "k8s.io/client-go/1.5/pkg/util/wait" 25 | "k8s.io/client-go/1.5/rest" 26 | ) 27 | 28 | // WaitForTPRReady waits for a third party resource to be available 29 | // for use. 30 | func WaitForTPRReady(restClient *rest.RESTClient, tprGroup, tprVersion, tprName string) error { 31 | return wait.Poll(3*time.Second, 30*time.Second, func() (bool, error) { 32 | req := restClient.Get().AbsPath("apis", tprGroup, tprVersion, tprName) 33 | res := req.Do() 34 | err := res.Error() 35 | if err != nil { 36 | if se, ok := err.(*errors.StatusError); ok { 37 | if se.Status().Code == http.StatusNotFound { 38 | return false, nil 39 | } 40 | } 41 | return false, err 42 | } 43 | 44 | var statusCode int 45 | res.StatusCode(&statusCode) 46 | if statusCode != http.StatusOK { 47 | return false, fmt.Errorf("invalid status code: %v", statusCode) 48 | } 49 | 50 | return true, nil 51 | }) 52 | } 53 | 54 | // PodRunningAndReady returns whether a pod is running and each container has 55 | // passed it's ready state. 56 | func PodRunningAndReady(pod v1.Pod) (bool, error) { 57 | switch pod.Status.Phase { 58 | case v1.PodFailed, v1.PodSucceeded: 59 | return false, fmt.Errorf("pod completed") 60 | case v1.PodRunning: 61 | for _, cond := range pod.Status.Conditions { 62 | if cond.Type != v1.PodReady { 63 | continue 64 | } 65 | return cond.Status == v1.ConditionTrue, nil 66 | } 67 | return false, fmt.Errorf("pod ready condition not found") 68 | } 69 | return false, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/k8sutil/kubectl_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package k8sutil 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/exec" 21 | "path/filepath" 22 | "runtime" 23 | "strings" 24 | 25 | "github.com/kardianos/osext" 26 | "k8s.io/client-go/1.5/kubernetes" 27 | "k8s.io/client-go/1.5/pkg/util/homedir" 28 | ) 29 | 30 | // ResolveKubectlBinary resolves the binary to use such as 'kubectl' or 'oc' 31 | func ResolveKubectlBinary(kubeclient *kubernetes.Clientset) (string, error) { 32 | isOpenshift := isOpenShiftCluster(kubeclient) 33 | kubeBinary := "kubectl" 34 | if isOpenshift { 35 | kubeBinary = "oc" 36 | } 37 | 38 | if runtime.GOOS == "windows" && !strings.HasSuffix(kubeBinary, ".exe") { 39 | kubeBinary += ".exe" 40 | } 41 | 42 | name, err := resolveBinaryLocation(kubeBinary) 43 | if err != nil { 44 | return "", err 45 | } 46 | if len(name) == 0 { 47 | return "", fmt.Errorf("Could not find binary %s on your PATH. Is it installed?", kubeBinary) 48 | } 49 | return name, nil 50 | } 51 | 52 | func isOpenShiftCluster(kubeclient *kubernetes.Clientset) bool { 53 | // The presence of "/oapi" on the API server is our hacky way of 54 | // determining if we're talking to OpenShift 55 | err := kubeclient.Core().GetRESTClient().Get().AbsPath("/oapi").Do().Error() 56 | if err != nil { 57 | return false 58 | } 59 | return true 60 | } 61 | 62 | // lets find the executable on the PATH or in the fabric8 directory 63 | func resolveBinaryLocation(executable string) (string, error) { 64 | path, err := exec.LookPath(executable) 65 | if err != nil || fileNotExist(path) { 66 | home := os.Getenv("HOME") 67 | if home == "" { 68 | fmt.Printf("No $HOME environment variable found") 69 | } 70 | writeFileLocation := getFabric8BinLocation() 71 | 72 | // lets try in the fabric8 folder 73 | path = filepath.Join(writeFileLocation, executable) 74 | if fileNotExist(path) { 75 | path = executable 76 | // lets try in the folder where we found the gofabric8 executable 77 | folder, err := osext.ExecutableFolder() 78 | if err != nil { 79 | return "", fmt.Errorf("Failed to find executable folder: %v\n", err) 80 | } else { 81 | path = filepath.Join(folder, executable) 82 | if fileNotExist(path) { 83 | fmt.Printf("Could not find executable at %v\n", path) 84 | path = executable 85 | } 86 | } 87 | } 88 | } 89 | return path, nil 90 | } 91 | 92 | func findExecutable(file string) error { 93 | d, err := os.Stat(file) 94 | if err != nil { 95 | return err 96 | } 97 | if m := d.Mode(); !m.IsDir() { 98 | return nil 99 | } 100 | return os.ErrPermission 101 | } 102 | 103 | func fileNotExist(path string) bool { 104 | return findExecutable(path) != nil 105 | } 106 | 107 | func getFabric8BinLocation() string { 108 | home := homedir.HomeDir() 109 | if home == "" { 110 | fmt.Printf("No user home environment variable found for OS %s\n", runtime.GOOS) 111 | } 112 | return filepath.Join(home, ".fabric8", "bin") 113 | } 114 | -------------------------------------------------------------------------------- /pkg/k8sutil/pod_watch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package k8sutil 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "os/signal" 22 | "sync" 23 | "syscall" 24 | "time" 25 | 26 | "k8s.io/client-go/1.5/kubernetes" 27 | "k8s.io/client-go/1.5/pkg/api" 28 | "k8s.io/client-go/1.5/pkg/api/v1" 29 | "k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1" 30 | "k8s.io/client-go/1.5/pkg/conversion" 31 | "k8s.io/client-go/1.5/pkg/runtime" 32 | "k8s.io/client-go/1.5/pkg/watch" 33 | "k8s.io/client-go/1.5/tools/cache" 34 | ) 35 | 36 | const ( 37 | resyncPeriod = 30 * time.Second 38 | ) 39 | 40 | type PodFunc func(pod *v1.Pod) error 41 | 42 | type PodAction struct { 43 | OnPodChange PodFunc 44 | 45 | latestPodName string 46 | podInformer cache.SharedIndexInformer 47 | } 48 | 49 | // V1BetaSelectorToListOptions converts a selector from a Deployment to an api.ListOptions object 50 | func V1BetaSelectorToListOptions(selector *v1beta1.LabelSelector) (*api.ListOptions, error) { 51 | labelSelector := selector.MatchLabels 52 | if labelSelector == nil { 53 | return nil, fmt.Errorf("Selector does not havea matchLabels") 54 | } 55 | labelString := toLabelString(labelSelector) 56 | oldListOpts := v1beta1.ListOptions{ 57 | LabelSelector: labelString, 58 | } 59 | newListOpts := api.ListOptions{} 60 | scope := conversion.Scope(nil) 61 | err := v1beta1.Convert_v1beta1_ListOptions_To_api_ListOptions(&oldListOpts, &newListOpts, scope) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &newListOpts, nil 66 | } 67 | 68 | func toLabelString(labels map[string]string) string { 69 | var buffer bytes.Buffer 70 | i := 0 71 | for k, v := range labels { 72 | i++ 73 | if i > 1 { 74 | buffer.WriteString(",") 75 | } 76 | buffer.WriteString(fmt.Sprintf("%s=%s", k, v)) 77 | } 78 | return buffer.String() 79 | } 80 | 81 | // WatchPods sets up the watcher for the given kubernetes client, namespace and listOpts 82 | func (p *PodAction) WatchPods(kubeclient *kubernetes.Clientset, namespace string, listOpts *api.ListOptions) cache.SharedIndexInformer { 83 | resources := kubeclient.Pods(namespace) 84 | listWatch := cache.ListWatch{ 85 | ListFunc: func(options api.ListOptions) (runtime.Object, error) { 86 | return resources.List(*listOpts) 87 | }, 88 | WatchFunc: func(options api.ListOptions) (watch.Interface, error) { 89 | return resources.Watch(*listOpts) 90 | }, 91 | } 92 | inf := cache.NewSharedIndexInformer( 93 | &listWatch, 94 | &v1.Pod{}, 95 | resyncPeriod, 96 | cache.Indexers{}, 97 | ) 98 | 99 | inf.AddEventHandler(cache.ResourceEventHandlerFuncs{ 100 | AddFunc: p.handlePodAdd, 101 | DeleteFunc: p.handlePodDelete, 102 | UpdateFunc: p.handlePodUpdate, 103 | }) 104 | p.podInformer = inf 105 | return inf 106 | } 107 | 108 | func (p *PodAction) handlePodAdd(obj interface{}) { 109 | p.CheckLatestPod() 110 | } 111 | 112 | func (p *PodAction) handlePodUpdate(old, obj interface{}) { 113 | p.CheckLatestPod() 114 | } 115 | 116 | func (p *PodAction) handlePodDelete(obj interface{}) { 117 | p.CheckLatestPod() 118 | } 119 | 120 | // WatchLoop is the loop waiting or the watch to fail 121 | func (p *PodAction) WatchLoop() error { 122 | stopc := make(chan struct{}) 123 | errc := make(chan error) 124 | var wg sync.WaitGroup 125 | 126 | wg.Add(1) 127 | go func() { 128 | go p.podInformer.Run(stopc) 129 | <-stopc 130 | wg.Done() 131 | }() 132 | 133 | term := make(chan os.Signal) 134 | signal.Notify(term, os.Interrupt, syscall.SIGTERM) 135 | select { 136 | case <-term: 137 | fmt.Fprintln(os.Stderr) 138 | fmt.Println("Received SIGTERM, exiting gracefully...") 139 | close(stopc) 140 | wg.Wait() 141 | case err := <-errc: 142 | fmt.Printf("Unexpected error received: %v\n", err) 143 | close(stopc) 144 | wg.Wait() 145 | return err 146 | } 147 | return nil 148 | } 149 | 150 | func (p *PodAction) CheckLatestPod() { 151 | var latestPod *v1.Pod 152 | latestPodName := "" 153 | l := p.podInformer.GetStore().List() 154 | if l != nil { 155 | for _, obj := range l { 156 | if obj != nil { 157 | pod := obj.(*v1.Pod) 158 | if pod != nil { 159 | if isPodReady(pod) { 160 | if latestPod == nil || isPodNewer(pod, latestPod) { 161 | latestPod = pod 162 | latestPodName = pod.Name 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | if latestPodName != p.latestPodName { 170 | p.latestPodName = latestPodName 171 | fn := p.OnPodChange 172 | if fn != nil { 173 | err := fn(latestPod) 174 | if err != nil { 175 | fmt.Printf("Unexpected error received: %v\n", err) 176 | } 177 | } 178 | } 179 | } 180 | 181 | func isPodReady(pod *v1.Pod) bool { 182 | status := pod.Status 183 | statusText := status.Phase 184 | if statusText == "Running" { 185 | for _, cond := range status.Conditions { 186 | if cond.Type == "Ready" { 187 | return cond.Status == "True" 188 | } 189 | } 190 | } 191 | return false 192 | } 193 | 194 | // isPodNewer returns true if a is newer than b 195 | func isPodNewer(a *v1.Pod, b *v1.Pod) bool { 196 | t1 := a.ObjectMeta.CreationTimestamp 197 | t2 := b.ObjectMeta.CreationTimestamp 198 | return t2.Before(t1) 199 | } 200 | -------------------------------------------------------------------------------- /pkg/queue/queue.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package queue 16 | 17 | import "sync" 18 | 19 | // New constructs a new workqueue. 20 | func New() *Queue { 21 | return &Queue{ 22 | dirty: set{}, 23 | processing: set{}, 24 | cond: sync.NewCond(&sync.Mutex{}), 25 | } 26 | } 27 | 28 | // Queue is a work queue. 29 | type Queue struct { 30 | // queue defines the order in which we will work on items. Every 31 | // element of queue should be in the dirty set and not in the 32 | // processing set. 33 | queue []t 34 | 35 | // dirty defines all of the items that need to be processed. 36 | dirty set 37 | 38 | // Things that are currently being processed are in the processing set. 39 | // These things may be simultaneously in the dirty set. When we finish 40 | // processing something and remove it from this set, we'll check if 41 | // it's in the dirty set, and if so, add it to the queue. 42 | processing set 43 | 44 | cond *sync.Cond 45 | 46 | shuttingDown bool 47 | } 48 | 49 | type empty struct{} 50 | type t interface{} 51 | type set map[t]empty 52 | 53 | func (s set) has(item t) bool { 54 | _, exists := s[item] 55 | return exists 56 | } 57 | 58 | func (s set) insert(item t) { 59 | s[item] = empty{} 60 | } 61 | 62 | func (s set) delete(item t) { 63 | delete(s, item) 64 | } 65 | 66 | // Add marks item as needing processing. 67 | func (q *Queue) Add(item interface{}) { 68 | q.cond.L.Lock() 69 | defer q.cond.L.Unlock() 70 | if q.shuttingDown { 71 | return 72 | } 73 | if q.dirty.has(item) { 74 | return 75 | } 76 | 77 | q.dirty.insert(item) 78 | if q.processing.has(item) { 79 | return 80 | } 81 | 82 | q.queue = append(q.queue, item) 83 | q.cond.Signal() 84 | } 85 | 86 | // Len returns the current queue length, for informational purposes only. You 87 | // shouldn't e.g. gate a call to Add() or Get() on Len() being a particular 88 | // value, that can't be synchronized properly. 89 | func (q *Queue) Len() int { 90 | q.cond.L.Lock() 91 | defer q.cond.L.Unlock() 92 | return len(q.queue) 93 | } 94 | 95 | // Get blocks until it can return an item to be processed. If shutdown = true, 96 | // the caller should end their goroutine. You must call Done with item when you 97 | // have finished processing it. 98 | func (q *Queue) Get() (item interface{}, shutdown bool) { 99 | q.cond.L.Lock() 100 | defer q.cond.L.Unlock() 101 | for len(q.queue) == 0 && !q.shuttingDown { 102 | q.cond.Wait() 103 | } 104 | if len(q.queue) == 0 { 105 | // We must be shutting down. 106 | return nil, true 107 | } 108 | 109 | item, q.queue = q.queue[0], q.queue[1:] 110 | 111 | q.processing.insert(item) 112 | q.dirty.delete(item) 113 | 114 | return item, false 115 | } 116 | 117 | // Done marks item as done processing, and if it has been marked as dirty again 118 | // while it was being processed, it will be re-added to the queue for 119 | // re-processing. 120 | func (q *Queue) Done(item interface{}) { 121 | q.cond.L.Lock() 122 | defer q.cond.L.Unlock() 123 | 124 | q.processing.delete(item) 125 | if q.dirty.has(item) { 126 | q.queue = append(q.queue, item) 127 | q.cond.Signal() 128 | } 129 | } 130 | 131 | // ShutDown will cause q to ignore all new items added to it. As soon as the 132 | // worker goroutines have drained the existing items in the queue, they will be 133 | // instructed to exit. 134 | func (q *Queue) ShutDown() { 135 | q.cond.L.Lock() 136 | defer q.cond.L.Unlock() 137 | 138 | q.shuttingDown = true 139 | q.cond.Broadcast() 140 | } 141 | 142 | func (q *Queue) ShuttingDown() bool { 143 | q.cond.L.Lock() 144 | defer q.cond.L.Unlock() 145 | 146 | return q.shuttingDown 147 | } 148 | -------------------------------------------------------------------------------- /pkg/spec/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package spec 16 | 17 | import ( 18 | "k8s.io/client-go/1.5/pkg/api/unversioned" 19 | "k8s.io/client-go/1.5/pkg/api/v1" 20 | "k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1" 21 | ) 22 | 23 | const ( 24 | EndpointKind = "endpoint" 25 | FunctionKind = "function" 26 | SetBodyKind = "setBody" 27 | SetHeadersKind = "setHeaders" 28 | ) 29 | 30 | // Connector defines how to create a Deployment for a Flow 31 | type Connector struct { 32 | unversioned.TypeMeta `json:",inline"` 33 | v1.ObjectMeta `json:"metadata,omitempty"` 34 | Spec ConnectorSpec `json:"spec"` 35 | } 36 | 37 | // ConnectorList is a list of Funktion. 38 | type ConnectorList struct { 39 | unversioned.TypeMeta `json:",inline"` 40 | unversioned.ListMeta `json:"metadata,omitempty"` 41 | 42 | Items []*Connector `json:"items"` 43 | } 44 | 45 | // ConnectorSpec holds specification parameters of a Flow deployment along with configuration metadata. 46 | type ConnectorSpec struct { 47 | DeploymentSpec *v1beta1.DeploymentSpec `json:"deploymentSpec"` 48 | 49 | // TODO lets add a JSON Schema for how to configure the endpoints? 50 | } 51 | 52 | // ComponentSpec holds the component metadata in a ConnectorSchema 53 | type ComponentSpec struct { 54 | Kind string `json:"kind"` 55 | Scheme string `json:"scheme"` 56 | Syntax string `json:"syntax"` 57 | Title string `json:"title"` 58 | Description string `json:"description"` 59 | Label string `json:"label"` 60 | Deprecated bool `json:"deprecated"` 61 | Async bool `json:"async"` 62 | JavaType string `json:"javaType"` 63 | GroupId string `json:"groupId"` 64 | ArtifactId string `json:"artifactId"` 65 | Version string `json:"version"` 66 | } 67 | 68 | // PropertySpec contains the metadata for an individual property on a component or endpoint 69 | type PropertySpec struct { 70 | Kind string `json:"kind"` 71 | Group string `json:"group"` 72 | Label string `json:"label"` 73 | Required bool `json:"required"` 74 | Type string `json:"type"` 75 | JavaType string `json:"javaType"` 76 | Enum []string `json:"enum"` 77 | Deprecated bool `json:"deprecated"` 78 | Secret bool `json:"secret"` 79 | Description string `json:"description"` 80 | } 81 | 82 | // ConnectorSchema holds the connector schema and metadata for the connector 83 | type ConnectorSchema struct { 84 | Component ComponentSpec `json:"component"` 85 | ComponentProperties map[string]PropertySpec `json:"componentProperties"` 86 | Properties map[string]PropertySpec `json:"properties"` 87 | } 88 | 89 | type FunkionConfig struct { 90 | Flows []FunktionFlow `json:"flows"` 91 | } 92 | 93 | type FunktionFlow struct { 94 | Name string `json:"name,omitempty"` 95 | Trace bool `json:"trace,omitempty"` 96 | LogResult bool `json:"logResult,omitempty"` 97 | Steps []FunktionStep `json:"steps"` 98 | } 99 | 100 | type FunktionStep struct { 101 | Kind string `json:"kind"` 102 | Name string `json:"name,omitempty"` 103 | URI string `json:"uri,omitempty"` 104 | Body string `json:"body,omitempty"` 105 | Headers map[string]string `json:"headers,omitempty"` 106 | } 107 | -------------------------------------------------------------------------------- /pkg/update/update.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package update 16 | 17 | import ( 18 | "crypto" 19 | "encoding/hex" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "net/http" 24 | "os" 25 | "runtime" 26 | "strings" 27 | "syscall" 28 | "time" 29 | 30 | "github.com/funktionio/funktion/pkg/config" 31 | "github.com/funktionio/funktion/pkg/constants" 32 | "github.com/funktionio/funktion/pkg/version" 33 | 34 | "github.com/blang/semver" 35 | "github.com/golang/glog" 36 | "github.com/google/go-github/github" 37 | update "github.com/inconshreveable/go-update" 38 | "github.com/kardianos/osext" 39 | githubutils "github.com/minishift/minishift/pkg/util/github" 40 | "github.com/spf13/viper" 41 | pb "gopkg.in/cheggaaa/pb.v1" 42 | "path/filepath" 43 | ) 44 | 45 | const ( 46 | timeLayout = time.RFC1123 47 | githubOwner = "funktionio" 48 | githubRepo = "funktion" 49 | ) 50 | 51 | var ( 52 | lastUpdateCheckFilePath = constants.MakeFunktionPath("last_update_check") 53 | ) 54 | 55 | func MaybeUpdateFromGithub(output io.Writer) { 56 | localVersion, err := version.GetSemverVersion() 57 | if err != nil { 58 | glog.Errorln(err) 59 | return 60 | } 61 | MaybeUpdate(output, githubOwner, githubRepo, githubRepo, lastUpdateCheckFilePath, localVersion, true) 62 | } 63 | 64 | func MaybeUpdate(output io.Writer, githubOwner, githubRepo, binaryName, lastUpdatePath string, localVersion semver.Version, viewLatest bool) { 65 | downloadBinary := binaryName + "-" + runtime.GOOS + "-" + runtime.GOARCH 66 | updateLinkPrefix := "https://github.com/" + githubOwner + "/" + githubRepo + "/releases/tag/" + version.VersionPrefix 67 | downloadLinkFormat := "https://github.com/" + githubOwner + "/" + githubRepo + "/releases/download/v%s/%s" 68 | 69 | if !shouldCheckURLVersion(lastUpdatePath) { 70 | return 71 | } 72 | latestVersion, err := getLatestVersionFromGitHub(githubOwner, githubRepo) 73 | if err != nil { 74 | glog.Errorln(err) 75 | return 76 | } 77 | if localVersion.Compare(latestVersion) < 0 { 78 | err := writeTimeToFile(lastUpdatePath, time.Now().UTC()) 79 | if err != nil { 80 | fmt.Println("Failed to update last update time") 81 | } 82 | fmt.Fprintf(output, `There is a newer version of %s available. Do you want to 83 | automatically update from %s%s to %s%s now? [y/N] `, 84 | binaryName, version.VersionPrefix, localVersion, version.VersionPrefix, latestVersion) 85 | 86 | var confirm string 87 | fmt.Scanln(&confirm) 88 | 89 | if strings.ToLower(confirm) == "y" { 90 | fmt.Printf("Updating to version %s\n", latestVersion) 91 | updateBinary(latestVersion, downloadBinary, updateLinkPrefix, downloadLinkFormat) 92 | return 93 | } 94 | 95 | fmt.Println("Skipping autoupdate") 96 | } else if viewLatest { 97 | fmt.Printf("latest available version is %s\n", latestVersion) 98 | } 99 | } 100 | 101 | func shouldCheckURLVersion(filePath string) bool { 102 | if !viper.GetBool(config.WantUpdateNotification) { 103 | fmt.Println("No update notification required") 104 | return false 105 | } 106 | lastUpdateTime := getTimeFromFileIfExists(filePath) 107 | if time.Since(lastUpdateTime).Hours() < viper.GetFloat64(config.ReminderWaitPeriodInHours) { 108 | return false 109 | } 110 | return true 111 | } 112 | 113 | func getLatestVersionFromGitHub(githubOwner, githubRepo string) (semver.Version, error) { 114 | client := githubutils.Client() 115 | var ( 116 | release *github.RepositoryRelease 117 | resp *github.Response 118 | err error 119 | ) 120 | release, resp, err = client.Repositories.GetLatestRelease(githubOwner, githubRepo) 121 | if err != nil { 122 | return semver.Version{}, err 123 | } 124 | defer func() { 125 | _ = resp.Body.Close() 126 | }() 127 | latestVersionString := release.TagName 128 | if latestVersionString != nil { 129 | return semver.Make(strings.TrimPrefix(*latestVersionString, "v")) 130 | 131 | } 132 | return semver.Version{}, fmt.Errorf("Cannot get release name") 133 | } 134 | 135 | func writeTimeToFile(path string, inputTime time.Time) error { 136 | dir := filepath.Dir(path) 137 | err := os.MkdirAll(dir, 0777) 138 | if err != nil { 139 | return fmt.Errorf("Failed to create directory %s due to: %v", dir, err) 140 | } 141 | err = ioutil.WriteFile(path, []byte(inputTime.Format(timeLayout)), 0644) 142 | if err != nil { 143 | return fmt.Errorf("Error writing current update time to file: %s", err) 144 | } 145 | return nil 146 | } 147 | 148 | func getTimeFromFileIfExists(path string) time.Time { 149 | lastUpdateCheckTime, err := ioutil.ReadFile(path) 150 | if err != nil { 151 | return time.Time{} 152 | } 153 | timeInFile, err := time.Parse(timeLayout, string(lastUpdateCheckTime)) 154 | if err != nil { 155 | return time.Time{} 156 | } 157 | return timeInFile 158 | } 159 | 160 | func updateBinary(v semver.Version, downloadBinary, updateLinkPrefix, downloadLinkFormat string) { 161 | checksum, err := downloadChecksum(v, downloadBinary, downloadLinkFormat) 162 | if err != nil { 163 | glog.Errorf("Cannot download checksum: %s", err) 164 | os.Exit(1) 165 | } 166 | 167 | currentBinary, err := osext.Executable() 168 | if err != nil { 169 | glog.Errorf("Cannot find current binary to exec: %s", err) 170 | os.Exit(1) 171 | } 172 | 173 | url := fmt.Sprintf(downloadLinkFormat, v, downloadBinary) 174 | updateBinaryFile(url, checksum) 175 | 176 | env := os.Environ() 177 | args := os.Args 178 | err = syscall.Exec(currentBinary, args, env) 179 | if err != nil { 180 | glog.Errorf("Failed to exec updated binary %s: %s", currentBinary, err) 181 | os.Exit(1) 182 | } 183 | } 184 | 185 | func updateBinaryFile(url string, checksum []byte) { 186 | fmt.Println("Downloading updated binary") 187 | httpResp, err := http.Get(url) 188 | if err != nil { 189 | glog.Errorf("Cannot download binary: %s", err) 190 | os.Exit(1) 191 | } 192 | defer func() { 193 | _ = httpResp.Body.Close() 194 | }() 195 | 196 | binary := httpResp.Body 197 | if httpResp.ContentLength > 0 { 198 | bar := pb.New64(httpResp.ContentLength).SetUnits(pb.U_BYTES) 199 | bar.Start() 200 | binary = bar.NewProxyReader(binary) 201 | defer func() { 202 | <-time.After(bar.RefreshRate) 203 | fmt.Println() 204 | }() 205 | } 206 | err = update.Apply(binary, update.Options{ 207 | Hash: crypto.SHA256, 208 | Checksum: checksum, 209 | }) 210 | if err != nil { 211 | glog.Errorf("Cannot apply binary update: %s", err) 212 | err := update.RollbackError(err) 213 | if err != nil { 214 | glog.Errorf("Failed to rollback update: %s", err) 215 | } 216 | os.Exit(1) 217 | } 218 | } 219 | 220 | func downloadChecksum(v semver.Version, downloadBinary, downloadLinkFormat string) ([]byte, error) { 221 | fmt.Println("Downloading updated binary checksum to validate updated binary") 222 | u := fmt.Sprintf(downloadLinkFormat, v, downloadBinary+".sha256") 223 | checksumResp, err := http.Get(u) 224 | if err != nil { 225 | return nil, err 226 | } 227 | defer func() { 228 | _ = checksumResp.Body.Close() 229 | }() 230 | 231 | checksum := checksumResp.Body 232 | if checksumResp.ContentLength > 0 { 233 | bar := pb.New64(checksumResp.ContentLength).SetUnits(pb.U_BYTES) 234 | bar.Start() 235 | checksum = bar.NewProxyReader(checksum) 236 | defer func() { 237 | <-time.After(2 * bar.RefreshRate) 238 | fmt.Println() 239 | }() 240 | } 241 | b, err := ioutil.ReadAll(checksum) 242 | if err != nil { 243 | return nil, err 244 | } 245 | if checksumResp.StatusCode != 200 { 246 | return nil, fmt.Errorf("received %d", checksumResp.StatusCode) 247 | } 248 | 249 | return hex.DecodeString(strings.TrimSpace(string(b))) 250 | } 251 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Red Hat, Inc. 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 | package version 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/blang/semver" 21 | ) 22 | 23 | const VersionPrefix = "v" 24 | 25 | // The current version of funktion 26 | // This is a private field and should be set when compiling with --ldflags="-X github.com/funktionio/funktion/pkg/version.version=vX.Y.Z" 27 | var version = "v0.0.0-unset" 28 | 29 | func GetVersion() string { 30 | return version 31 | } 32 | 33 | func GetSemverVersion() (semver.Version, error) { 34 | return semver.Make(strings.TrimPrefix(GetVersion(), VersionPrefix)) 35 | } 36 | -------------------------------------------------------------------------------- /scripts/check_license.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | licRes=$( 4 | for file in $(find . -type f -name '*.go' ! -path './vendor/*'); do 5 | head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}" 6 | done;) 7 | if [ -n "${licRes}" ]; then 8 | echo -e "license header checking failed:\n${licRes}" 9 | exit 255 10 | fi 11 | -------------------------------------------------------------------------------- /version/VERSION: -------------------------------------------------------------------------------- 1 | 1.0.14 --------------------------------------------------------------------------------