├── .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 |
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
--------------------------------------------------------------------------------