Compare commits

...
34 Commits
Author SHA1 Message Date
Ta-Ching Chen d6a8734aae Fission 1.7.0 2019-12-02 21:26:28 +08:00
Ta-Ching ChenandGitHub 3ee98cd13a Fix release script not uploads OpenShift deploy YAML file (#1456) 2019-12-02 21:12:15 +08:00
Ta-Ching ChenandGitHub 275da18cf6 Let executor type manages how to do cleanup for old kubeobjects (#1455)
Add CleanupOldExecutorObjects to executor type interface in order
to let an executor type manages how to clean up the resources it created.
2019-12-02 19:47:31 +08:00
Ta-Ching ChenandGitHub 3f3b11ffbf Prevent deployment from rolling update due to different instance-id (#1454)
The pod template is embedded inside the deployment. So if
the pod annotation contains instance-id, the deployment
will get updated and thus triggers a rolling update whenever
a new executor starts which is unwanted.

After this PR, poolmanager will patches instance-id when a
pod is chosen for a function.

For newdeploy, unlike poolmanager manages the lifecycle
of function pod directly, newdeploy is only responsible
to create the deployment so we append instance-id to top-
level controller (deployment) only.
2019-12-02 17:28:19 +08:00
Ta-Ching ChenandGitHub 7f8cb69326 Make AdoptExistingResources optional (#1453) 2019-12-02 07:34:07 +08:00
Ta-Ching ChenandGitHub 003c304105 Prevent newdeploy updates deployment if no resources changed (#1452)
We used to update timestamp in the deployment environment field 
in order to trigger a rolling update when the function referenced 
resources get updated. However, use timestamp means we are not 
able to avoid triggering a rolling update when executor tries to adopt 
orphaned deployment due to timestamp changed which is unwanted. 
In order to let executor adopt deployment without triggering a rolling 
update, we need an identical way to get a value that can reflect res-
ources changed without affecting by time.

To achieve this goal, the sum of the resource version of all referenced 
resources is a good fit for our scenario since the sum of the resource 
version is always the same as long as no resources changed.
2019-12-02 06:35:07 +08:00
Ta-Ching ChenandGitHub 763ab475f2 Fix CLI unable to get pod logs from controller (#1451) 2019-12-01 22:38:35 +08:00
Ta-Ching ChenandGitHub 4b3f48b537 Fix spec shows source archive is not used (#1448) 2019-12-01 16:51:13 +08:00
Ta-Ching ChenandGitHub 6301a78814 Ignore hidden file when creating archive file (#1450) 2019-12-01 09:58:16 +08:00
Ta-Ching ChenandGitHub 506b427124 Fix spec init overrides existing deploymentconfig (#1449) 2019-11-29 23:59:10 +08:00
Ta-Ching ChenandGitHub 19ae7d5ac6 Fix adopted deployment uses old fetcher image (#1447)
When a new executor starts up, it adopts the orphan kubernetes resources created
by the old executor instance. However, the adopted resource won't reflect the changes
come with the new executor, for example, the fetcher image inside won't be changed.

To solve this, executor updates the resource spec (HPA/Deployment/Service) with the
latest resources spec. By doing this, we can prevent the inconsistency between resources
created by different executor instance, also minimizes the impact on users.
2019-11-29 21:52:59 +08:00
Ta-Ching ChenandGitHub 47aaa85108 Improve executor bootstrap speed (#1446) 2019-11-29 14:18:23 +08:00
Ta-Ching Chen 3067ecdee3 CHANGELOG 1.7.0-rc.2 2019-11-27 17:10:57 +00:00
Ta-Ching Chen d9a0ee1e33 Fission 1.7.0-rc.2 2019-11-28 00:40:47 +08:00
Ta-Ching ChenandGitHub 5cfc8dee5e Push extra tag to fit go module semver tag format (#1444) 2019-11-28 00:40:20 +08:00
Ta-Ching ChenandGitHub 1ad7ac2dcf Adopt existing orphan kubernetes resources when executor starts up (#1443)
Previously, once the executor is deleted for reasons (like upgrade or cluster scale-in),
the new executor deletes all existing resources created by the old executor and creates
new one. This mechanism becomes a problem when there are requests connecting to the
existing pods. Also in the worst case, the cluster may not have enough resources to create
new pods and cause service downtime.

This PR let each executor type adopts existing resources before starting the executor
API services, and so the alive connections won't experience failure. However, the requests
send to the function that doesn't have alive function pods will still fail due to the
executor is in bootstrapping.
2019-11-27 23:08:45 +08:00
Ta-Ching ChenandGitHub 86446c8879 Revert "Try to fix flaky canary test (#1441)" (#1442)
This reverts commit df857e3194.
Looks like there are some problems when running the test against GKE, so revert it.
2019-11-27 16:19:33 +08:00
Ta-Ching ChenandGitHub df857e3194 Try to fix flaky canary test (#1441) 2019-11-27 02:57:22 +08:00
Ta-Ching ChenandGitHub ad7a3951c5 Fix router tries to update ingress when createIngress is false (#1440) 2019-11-27 00:28:48 +08:00
Ta-Ching ChenandGitHub 7e8e968013 Fix poolmanager sets 0 timeout for function specialization (#1439) 2019-11-26 19:22:49 +08:00
Ta-Ching ChenandGitHub e38baeec36 Add huge response body test (#1437) 2019-11-26 12:50:41 +08:00
Ta-Ching ChenandGitHub ca28f962d4 Return error when specialization failed (#1436) 2019-11-26 09:23:06 +08:00
Ta-Ching ChenandGitHub dfb2c073d2 Collect function metrics after finishing request (#1433) 2019-11-26 02:33:40 +08:00
Ta-Ching ChenandGitHub 51b264e8ca Fix poolmanager terminates running function pod periodically (#1435)
The pool manager keeps terminating function pod periodically even there are
traffic to the function. The root cause is that executor, poolmgr, newdeploy
manage their own functionServiceCache separately. And when router taps a
function, executor updates the access time of the function service entry in its
own cache without notifying executor types to do the update as well. Hence,
the access time of function service entry in poolmanager cache never gets updated.
Due to the access time never gets updated, the idle pod reaper in poolmanager
then thinks the function pod is in idle state and recycle it.

This PR removes the cache in executor itself, and when router tries to tap a function,
executor will call executor type to tap the function and update access time.
2019-11-26 01:16:30 +08:00
Ta-Ching ChenandGitHub 6d2fe08973 Allow to tap multiple function services at one time (#1434)
The router taps function service one by one which is inefficient and
increases the burden of executor. This PR aggregates all requests into
one to solve the problem mentioned above.
2019-11-25 18:44:48 +08:00
Ta-Ching ChenandGitHub ccc551112b Fix poolmanager crashes when failed to list environment (#1432) 2019-11-24 05:59:38 +08:00
Ta-Ching ChenandGitHub 1a5537f4ba Ability to pull builder image from private registry (#1431) 2019-11-24 05:17:56 +08:00
Ta-Ching ChenandGitHub 5c2f0c5f4a Add checksum and insecure flag for user to skip checksum generation (#1430) 2019-11-24 01:35:01 +08:00
Ta-Ching ChenandGitHub a66de4c601 Support to set imagePullSecret when creating environment (#1429) 2019-11-22 18:19:57 +08:00
Ta-Ching ChenandGitHub af73d0ce1a Fix no kubeobjs get created if fn created before env creation (#1428)
When a function is created before the creation of the environment it's used, the newdeploy will not be able to create kube objs. Hence no function service record is inserted into the cache.

When getFuncSvc is called, the newdeploy tries to find the record in service cache in order to create kube objs with the same name used in previous kubeobjs creation. However, due to no record in the cache, a NotFound error is returned directly and causes the problem. To solve this, we use fn meta UID to ensure we always get the same obj name instead of getting it from the cache.
2019-11-22 01:37:02 +08:00
Ta-Ching ChenandGitHub 04654ca465 Improve compatibility with Openshift (#1424) 2019-11-21 16:56:53 +08:00
Ta-Ching ChenandGitHub af10579be9 Fix verbosity flag not show in usage (#1425) 2019-11-21 12:36:50 +08:00
Ta-Ching ChenandGitHub 30d24a85b5 Fix truncated body returned from router (#1420)
If the context of request is closed before ReverseProxy finishing writing
a huge response body to the response writer, the client will only receive
a truncated response body.

To solve this, move the context cancel after ReverseProxy finished.
2019-11-21 01:04:35 +08:00
Ta-Ching Chen d8b386a812 Update CHANGELOG for 1.7.0-rc.1 2019-11-18 10:32:46 +00:00
80 changed files with 10822 additions and 1262 deletions
+117 -18
View File
@@ -1,35 +1,134 @@
# 1.6.0
# 1.7.0-rc.2
[Documentation](https://docs.fission.io/)
## Downloads for 1.6.0
## Downloads for 1.7.0-rc.2
filename | sha256 hash
-------- | -----------
[fission-core-1.6.0-minikube.yaml](https://github.com/fission/fission/releases/download/1.6.0/fission-core-1.6.0-minikube.yaml) | `a9786ff2b73f2c3b67337278707c7fb2d55bcee40524dc38bc9e3ec48410fa8b`
[fission-all-1.6.0.yaml](https://github.com/fission/fission/releases/download/1.6.0/fission-all-1.6.0.yaml) | `ef1c69a69d565a07f82e52936e256f7515eae1bc2b36d111ec43a2e5b6f07755`
[fission-core-1.6.0.yaml](https://github.com/fission/fission/releases/download/1.6.0/fission-core-1.6.0.yaml) | `7640294c0ab7a3192f147c1f17a3949c2f2c76ec9be70509f7a13ad7083318b6`
[fission-all-1.6.0-minikube.yaml](https://github.com/fission/fission/releases/download/1.6.0/fission-all-1.6.0-minikube.yaml) | `eb07b2ead721b98d4f26ffefcd03497125e8d017d82f1eaf9c0c2541d7c80288`
[fission-core-1.6.0.tgz](https://github.com/fission/fission/releases/download/1.6.0/fission-core-1.6.0.tgz) | `1dfbbc8dfa2f28bb498d3729a4bb608772d32b9644011f5e87d0dd79baa07c23`
[fission-all-1.6.0.tgz](https://github.com/fission/fission/releases/download/1.6.0/fission-all-1.6.0.tgz) | `83094e61b10a8bbc6ef7697b0f00f9f52f3c2cfb1590fcb7890eb1327d1b3c94`
[fission-cli-osx](https://github.com/fission/fission/releases/download/1.6.0/fission-cli-osx) | `0f15869c30988667e04780e01ad2ab3c5a51578c6dbbe7a54be43496d1973be5`
[fission-cli-windows.exe](https://github.com/fission/fission/releases/download/1.6.0/fission-cli-windows.exe) | `56f09af9cb26c8f4cf9cd27d06865f2f4b4138ea19fe13e9a0e8d17a2a2d405e`
[fission-cli-linux](https://github.com/fission/fission/releases/download/1.6.0/fission-cli-linux) | `90b1c14b584d52452089af351d5f615730fb5d5dd626884ec9b2808904b12df8`
[fission-all-1.7.0-rc.2-openshift.yaml](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-all-1.7.0-rc.2-openshift.yaml) | `67c0ffcb0e156b87237cd3a712a1435f09365701f4c516d5e9a83cb3f1c73041`
[fission-core-1.7.0-rc.2.yaml](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-core-1.7.0-rc.2.yaml) | `79e620b73b6a6b9f3f0290aa5b8b6daf7f77e938e34e839fc8b84cee00351b49`
[fission-core-1.7.0-rc.2-openshift.yaml](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-core-1.7.0-rc.2-openshift.yaml) | `a180331a308500cc6bc936b6c6f262bef69e6d75d5d0a1a54e5912845822ff51`
[fission-all-1.7.0-rc.2-minikube.yaml](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-all-1.7.0-rc.2-minikube.yaml) | `7c43a8acc3125e4a25444cedda15e045c90d911689cb18042ce7c4a0f7b6cc73`
[fission-core-1.7.0-rc.2-minikube.yaml](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-core-1.7.0-rc.2-minikube.yaml) | `9c052808104ae833e7d2ed23403f2c84a595f525ef7b9e53dee6b70a810cf032`
[fission-all-1.7.0-rc.2.yaml](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-all-1.7.0-rc.2.yaml) | `594ebff290c13ad590a0fa44bbbd45a5b5e0ee6fac853d197e7bc756773c5089`
[fission-core-1.7.0-rc.2.tgz](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-core-1.7.0-rc.2.tgz) | `8ba194e2bf9cf5ea95b851093675268d1bcb713bdf2f9f8ed0fae26fe7731718`
[fission-all-1.7.0-rc.2.tgz](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-all-1.7.0-rc.2.tgz) | `76359916272b408ee21e18a74e230d5f344bea5d95a050fc931547760a8b52b1`
[fission-cli-osx](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-cli-osx) | `0b79ba3bd1d74e3fdac12f966753c5268df3f32f7fdab3d11431fdfbd8d3e5d2`
[fission-cli-windows.exe](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-cli-windows.exe) | `9f94e3ad7248b7f5883e78a8a7c0fb41ec7467680be445582e7e5fb1f6a62a50`
[fission-cli-linux](https://github.com/fission/fission/releases/download/1.7.0-rc.2/fission-cli-linux) | `0f37a1164df007c540e36dead66226fc9919ad5182aa479baabc46da2e9ee72f`
# Change Log
## [1.6.0](https://github.com/fission/fission/tree/1.6.0) (2019-10-10)
[Full Changelog](https://github.com/fission/fission/compare/1.5.0...1.6.0)
## [1.7.0-rc.2](https://github.com/fission/fission/tree/1.7.0-rc.2) (2019-11-27)
[Full Changelog](https://github.com/fission/fission/compare/v1.7.0-rc.2...1.7.0-rc.2)
## [v1.7.0-rc.2](https://github.com/fission/fission/tree/v1.7.0-rc.2) (2019-11-27)
[Full Changelog](https://github.com/fission/fission/compare/1.7.0-rc.1...v1.7.0-rc.2)
**Merged pull requests:**
- Push extra tag to fit go module semver tag format [\#1444](https://github.com/fission/fission/pull/1444) ([life1347](https://github.com/life1347))
- Adopt existing orphan kubernetes resources when executor starts up [\#1443](https://github.com/fission/fission/pull/1443) ([life1347](https://github.com/life1347))
- Revert "Try to fix flaky canary test" [\#1442](https://github.com/fission/fission/pull/1442) ([life1347](https://github.com/life1347))
- Try to fix flaky canary test [\#1441](https://github.com/fission/fission/pull/1441) ([life1347](https://github.com/life1347))
- Fix router tries to update ingress when createIngress is false [\#1440](https://github.com/fission/fission/pull/1440) ([life1347](https://github.com/life1347))
- Fix poolmanager sets 0 timeout for function specialization [\#1439](https://github.com/fission/fission/pull/1439) ([life1347](https://github.com/life1347))
- Add huge response body test [\#1437](https://github.com/fission/fission/pull/1437) ([life1347](https://github.com/life1347))
- Return error when specialization failed [\#1436](https://github.com/fission/fission/pull/1436) ([life1347](https://github.com/life1347))
- Fix poolmanager terminates running function pod periodically [\#1435](https://github.com/fission/fission/pull/1435) ([life1347](https://github.com/life1347))
- Allow to tap multiple function services at one time [\#1434](https://github.com/fission/fission/pull/1434) ([life1347](https://github.com/life1347))
- Collect function metrics after finishing request [\#1433](https://github.com/fission/fission/pull/1433) ([life1347](https://github.com/life1347))
- Fix poolmanager crashes when failed to list environment [\#1432](https://github.com/fission/fission/pull/1432) ([life1347](https://github.com/life1347))
- Ability to pull builder image from private registry [\#1431](https://github.com/fission/fission/pull/1431) ([life1347](https://github.com/life1347))
- Add checksum and insecure flag for user to skip checksum generation [\#1430](https://github.com/fission/fission/pull/1430) ([life1347](https://github.com/life1347))
- Support to set imagePullSecret when creating environment [\#1429](https://github.com/fission/fission/pull/1429) ([life1347](https://github.com/life1347))
- Fix no kubeobjs get created if fn created before env creation [\#1428](https://github.com/fission/fission/pull/1428) ([life1347](https://github.com/life1347))
- Fix verbosity flag not found in subcommand [\#1425](https://github.com/fission/fission/pull/1425) ([life1347](https://github.com/life1347))
- Improve compatibility with Openshift [\#1424](https://github.com/fission/fission/pull/1424) ([life1347](https://github.com/life1347))
- Fix truncated body returned from router [\#1420](https://github.com/fission/fission/pull/1420) ([life1347](https://github.com/life1347))
- Fission 1.7.0-rc.1 [\#1419](https://github.com/fission/fission/pull/1419) ([life1347](https://github.com/life1347))
## [1.7.0-rc.1](https://github.com/fission/fission/tree/1.7.0-rc.1) (2019-11-18)
[Full Changelog](https://github.com/fission/fission/compare/v1.7.0-rc.1...1.7.0-rc.1)
## [v1.7.0-rc.1](https://github.com/fission/fission/tree/v1.7.0-rc.1) (2019-11-18)
[Full Changelog](https://github.com/fission/fission/compare/1.6.0...v1.7.0-rc.1)
**Merged pull requests:**
- Update release-builder go and helm version [\#1418](https://github.com/fission/fission/pull/1418) ([life1347](https://github.com/life1347))
- Fix failed to find init\_tools.sh [\#1417](https://github.com/fission/fission/pull/1417) ([life1347](https://github.com/life1347))
- Support semantic version tags [\#1416](https://github.com/fission/fission/pull/1416) ([life1347](https://github.com/life1347))
- Show warning when referencing nonexistent resources in spec [\#1415](https://github.com/fission/fission/pull/1415) ([life1347](https://github.com/life1347))
- Add resource info to output message when saving spec file [\#1414](https://github.com/fission/fission/pull/1414) ([life1347](https://github.com/life1347))
- Always embed given URL in the archive [\#1413](https://github.com/fission/fission/pull/1413) ([life1347](https://github.com/life1347))
- Fix wrong command and flag usage description [\#1412](https://github.com/fission/fission/pull/1412) ([life1347](https://github.com/life1347))
- Add --spec to package command [\#1411](https://github.com/fission/fission/pull/1411) ([life1347](https://github.com/life1347))
- Drop unreleased features \(record & replay\) [\#1406](https://github.com/fission/fission/pull/1406) ([life1347](https://github.com/life1347))
- Build error formatting on fission spec apply --wait [\#1403](https://github.com/fission/fission/pull/1403) ([life1347](https://github.com/life1347))
- Refactor controller client package [\#1402](https://github.com/fission/fission/pull/1402) ([life1347](https://github.com/life1347))
- Fix fn test failed to query logs from log database [\#1401](https://github.com/fission/fission/pull/1401) ([life1347](https://github.com/life1347))
- Skip trace for router healthz endpoint [\#1400](https://github.com/fission/fission/pull/1400) ([life1347](https://github.com/life1347))
- Set jaeger collector endpoint as an environment variable [\#1399](https://github.com/fission/fission/pull/1399) ([life1347](https://github.com/life1347))
- Replace deprecated serviceAccount with serviceAccountName [\#1398](https://github.com/fission/fission/pull/1398) ([life1347](https://github.com/life1347))
- Fix helm pre-upgrade check failure problem [\#1397](https://github.com/fission/fission/pull/1397) ([life1347](https://github.com/life1347))
- Prettify console output message [\#1396](https://github.com/fission/fission/pull/1396) ([life1347](https://github.com/life1347))
- fix typo [\#1395](https://github.com/fission/fission/pull/1395) ([jjmengze](https://github.com/jjmengze))
- Reorder command flags and add missing flags [\#1394](https://github.com/fission/fission/pull/1394) ([life1347](https://github.com/life1347))
- Fix CLI exits with status 0 when error occurs [\#1393](https://github.com/fission/fission/pull/1393) ([life1347](https://github.com/life1347))
- Poolmanager wait for function pod till specialization timeout [\#1392](https://github.com/fission/fission/pull/1392) ([life1347](https://github.com/life1347))
- Replace flag text with const [\#1391](https://github.com/fission/fission/pull/1391) ([life1347](https://github.com/life1347))
- Update prometheus version and disable it from the default installation [\#1389](https://github.com/fission/fission/pull/1389) ([life1347](https://github.com/life1347))
- Fix helm shows "Not a table" issue when install Fission [\#1387](https://github.com/fission/fission/pull/1387) ([life1347](https://github.com/life1347))
- Fix githook not aborting push if error occurs [\#1386](https://github.com/fission/fission/pull/1386) ([life1347](https://github.com/life1347))
- Migrate from urfave/cli to cobra [\#1385](https://github.com/fission/fission/pull/1385) ([life1347](https://github.com/life1347))
- Update maintainer info [\#1383](https://github.com/fission/fission/pull/1383) ([life1347](https://github.com/life1347))
- Update Makefile and add git pre-push hook [\#1382](https://github.com/fission/fission/pull/1382) ([life1347](https://github.com/life1347))
- Update staticcheck version and fix all warnings [\#1381](https://github.com/fission/fission/pull/1381) ([life1347](https://github.com/life1347))
- Make CLI functions return error instead of fatal out [\#1379](https://github.com/fission/fission/pull/1379) ([life1347](https://github.com/life1347))
- Refactor record command [\#1378](https://github.com/fission/fission/pull/1378) ([life1347](https://github.com/life1347))
- Fix reverse proxy shows 404 not found when Istio enabled [\#1377](https://github.com/fission/fission/pull/1377) ([life1347](https://github.com/life1347))
- Refactor time trigger command [\#1376](https://github.com/fission/fission/pull/1376) ([life1347](https://github.com/life1347))
- Refactor mqtrigger command [\#1375](https://github.com/fission/fission/pull/1375) ([life1347](https://github.com/life1347))
- added example for builder podspec [\#1374](https://github.com/fission/fission/pull/1374) ([viveksinghggits](https://github.com/viveksinghggits))
- Refactor function command [\#1372](https://github.com/fission/fission/pull/1372) ([life1347](https://github.com/life1347))
- Allow to set API type for tensorflow serving environment [\#1371](https://github.com/fission/fission/pull/1371) ([life1347](https://github.com/life1347))
- Refactor canary config command [\#1370](https://github.com/fission/fission/pull/1370) ([life1347](https://github.com/life1347))
- PodSpec support in environment builder [\#1369](https://github.com/fission/fission/pull/1369) ([viveksinghggits](https://github.com/viveksinghggits))
- Fix utility function uses the wrong flag text to get value [\#1368](https://github.com/fission/fission/pull/1368) ([life1347](https://github.com/life1347))
- Refactor HTTP trigger command [\#1367](https://github.com/fission/fission/pull/1367) ([life1347](https://github.com/life1347))
- Update READEME link and add back the basic usage [\#1366](https://github.com/fission/fission/pull/1366) ([life1347](https://github.com/life1347))
- Refactor kubewatch command [\#1365](https://github.com/fission/fission/pull/1365) ([life1347](https://github.com/life1347))
- Fix accidentally removed timestamp when listing package [\#1364](https://github.com/fission/fission/pull/1364) ([life1347](https://github.com/life1347))
- Move route creation to function [\#1362](https://github.com/fission/fission/pull/1362) ([life1347](https://github.com/life1347))
- Remove UID from CLI output [\#1361](https://github.com/fission/fission/pull/1361) ([life1347](https://github.com/life1347))
- Allow using URL as archive source when creating functions [\#1360](https://github.com/fission/fission/pull/1360) ([life1347](https://github.com/life1347))
- Refactor plugin & version subcommands [\#1359](https://github.com/fission/fission/pull/1359) ([life1347](https://github.com/life1347))
- Provide secrets and configmaps while updating the functions [\#1358](https://github.com/fission/fission/pull/1358) ([viveksinghggits](https://github.com/viveksinghggits))
- Update fission architecture doc [\#1356](https://github.com/fission/fission/pull/1356) ([life1347](https://github.com/life1347))
- calling the function that handles kafka messages, asynchronously [\#1355](https://github.com/fission/fission/pull/1355) ([viveksinghggits](https://github.com/viveksinghggits))
- Fix release script tags wrong image name & sed problem on Linux [\#1353](https://github.com/fission/fission/pull/1353) ([life1347](https://github.com/life1347))
- Update CHANGELOG for 1.6.0 [\#1352](https://github.com/fission/fission/pull/1352) ([life1347](https://github.com/life1347))
- Replace `AlwaysSample` with `ProbabilitySampler` in router \(\#1215\) [\#1348](https://github.com/fission/fission/pull/1348) ([ccamel](https://github.com/ccamel))
- Refactor package CLI command [\#1345](https://github.com/fission/fission/pull/1345) ([life1347](https://github.com/life1347))
## [1.6.0](https://github.com/fission/fission/tree/1.6.0) (2019-10-10)
[Full Changelog](https://github.com/fission/fission/compare/v1.6.0...1.6.0)
## [v1.6.0](https://github.com/fission/fission/tree/v1.6.0) (2019-10-10)
[Full Changelog](https://github.com/fission/fission/compare/1.5.0...v1.6.0)
**Merged pull requests:**
- Fission 1.6.0 [\#1351](https://github.com/fission/fission/pull/1351) ([life1347](https://github.com/life1347))
- Move statefulset to apps/v1 [\#1350](https://github.com/fission/fission/pull/1350) ([life1347](https://github.com/life1347))
- Fix newdeploy failed to find serviceEntry in cache [\#1349](https://github.com/fission/fission/pull/1349) ([life1347](https://github.com/life1347))
- Support encoded path in router [\#1347](https://github.com/fission/fission/pull/1347) ([life1347](https://github.com/life1347))
- Support custom private go vendor [\#1346](https://github.com/fission/fission/pull/1346) ([ti](https://github.com/ti))
- Support custom private go vendor [\#1346](https://github.com/fission/fission/pull/1346) ([life1347](https://github.com/life1347))
- Fix the namespace mismatch problem when deploying with a single YAML file [\#1344](https://github.com/fission/fission/pull/1344) ([life1347](https://github.com/life1347))
- Move charts executor service to the right place [\#1343](https://github.com/fission/fission/pull/1343) ([life1347](https://github.com/life1347))
- Allow to deploy router as DaemonSet [\#1342](https://github.com/fission/fission/pull/1342) ([life1347](https://github.com/life1347))
- add package filter feature [\#1341](https://github.com/fission/fission/pull/1341) ([MengZn](https://github.com/MengZn))
- add package filter feature [\#1341](https://github.com/fission/fission/pull/1341) ([jjmengze](https://github.com/jjmengze))
- Bump jvm builder JDK version [\#1340](https://github.com/fission/fission/pull/1340) ([life1347](https://github.com/life1347))
- Fix executor doesn't apply user-configured container spec correctly [\#1339](https://github.com/fission/fission/pull/1339) ([life1347](https://github.com/life1347))
- Allow to add annotations to router service in helm chart [\#1338](https://github.com/fission/fission/pull/1338) ([prabhu43](https://github.com/prabhu43))
@@ -155,6 +254,7 @@ filename | sha256 hash
- V1.2.1 [\#1178](https://github.com/fission/fission/pull/1178) ([vishal-biyani](https://github.com/vishal-biyani))
- Skaffold for Fission [\#1172](https://github.com/fission/fission/pull/1172) ([vishal-biyani](https://github.com/vishal-biyani))
- Add affinity support [\#1170](https://github.com/fission/fission/pull/1170) ([laurence-hudson-mindfoundry](https://github.com/laurence-hudson-mindfoundry))
- Using templated imagePullPolicy for containers in deployment.yaml [\#1137](https://github.com/fission/fission/pull/1137) ([msshroff](https://github.com/msshroff))
- Refactor test framework [\#1128](https://github.com/fission/fission/pull/1128) ([darkgerm](https://github.com/darkgerm))
- Pod specs [\#1106](https://github.com/fission/fission/pull/1106) ([vishal-biyani](https://github.com/vishal-biyani))
- Allow non-toplevel modules in python environment [\#1042](https://github.com/fission/fission/pull/1042) ([soamvasani](https://github.com/soamvasani))
@@ -181,13 +281,13 @@ filename | sha256 hash
- Fix TravisCI go environment version to avoid go bugs [\#1154](https://github.com/fission/fission/pull/1154) ([life1347](https://github.com/life1347))
- \#1132 nodejs environment, increase body size [\#1149](https://github.com/fission/fission/pull/1149) ([JannikZed](https://github.com/JannikZed))
- Added php builder to release script fixes \#1140 [\#1145](https://github.com/fission/fission/pull/1145) ([vishal-biyani](https://github.com/vishal-biyani))
- Using templated imagePullPolicy for containers in deployment.yaml [\#1137](https://github.com/fission/fission/pull/1137) ([msshroff](https://github.com/msshroff))
- Migrate from glide to official dependencies management tool: Go Module [\#1136](https://github.com/fission/fission/pull/1136) ([life1347](https://github.com/life1347))
- Fix misleading log when setup portforward [\#1134](https://github.com/fission/fission/pull/1134) ([life1347](https://github.com/life1347))
- V1.1.0 [\#1129](https://github.com/fission/fission/pull/1129) ([vishal-biyani](https://github.com/vishal-biyani))
- support KUBECONFIG with multiple kube config files [\#1126](https://github.com/fission/fission/pull/1126) ([grounded042](https://github.com/grounded042))
- Function update after change in env [\#1116](https://github.com/fission/fission/pull/1116) ([vishal-biyani](https://github.com/vishal-biyani))
- Add configurable timeout to fission function test [\#1091](https://github.com/fission/fission/pull/1091) ([erwinvaneyk](https://github.com/erwinvaneyk))
- Add links to examples for each Fission environment [\#1090](https://github.com/fission/fission/pull/1090) ([erwinvaneyk](https://github.com/erwinvaneyk))
## [1.1.0](https://github.com/fission/fission/tree/1.1.0) (2019-03-25)
[Full Changelog](https://github.com/fission/fission/compare/1.0.0...1.1.0)
@@ -205,7 +305,6 @@ filename | sha256 hash
- Added support for Ruby v2 Specialization [\#1101](https://github.com/fission/fission/pull/1101) ([brendanstennett](https://github.com/brendanstennett))
- V1.0.0 [\#1100](https://github.com/fission/fission/pull/1100) ([vishal-biyani](https://github.com/vishal-biyani))
- Adding annotations for prometheus scraping to fission-core [\#1098](https://github.com/fission/fission/pull/1098) ([vishal-biyani](https://github.com/vishal-biyani))
- Add links to examples for each Fission environment [\#1090](https://github.com/fission/fission/pull/1090) ([erwinvaneyk](https://github.com/erwinvaneyk))
- Switch from fluentd to fluentbit for log forwarding [\#1086](https://github.com/fission/fission/pull/1086) ([soamvasani](https://github.com/soamvasani))
- Added draft proposal for CI/CD [\#1084](https://github.com/fission/fission/pull/1084) ([vishal-biyani](https://github.com/vishal-biyani))
- \[Kafka MQT\] Add warning about Kafka version [\#1083](https://github.com/fission/fission/pull/1083) ([bhavin192](https://github.com/bhavin192))
@@ -470,7 +569,6 @@ filename | sha256 hash
- Changes needed for release 0.7.1 [\#622](https://github.com/fission/fission/pull/622) ([smruthi2187](https://github.com/smruthi2187))
- Add default value to cli flag [\#619](https://github.com/fission/fission/pull/619) ([life1347](https://github.com/life1347))
- Remove port forward in tests for router, controller and nats pods [\#611](https://github.com/fission/fission/pull/611) ([smruthi2187](https://github.com/smruthi2187))
- updates to changelog. [\#598](https://github.com/fission/fission/pull/598) ([smruthi2187](https://github.com/smruthi2187))
- meaningful error message when fetch request is received for a package when build is not successful. [\#661](https://github.com/fission/fission/pull/661) ([smruthi2187](https://github.com/smruthi2187))
- Delete deployment with proper delete propagation policy [\#630](https://github.com/fission/fission/pull/630) ([life1347](https://github.com/life1347))
- Fix buildmgr SEGFAULT when it failed to update package [\#626](https://github.com/fission/fission/pull/626) ([life1347](https://github.com/life1347))
@@ -485,6 +583,7 @@ filename | sha256 hash
- Prevent releasing idle connections because transport is shared. [\#609](https://github.com/fission/fission/pull/609) ([smruthi2187](https://github.com/smruthi2187))
- Fix components crash before crds creation [\#602](https://github.com/fission/fission/pull/602) ([life1347](https://github.com/life1347))
- updates to changelog. [\#598](https://github.com/fission/fission/pull/598) ([smruthi2187](https://github.com/smruthi2187))
- changes needed for release 0.7.0 [\#597](https://github.com/fission/fission/pull/597) ([smruthi2187](https://github.com/smruthi2187))
- `fission X create --spec` flags for env and trigger create commands [\#607](https://github.com/fission/fission/pull/607) ([soamvasani](https://github.com/soamvasani))
- Updating releasing guideliness with a few more details. [\#599](https://github.com/fission/fission/pull/599) ([smruthi2187](https://github.com/smruthi2187))
+3 -2
View File
@@ -43,10 +43,10 @@ Parameter | Description | Default
`routerServiceType` | Type of Fission Router service to use. For minikube, set this to NodePort, elsewhere use LoadBalancer or ClusterIP. | `LoadBalancer`
`repository` | Image base repository | `index.docker.io`
`image` | Fission image repository | `fission/fission-bundle`
`imageTag` | Fission image tag | `1.7.0-rc.1`
`imageTag` | Fission image tag | `1.7.0`
`pullPolicy` | Image pull policy | `IfNotPresent`
`fetcherImage` | Fission fetcher repository | `fission/fetcher`
`fetcherImageTag` | Fission fetcher image tag | `1.7.0-rc.1`
`fetcherImageTag` | Fission fetcher image tag | `1.7.0`
`controllerPort` | Fission Controller service port | `31313`
`routerPort` | Fission Router service port | ` 31314`
`functionNamespace` | Namespace in which to run fission functions (this is different from the release namespace) | `fission-function`
@@ -66,6 +66,7 @@ Parameter | Description | Default
`prometheus.serviceEndpoint` | If prometheus.enabled is false, please assign the prometheus service URL that is accessible by components. | `nil`
`canaryDeployment.enabled` | Set to true if you need canary deployment feature | `true` in `fission-all`, `false` in `fission-core`
`extraCoreComponentPodConfig` | Extend the container specs for the core fission pods. Can be used to add things like affinty/tolerations/nodeSelectors/etc. | None
`executor.adoptExistingResources` | If true, executor will try to adopt existing resources created by the old executor instance. | `false`
`router.deployAsDaemonSet` | Deploy router as DaemonSet instead of Deployment | `false`
`router.svcAddressMaxRetries` | Max retries times for router to retry on a certain service URL returns from cache/executor | `5`
`router.svcAddressUpdateTimeout` | The length of update lock expiry time for router to get a service URL returns from executor | `30`
+2 -2
View File
@@ -1,6 +1,6 @@
apiVersion: v1
name: fission-all
version: 1.7.0-rc.1
version: 1.7.0
description: Fission is a fast serverless framework for Kubernetes.
keywords:
- fission
@@ -12,4 +12,4 @@ maintainers:
- name: Ta Ching Chen
email: hello@tachingchen.com
engine: gotpl
appVersion: 1.7.0-rc.1
appVersion: 1.7.0
+2 -2
View File
@@ -2,5 +2,5 @@ dependencies:
- name: prometheus
repository: https://kubernetes-charts.storage.googleapis.com
version: 9.3.1
digest: sha256:33395db6ac17f57dab0e60a47a3e029c59aa4a58fb1695c73b8ea6f0754eaf0c
generated: 2019-11-09T02:56:08.384381+08:00
digest: sha256:89a63a5f01c08032106a94c59fc7a5929e449458bf5fd06d86c8b048ac628bf4
generated: "2019-11-18T10:16:20.334338911Z"
+2 -2
View File
@@ -215,6 +215,8 @@ spec:
value: "{{ .Values.pullPolicy }}"
- name: RUNTIME_IMAGE_PULL_POLICY
value: "{{ .Values.pullPolicy }}"
- name: ADOPT_EXISTING_RESOURCES
value: {{ .Values.executor.adoptExistingResources | default false | quote }}
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
- name: TRACE_JAEGER_COLLECTOR_ENDPOINT
@@ -534,10 +536,8 @@ spec:
]
ports:
- containerPort: 4222
hostPort: 4222
protocol: TCP
- containerPort: 4223
hostPort: 4223
protocol: TCP
readinessProbe:
httpGet:
@@ -45,6 +45,10 @@ spec:
- name: container-log
mountPath: /var/log/
readOnly: false
{{- if .Values.logger.enableSecurityContext }}
securityContext:
privileged: true
{{- end }}
containers:
- name: logger
{{ if .Values.repository }}
@@ -68,6 +72,10 @@ spec:
- name: docker-log
mountPath: /var/lib/docker/containers
readOnly: true
{{- if .Values.logger.enableSecurityContext }}
securityContext:
privileged: true
{{- end }}
- name: fluentbit
{{- if .Values.repository }}
image: "{{ .Values.logger.fluentdImageRepository }}/{{ .Values.logger.fluentdImage }}:{{ .Values.logger.fluentdImageTag }}"
@@ -96,6 +104,10 @@ spec:
key: password
- name: LOG_PATH
value: /var/log/fission/*.log
{{- if .Values.logger.enableSecurityContext }}
securityContext:
privileged: true
{{- end }}
volumeMounts:
- name: container-log
mountPath: /var/log/
+12 -2
View File
@@ -20,13 +20,13 @@ image: fission/fission-bundle
pullPolicy: IfNotPresent
## Fission image version
imageTag: 1.7.0-rc.1
imageTag: 1.7.0
## Fission fetcher repository
fetcherImage: fission/fetcher
## Fission fetcher image version
fetcherImageTag: 1.7.0-rc.1
fetcherImageTag: 1.7.0
## Port at which Fission controller service should be exposed
controllerPort: 31313
@@ -57,6 +57,16 @@ logger:
fluentdImageRepository: index.docker.io
fluentdImage: fluent/fluent-bit
fluentdImageTag: 1.0.4
## Fluent-bit writes/reads its own sqlite database to record a history of tracked
## files and a state of offsets, this is very useful to resume a state if the ser-
## vice is restarted. For Kubernetes environment with constraints like OpenShift,
## the containers are limited to write hostPath volume. Hence, we have to enable
## security context and set privileged to true.
enableSecurityContext: false
executor:
adoptExistingResources: false
## Router config
router:
+2 -2
View File
@@ -1,6 +1,6 @@
apiVersion: v1
name: fission-core
version: 1.7.0-rc.1
version: 1.7.0
description: Fission is a fast serverless framework for Kubernetes.
keywords:
- fission
@@ -12,4 +12,4 @@ maintainers:
- name: Ta Ching Chen
email: hello@tachingchen.com
engine: gotpl
appVersion: 1.7.0-rc.1
appVersion: 1.7.0
+2 -2
View File
@@ -2,5 +2,5 @@ dependencies:
- name: prometheus
repository: https://kubernetes-charts.storage.googleapis.com
version: 9.3.1
digest: sha256:33395db6ac17f57dab0e60a47a3e029c59aa4a58fb1695c73b8ea6f0754eaf0c
generated: 2019-11-09T02:56:58.282692+08:00
digest: sha256:89a63a5f01c08032106a94c59fc7a5929e449458bf5fd06d86c8b048ac628bf4
generated: "2019-11-18T10:16:25.945208326Z"
@@ -217,6 +217,8 @@ spec:
value: "{{ .Values.traceCollectorEndpoint }}"
- name: TRACING_SAMPLING_RATE
value: {{ .Values.traceSamplingRate | default "0.5" | quote }}
- name: ADOPT_EXISTING_RESOURCES
value: {{ .Values.executor.adoptExistingResources | default false | quote }}
- name: ENABLE_ISTIO
value: "{{ .Values.enableIstio }}"
- name: FETCHER_MINCPU
+5 -2
View File
@@ -17,7 +17,7 @@ repository: index.docker.io
image: fission/fission-bundle
## Fission image version
imageTag: 1.7.0-rc.1
imageTag: 1.7.0
## Image pull policy
pullPolicy: IfNotPresent
@@ -26,7 +26,7 @@ pullPolicy: IfNotPresent
fetcherImage: fission/fetcher
## Fission fetcher image version
fetcherImageTag: 1.7.0-rc.1
fetcherImageTag: 1.7.0
## Port at which Fission controller service should be exposed
controllerPort: 31313
@@ -45,6 +45,9 @@ builderNamespace: fission-builder
## Enable istio integration
enableIstio: false
executor:
adoptExistingResources: false
## Router config
router:
deployAsDaemonSet: false
+5 -3
View File
@@ -37,7 +37,7 @@ func App() *cobra.Command {
Use: "fission",
Long: usage,
//SilenceUsage: true,
PreRunE: wrapper.Wrapper(
PersistentPreRunE: wrapper.Wrapper(
func(input cli.Input) error {
console.Verbosity = input.Int(flagkey.Verbosity)
return nil
@@ -53,7 +53,7 @@ func App() *cobra.Command {
})
wrapper.SetFlags(rootCmd, flag.FlagSet{
Optional: []flag.Flag{flag.GlobalServer, flag.GlobalVerbosity},
Global: []flag.Flag{flag.GlobalServer, flag.GlobalVerbosity},
})
groups := helptemplate.CommandGroups{}
@@ -64,7 +64,9 @@ func App() *cobra.Command {
groups = append(groups, helptemplate.CreateCmdGroup("Other Commands", support.Commands(), version.Commands()))
groups.Add(rootCmd)
helptemplate.ActsAsRootCommand(rootCmd, nil, groups...)
flagExposer := helptemplate.ActsAsRootCommand(rootCmd, nil, groups...)
// show global options in usage
flagExposer.ExposeFlags(rootCmd, flagkey.Server, flagkey.Verbosity)
return rootCmd
}
+2 -3
View File
@@ -75,8 +75,7 @@ a. Switch to fission/fission-charts repo and run `index.sh` in that repo. (Don'
b. You should have index.yaml and fission-core-*, fission-all-* charts as change, add, commit and push it to repo.
8. Go to Github Web UI --> Releases, here you will notice a pre-release for the version x.y.z you just created.
9. Go to Github Web UI --> Releases, here you will notice a pre-release for the version x.y.z you just created.
Edit it and add links to:
@@ -88,7 +87,7 @@ Before you save the release - UNCHECK the "This is a pre-release" checkbox. This
Updating Fission Docs (https://github.com/fission/docs.fission.io)
------------------------------
9. Documentation Update
10. Documentation Update
a. Merge documentation PRs that are peer reviewed and get latest master locally.
+4 -2
View File
@@ -198,8 +198,8 @@ build_charts() {
do
# https://github.com/kubernetes/helm/issues/1732
helm init --client-only
helm package -u $c/
mv *.tgz $BUILDDIR/charts/
helm package -u $c/
mv *.tgz $BUILDDIR/charts/
done
popd
}
@@ -226,6 +226,8 @@ build_yamls() {
helm template ${c} -n ${releaseName} --namespace fission --set analytics=false,analyticsNonHelmInstall=true,serviceType=NodePort,routerServiceType=NodePort > ${c}-${version}-minikube.yaml
# for environments that support LoadBalancer
helm template ${c} -n ${releaseName} --namespace fission --set analytics=false,analyticsNonHelmInstall=true > ${c}-${version}.yaml
# for OpenShift
helm template ${c} -n ${releaseName} --namespace fission --set analytics=false,analyticsNonHelmInstall=true,logger.enableSecurityContext=true,prometheus.enabled=false > ${c}-${version}-openshift.yaml
# copy yaml files to build directory
mv *.yaml ${BUILDDIR}/yamls/
+29 -12
View File
@@ -14,8 +14,8 @@ check_branch() {
curr_branch=$(git rev-parse --abbrev-ref HEAD)
if [ $curr_branch != "release-${version}" ]
then
echo "Not on release-${version} branch."
exit 1
echo "Not on release-${version} branch."
exit 1
fi
}
@@ -23,8 +23,8 @@ check_branch() {
check_clean() {
if ! git diff-index --quiet HEAD --
then
echo "Unclean tree"
exit 1
echo "Unclean tree"
exit 1
fi
}
@@ -150,13 +150,22 @@ push_all() {
tag_and_release() {
local version=$1
local gittag="v${version}"
local gittag=$version
local prefix="v"
local gopkgtag=${version/#/${prefix}}
if [[ ${version} == v* ]]; # if version starts with "v", don't append prefix.
then
gopkgtag=${version}
fi
# tag the release
git tag $gittag
git tag -a $gopkgtag -m "Fission $gopkgtag"
# push tag
git push origin $gittag
git push origin $gopkgtag
# create gh release
gothub release \
@@ -170,7 +179,7 @@ tag_and_release() {
attach_github_release_cli() {
local version=$1
local gittag="v${version}"
local gittag=$version
# cli
echo "Uploading osx cli"
gothub upload \
@@ -195,21 +204,21 @@ attach_github_release_cli() {
--replace \
--user fission \
--repo fission \
--tag $gittag \
--tag $gittag \
--name fission-cli-windows.exe \
--file $BUILDDIR/cli/windows/fission-cli-windows.exe
}
attach_github_release_charts() {
local version=$1
local gittag="v${version}"
local gittag=$version
# helm charts
gothub upload \
--replace \
--user fission \
--repo fission \
--tag $gittag \
--tag $gittag \
--name fission-all-$version.tgz \
--file $BUILDDIR/charts/fission-all-$version.tgz
@@ -217,7 +226,7 @@ attach_github_release_charts() {
--replace \
--user fission \
--repo fission \
--tag $gittag \
--tag $gittag \
--name fission-core-$version.tgz \
--file $BUILDDIR/charts/fission-core-$version.tgz
@@ -225,7 +234,7 @@ attach_github_release_charts() {
attach_github_release_yamls() {
local version=$1
local gittag="v${version}"
local gittag=$version
for c in fission-all fission-core
do
@@ -245,6 +254,14 @@ attach_github_release_yamls() {
--tag $gittag \
--name ${c}-${version}.yaml \
--file $BUILDDIR/yamls/${c}-${version}.yaml
gothub upload \
--replace \
--user fission \
--repo fission \
--tag $gittag \
--name ${c}-${version}-openshift.yaml \
--file $BUILDDIR/yamls/${c}-${version}-openshift.yaml
done
}
+4 -5
View File
@@ -18,13 +18,12 @@ package v1
const (
EXECUTOR_INSTANCEID_LABEL string = "executorInstanceId"
POOLMGR_INSTANCEID_LABEL string = "poolmgrInstanceId"
DEFAULT_FUNCTION_TIMEOUT int = 60
)
const (
//LastUpdateTimestamp env variable is used for updating configmaps and secrets in pods
LastUpdateTimestamp string = "LASTUPDATE_TIMESTAMP"
// ResourceVersionCount env variable is used for updating configmaps and secrets in pods
ResourceVersionCount string = "RESOURCE_VERSION_COUNT"
)
const (
@@ -54,8 +53,8 @@ const (
)
const (
ExecutorTypePoolmgr = "poolmgr"
ExecutorTypeNewdeploy = "newdeploy"
ExecutorTypePoolmgr ExecutorType = "poolmgr"
ExecutorTypeNewdeploy ExecutorType = "newdeploy"
)
const (
+4 -1
View File
@@ -381,7 +381,6 @@ type (
TargetCPUPercent int
// This is the timeout setting for executor to wait for pod specialization.
// Currently, only newdeploy utilizes this value.
SpecializationTimeout int
}
@@ -529,6 +528,10 @@ type (
// or unarchived file should be placed, which is then used by specialize handler.
// (This is mainly for the JVM environment because .jar is one kind of zip archive.)
KeepArchive bool `json:"keeparchive"`
// ImagePullSecret is the secret for Kubernetes to pull an image from a
// private registry.
ImagePullSecret string `json:"imagepullsecret"`
}
AllowedFunctionsPerContainer string
+1 -2
View File
@@ -495,7 +495,6 @@ func (envw *environmentWatcher) createBuilderDeployment(env *fv1.Environment, ns
}
finalPodSpec, err := util.MergePodSpec(&podSpec, env.Spec.Builder.PodSpec)
if err != nil {
return nil, err
}
@@ -516,7 +515,7 @@ func (envw *environmentWatcher) createBuilderDeployment(env *fv1.Environment, ns
Labels: sel,
Annotations: podAnnotations,
},
Spec: *finalPodSpec,
Spec: *(util.ApplyImagePullSecret(env.Spec.ImagePullSecret, *finalPodSpec)),
},
},
}
+31
View File
@@ -19,10 +19,15 @@ package client
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/fission-cli/console"
)
func (c *Client) FunctionCreate(f *fv1.Function) (*metav1.ObjectMeta, error) {
@@ -152,3 +157,29 @@ func (c *Client) FunctionList(functionNamespace string) ([]fv1.Function, error)
return funcs, nil
}
func (c *Client) FunctionPodLogs(m *metav1.ObjectMeta) (io.ReadCloser, int, error) {
relativeUrl := fmt.Sprintf("functions/%v", m.Name)
relativeUrl += fmt.Sprintf("?namespace=%v", m.Namespace)
queryURL, err := url.Parse(c.Url)
if err != nil {
return nil, 0, errors.Wrapf(err, "error parsing the base URL '%v'", c.Url)
}
queryURL.Path = fmt.Sprintf("/proxy/logs/%s", m.Name)
console.Verbose(2, fmt.Sprintf("Try to get pod logs from controller '%v'", queryURL.String()))
req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil)
if err != nil {
return nil, 0, errors.Wrap(err, "error creating logs request")
}
httpClient := http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
return nil, 0, errors.Wrap(err, "error executing get logs request")
}
return resp.Body, resp.StatusCode, nil
}
+58 -28
View File
@@ -18,12 +18,15 @@ package controller
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"sort"
"strconv"
"strings"
"github.com/emicklei/go-restful"
restfulspec "github.com/emicklei/go-restful-openapi"
@@ -33,10 +36,12 @@ import (
"go.uber.org/zap"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
restclient "k8s.io/client-go/rest"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
ferror "github.com/fission/fission/pkg/error"
"github.com/fission/fission/pkg/types"
)
func RegisterFunctionRoute(ws *restful.WebService) {
@@ -280,27 +285,35 @@ func (a *API) FunctionLogsApiPost(w http.ResponseWriter, r *http.Request) {
func (a *API) FunctionPodLogs(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fnName := vars["function"]
ns := vars["namespace"]
ns := a.extractQueryParamFromRequest(r, "namespace")
podNs := "fission-function"
if len(ns) == 0 {
ns = "fission-function"
ns = metav1.NamespaceDefault
} else if ns != metav1.NamespaceDefault {
// If the function namespace is "default", executor
// will create function pods under "fission-function".
// Otherwise, the function pod will be created under
// the same namespace of function.
podNs = ns
}
f, err := a.fissionClient.Functions(ns).Get(fnName)
if err != nil {
a.respondWithError(w, err)
return
}
envName := f.Spec.Environment.Name
if err != nil {
a.respondWithError(w, err)
return
}
// Get function Pods first
selector := "functionName=" + fnName
podList, err := a.kubernetesClient.CoreV1().Pods(ns).List(metav1.ListOptions{LabelSelector: selector})
selector := map[string]string{
types.FUNCTION_UID: string(f.Metadata.UID),
types.ENVIRONMENT_NAME: f.Spec.Environment.Name,
types.ENVIRONMENT_NAMESPACE: f.Spec.Environment.Namespace,
}
podList, err := a.kubernetesClient.CoreV1().Pods(podNs).List(metav1.ListOptions{
LabelSelector: labels.Set(selector).AsSelector().String(),
})
if err != nil {
a.respondWithError(w, err)
return
@@ -309,30 +322,47 @@ func (a *API) FunctionPodLogs(w http.ResponseWriter, r *http.Request) {
// Get the logs for last Pod executed
pods := podList.Items
sort.Slice(pods, func(i, j int) bool {
itime := pods[i].ObjectMeta.CreationTimestamp.Time
jtime := pods[j].ObjectMeta.CreationTimestamp.Time
return itime.After(jtime)
rv1, _ := strconv.ParseInt(pods[i].ObjectMeta.ResourceVersion, 10, 32)
rv2, _ := strconv.ParseInt(pods[j].ObjectMeta.ResourceVersion, 10, 32)
return rv1 > rv2
})
podLogOpts := apiv1.PodLogOptions{Container: envName} // Only the env container, not fetcher
var podLogsReq *restclient.Request
if len(pods) > 0 {
podLogsReq = a.kubernetesClient.CoreV1().Pods(ns).GetLogs(pods[0].ObjectMeta.Name, &podLogOpts)
} else {
if len(pods) <= 0 {
a.respondWithError(w, errors.New("no active pods found"))
return
}
podLogs, err := podLogsReq.Stream()
// get the pod with highest resource version
err = getContainerLog(a.kubernetesClient, w, f, &pods[0])
if err != nil {
a.respondWithError(w, err)
return
}
defer podLogs.Close()
_, err = io.Copy(w, podLogs)
if err != nil {
a.respondWithError(w, err)
a.respondWithError(w, errors.Wrapf(err, "error getting container logs"))
return
}
}
func getContainerLog(kubernetesClient *kubernetes.Clientset, w http.ResponseWriter, fn *fv1.Function, pod *apiv1.Pod) error {
seq := strings.Repeat("=", 35)
for _, container := range pod.Spec.Containers {
podLogOpts := apiv1.PodLogOptions{Container: container.Name} // Only the env container, not fetcher
podLogsReq := kubernetesClient.CoreV1().Pods(pod.Namespace).GetLogs(pod.ObjectMeta.Name, &podLogOpts)
podLogs, err := podLogsReq.Stream()
if err != nil {
return errors.Wrapf(err, "error streaming pod log")
}
msg := fmt.Sprintf("\n%v\nFunction: %v\nEnvironment: %v\nNamespace: %v\nPod: %v\nContainer: %v\nNode: %v\n%v\n", seq,
fn.Metadata.Name, fn.Spec.Environment.Name, pod.Namespace, pod.Name, container.Name, pod.Spec.NodeName, seq)
w.Write([]byte(msg))
_, err = io.Copy(w, podLogs)
if err != nil {
return errors.Wrapf(err, "error copying pod log")
}
podLogs.Close()
}
return nil
}
+53 -20
View File
@@ -17,7 +17,6 @@ limitations under the License.
package executor
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@@ -25,6 +24,8 @@ import (
"strings"
"github.com/gorilla/mux"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"go.opencensus.io/plugin/ochttp"
"go.uber.org/zap"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -32,6 +33,7 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
ferror "github.com/fission/fission/pkg/error"
"github.com/fission/fission/pkg/executor/client"
)
func (executor *Executor) getServiceForFunctionApi(w http.ResponseWriter, r *http.Request) {
@@ -88,9 +90,15 @@ func (executor *Executor) getServiceForFunction(fn *fv1.Function) (string, error
zap.String("function_name", fn.Metadata.Name),
zap.String("function_namespace", fn.Metadata.Namespace))
fsvc, err := executor.fsCache.GetByFunction(&fn.Metadata)
t := fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType
et, exists := executor.executorTypes[t]
if !exists {
return "", errors.Errorf("Unknown executor type '%v'", t)
}
fsvc, err := et.GetFuncSvcFromCache(fn)
if err == nil {
if executor.isValidAddress(fsvc) {
if et.IsValid(fsvc) {
// Cached, return svc address
return fsvc.Address, nil
} else {
@@ -98,7 +106,7 @@ func (executor *Executor) getServiceForFunction(fn *fv1.Function) (string, error
zap.String("function_name", fn.Metadata.Name),
zap.String("function_namespace", fn.Metadata.Namespace),
zap.String("address", fsvc.Address))
executor.fsCache.DeleteEntry(fsvc)
et.DeleteFuncSvcFromCache(fsvc)
}
}
@@ -115,25 +123,57 @@ func (executor *Executor) getServiceForFunction(fn *fv1.Function) (string, error
}
// find funcSvc and update its atime
// TODO: Deprecated tapService
func (executor *Executor) tapService(w http.ResponseWriter, r *http.Request) {
// only for upgrade compatibility
w.WriteHeader(http.StatusOK)
}
// find funcSvc and update its atime
func (executor *Executor) tapServices(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
executor.logger.Error("failed to read tap service request", zap.Error(err))
http.Error(w, "Failed to read request", http.StatusInternalServerError)
return
}
svcName := string(body)
svcHost := strings.TrimPrefix(svcName, "http://")
err = executor.fsCache.TouchByAddress(svcHost)
tapSvcReqs := []client.TapServiceRequest{}
err = json.Unmarshal(body, &tapSvcReqs)
if err != nil {
executor.logger.Error("error tapping function service",
executor.logger.Error("failed to decode tap service request",
zap.Error(err),
zap.String("service", svcName),
zap.String("host", svcHost))
zap.String("request-payload", string(body)))
http.Error(w, "Failed to decode tap service request", http.StatusBadRequest)
return
}
errs := &multierror.Error{}
for _, req := range tapSvcReqs {
svcHost := strings.TrimPrefix(req.ServiceUrl, "http://")
et, exists := executor.executorTypes[req.FnExecutorType]
if !exists {
errs = multierror.Append(errs,
errors.Errorf("error tapping service due to unknown executor type '%v' found",
req.FnExecutorType))
continue
}
err = et.TapService(svcHost)
if err != nil {
errs = multierror.Append(errs,
errors.Wrapf(err, "'%v' failed to tap function '%v' in '%v' with service url '%v'",
req.FnMetadata.Name, req.FnMetadata.Namespace, req.ServiceUrl, req.FnExecutorType))
}
}
if errs.ErrorOrNil() != nil {
executor.logger.Error("error tapping function service", zap.Error(errs))
http.Error(w, "Not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
}
@@ -144,21 +184,14 @@ func (executor *Executor) healthHandler(w http.ResponseWriter, r *http.Request)
func (executor *Executor) GetHandler() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/v2/getServiceForFunction", executor.getServiceForFunctionApi).Methods("POST")
r.HandleFunc("/v2/tapService", executor.tapService).Methods("POST")
r.HandleFunc("/v2/tapService", executor.tapService).Methods("POST") // for backward compatibility
r.HandleFunc("/v2/tapServices", executor.tapServices).Methods("POST")
r.HandleFunc("/healthz", executor.healthHandler).Methods("GET")
return r
}
func (executor *Executor) Serve(port int) {
executor.logger.Info("starting executor", zap.Int("port", port))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
executor.ndm.Run(ctx)
executor.gpm.Run(ctx)
executor.cms.Run(ctx)
executor.logger.Info("starting executor API", zap.Int("port", port))
address := fmt.Sprintf(":%v", port)
err := http.ListenAndServe(address, &ochttp.Handler{
Handler: executor.GetHandler(),
+60 -28
View File
@@ -32,23 +32,31 @@ import (
"golang.org/x/net/context/ctxhttp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
ferror "github.com/fission/fission/pkg/error"
)
type Client struct {
logger *zap.Logger
executorUrl string
tappedByUrl map[string]bool
requestChan chan string
httpClient *http.Client
}
type (
Client struct {
logger *zap.Logger
executorUrl string
tappedByUrl map[string]TapServiceRequest
requestChan chan TapServiceRequest
httpClient *http.Client
}
TapServiceRequest struct {
FnMetadata metav1.ObjectMeta
FnExecutorType fv1.ExecutorType
ServiceUrl string
}
)
func MakeClient(logger *zap.Logger, executorUrl string) *Client {
c := &Client{
logger: logger.Named("executor_client"),
executorUrl: strings.TrimSuffix(executorUrl, "/"),
tappedByUrl: make(map[string]bool),
requestChan: make(chan string),
tappedByUrl: make(map[string]TapServiceRequest),
requestChan: make(chan TapServiceRequest, 100),
httpClient: &http.Client{
Transport: &ochttp.Transport{},
},
@@ -87,40 +95,64 @@ func (c *Client) service() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case serviceUrl := <-c.requestChan:
c.tappedByUrl[serviceUrl] = true
case svcReq := <-c.requestChan:
c.tappedByUrl[svcReq.ServiceUrl] = svcReq
case <-ticker.C:
urls := c.tappedByUrl
c.tappedByUrl = make(map[string]bool)
if len(urls) > 0 {
go func() {
for u := range urls {
err := c._tapService(u)
if err != nil {
c.logger.Error("error tapping function service address", zap.Error(err), zap.String("address", u))
}
}
c.logger.Debug("tapped services in batch", zap.Int("service_count", len(urls)))
}()
if len(c.tappedByUrl) == 0 {
continue
}
urls := c.tappedByUrl
c.tappedByUrl = make(map[string]TapServiceRequest)
go func() {
svcReqs := []TapServiceRequest{}
for _, req := range urls {
svcReqs = append(svcReqs, req)
}
c.logger.Debug("tapped services in batch", zap.Int("service_count", len(urls)))
err := c._tapService(svcReqs)
if err != nil {
c.logger.Error("error tapping function service address", zap.Error(err))
}
}()
}
}
}
func (c *Client) TapService(serviceUrl *url.URL) {
c.requestChan <- serviceUrl.String()
func (c *Client) TapService(fnMeta metav1.ObjectMeta, executorType fv1.ExecutorType, serviceUrl *url.URL) {
c.requestChan <- TapServiceRequest{
FnMetadata: metav1.ObjectMeta{
Name: fnMeta.Name,
Namespace: fnMeta.Namespace,
ResourceVersion: fnMeta.ResourceVersion,
UID: fnMeta.UID,
},
FnExecutorType: executorType,
// service url is for executor to know which
// pod/service is currently used to serve user function.
ServiceUrl: serviceUrl.String(),
}
}
func (c *Client) _tapService(serviceUrlStr string) error {
executorUrl := c.executorUrl + "/v2/tapService"
func (c *Client) _tapService(tapSvcReqs []TapServiceRequest) error {
executorUrl := c.executorUrl + "/v2/tapServices"
resp, err := http.Post(executorUrl, "application/octet-stream", bytes.NewReader([]byte(serviceUrlStr)))
body, err := json.Marshal(tapSvcReqs)
if err != nil {
return err
}
resp, err := http.Post(executorUrl, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return ferror.MakeErrorFromHTTP(resp)
}
return nil
}
+17 -20
View File
@@ -20,6 +20,7 @@ import (
"context"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,8 +30,7 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/crd"
nd "github.com/fission/fission/pkg/executor/newdeploy"
gpm "github.com/fission/fission/pkg/executor/poolmgr"
"github.com/fission/fission/pkg/executor/executortype"
)
type (
@@ -46,10 +46,10 @@ type (
//MakeConfigSecretController makes a controller for configmaps and secrets which changes related functions
func MakeConfigSecretController(logger *zap.Logger, fissionClient *crd.FissionClient,
kubernetesClient *kubernetes.Clientset, ndm *nd.NewDeploy, gpm *gpm.GenericPoolManager) *ConfigSecretController {
kubernetesClient *kubernetes.Clientset, types map[fv1.ExecutorType]executortype.ExecutorType) *ConfigSecretController {
logger.Debug("Creating ConfigMap & Secret Controller")
_, cmcontroller := initConfigmapController(logger, fissionClient, kubernetesClient, ndm, gpm)
_, scontroller := initSecretController(logger, fissionClient, kubernetesClient, ndm, gpm)
_, cmcontroller := initConfigmapController(logger, fissionClient, kubernetesClient, types)
_, scontroller := initSecretController(logger, fissionClient, kubernetesClient, types)
cmsController := &ConfigSecretController{
logger: logger,
configmapController: cmcontroller,
@@ -66,7 +66,7 @@ func (csController *ConfigSecretController) Run(ctx context.Context) {
}
func initConfigmapController(logger *zap.Logger, fissionClient *crd.FissionClient,
kubernetesClient *kubernetes.Clientset, ndm *nd.NewDeploy, gpm *gpm.GenericPoolManager) (cache.Store, cache.Controller) {
kubernetesClient *kubernetes.Clientset, types map[fv1.ExecutorType]executortype.ExecutorType) (cache.Store, cache.Controller) {
resyncPeriod := 30 * time.Second
listWatch := cache.NewListWatchFromClient(kubernetesClient.CoreV1().RESTClient(), "configmaps", metav1.NamespaceAll, fields.Everything())
store, controller := cache.NewInformer(listWatch, &apiv1.ConfigMap{}, resyncPeriod, cache.ResourceEventHandlerFuncs{
@@ -81,12 +81,11 @@ func initConfigmapController(logger *zap.Logger, fissionClient *crd.FissionClien
zap.String("configmap_name", newCm.ObjectMeta.Name),
zap.String("configmap_namespace", newCm.ObjectMeta.Namespace))
}
funcs, err := getConfigmapRelatedFuncs(logger, &newCm.ObjectMeta, fissionClient)
if err != nil {
logger.Error("Failed to get functions related to secret", zap.String("secret_name", newCm.ObjectMeta.Name), zap.String("secret_namespace", newCm.ObjectMeta.Namespace))
logger.Error("Failed to get functions related to configmap", zap.String("configmap_name", newCm.ObjectMeta.Name), zap.String("configmap_namespace", newCm.ObjectMeta.Namespace))
}
recyclePods(logger, funcs, ndm, gpm)
refreshPods(logger, funcs, types)
}
},
})
@@ -112,7 +111,7 @@ func getConfigmapRelatedFuncs(logger *zap.Logger, m *metav1.ObjectMeta, fissionC
}
func initSecretController(logger *zap.Logger, fissionClient *crd.FissionClient,
kubernetesClient *kubernetes.Clientset, ndm *nd.NewDeploy, gpm *gpm.GenericPoolManager) (cache.Store, cache.Controller) {
kubernetesClient *kubernetes.Clientset, types map[fv1.ExecutorType]executortype.ExecutorType) (cache.Store, cache.Controller) {
resyncPeriod := 30 * time.Second
listWatch := cache.NewListWatchFromClient(kubernetesClient.CoreV1().RESTClient(), "secrets", metav1.NamespaceAll, fields.Everything())
store, controller := cache.NewInformer(listWatch, &apiv1.Secret{}, resyncPeriod, cache.ResourceEventHandlerFuncs{
@@ -128,14 +127,12 @@ func initSecretController(logger *zap.Logger, fissionClient *crd.FissionClient,
zap.String("configmap_namespace", newS.ObjectMeta.Namespace))
}
funcs, err := getSecretRelatedFuncs(logger, &newS.ObjectMeta, fissionClient)
if err != nil {
logger.Error("Failed to get functions related to secret", zap.String("secret_name", newS.ObjectMeta.Name), zap.String("secret_namespace", newS.ObjectMeta.Namespace))
}
recyclePods(logger, funcs, ndm, gpm)
refreshPods(logger, funcs, types)
}
},
})
return store, controller
@@ -160,19 +157,19 @@ func getSecretRelatedFuncs(logger *zap.Logger, m *metav1.ObjectMeta, fissionClie
return relatedFunctions, nil
}
func recyclePods(logger *zap.Logger, funcs []fv1.Function, ndm *nd.NewDeploy, gpm *gpm.GenericPoolManager) {
func refreshPods(logger *zap.Logger, funcs []fv1.Function, types map[fv1.ExecutorType]executortype.ExecutorType) {
for _, f := range funcs {
var err error
switch f.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType {
case fv1.ExecutorTypeNewdeploy:
err = ndm.RefreshFuncPods(logger, f)
case fv1.ExecutorTypePoolmgr:
err = gpm.RefreshFuncPods(logger, f)
et, exists := types[f.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType]
if exists {
err = et.RefreshFuncPods(logger, f)
} else {
err = errors.Errorf("Unknown executor type '%v'", f.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType)
}
if err != nil {
logger.Error("Failed to recycle pods for function after configmap changed",
logger.Error("Failed to recycle pods for function after configmap/secret changed",
zap.Error(err),
zap.Any("function", f))
}
+72 -46
View File
@@ -20,6 +20,8 @@ import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
@@ -32,10 +34,12 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/crd"
"github.com/fission/fission/pkg/executor/cms"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/executor/executortype/newdeploy"
"github.com/fission/fission/pkg/executor/executortype/poolmgr"
"github.com/fission/fission/pkg/executor/fscache"
"github.com/fission/fission/pkg/executor/newdeploy"
"github.com/fission/fission/pkg/executor/poolmgr"
"github.com/fission/fission/pkg/executor/reaper"
"github.com/fission/fission/pkg/executor/util"
fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
)
@@ -43,12 +47,10 @@ type (
Executor struct {
logger *zap.Logger
gpm *poolmgr.GenericPoolManager
ndm *newdeploy.NewDeploy
cms *cms.ConfigSecretController
executorTypes map[fv1.ExecutorType]executortype.ExecutorType
cms *cms.ConfigSecretController
fissionClient *crd.FissionClient
fsCache *fscache.FunctionServiceCache
requestChan chan *createFuncServiceRequest
fsCreateWg map[string]*sync.WaitGroup
@@ -64,21 +66,26 @@ type (
}
)
func MakeExecutor(logger *zap.Logger, gpm *poolmgr.GenericPoolManager, ndm *newdeploy.NewDeploy, cms *cms.ConfigSecretController, fissionClient *crd.FissionClient, fsCache *fscache.FunctionServiceCache) *Executor {
func MakeExecutor(logger *zap.Logger, cms *cms.ConfigSecretController,
fissionClient *crd.FissionClient, types map[fv1.ExecutorType]executortype.ExecutorType) (*Executor, error) {
executor := &Executor{
logger: logger.Named("executor"),
gpm: gpm,
ndm: ndm,
cms: cms,
fissionClient: fissionClient,
fsCache: fsCache,
executorTypes: types,
requestChan: make(chan *createFuncServiceRequest),
fsCreateWg: make(map[string]*sync.WaitGroup),
}
for _, et := range types {
go func(et executortype.ExecutorType) {
et.Run(context.Background())
}(et)
}
go cms.Run(context.Background())
go executor.serveCreateFuncServices()
return executor
return executor, nil
}
// All non-cached function service requests go through this goroutine
@@ -113,8 +120,18 @@ func (executor *Executor) serveCreateFuncServices() {
// still can serve other subsequent requests.
buffer := 10 // add some buffer time for specialization
specializationTimeout := req.function.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout
// set minimum specialization timeout to avoid illegal input and
// compatibility problem when applying old spec file that doesn't
// have specialization timeout field.
if specializationTimeout < fv1.DefaultSpecializationTimeOut {
specializationTimeout = fv1.DefaultSpecializationTimeOut
}
fnSpecializationTimeoutContext, cancel := context.WithTimeout(context.Background(),
time.Duration(req.function.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout+buffer)*time.Second)
time.Duration(specializationTimeout+buffer)*time.Second)
defer cancel()
fsvc, err := executor.createServiceForFunction(fnSpecializationTimeoutContext, req.function)
req.respChan <- &createFuncServiceResponse{
@@ -122,8 +139,6 @@ func (executor *Executor) serveCreateFuncServices() {
err: err,
}
delete(executor.fsCreateWg, crd.CacheKey(fnMetadata))
cancel()
wg.Done()
}()
} else {
@@ -134,7 +149,7 @@ func (executor *Executor) serveCreateFuncServices() {
wg.Wait()
// get the function service from the cache
fsvc, err := executor.fsCache.GetByFunction(fnMetadata)
fsvc, err := executor.getFunctionServiceFromCache(req.function)
// fsCache return error when the entry does not exist/expire.
// It normally happened if there are multiple requests are
@@ -155,18 +170,13 @@ func (executor *Executor) createServiceForFunction(ctx context.Context, fn *fv1.
zap.String("function_name", fn.Metadata.Name),
zap.String("function_namespace", fn.Metadata.Namespace))
executorType := fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType
var fsvc *fscache.FuncSvc
var fsvcErr error
switch executorType {
case fv1.ExecutorTypeNewdeploy:
fsvc, fsvcErr = executor.ndm.GetFuncSvc(ctx, fn)
default:
fsvc, fsvcErr = executor.gpm.GetFuncSvc(ctx, fn)
t := fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType
e, ok := executor.executorTypes[t]
if !ok {
return nil, errors.Errorf("Unknown executor type '%v'", t)
}
fsvc, fsvcErr := e.GetFuncSvc(ctx, fn)
if fsvcErr != nil {
e := "error creating service for function"
executor.logger.Error(e,
@@ -174,25 +184,18 @@ func (executor *Executor) createServiceForFunction(ctx context.Context, fn *fv1.
zap.String("function_name", fn.Metadata.Name),
zap.String("function_namespace", fn.Metadata.Namespace))
fsvcErr = errors.Wrap(fsvcErr, fmt.Sprintf("[%s] %s", fn.Metadata.Name, e))
} else if fsvc != nil {
_, err := executor.fsCache.Add(*fsvc)
if err != nil {
return nil, err
}
}
executor.fsCache.IncreaseColdStarts(fn.Metadata.Name, string(fn.Metadata.UID))
return fsvc, fsvcErr
}
// isValidAddress invokes isValidService or isValidPod depending on the type of executor
func (executor *Executor) isValidAddress(fsvc *fscache.FuncSvc) bool {
if fsvc.Executor == fscache.NEWDEPLOY {
return executor.ndm.IsValid(fsvc)
} else {
return executor.gpm.IsValid(fsvc)
func (executor *Executor) getFunctionServiceFromCache(fn *fv1.Function) (*fscache.FuncSvc, error) {
t := fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType
e, ok := executor.executorTypes[t]
if !ok {
return nil, errors.Errorf("Unknown executor type '%v'", t)
}
return e.GetFuncSvcFromCache(fn)
}
func serveMetric(logger *zap.Logger) {
@@ -223,26 +226,49 @@ func StartExecutor(logger *zap.Logger, functionNamespace string, envBuilderNames
}
restClient := fissionClient.GetCrdClient()
fsCache := fscache.MakeFunctionServiceCache(logger)
executorInstanceID := strings.ToLower(uniuri.NewLen(8))
poolID := strings.ToLower(uniuri.NewLen(8))
reaper.CleanupOldExecutorObjects(logger, kubernetesClient, poolID)
go reaper.CleanupRoleBindings(logger, kubernetesClient, fissionClient, functionNamespace, envBuilderNamespace, time.Minute*30)
logger.Info("Starting executor", zap.String("instanceID", executorInstanceID))
gpm := poolmgr.MakeGenericPoolManager(
logger,
fissionClient, kubernetesClient,
functionNamespace, fetcherConfig, poolID)
functionNamespace, fetcherConfig, executorInstanceID)
ndm := newdeploy.MakeNewDeploy(
logger,
fissionClient, kubernetesClient, restClient,
functionNamespace, fetcherConfig, poolID)
functionNamespace, fetcherConfig, executorInstanceID)
cms := cms.MakeConfigSecretController(logger, fissionClient, kubernetesClient, ndm, gpm)
executorTypes := make(map[fv1.ExecutorType]executortype.ExecutorType)
executorTypes[gpm.GetTypeName()] = gpm
executorTypes[ndm.GetTypeName()] = ndm
api := MakeExecutor(logger, gpm, ndm, cms, fissionClient, fsCache)
adoptExistingResources, _ := strconv.ParseBool(os.Getenv("ADOPT_EXISTING_RESOURCES"))
wg := &sync.WaitGroup{}
for _, et := range executorTypes {
wg.Add(1)
go func(et executortype.ExecutorType) {
defer wg.Done()
if adoptExistingResources {
et.AdoptExistingResources()
}
et.CleanupOldExecutorObjects()
}(et)
}
// set hard timeout for resource adoption
// TODO: use context to control the waiting time once kubernetes client supports it.
util.WaitTimeout(wg, 30*time.Second)
cms := cms.MakeConfigSecretController(logger, fissionClient, kubernetesClient, executorTypes)
api, err := MakeExecutor(logger, cms, fissionClient, executorTypes)
if err != nil {
return err
}
go reaper.CleanupRoleBindings(logger, kubernetesClient, fissionClient, functionNamespace, envBuilderNamespace, time.Minute*30)
go api.Serve(port)
go serveMetric(logger)
+59
View File
@@ -0,0 +1,59 @@
/*
Copyright 2019 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package executortype
import (
"context"
"go.uber.org/zap"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/executor/fscache"
)
type ExecutorType interface {
// Run runs background job.
Run(context.Context)
// GetTypeName returns the name of executor type
GetTypeName() fv1.ExecutorType
// GetFuncSvc specializes function pod(s) and returns a service URL for the function.
GetFuncSvc(context.Context, *fv1.Function) (*fscache.FuncSvc, error)
// GetFuncSvcFromCache retrieves function service from cache.
GetFuncSvcFromCache(*fv1.Function) (*fscache.FuncSvc, error)
// DeleteFuncSvcFromCache deletes function service entry in cache.
DeleteFuncSvcFromCache(*fscache.FuncSvc)
// TapService updates the access time of function service entry to
// avoid idle pod reaper recycles pods.
TapService(serviceUrl string) error
// IsValid returns true if a function service is valid. Different executor types
// use distinct ways to examine the function service.
IsValid(*fscache.FuncSvc) bool
// RefreshFuncPods refreshes function pods if the secrets/configmaps pods reference to get updated.
RefreshFuncPods(*zap.Logger, fv1.Function) error
// AdoptOrphanResources adopts existing resources created by the deleted executor.
AdoptExistingResources()
// CleanupOldExecutorObjects cleans up resources created by old executor instances
CleanupOldExecutorObjects()
}
@@ -17,11 +17,12 @@ limitations under the License.
package newdeploy
import (
"errors"
"fmt"
"strconv"
"time"
multierror "github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
asv1 "k8s.io/api/autoscaling/v1"
@@ -30,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/executor/util"
@@ -43,32 +45,53 @@ const (
)
func (deploy *NewDeploy) createOrGetDeployment(fn *fv1.Function, env *fv1.Environment,
deployName string, deployLabels map[string]string, deployNamespace string, firstcreate bool) (*appsv1.Deployment, error) {
deployName string, deployLabels map[string]string, deployAnnotations map[string]string, deployNamespace string) (*appsv1.Deployment, error) {
minScale := int32(fn.Spec.InvokeStrategy.ExecutionStrategy.MinScale)
specializationTimeout := int(fn.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout)
minScale := int32(fn.Spec.InvokeStrategy.ExecutionStrategy.MinScale)
// If it's not the first time creation and minscale is 0 means that all pods for function were recycled,
// in such cases we need set minscale to 1 for router to serve requests.
if !firstcreate && minScale <= 0 {
// Always scale to at least one pod when createOrGetDeployment
// is called. The idleObjectReaper will scale-in the deployment
// later if no requests to the function.
if minScale <= 0 {
minScale = 1
}
waitForDeploy := minScale > 0
deployment, err := deploy.getDeploymentSpec(fn, env, &minScale, deployName, deployNamespace, deployLabels, deployAnnotations)
if err != nil {
return nil, err
}
existingDepl, err := deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Get(deployName, metav1.GetOptions{})
if err == nil {
if waitForDeploy {
// Try to adopt orphan deployment created by the old executor.
if existingDepl.Annotations[fv1.EXECUTOR_INSTANCEID_LABEL] != deploy.instanceID {
existingDepl.Annotations = deployment.Annotations
existingDepl.Labels = deployment.Labels
existingDepl.Spec.Template.Spec.Containers = deployment.Spec.Template.Spec.Containers
existingDepl.Spec.Template.Spec.ServiceAccountName = deployment.Spec.Template.Spec.ServiceAccountName
existingDepl.Spec.Template.Spec.TerminationGracePeriodSeconds = deployment.Spec.Template.Spec.TerminationGracePeriodSeconds
existingDepl, err = deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Update(existingDepl)
if err != nil {
deploy.logger.Warn("error adopting deploy", zap.Error(err),
zap.String("deploy", deployName), zap.String("ns", deployNamespace))
return nil, err
}
// In this case, we just return without waiting for it for fast bootstraping.
return existingDepl, nil
}
if *existingDepl.Spec.Replicas < minScale {
err = deploy.scaleDeployment(existingDepl.Namespace, existingDepl.Name, minScale)
if err != nil {
deploy.logger.Error("error scaling up function deployment", zap.Error(err), zap.String("function", fn.Metadata.Name))
return nil, err
}
if existingDepl.Status.AvailableReplicas < minScale {
existingDepl, err = deploy.waitForDeploy(existingDepl, minScale, specializationTimeout)
}
}
if existingDepl.Status.AvailableReplicas < minScale {
existingDepl, err = deploy.waitForDeploy(existingDepl, minScale, specializationTimeout)
}
return existingDepl, err
} else if k8s_err.IsNotFound(err) {
err := deploy.setupRBACObjs(deployNamespace, fn)
@@ -76,30 +99,26 @@ func (deploy *NewDeploy) createOrGetDeployment(fn *fv1.Function, env *fv1.Enviro
return nil, err
}
deployment, err := deploy.getDeploymentSpec(fn, env, deployName, deployLabels)
if err != nil {
return nil, err
}
depl, err := deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Create(deployment)
if err != nil {
deploy.logger.Error("error while creating function deployment",
zap.Error(err),
zap.String("function", fn.Metadata.Name),
zap.String("deployment_name", deployName),
zap.String("deployment_namespace", deployNamespace))
return nil, err
if k8s_err.IsAlreadyExists(err) {
depl, err = deploy.kubernetesClient.AppsV1().Deployments(deployNamespace).Get(deployName, metav1.GetOptions{})
}
if err != nil {
deploy.logger.Error("error while creating function deployment",
zap.Error(err),
zap.String("function", fn.Metadata.Name),
zap.String("deployment_name", deployName),
zap.String("deployment_namespace", deployNamespace))
return nil, err
}
}
if waitForDeploy {
if minScale > 0 {
depl, err = deploy.waitForDeploy(depl, minScale, specializationTimeout)
}
return depl, err
}
return nil, err
}
func (deploy *NewDeploy) setupRBACObjs(deployNamespace string, fn *fv1.Function) error {
@@ -157,10 +176,13 @@ func (deploy *NewDeploy) deleteDeployment(ns string, name string) error {
})
}
func (deploy *NewDeploy) getDeploymentSpec(fn *fv1.Function, env *fv1.Environment,
deployName string, deployLabels map[string]string) (*appsv1.Deployment, error) {
func (deploy *NewDeploy) getDeploymentSpec(fn *fv1.Function, env *fv1.Environment, targetReplicas *int32,
deployName string, deployNamespace string, deployLabels map[string]string, deployAnnotations map[string]string) (*appsv1.Deployment, error) {
replicas := int32(fn.Spec.InvokeStrategy.ExecutionStrategy.MinScale)
if targetReplicas != nil {
replicas = *targetReplicas
}
gracePeriodSeconds := int64(6 * 60)
if env.Spec.TerminationGracePeriod > 0 {
@@ -171,6 +193,10 @@ func (deploy *NewDeploy) getDeploymentSpec(fn *fv1.Function, env *fv1.Environmen
if podAnnotations == nil {
podAnnotations = make(map[string]string)
}
// Here, we don't append deployAnnotations to podAnnotations
// since newdeploy doesn't manager pod lifecycle directly.
if deploy.useIstio && env.Spec.AllowAccessToExternalNetwork {
podAnnotations["sidecar.istio.io/inject"] = "false"
}
@@ -193,6 +219,11 @@ func (deploy *NewDeploy) getDeploymentSpec(fn *fv1.Function, env *fv1.Environmen
// rollback, set RevisionHistoryLimit to 0 to disable this feature.
revisionHistoryLimit := int32(0)
rvCount, err := referencedResourcesRVSum(deploy.kubernetesClient, fn.Metadata.Namespace, fn.Spec.Secrets, fn.Spec.ConfigMaps)
if err != nil {
return nil, err
}
container, err := util.MergeContainer(&apiv1.Container{
Name: fn.Metadata.Name,
Image: env.Spec.Runtime.Image,
@@ -210,8 +241,8 @@ func (deploy *NewDeploy) getDeploymentSpec(fn *fv1.Function, env *fv1.Environmen
},
Env: []apiv1.EnvVar{
{
Name: fv1.LastUpdateTimestamp,
Value: time.Now().String(),
Name: fv1.ResourceVersionCount,
Value: fmt.Sprintf("%v", rvCount),
},
},
// https://istio.io/docs/setup/kubernetes/additional-setup/requirements/
@@ -227,27 +258,32 @@ func (deploy *NewDeploy) getDeploymentSpec(fn *fv1.Function, env *fv1.Environmen
return nil, err
}
pod := apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deployLabels,
Annotations: podAnnotations,
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{*container},
ServiceAccountName: "fission-fetcher",
TerminationGracePeriodSeconds: &gracePeriodSeconds,
},
}
pod.Spec = *(util.ApplyImagePullSecret(env.Spec.ImagePullSecret, pod.Spec))
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: deployName,
Labels: deployLabels,
Name: deployName,
Labels: deployLabels,
Annotations: deployAnnotations,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: deployLabels,
},
Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deployLabels,
Annotations: podAnnotations,
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{*container},
ServiceAccountName: "fission-fetcher",
TerminationGracePeriodSeconds: &gracePeriodSeconds,
},
},
Template: pod,
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
RollingUpdate: &appsv1.RollingUpdateDeployment{
@@ -315,7 +351,12 @@ func (deploy *NewDeploy) getResources(env *fv1.Environment, fn *fv1.Function) ap
return resources
}
func (deploy *NewDeploy) createOrGetHpa(hpaName string, execStrategy *fv1.ExecutionStrategy, depl *appsv1.Deployment) (*asv1.HorizontalPodAutoscaler, error) {
func (deploy *NewDeploy) createOrGetHpa(hpaName string, execStrategy *fv1.ExecutionStrategy,
depl *appsv1.Deployment, deployLabels map[string]string, deployAnnotations map[string]string) (*asv1.HorizontalPodAutoscaler, error) {
if depl == nil {
return nil, errors.New("failed to create HPA, found empty deployment")
}
minRepl := int32(execStrategy.MinScale)
if minRepl == 0 {
@@ -327,42 +368,52 @@ func (deploy *NewDeploy) createOrGetHpa(hpaName string, execStrategy *fv1.Execut
}
targetCPU := int32(execStrategy.TargetCPUPercent)
hpa := &asv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: hpaName,
Labels: deployLabels,
Annotations: deployAnnotations,
},
Spec: asv1.HorizontalPodAutoscalerSpec{
ScaleTargetRef: asv1.CrossVersionObjectReference{
Kind: DeploymentKind,
Name: depl.ObjectMeta.Name,
APIVersion: DeploymentVersion,
},
MinReplicas: &minRepl,
MaxReplicas: maxRepl,
TargetCPUUtilizationPercentage: &targetCPU,
},
}
existingHpa, err := deploy.kubernetesClient.AutoscalingV1().HorizontalPodAutoscalers(depl.ObjectMeta.Namespace).Get(hpaName, metav1.GetOptions{})
if err == nil {
return existingHpa, err
}
if depl == nil {
return nil, errors.New("failed to create HPA, found empty deployment")
}
if err != nil && k8s_err.IsNotFound(err) {
hpa := asv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: hpaName,
Labels: depl.Labels,
},
Spec: asv1.HorizontalPodAutoscalerSpec{
ScaleTargetRef: asv1.CrossVersionObjectReference{
Kind: DeploymentKind,
Name: depl.ObjectMeta.Name,
APIVersion: DeploymentVersion,
},
MinReplicas: &minRepl,
MaxReplicas: maxRepl,
TargetCPUUtilizationPercentage: &targetCPU,
},
// to adopt orphan service
if existingHpa.Annotations[fv1.EXECUTOR_INSTANCEID_LABEL] != deploy.instanceID {
existingHpa.Annotations = hpa.Annotations
existingHpa.Labels = hpa.Labels
existingHpa.Spec = hpa.Spec
existingHpa, err = deploy.kubernetesClient.AutoscalingV1().HorizontalPodAutoscalers(depl.ObjectMeta.Namespace).Update(existingHpa)
if err != nil {
deploy.logger.Warn("error adopting HPA", zap.Error(err),
zap.String("HPA", hpaName), zap.String("ns", depl.ObjectMeta.Namespace))
return nil, err
}
}
cHpa, err := deploy.kubernetesClient.AutoscalingV1().HorizontalPodAutoscalers(depl.ObjectMeta.Namespace).Create(&hpa)
return existingHpa, err
} else if k8s_err.IsNotFound(err) {
cHpa, err := deploy.kubernetesClient.AutoscalingV1().HorizontalPodAutoscalers(depl.ObjectMeta.Namespace).Create(hpa)
if err != nil {
return nil, err
if k8s_err.IsAlreadyExists(err) {
cHpa, err = deploy.kubernetesClient.AutoscalingV1().HorizontalPodAutoscalers(depl.ObjectMeta.Namespace).Get(hpaName, metav1.GetOptions{})
}
if err != nil {
return nil, err
}
}
return cHpa, nil
}
return nil, err
}
func (deploy *NewDeploy) getHpa(ns, name string) (*asv1.HorizontalPodAutoscaler, error) {
@@ -378,32 +429,52 @@ func (deploy *NewDeploy) deleteHpa(ns string, name string) error {
return deploy.kubernetesClient.AutoscalingV1().HorizontalPodAutoscalers(ns).Delete(name, &metav1.DeleteOptions{})
}
func (deploy *NewDeploy) createOrGetSvc(deployLabels map[string]string, svcName string, svcNamespace string) (*apiv1.Service, error) {
func (deploy *NewDeploy) createOrGetSvc(deployLabels map[string]string, deployAnnotations map[string]string, svcName string, svcNamespace string) (*apiv1.Service, error) {
service := &apiv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
Labels: deployLabels,
Annotations: deployAnnotations,
},
Spec: apiv1.ServiceSpec{
Ports: []apiv1.ServicePort{
{
Name: "http-env",
Port: int32(80),
TargetPort: intstr.FromInt(8888),
},
},
Selector: deployLabels,
Type: apiv1.ServiceTypeClusterIP,
},
}
existingSvc, err := deploy.kubernetesClient.CoreV1().Services(svcNamespace).Get(svcName, metav1.GetOptions{})
if err == nil {
// to adopt orphan service
if existingSvc.Annotations[fv1.EXECUTOR_INSTANCEID_LABEL] != deploy.instanceID {
existingSvc.Annotations = service.Annotations
existingSvc.Labels = service.Labels
existingSvc.Spec.Ports = service.Spec.Ports
existingSvc.Spec.Selector = service.Spec.Selector
existingSvc.Spec.Type = service.Spec.Type
existingSvc, err = deploy.kubernetesClient.CoreV1().Services(svcNamespace).Update(existingSvc)
if err != nil {
deploy.logger.Warn("error adopting service", zap.Error(err),
zap.String("service", svcName), zap.String("ns", svcNamespace))
return nil, err
}
}
return existingSvc, err
} else if k8s_err.IsNotFound(err) {
service := &apiv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
Labels: deployLabels,
},
Spec: apiv1.ServiceSpec{
Ports: []apiv1.ServicePort{
{
Name: "http-env",
Port: int32(80),
TargetPort: intstr.FromInt(8888),
},
},
Selector: deployLabels,
Type: apiv1.ServiceTypeClusterIP,
},
}
svc, err := deploy.kubernetesClient.CoreV1().Services(svcNamespace).Create(service)
if err != nil {
return nil, err
if k8s_err.IsAlreadyExists(err) {
svc, err = deploy.kubernetesClient.CoreV1().Services(svcNamespace).Get(svcName, metav1.GetOptions{})
}
if err != nil {
return nil, err
}
}
return svc, nil
}
@@ -472,3 +543,57 @@ func (deploy *NewDeploy) cleanupNewdeploy(ns string, name string) error {
return result.ErrorOrNil()
}
// referencedResourcesRVSum returns the sum of resource version of all resources the function references to.
// We used to update timestamp in the deployment environment field in order to trigger a rolling update when
// the function referenced resources get updated. However, use timestamp means we are not able to avoid tri-
// ggering a rolling update when executor tries to adopt orphaned deployment due to timestamp changed which
// is unwanted. In order to let executor adopt deployment without triggering a rolling update, we need an
// identical way to get a value that can reflect resources changed without affecting by the time.
// To achieve this goal, the sum of the resource version of all referenced resources is a good fit for our
// scenario since the sum of the resource version is always the same as long as no resources changed.
func referencedResourcesRVSum(client *kubernetes.Clientset, namespace string, secrets []fv1.SecretReference, cfgmaps []fv1.ConfigMapReference) (int, error) {
rvCount := 0
if len(secrets) > 0 {
list, err := client.CoreV1().Secrets(namespace).List(metav1.ListOptions{})
if err != nil {
return 0, err
}
objmap := make(map[string]apiv1.Secret)
for _, secret := range list.Items {
objmap[secret.Namespace+"/"+secret.Name] = secret
}
for _, ref := range secrets {
s, ok := objmap[ref.Namespace+"/"+ref.Name]
if ok {
rv, _ := strconv.ParseInt(s.ResourceVersion, 10, 32)
rvCount += int(rv)
}
}
}
if len(cfgmaps) > 0 {
list, err := client.CoreV1().ConfigMaps(namespace).List(metav1.ListOptions{})
if err != nil {
return 0, err
}
objmap := make(map[string]apiv1.ConfigMap)
for _, cfg := range list.Items {
objmap[cfg.Namespace+"/"+cfg.Name] = cfg
}
for _, ref := range cfgmaps {
s, ok := objmap[ref.Namespace+"/"+ref.Name]
if ok {
rv, _ := strconv.ParseInt(s.ResourceVersion, 10, 32)
rvCount += int(rv)
}
}
}
return rvCount, nil
}
@@ -22,11 +22,9 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/dchest/uniuri"
"github.com/fission/fission/pkg/throttler"
"github.com/fission/fission/pkg/utils"
multierror "github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"go.uber.org/zap"
@@ -43,11 +41,17 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/crd"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/executor/fscache"
"github.com/fission/fission/pkg/executor/reaper"
fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
"github.com/fission/fission/pkg/throttler"
"github.com/fission/fission/pkg/types"
"github.com/fission/fission/pkg/utils"
)
var _ executortype.ExecutorType = &NewDeploy{}
type (
NewDeploy struct {
logger *zap.Logger
@@ -83,7 +87,7 @@ func MakeNewDeploy(
namespace string,
fetcherConfig *fetcherConfig.Config,
instanceID string,
) *NewDeploy {
) executortype.ExecutorType {
enableIstio := false
if len(os.Getenv("ENABLE_ISTIO")) > 0 {
istio, err := strconv.ParseBool(os.Getenv("ENABLE_ISTIO"))
@@ -131,6 +135,173 @@ func (deploy *NewDeploy) Run(ctx context.Context) {
go deploy.idleObjectReaper()
}
func (deploy *NewDeploy) GetTypeName() fv1.ExecutorType {
return fv1.ExecutorTypeNewdeploy
}
func (deploy *NewDeploy) GetFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) {
// TODO: client-go doesn't support to pass in context.
// Once it supports context, we should change the signature of method.
// https://github.com/kubernetes/kubernetes/issues/46503
return deploy.createFunction(fn)
}
func (deploy *NewDeploy) GetFuncSvcFromCache(fn *fv1.Function) (*fscache.FuncSvc, error) {
return deploy.fsCache.GetByFunction(&fn.Metadata)
}
func (deploy *NewDeploy) DeleteFuncSvcFromCache(fsvc *fscache.FuncSvc) {
deploy.fsCache.DeleteEntry(fsvc)
}
func (deploy *NewDeploy) TapService(svcHost string) error {
err := deploy.fsCache.TouchByAddress(svcHost)
if err != nil {
return err
}
return nil
}
// IsValid does a get on the service address to ensure it's a valid service, then
// scale deployment to 1 replica if there are no available replicas for function.
// Return true if no error occurs, return false otherwise.
func (deploy *NewDeploy) IsValid(fsvc *fscache.FuncSvc) bool {
service := strings.Split(fsvc.Address, ".")
if len(service) == 0 {
return false
}
_, err := deploy.kubernetesClient.CoreV1().Services(service[1]).Get(service[0], metav1.GetOptions{})
if err != nil {
if !k8sErrs.IsNotFound(err) {
deploy.logger.Error("error validating function service address", zap.String("function", fsvc.Function.Name), zap.Error(err))
}
return false
}
deployObj := getDeploymentObj(fsvc.KubernetesObjects)
if deployObj == nil {
deploy.logger.Error("deployment obj for function does not exist", zap.String("function", fsvc.Function.Name))
return false
}
currentDeploy, err := deploy.kubernetesClient.AppsV1().
Deployments(deployObj.Namespace).Get(deployObj.Name, metav1.GetOptions{})
if err != nil {
if !k8sErrs.IsNotFound(err) {
deploy.logger.Error("error validating function deployment", zap.Error(err), zap.String("function", fsvc.Function.Name))
}
return false
}
// return directly when available replicas > 0
if currentDeploy.Status.AvailableReplicas > 0 {
return true
}
return false
}
// RefreshFuncPods deleted pods related to the function so that new pods are replenished
func (deploy *NewDeploy) RefreshFuncPods(logger *zap.Logger, f fv1.Function) error {
env, err := deploy.fissionClient.Environments(f.Spec.Environment.Namespace).Get(f.Spec.Environment.Name)
if err != nil {
return err
}
funcLabels := deploy.getDeployLabels(f.Metadata, metav1.ObjectMeta{
Name: f.Spec.Environment.Name,
Namespace: f.Spec.Environment.Namespace,
UID: env.Metadata.UID,
})
dep, err := deploy.kubernetesClient.AppsV1().Deployments(metav1.NamespaceAll).List(metav1.ListOptions{
LabelSelector: labels.Set(funcLabels).AsSelector().String(),
})
if err != nil {
return err
}
// Ideally there should be only one deployment but for now we rely on label/selector to ensure that condition
for _, deployment := range dep.Items {
rvCount, err := referencedResourcesRVSum(deploy.kubernetesClient, deployment.Namespace, f.Spec.Secrets, f.Spec.ConfigMaps)
if err != nil {
return err
}
patch := fmt.Sprintf(`{"spec" : {"template": {"spec":{"containers":[{"name": "%s", "env":[{"name": "%s", "value": "%v"}]}]}}}}`,
f.Metadata.Name, fv1.ResourceVersionCount, rvCount)
_, err = deploy.kubernetesClient.AppsV1().Deployments(deployment.ObjectMeta.Namespace).Patch(deployment.ObjectMeta.Name,
k8sTypes.StrategicMergePatchType,
[]byte(patch))
if err != nil {
return err
}
}
return nil
}
func (deploy *NewDeploy) AdoptExistingResources() {
fnList, err := deploy.fissionClient.Functions(metav1.NamespaceAll).List(metav1.ListOptions{})
if err != nil {
deploy.logger.Error("error getting function list", zap.Error(err))
return
}
wg := &sync.WaitGroup{}
for i := range fnList.Items {
fn := &fnList.Items[i]
if fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType == fv1.ExecutorTypeNewdeploy {
wg.Add(1)
go func() {
defer wg.Done()
_, err = deploy.fnCreate(fn)
if err != nil {
deploy.logger.Warn("failed to adopt resources for function", zap.Error(err))
return
}
deploy.logger.Info("adopt resources for function", zap.String("function", fn.Metadata.Name))
}()
}
}
wg.Wait()
}
func (deploy *NewDeploy) CleanupOldExecutorObjects() {
deploy.logger.Info("Newdeploy starts to clean orphaned resources", zap.String("instanceID", deploy.instanceID))
errs := &multierror.Error{}
listOpts := metav1.ListOptions{
LabelSelector: labels.Set(map[string]string{types.EXECUTOR_TYPE: string(fv1.ExecutorTypeNewdeploy)}).AsSelector().String(),
}
err := reaper.CleanupHpa(deploy.logger, deploy.kubernetesClient, deploy.instanceID, listOpts)
if err != nil {
errs = multierror.Append(errs, err)
}
err = reaper.CleanupDeployments(deploy.logger, deploy.kubernetesClient, deploy.instanceID, listOpts)
if err != nil {
errs = multierror.Append(errs, err)
}
err = reaper.CleanupServices(deploy.logger, deploy.kubernetesClient, deploy.instanceID, listOpts)
if err != nil {
errs = multierror.Append(errs, err)
}
if errs.ErrorOrNil() != nil {
// TODO retry reaper; logged and ignored for now
deploy.logger.Error("Failed to cleanup old executor objects", zap.Error(err))
}
}
func (deploy *NewDeploy) initFuncController() (k8sCache.Store, k8sCache.Controller) {
resyncPeriod := 30 * time.Second
listWatch := k8sCache.NewListWatchFromClient(deploy.crdClient, "functions", metav1.NamespaceAll, fields.Everything())
@@ -142,7 +313,7 @@ func (deploy *NewDeploy) initFuncController() (k8sCache.Store, k8sCache.Controll
go func() {
fn := obj.(*fv1.Function)
deploy.logger.Debug("create deployment for function", zap.Any("fn", fn.Metadata), zap.Any("fnspec", fn.Spec))
_, err := deploy.createFunction(fn, true)
_, err := deploy.createFunction(fn)
if err != nil {
deploy.logger.Error("error eager creating function",
zap.Error(err),
@@ -224,57 +395,14 @@ func (deploy *NewDeploy) getEnvFunctions(m *metav1.ObjectMeta) []fv1.Function {
return relatedFunctions
}
func (deploy *NewDeploy) GetFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) {
return deploy.createFunction(fn, false)
}
// RefreshFuncPods deleted pods related to the function so that new pods are replenished
func (deploy *NewDeploy) RefreshFuncPods(logger *zap.Logger, f fv1.Function) error {
env, err := deploy.fissionClient.Environments(f.Spec.Environment.Namespace).Get(f.Spec.Environment.Name)
if err != nil {
return err
}
funcLabels := deploy.getDeployLabels(f.Metadata, metav1.ObjectMeta{
Name: f.Spec.Environment.Name,
Namespace: f.Spec.Environment.Namespace,
UID: env.Metadata.UID,
})
dep, err := deploy.kubernetesClient.AppsV1().Deployments(metav1.NamespaceAll).List(metav1.ListOptions{
LabelSelector: labels.Set(funcLabels).AsSelector().String(),
})
if err != nil {
return err
}
patch := fmt.Sprintf(`{"spec" : {"template": {"spec":{"containers":[{"name": "%s", "env":[{"name": "%s", "value": "%s"}]}]}}}}`,
f.Metadata.Name,
fv1.LastUpdateTimestamp,
time.Now().String())
// Ideally there should be only one deployment but for now we rely on label/selector to ensure that condition
for _, deployment := range dep.Items {
_, err := deploy.kubernetesClient.AppsV1().Deployments(deployment.ObjectMeta.Namespace).Patch(deployment.ObjectMeta.Name,
k8sTypes.StrategicMergePatchType,
[]byte(patch))
if err != nil {
return err
}
}
return nil
}
func (deploy *NewDeploy) createFunction(fn *fv1.Function, firstcreate bool) (*fscache.FuncSvc, error) {
func (deploy *NewDeploy) createFunction(fn *fv1.Function) (*fscache.FuncSvc, error) {
if fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType != fv1.ExecutorTypeNewdeploy {
return nil, nil
}
fsvcObj, err := deploy.throttler.RunOnce(string(fn.Metadata.UID), func(ableToCreate bool) (interface{}, error) {
if ableToCreate {
return deploy.fnCreate(fn, firstcreate)
return deploy.fnCreate(fn)
}
return deploy.fsCache.GetByFunctionUID(fn.Metadata.UID)
})
@@ -307,7 +435,7 @@ func (deploy *NewDeploy) deleteFunction(fn *fv1.Function) error {
return err
}
func (deploy *NewDeploy) fnCreate(fn *fv1.Function, firstcreate bool) (*fscache.FuncSvc, error) {
func (deploy *NewDeploy) fnCreate(fn *fv1.Function) (*fscache.FuncSvc, error) {
env, err := deploy.fissionClient.
Environments(fn.Spec.Environment.Namespace).
Get(fn.Spec.Environment.Name)
@@ -316,15 +444,8 @@ func (deploy *NewDeploy) fnCreate(fn *fv1.Function, firstcreate bool) (*fscache.
}
objName := deploy.getObjName(fn)
if !firstcreate {
// retrieve back the previous obj name for later use.
fsvc, err := deploy.fsCache.GetByFunctionUID(fn.Metadata.UID)
if err != nil {
return nil, errors.Wrap(err, "error getting existed function service cache")
}
objName = fsvc.Name
}
deployLabels := deploy.getDeployLabels(fn.Metadata, env.Metadata)
deployAnnotations := deploy.getDeployAnnotations(fn.Metadata)
// to support backward compatibility, if the function was created in default ns, we fall back to creating the
// deployment of the function in fission-function ns
@@ -338,7 +459,7 @@ func (deploy *NewDeploy) fnCreate(fn *fv1.Function, firstcreate bool) (*fscache.
// Since newdeploy waits for pods of deployment to be ready,
// change the order of kubeObject creation (create service first,
// then deployment) to take advantage of waiting time.
svc, err := deploy.createOrGetSvc(deployLabels, objName, ns)
svc, err := deploy.createOrGetSvc(deployLabels, deployAnnotations, objName, ns)
if err != nil {
deploy.logger.Error("error creating service", zap.Error(err), zap.String("service", objName))
go deploy.cleanupNewdeploy(ns, objName)
@@ -346,14 +467,14 @@ func (deploy *NewDeploy) fnCreate(fn *fv1.Function, firstcreate bool) (*fscache.
}
svcAddress := fmt.Sprintf("%v.%v", svc.Name, svc.Namespace)
depl, err := deploy.createOrGetDeployment(fn, env, objName, deployLabels, ns, firstcreate)
depl, err := deploy.createOrGetDeployment(fn, env, objName, deployLabels, deployAnnotations, ns)
if err != nil {
deploy.logger.Error("error creating deployment", zap.Error(err), zap.String("deployment", objName))
go deploy.cleanupNewdeploy(ns, objName)
return nil, errors.Wrapf(err, "error creating deployment %v", objName)
}
hpa, err := deploy.createOrGetHpa(objName, &fn.Spec.InvokeStrategy.ExecutionStrategy, depl)
hpa, err := deploy.createOrGetHpa(objName, &fn.Spec.InvokeStrategy.ExecutionStrategy, depl, deployLabels, deployAnnotations)
if err != nil {
deploy.logger.Error("error creating HPA", zap.Error(err), zap.String("hpa", objName))
go deploy.cleanupNewdeploy(ns, objName)
@@ -394,7 +515,7 @@ func (deploy *NewDeploy) fnCreate(fn *fv1.Function, firstcreate bool) (*fscache.
Environment: env,
Address: svcAddress,
KubernetesObjects: kubeObjRefs,
Executor: fscache.NEWDEPLOY,
Executor: fv1.ExecutorTypeNewdeploy,
}
_, err = deploy.fsCache.Add(*fsvc)
@@ -402,6 +523,9 @@ func (deploy *NewDeploy) fnCreate(fn *fv1.Function, firstcreate bool) (*fscache.
deploy.logger.Error("error adding function to cache", zap.Error(err), zap.Any("function", fsvc.Function))
return fsvc, err
}
deploy.fsCache.IncreaseColdStarts(fn.Metadata.Name, string(fn.Metadata.UID))
return fsvc, nil
}
@@ -432,7 +556,7 @@ func (deploy *NewDeploy) updateFunction(oldFn *fv1.Function, newFn *fv1.Function
deploy.logger.Info("function type changed to new deployment, creating resources",
zap.Any("old_function", oldFn.Metadata),
zap.Any("new_function", newFn.Metadata))
_, err := deploy.createFunction(newFn, true)
_, err := deploy.createFunction(newFn)
if err != nil {
deploy.updateStatus(oldFn, err, "error changing the function's type to newdeploy")
}
@@ -544,12 +668,6 @@ func (deploy *NewDeploy) updateFuncDeployment(fn *fv1.Function, env *fv1.Environ
deploy.logger.Info("updating deployment due to function/environment update",
zap.String("deployment", fnObjName), zap.Any("function", fn.Metadata.Name))
newDeployment, err := deploy.getDeploymentSpec(fn, env, fnObjName, deployLabels)
if err != nil {
deploy.updateStatus(fn, err, "failed to get new deployment spec while updating function")
return err
}
// to support backward compatibility, if the function was created in default ns, we fall back to creating the
// deployment of the function in fission-function ns
ns := deploy.namespace
@@ -557,6 +675,22 @@ func (deploy *NewDeploy) updateFuncDeployment(fn *fv1.Function, env *fv1.Environ
ns = fn.Metadata.Namespace
}
existingDepl, err := deploy.kubernetesClient.AppsV1().Deployments(ns).Get(fnObjName, metav1.GetOptions{})
if err != nil {
return err
}
// the resource version inside function packageRef is changed,
// so the content of fetchRequest in deployment cmd is different.
// Therefore, the deployment update will trigger a rolling update.
newDeployment, err := deploy.getDeploymentSpec(fn, env,
existingDepl.Spec.Replicas, // use current replicas instead of minscale in the ExecutionStrategy.
fnObjName, ns, deployLabels, deploy.getDeployAnnotations(fn.Metadata))
if err != nil {
deploy.updateStatus(fn, err, "failed to get new deployment spec while updating function")
return err
}
err = deploy.updateDeployment(newDeployment, ns)
if err != nil {
deploy.updateStatus(fn, err, "failed to update deployment while updating function")
@@ -603,19 +737,27 @@ func (deploy *NewDeploy) fnDelete(fn *fv1.Function) error {
// getObjName returns a unique name for kubernetes objects of function
func (deploy *NewDeploy) getObjName(fn *fv1.Function) string {
return strings.ToLower(fmt.Sprintf("newdeploy-%v-%v-%v", fn.Metadata.Name, fn.Metadata.Namespace, uniuri.NewLen(8)))
// use meta uuid of function, this ensure we always get the same name for the same function.
uid := fn.Metadata.UID[len(fn.Metadata.UID)-17:]
return strings.ToLower(fmt.Sprintf("newdeploy-%v-%v-%v", fn.Metadata.Name, fn.Metadata.Namespace, uid))
}
func (deploy *NewDeploy) getDeployLabels(fnMeta metav1.ObjectMeta, envMeta metav1.ObjectMeta) map[string]string {
return map[string]string{
types.EXECUTOR_TYPE: string(fv1.ExecutorTypeNewdeploy),
types.ENVIRONMENT_NAME: envMeta.Name,
types.ENVIRONMENT_NAMESPACE: envMeta.Namespace,
types.ENVIRONMENT_UID: string(envMeta.UID),
types.FUNCTION_NAME: fnMeta.Name,
types.FUNCTION_NAMESPACE: fnMeta.Namespace,
types.FUNCTION_UID: string(fnMeta.UID),
}
}
func (deploy *NewDeploy) getDeployAnnotations(fnMeta metav1.ObjectMeta) map[string]string {
return map[string]string{
types.EXECUTOR_INSTANCEID_LABEL: deploy.instanceID,
types.EXECUTOR_TYPE: fv1.ExecutorTypeNewdeploy,
types.ENVIRONMENT_NAME: envMeta.Name,
types.ENVIRONMENT_NAMESPACE: envMeta.Namespace,
types.ENVIRONMENT_UID: string(envMeta.UID),
types.FUNCTION_NAME: fnMeta.Name,
types.FUNCTION_NAMESPACE: fnMeta.Namespace,
types.FUNCTION_UID: string(fnMeta.UID),
types.FUNCTION_RESOURCE_VERSION: fnMeta.ResourceVersion,
}
}
@@ -625,42 +767,6 @@ func (deploy *NewDeploy) updateStatus(fn *fv1.Function, err error, message strin
deploy.logger.Error("function status update", zap.Error(err), zap.Any("function", fn), zap.String("message", message))
}
// IsValid does a get on the service address to ensure it's a valid service, then
// scale deployment to 1 replica if there are no available replicas for function.
// Return true if no error occurs, return false otherwise.
func (deploy *NewDeploy) IsValid(fsvc *fscache.FuncSvc) bool {
service := strings.Split(fsvc.Address, ".")
if len(service) == 0 {
return false
}
_, err := deploy.kubernetesClient.CoreV1().Services(service[1]).Get(service[0], metav1.GetOptions{})
if err != nil {
deploy.logger.Error("error validating function service address", zap.String("function", fsvc.Function.Name), zap.Error(err))
return false
}
deployObj := getDeploymentObj(fsvc.KubernetesObjects)
if deployObj == nil {
deploy.logger.Error("deployment obj for function does not exist", zap.String("function", fsvc.Function.Name))
return false
}
currentDeploy, err := deploy.kubernetesClient.AppsV1().
Deployments(deployObj.Namespace).Get(deployObj.Name, metav1.GetOptions{})
if err != nil {
deploy.logger.Error("error validating function deployment", zap.Error(err), zap.String("function", fsvc.Function.Name))
return false
}
// return directly when available replicas > 0
if currentDeploy.Status.AvailableReplicas > 0 {
return true
}
return false
}
// idleObjectReaper reaps objects after certain idle time
func (deploy *NewDeploy) idleObjectReaper() {
@@ -685,7 +791,7 @@ func (deploy *NewDeploy) idleObjectReaper() {
}
for _, fsvc := range funcSvcs {
if fsvc.Executor != fscache.NEWDEPLOY {
if fsvc.Executor != fv1.ExecutorTypeNewdeploy {
continue
}
@@ -701,7 +807,7 @@ func (deploy *NewDeploy) idleObjectReaper() {
if err != nil {
// Newdeploy manager handles the function delete event and clean cache/kubeobjs itself,
// so we ignore the not found error for functions with newdeploy executor type here.
if k8sErrs.IsNotFound(err) && fsvc.Executor == fscache.NEWDEPLOY {
if k8sErrs.IsNotFound(err) && fsvc.Executor == fv1.ExecutorTypeNewdeploy {
continue
}
deploy.logger.Error("error getting function", zap.Error(err), zap.String("function", fsvc.Function.Name))
@@ -717,7 +823,7 @@ func (deploy *NewDeploy) idleObjectReaper() {
currentDeploy, err := deploy.kubernetesClient.AppsV1().
Deployments(deployObj.Namespace).Get(deployObj.Name, metav1.GetOptions{})
if err != nil {
deploy.logger.Error("error validating function deployment", zap.Error(err), zap.String("function", fsvc.Function.Name))
deploy.logger.Error("error getting function deployment", zap.Error(err), zap.String("function", fsvc.Function.Name))
continue
}
@@ -33,8 +33,10 @@ import (
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
k8sErrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
k8sTypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
@@ -64,9 +66,9 @@ type (
kubernetesClient *kubernetes.Clientset
fissionClient *crd.FissionClient
instanceId string // poolmgr instance id
labelsForPool map[string]string
requestChannel chan *choosePodRequest
fetcherConfig *fetcherConfig.Config
stopCh context.CancelFunc
}
// serialize the choosing of pods so that choices don't conflict
@@ -97,6 +99,8 @@ func MakeGenericPool(
gpLogger.Info("creating pool", zap.Any("environment", env.Metadata))
ctx, stopCh := context.WithCancel(context.Background())
// TODO: in general we need to provide the user a way to configure pools. Initial
// replicas, autoscaling params, various timeouts, etc.
gp := &GenericPool{
@@ -116,6 +120,7 @@ func MakeGenericPool(
instanceId: instanceId,
useSvc: false, // defaults off -- svc takes a second or more to become routable, slowing cold start
useIstio: enableIstio, // defaults off -- istio integration requires pod relabeling and it takes a second or more to become routable, slowing cold start
stopCh: stopCh,
}
gp.runtimeImagePullPolicy = utils.GetImagePullPolicy(os.Getenv("RUNTIME_IMAGE_PULL_POLICY"))
@@ -127,7 +132,7 @@ func MakeGenericPool(
}
// Labels for generic deployment/RS/pods.
gp.labelsForPool = gp.getDeployLabels()
//gp.labelsForPool = gp.getDeployLabels()
// create the pool
err = gp.createPool()
@@ -136,31 +141,41 @@ func MakeGenericPool(
}
gpLogger.Info("deployment created", zap.Any("environment", env.Metadata))
go gp.choosePodService()
go gp.choosePodService(ctx)
return gp, nil
}
func (gp *GenericPool) getDeployLabels() map[string]string {
func (gp *GenericPool) getEnvironmentPoolLabels() map[string]string {
return map[string]string{
types.EXECUTOR_TYPE: string(fv1.ExecutorTypePoolmgr),
types.ENVIRONMENT_NAME: gp.env.Metadata.Name,
types.ENVIRONMENT_NAMESPACE: gp.env.Metadata.Namespace,
types.ENVIRONMENT_UID: string(gp.env.Metadata.UID),
"managed": "true", // this allows us to easily find pods managed by the deployment
}
}
func (gp *GenericPool) getDeployAnnotations() map[string]string {
return map[string]string{
fv1.EXECUTOR_INSTANCEID_LABEL: gp.instanceId,
types.EXECUTOR_TYPE: fv1.ExecutorTypePoolmgr,
types.ENVIRONMENT_NAME: gp.env.Metadata.Name,
types.ENVIRONMENT_NAMESPACE: gp.env.Metadata.Namespace,
types.ENVIRONMENT_UID: string(gp.env.Metadata.UID),
"managed": "true", // this allows us to easily find pods managed by the deployment
}
}
// choosePodService serializes the choosing of pods
func (gp *GenericPool) choosePodService() {
for req := range gp.requestChannel {
pod, err := gp._choosePod(req.newLabels)
if err != nil {
req.responseChannel <- &choosePodResponse{error: err}
continue
func (gp *GenericPool) choosePodService(ctx context.Context) {
for {
select {
case req := <-gp.requestChannel:
pod, err := gp._choosePod(req.newLabels)
if err != nil {
req.responseChannel <- &choosePodResponse{error: err}
continue
}
req.responseChannel <- &choosePodResponse{pod: pod}
case <-ctx.Done():
return
}
req.responseChannel <- &choosePodResponse{pod: pod}
}
}
@@ -231,6 +246,11 @@ func (gp *GenericPool) _choosePod(newLabels map[string]string) (*apiv1.Pod, erro
// modified, this should fail; in that case just
// retry.
chosenPod.ObjectMeta.Labels = newLabels
// Append executor instance id to pod annotations to
// indicate this pod is managed by this executor.
chosenPod.ObjectMeta.Annotations = gp.getDeployAnnotations()
_, err = gp.kubernetesClient.CoreV1().Pods(gp.namespace).Update(chosenPod)
if err != nil {
gp.logger.Error("failed to relabel pod", zap.Error(err), zap.String("pod", chosenPod.ObjectMeta.Name))
@@ -244,7 +264,7 @@ func (gp *GenericPool) _choosePod(newLabels map[string]string) (*apiv1.Pod, erro
}
func (gp *GenericPool) labelsForFunction(metadata *metav1.ObjectMeta) map[string]string {
label := gp.getDeployLabels()
label := gp.getEnvironmentPoolLabels()
label[types.FUNCTION_NAME] = metadata.Name
label[types.FUNCTION_UID] = string(metadata.UID)
label[types.FUNCTION_NAMESPACE] = metadata.Namespace // function CRD must stay within same namespace of environment CRD
@@ -259,11 +279,6 @@ func (gp *GenericPool) scheduleDeletePod(name string) {
// cleaned up. (We need a better solutions for both those things; log
// aggregation and storage will help.)
gp.logger.Error("error in pod - scheduling cleanup", zap.String("pod", name))
// Ignore sleep here if istio feature is enabled, function pod
// will be deleted after 6 mins (terminationGracePeriodSeconds).
if !gp.useIstio {
time.Sleep(5 * time.Minute)
}
gp.kubernetesClient.CoreV1().Pods(gp.namespace).Delete(name, nil)
}()
}
@@ -330,12 +345,15 @@ func (gp *GenericPool) specializePod(ctx context.Context, pod *apiv1.Pod, fn *fv
// getPoolName returns a unique name of an environment
func (gp *GenericPool) getPoolName() string {
return strings.ToLower(fmt.Sprintf("poolmgr-%v-%v-%v", gp.env.Metadata.Name, gp.env.Metadata.Namespace, uniuri.NewLen(8)))
return strings.ToLower(fmt.Sprintf("poolmgr-%v-%v-%v", gp.env.Metadata.Name, gp.env.Metadata.Namespace, gp.env.Metadata.ResourceVersion))
}
// A pool is a deployment of generic containers for an env. This
// creates the pool but doesn't wait for any pods to be ready.
func (gp *GenericPool) createPool() error {
deployLabels := gp.getEnvironmentPoolLabels()
deployAnnotations := gp.getDeployAnnotations()
// Use long terminationGracePeriodSeconds for connection draining in case that
// pod still runs user functions.
gracePeriodSeconds := int64(6 * 60)
@@ -347,6 +365,12 @@ func (gp *GenericPool) createPool() error {
if podAnnotations == nil {
podAnnotations = make(map[string]string)
}
// Here, we don't append executor instance-id to pod annotations
// to prevent unwanted rolling updates occur. Pool manager will
// append executor instance-id to pod annotations when a pod is chosen
// for function specialization.
if gp.useIstio && gp.env.Spec.AllowAccessToExternalNetwork {
podAnnotations["sidecar.istio.io/inject"] = "false"
}
@@ -389,30 +413,35 @@ func (gp *GenericPool) createPool() error {
return err
}
pod := apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deployLabels,
Annotations: podAnnotations,
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{*container},
ServiceAccountName: "fission-fetcher",
// TerminationGracePeriodSeconds should be equal to the
// sleep time of preStop to make sure that SIGTERM is sent
// to pod after 6 mins.
TerminationGracePeriodSeconds: &gracePeriodSeconds,
},
}
pod.Spec = *(util.ApplyImagePullSecret(gp.env.Spec.ImagePullSecret, pod.Spec))
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: gp.getPoolName(),
Labels: gp.labelsForPool,
Name: gp.getPoolName(),
Labels: deployLabels,
Annotations: deployAnnotations,
},
Spec: appsv1.DeploymentSpec{
Replicas: &gp.replicas,
Selector: &metav1.LabelSelector{
MatchLabels: gp.labelsForPool,
},
Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: gp.labelsForPool,
Annotations: podAnnotations,
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{*container},
ServiceAccountName: "fission-fetcher",
// TerminationGracePeriodSeconds should be equal to the
// sleep time of preStop to make sure that SIGTERM is sent
// to pod after 6 mins.
TerminationGracePeriodSeconds: &gracePeriodSeconds,
},
MatchLabels: deployLabels,
},
Template: pod,
},
}
@@ -430,11 +459,25 @@ func (gp *GenericPool) createPool() error {
deployment.Spec.Template.Spec = *newPodSpec
}
depl, err := gp.kubernetesClient.AppsV1().Deployments(gp.namespace).Create(deployment)
depl, err := gp.kubernetesClient.AppsV1().Deployments(gp.namespace).Get(deployment.Name, metav1.GetOptions{})
if err == nil {
if depl.Annotations[fv1.EXECUTOR_INSTANCEID_LABEL] != gp.instanceId {
deployment.Annotations[fv1.EXECUTOR_INSTANCEID_LABEL] = gp.instanceId
depl, err = gp.kubernetesClient.AppsV1().Deployments(gp.namespace).Update(deployment)
}
gp.deployment = depl
return err
} else if !k8sErrs.IsNotFound(err) {
gp.logger.Error("error getting deployment in kubernetes", zap.Error(err), zap.String("deployment", deployment.Name))
return err
}
depl, err = gp.kubernetesClient.AppsV1().Deployments(gp.namespace).Create(deployment)
if err != nil {
gp.logger.Error("error creating deployment in kubernetes", zap.Error(err), zap.String("deployment", deployment.Name))
return err
}
gp.deployment = depl
return nil
}
@@ -467,14 +510,17 @@ func (gp *GenericPool) waitForReadyPod() error {
// Since even single pod is not ready, choosing the first pod to inspect is a good approximation. In future this can be done better
pod := podList.Items[0]
multierr := &multierror.Error{}
errs := &multierror.Error{}
for _, cStatus := range pod.Status.ContainerStatuses {
if !cStatus.Ready {
multierr = multierror.Append(multierr, errors.New(fmt.Sprintf("%v: %v", cStatus.State.Waiting.Reason, cStatus.State.Waiting.Message)))
errs = multierror.Append(errs, errors.New(fmt.Sprintf("%v: %v", cStatus.State.Waiting.Reason, cStatus.State.Waiting.Message)))
}
}
return errors.Wrapf(multierr, "Timeout: waited too long for pod of deployment %v in namespace %v to be ready",
gp.deployment.ObjectMeta.Name, gp.namespace)
if errs.ErrorOrNil() != nil {
return errors.Wrapf(errs, "Timeout: waited too long for pod of deployment %v in namespace %v to be ready",
gp.deployment.ObjectMeta.Name, gp.namespace)
}
return nil
}
time.Sleep(1000 * time.Millisecond)
}
@@ -483,14 +529,15 @@ func (gp *GenericPool) waitForReadyPod() error {
func (gp *GenericPool) createSvc(name string, labels map[string]string) (*apiv1.Service, error) {
service := apiv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Name: name,
Labels: labels,
},
Spec: apiv1.ServiceSpec{
Type: apiv1.ServiceTypeClusterIP,
Ports: []apiv1.ServicePort{
{
Protocol: apiv1.ProtocolTCP,
Port: 80,
Port: 8888,
TargetPort: intstr.FromInt(8888),
},
},
@@ -503,7 +550,7 @@ func (gp *GenericPool) createSvc(name string, labels map[string]string) (*apiv1.
func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) {
gp.logger.Info("choosing pod from pool", zap.Any("function", fn.Metadata))
newLabels := gp.labelsForFunction(&fn.Metadata)
funcLabels := gp.labelsForFunction(&fn.Metadata)
if gp.useIstio {
// Istio only allows accessing pod through k8s service, and requests come to
@@ -546,7 +593,7 @@ func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscac
}
}
pod, err := gp.choosePod(newLabels)
pod, err := gp.choosePod(funcLabels)
if err != nil {
return nil, err
}
@@ -565,7 +612,7 @@ func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscac
svcName = fmt.Sprintf("%s-%v", svcName, fn.Metadata.UID)
}
svc, err := gp.createSvc(svcName, newLabels)
svc, err := gp.createSvc(svcName, funcLabels)
if err != nil {
gp.scheduleDeletePod(pod.ObjectMeta.Name)
return nil, err
@@ -577,7 +624,7 @@ func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscac
// the fission router isn't in the same namespace, so return a
// namespace-qualified hostname
svcHost = fmt.Sprintf("%v.%v", svcName, gp.namespace)
svcHost = fmt.Sprintf("%v.%v:8888", svcName, gp.namespace)
} else if gp.useIstio {
svc := utils.GetFunctionIstioServiceName(fn.Metadata.Name, fn.Metadata.Namespace)
svcHost = fmt.Sprintf("%v.%v:8888", svc, gp.namespace)
@@ -585,6 +632,18 @@ func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscac
svcHost = fmt.Sprintf("%v:8888", pod.Status.PodIP)
}
// patch svc-host and resource version to the pod annotations for new executor to adopt the pod
patch := fmt.Sprintf(`{"metadata":{"annotations":{"%v":"%v","%v":"%v"}}}`,
types.ANNOTATION_SVC_HOST, svcHost, types.FUNCTION_RESOURCE_VERSION, fn.Metadata.ResourceVersion)
p, err := gp.kubernetesClient.CoreV1().Pods(pod.Namespace).Patch(pod.Name, k8sTypes.StrategicMergePatchType, []byte(patch))
if err != nil {
// just log the error since it won't affect the function serving
gp.logger.Warn("error patching svc-host to pod", zap.Error(err),
zap.String("pod", pod.Name), zap.String("ns", pod.Namespace))
} else {
pod = p
}
gp.logger.Info("specialized pod",
zap.String("pod", pod.ObjectMeta.Name),
zap.String("podNamespace", pod.ObjectMeta.Namespace),
@@ -610,7 +669,7 @@ func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscac
Environment: gp.env,
Address: svcHost,
KubernetesObjects: kubeObjRefs,
Executor: fscache.POOLMGR,
Executor: fv1.ExecutorTypePoolmgr,
Ctime: time.Now(),
Atime: time.Now(),
}
@@ -619,15 +678,21 @@ func (gp *GenericPool) getFuncSvc(ctx context.Context, fn *fv1.Function) (*fscac
if err != nil {
return nil, err
}
gp.fsCache.IncreaseColdStarts(fn.Metadata.Name, string(fn.Metadata.UID))
return fsvc, nil
}
// destroys the pool -- the deployment, replicaset and pods
func (gp *GenericPool) destroy() error {
gp.stopCh()
deletePropagation := metav1.DeletePropagationBackground
delOpt := metav1.DeleteOptions{
PropagationPolicy: &deletePropagation,
}
err := gp.kubernetesClient.AppsV1().
Deployments(gp.namespace).Delete(gp.deployment.ObjectMeta.Name, &delOpt)
if err != nil {
@@ -18,13 +18,17 @@ package poolmgr
import (
"context"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/fission/fission/pkg/utils"
"github.com/hashicorp/go-multierror"
"go.uber.org/zap"
apiv1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
@@ -35,12 +39,16 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/cache"
"github.com/fission/fission/pkg/crd"
"github.com/fission/fission/pkg/executor/executortype"
"github.com/fission/fission/pkg/executor/fscache"
"github.com/fission/fission/pkg/executor/reaper"
fetcherConfig "github.com/fission/fission/pkg/fetcher/config"
"github.com/fission/fission/pkg/types"
"github.com/fission/fission/pkg/utils"
)
var _ executortype.ExecutorType = &GenericPoolManager{}
type requestType int
const (
@@ -90,7 +98,7 @@ func MakeGenericPoolManager(
kubernetesClient *kubernetes.Clientset,
functionNamespace string,
fetcherConfig *fetcherConfig.Config,
instanceId string) *GenericPoolManager {
instanceId string) executortype.ExecutorType {
gpmLogger := logger.Named("generic_pool_manager")
@@ -107,6 +115,7 @@ func MakeGenericPoolManager(
idlePodReapTime: 2 * time.Minute,
fetcherConfig: fetcherConfig,
}
go gpm.service()
go gpm.eagerPoolCreator()
@@ -132,6 +141,71 @@ func (gpm *GenericPoolManager) Run(ctx context.Context) {
go gpm.idleObjectReaper()
}
func (gpm *GenericPoolManager) GetTypeName() fv1.ExecutorType {
return fv1.ExecutorTypePoolmgr
}
func (gpm *GenericPoolManager) GetFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) {
// from Func -> get Env
gpm.logger.Debug("getting environment for function", zap.String("function", fn.Metadata.Name))
env, err := gpm.getFunctionEnv(fn)
if err != nil {
return nil, err
}
pool, err := gpm.getPool(env)
if err != nil {
return nil, err
}
// from GenericPool -> get one function container
// (this also adds to the cache)
gpm.logger.Debug("getting function service from pool", zap.String("function", fn.Metadata.Name))
return pool.getFuncSvc(ctx, fn)
}
func (gpm *GenericPoolManager) GetFuncSvcFromCache(fn *fv1.Function) (*fscache.FuncSvc, error) {
return gpm.fsCache.GetByFunction(&fn.Metadata)
}
func (gpm *GenericPoolManager) DeleteFuncSvcFromCache(fsvc *fscache.FuncSvc) {
gpm.fsCache.DeleteEntry(fsvc)
}
func (gpm *GenericPoolManager) TapService(svcHost string) error {
err := gpm.fsCache.TouchByAddress(svcHost)
if err != nil {
return err
}
return nil
}
// IsValid checks if pod is not deleted and that it has the address passed as the argument. Also checks that all the
// containers in it are reporting a ready status for the healthCheck.
func (gpm *GenericPoolManager) IsValid(fsvc *fscache.FuncSvc) bool {
for _, obj := range fsvc.KubernetesObjects {
if strings.ToLower(obj.Kind) == "pod" {
pod, err := gpm.kubernetesClient.CoreV1().Pods(obj.Namespace).Get(obj.Name, metav1.GetOptions{})
if err == nil && utils.IsReadyPod(pod) {
// Normally, the address format is http://[pod-ip]:[port], however, if the
// Istio is enabled the address format changes to http://[svc-name]:[port].
// So if the Istio is enabled and pod is in ready state, we return true directly;
// Otherwise, we need to ensure that the address contains pod ip.
if gpm.enableIstio ||
(!gpm.enableIstio && strings.Contains(fsvc.Address, pod.Status.PodIP)) {
gpm.logger.Debug("valid address",
zap.String("address", fsvc.Address),
zap.Any("function", fsvc.Function),
zap.String("executor", string(fsvc.Executor)),
)
return true
}
}
}
}
return false
}
func (gpm *GenericPoolManager) RefreshFuncPods(logger *zap.Logger, f fv1.Function) error {
env, err := gpm.fissionClient.Environments(f.Spec.Environment.Namespace).Get(f.Spec.Environment.Name)
@@ -174,6 +248,159 @@ func (gpm *GenericPoolManager) RefreshFuncPods(logger *zap.Logger, f fv1.Functio
return nil
}
func (gpm *GenericPoolManager) AdoptExistingResources() {
envs, err := gpm.fissionClient.Environments(metav1.NamespaceAll).List(metav1.ListOptions{})
if err != nil {
gpm.logger.Error("error getting environment list", zap.Error(err))
return
}
envMap := make(map[string]fv1.Environment, len(envs.Items))
wg := &sync.WaitGroup{}
for i := range envs.Items {
env := envs.Items[i]
if gpm.getEnvPoolsize(&env) > 0 {
wg.Add(1)
go func() {
defer wg.Done()
_, err := gpm.getPool(&env)
if err != nil {
gpm.logger.Error("adopt pool failed", zap.Error(err))
}
}()
}
// create environment map for later use
key := fmt.Sprintf("%v/%v", env.Metadata.Namespace, env.Metadata.Name)
envMap[key] = env
}
l := map[string]string{
types.EXECUTOR_TYPE: string(fv1.ExecutorTypePoolmgr),
}
podList, err := gpm.kubernetesClient.CoreV1().Pods(metav1.NamespaceAll).List(metav1.ListOptions{
LabelSelector: labels.Set(l).AsSelector().String(),
})
if err != nil {
gpm.logger.Error("error getting pod list", zap.Error(err))
return
}
for i := range podList.Items {
pod := &podList.Items[i]
if !utils.IsReadyPod(pod) {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
// avoid too many requests arrive Kubernetes API server at the same time.
time.Sleep(time.Duration(rand.Intn(30)) * time.Millisecond)
patch := fmt.Sprintf(`{"metadata":{"annotations":{"%v":"%v"}}}`, types.EXECUTOR_INSTANCEID_LABEL, gpm.instanceId)
pod, err = gpm.kubernetesClient.CoreV1().Pods(pod.Namespace).Patch(pod.Name, k8sTypes.StrategicMergePatchType, []byte(patch))
if err != nil {
// just log the error since it won't affect the function serving
gpm.logger.Warn("error patching executor instance ID of pod", zap.Error(err),
zap.String("pod", pod.Name), zap.String("ns", pod.Namespace))
return
}
// for unspecialized pod, we only update its annotations
if pod.Labels["managed"] == "true" {
return
}
fnName, ok1 := pod.Labels[types.FUNCTION_NAME]
fnNS, ok2 := pod.Labels[types.FUNCTION_NAMESPACE]
fnUID, ok3 := pod.Labels[types.FUNCTION_UID]
fnRV, ok4 := pod.Annotations[types.FUNCTION_RESOURCE_VERSION]
envName, ok5 := pod.Labels[types.ENVIRONMENT_NAME]
envNS, ok6 := pod.Labels[types.ENVIRONMENT_NAMESPACE]
svcHost, ok7 := pod.Annotations[types.ANNOTATION_SVC_HOST]
env, ok8 := envMap[fmt.Sprintf("%v/%v", envNS, envName)]
if !(ok1 && ok2 && ok3 && ok4 && ok5 && ok6 && ok7 && ok8) {
gpm.logger.Warn("failed to adopt pod for function due to lack of necessary information",
zap.String("pod", pod.Name), zap.Any("labels", pod.Labels), zap.Any("annotations", pod.Annotations),
zap.String("env", env.Metadata.Name))
return
}
fsvc := fscache.FuncSvc{
Name: pod.Name,
Function: &metav1.ObjectMeta{
Name: fnName,
Namespace: fnNS,
UID: k8sTypes.UID(fnUID),
ResourceVersion: fnRV,
},
Environment: &env,
Address: svcHost,
KubernetesObjects: []apiv1.ObjectReference{
{
Kind: "pod",
Name: pod.Name,
APIVersion: pod.TypeMeta.APIVersion,
Namespace: pod.ObjectMeta.Namespace,
ResourceVersion: pod.ObjectMeta.ResourceVersion,
UID: pod.ObjectMeta.UID,
},
},
Executor: fv1.ExecutorTypePoolmgr,
Ctime: time.Now(),
Atime: time.Now(),
}
_, err = gpm.fsCache.Add(fsvc)
if err != nil {
// If fsvc already exists we just skip the duplicate one. And let reaper to recycle the duplicate pods.
// This is for the case that there are multiple function pods for the same function due to unknown reason.
if !fscache.IsNameExistError(err) {
gpm.logger.Warn("failed to adopt pod for function", zap.Error(err), zap.String("pod", pod.Name))
}
return
}
gpm.logger.Info("adopt function pod",
zap.String("pod", pod.Name), zap.Any("labels", pod.Labels), zap.Any("annotations", pod.Annotations))
}()
}
wg.Wait()
}
func (gpm *GenericPoolManager) CleanupOldExecutorObjects() {
gpm.logger.Info("Poolmanager starts to clean orphaned resources", zap.String("instanceID", gpm.instanceId))
errs := &multierror.Error{}
listOpts := metav1.ListOptions{
LabelSelector: labels.Set(map[string]string{types.EXECUTOR_TYPE: string(fv1.ExecutorTypePoolmgr)}).AsSelector().String(),
}
err := reaper.CleanupDeployments(gpm.logger, gpm.kubernetesClient, gpm.instanceId, listOpts)
if err != nil {
errs = multierror.Append(errs, err)
}
err = reaper.CleanupPods(gpm.logger, gpm.kubernetesClient, gpm.instanceId, listOpts)
if err != nil {
errs = multierror.Append(errs, err)
}
if errs.ErrorOrNil() != nil {
// TODO retry reaper; logged and ignored for now
gpm.logger.Error("Failed to cleanup old executor objects", zap.Error(err))
}
}
func (gpm *GenericPoolManager) service() {
for {
req := <-gpm.requestChannel
@@ -246,25 +473,6 @@ func (gpm *GenericPoolManager) cleanupPools(envs []fv1.Environment) {
}
}
func (gpm *GenericPoolManager) GetFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) {
// from Func -> get Env
gpm.logger.Debug("getting environment for function", zap.String("function", fn.Metadata.Name))
env, err := gpm.getFunctionEnv(fn)
if err != nil {
return nil, err
}
pool, err := gpm.getPool(env)
if err != nil {
return nil, err
}
// from GenericPool -> get one function container
// (this also adds to the cache)
gpm.logger.Debug("getting function service from pool", zap.String("function", fn.Metadata.Name))
return pool.getFuncSvc(ctx, fn)
}
func (gpm *GenericPoolManager) getFunctionEnv(fn *fv1.Function) (*fv1.Environment, error) {
var env *fv1.Environment
@@ -297,29 +505,38 @@ func (gpm *GenericPoolManager) eagerPoolCreator() {
if err != nil {
if utils.IsNetworkError(err) {
gpm.logger.Error("encountered network error, retrying", zap.Error(err))
time.Sleep(5 * time.Second)
continue
} else {
gpm.logger.Error("failed to get environment list", zap.Error(err))
}
gpm.logger.Error("failed to get environment list", zap.Error(err))
time.Sleep(5 * time.Second)
continue
}
// Create pools for all envs. TODO: we should make this a bit less eager, only
// creating pools for envs that are actually used by functions. Also we might want
// to keep these eagerly created pools smaller than the ones created when there are
// actual function calls.
wg := &sync.WaitGroup{}
for i := range envs.Items {
env := envs.Items[i]
// Create pool only if poolsize greater than zero
if gpm.getEnvPoolsize(&env) > 0 {
_, err := gpm.getPool(&envs.Items[i])
if err != nil {
gpm.logger.Error("eager-create pool failed", zap.Error(err))
}
wg.Add(1)
go func() {
defer wg.Done()
_, err := gpm.getPool(&env)
if err != nil {
gpm.logger.Error("eager-create pool failed", zap.Error(err))
}
}()
}
}
// Clean up pools whose env was deleted
gpm.cleanupPools(envs.Items)
wg.Wait()
time.Sleep(pollSleep)
}
}
@@ -334,28 +551,6 @@ func (gpm *GenericPoolManager) getEnvPoolsize(env *fv1.Environment) int32 {
return poolsize
}
// IsValid checks if pod is not deleted and that it has the address passed as the argument. Also checks that all the
// containers in it are reporting a ready status for the healthCheck.
func (gpm *GenericPoolManager) IsValid(fsvc *fscache.FuncSvc) bool {
for _, obj := range fsvc.KubernetesObjects {
if obj.Kind == "pod" {
pod, err := gpm.kubernetesClient.CoreV1().Pods(obj.Namespace).Get(obj.Name, metav1.GetOptions{})
if err == nil && utils.IsReadyPod(pod) {
// Normally, the address format is http://[pod-ip]:[port], however, if the
// Istio is enabled the address format changes to http://[svc-name]:[port].
// So if the Istio is enabled and pod is in ready state, we return true directly;
// Otherwise, we need to ensure that the address contains pod ip.
if gpm.enableIstio ||
(!gpm.enableIstio && strings.Contains(fsvc.Address, pod.Status.PodIP)) {
gpm.logger.Debug("valid address", zap.String("address", fsvc.Address))
return true
}
}
}
}
return false
}
// idleObjectReaper reaps objects after certain idle time
func (gpm *GenericPoolManager) idleObjectReaper() {
@@ -379,8 +574,10 @@ func (gpm *GenericPoolManager) idleObjectReaper() {
continue
}
for _, fsvc := range funcSvcs {
if fsvc.Executor != fscache.POOLMGR {
for i := range funcSvcs {
fsvc := funcSvcs[i]
if fsvc.Executor != fv1.ExecutorTypePoolmgr {
continue
}
@@ -396,20 +593,26 @@ func (gpm *GenericPoolManager) idleObjectReaper() {
continue
}
deleted, err := gpm.fsCache.DeleteOld(fsvc, gpm.idlePodReapTime)
if err != nil {
gpm.logger.Error("error deleting Kubernetes objects for function service",
zap.Error(err),
zap.Any("service", fsvc))
}
if !deleted {
continue
}
for _, kubeobj := range fsvc.KubernetesObjects {
reaper.CleanupKubeObject(gpm.logger, gpm.kubernetesClient, &kubeobj)
}
go func() {
deleted, err := gpm.fsCache.DeleteOld(fsvc, gpm.idlePodReapTime)
if err != nil {
gpm.logger.Error("error deleting Kubernetes objects for function service",
zap.Error(err),
zap.Any("service", fsvc))
}
if deleted {
for i := range fsvc.KubernetesObjects {
gpm.logger.Info("release idle function resources",
zap.String("function", fsvc.Function.Name),
zap.String("address", fsvc.Address),
zap.String("executor", string(fsvc.Executor)),
zap.String("pod", fsvc.Name),
)
reaper.CleanupKubeObject(gpm.logger, gpm.kubernetesClient, &fsvc.KubernetesObjects[i])
time.Sleep(50 * time.Millisecond)
}
}
}()
}
}
}
+3 -7
View File
@@ -33,7 +33,8 @@ import (
)
type fscRequestType int
type executorType int
//type executorType int
const (
TOUCH fscRequestType = iota
@@ -41,11 +42,6 @@ const (
LOG
)
const (
POOLMGR executorType = iota
NEWDEPLOY
)
type (
FuncSvc struct {
Name string // Name of object
@@ -53,7 +49,7 @@ type (
Environment *fv1.Environment // function's environment
Address string // Host:Port or IP:Port that the function's service can be reached at.
KubernetesObjects []apiv1.ObjectReference // Kubernetes Objects (within the function namespace)
Executor executorType
Executor fv1.ExecutorType
Ctime time.Time
Atime time.Time
+38 -93
View File
@@ -20,14 +20,15 @@ import (
"strings"
"time"
"github.com/fission/fission/pkg/utils"
"go.uber.org/zap"
apiv1 "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/crd"
"github.com/fission/fission/pkg/types"
"github.com/fission/fission/pkg/utils"
)
var (
@@ -35,52 +36,6 @@ var (
delOpt = meta_v1.DeleteOptions{PropagationPolicy: &deletePropagation}
)
// CleanupOldExecutorObjects cleans up resources created by old executor instances
func CleanupOldExecutorObjects(logger *zap.Logger, kubernetesClient *kubernetes.Clientset, instanceId string) {
go func() {
err := cleanup(logger, kubernetesClient, instanceId)
if err != nil {
// TODO retry reaper; logged and ignored for now
logger.Error("Failed to cleanup old executor objects", zap.Error(err))
}
}()
}
func cleanup(logger *zap.Logger, client *kubernetes.Clientset, instanceId string) error {
err := cleanupServices(logger, client, instanceId)
if err != nil {
return err
}
err = cleanupHpa(logger, client, instanceId)
if err != nil {
return err
}
// Deployments are used for idle pools and can be cleaned up
// immediately. (We should "adopt" these instead of creating
// a new pool.)
err = cleanupDeployments(logger, client, instanceId)
if err != nil {
return err
}
// Pods might still be running user functions, so we give them
// a few minutes before terminating them. This time is the
// maximum function runtime, plus the time a router might
// still route to an old instance, i.e. router cache expiry
// time.
time.Sleep(6 * time.Minute)
err = cleanupPods(logger, client, instanceId)
if err != nil {
return err
}
return nil
}
// CleanupKubeObject deletes given kubernetes object
func CleanupKubeObject(logger *zap.Logger, kubeClient *kubernetes.Clientset, kubeobj *apiv1.ObjectReference) {
switch strings.ToLower(kubeobj.Kind) {
@@ -114,28 +69,19 @@ func CleanupKubeObject(logger *zap.Logger, kubeClient *kubernetes.Clientset, kub
}
}
func cleanupDeployments(logger *zap.Logger, client *kubernetes.Clientset, instanceId string) error {
deploymentList, err := client.AppsV1().Deployments(meta_v1.NamespaceAll).List(meta_v1.ListOptions{})
func CleanupDeployments(logger *zap.Logger, client *kubernetes.Clientset, instanceId string, listOps meta_v1.ListOptions) error {
deploymentList, err := client.AppsV1().Deployments(meta_v1.NamespaceAll).List(listOps)
if err != nil {
return err
}
for _, dep := range deploymentList.Items {
id, ok := dep.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
if ok && id != instanceId {
logger.Debug("cleaning up deployment", zap.String("deployment", dep.ObjectMeta.Name))
err := client.AppsV1().Deployments(dep.ObjectMeta.Namespace).Delete(dep.ObjectMeta.Name, &delOpt)
if err != nil {
logger.Error("error cleaning up deployment",
zap.Error(err),
zap.String("deployment_name", dep.ObjectMeta.Name),
zap.String("deployment_namespace", dep.ObjectMeta.Namespace))
}
// ignore err
id, ok := dep.ObjectMeta.Annotations[types.EXECUTOR_INSTANCEID_LABEL]
if !ok {
// Backward compatibility with older label name
id, ok = dep.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
}
// Backward compatibility with older label name
pid, pok := dep.ObjectMeta.Labels[types.POOLMGR_INSTANCEID_LABEL]
if pok && pid != instanceId {
logger.Debug("cleaning up deployment", zap.String("deployment", dep.ObjectMeta.Name))
if ok && id != instanceId {
logger.Info("cleaning up deployment", zap.String("deployment", dep.ObjectMeta.Name))
err := client.AppsV1().Deployments(dep.ObjectMeta.Namespace).Delete(dep.ObjectMeta.Name, &delOpt)
if err != nil {
logger.Error("error cleaning up deployment",
@@ -149,15 +95,19 @@ func cleanupDeployments(logger *zap.Logger, client *kubernetes.Clientset, instan
return nil
}
func cleanupPods(logger *zap.Logger, client *kubernetes.Clientset, instanceId string) error {
podList, err := client.CoreV1().Pods(meta_v1.NamespaceAll).List(meta_v1.ListOptions{})
func CleanupPods(logger *zap.Logger, client *kubernetes.Clientset, instanceId string, listOps meta_v1.ListOptions) error {
podList, err := client.CoreV1().Pods(meta_v1.NamespaceAll).List(listOps)
if err != nil {
return err
}
for _, pod := range podList.Items {
id, ok := pod.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
id, ok := pod.ObjectMeta.Annotations[types.EXECUTOR_INSTANCEID_LABEL]
if !ok {
// Backward compatibility with older label name
id, ok = pod.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
}
if ok && id != instanceId {
logger.Debug("cleaning up pod", zap.String("pod", pod.ObjectMeta.Name))
logger.Info("cleaning up pod", zap.String("pod", pod.ObjectMeta.Name))
err := client.CoreV1().Pods(pod.ObjectMeta.Namespace).Delete(pod.ObjectMeta.Name, nil)
if err != nil {
logger.Error("error cleaning up pod",
@@ -167,31 +117,23 @@ func cleanupPods(logger *zap.Logger, client *kubernetes.Clientset, instanceId st
}
// ignore err
}
// Backward compatibility with older label name
pid, pok := pod.ObjectMeta.Labels[types.POOLMGR_INSTANCEID_LABEL]
if pok && pid != instanceId {
logger.Debug("cleaning up pod", zap.String("pod", pod.ObjectMeta.Name))
err := client.CoreV1().Pods(pod.ObjectMeta.Namespace).Delete(pod.ObjectMeta.Name, nil)
if err != nil {
logger.Error("error cleaning up pod",
zap.Error(err),
zap.String("pod_name", pod.ObjectMeta.Name),
zap.String("pod_namespace", pod.ObjectMeta.Namespace))
}
}
}
return nil
}
func cleanupServices(logger *zap.Logger, client *kubernetes.Clientset, instanceId string) error {
svcList, err := client.CoreV1().Services(meta_v1.NamespaceAll).List(meta_v1.ListOptions{})
func CleanupServices(logger *zap.Logger, client *kubernetes.Clientset, instanceId string, listOps meta_v1.ListOptions) error {
svcList, err := client.CoreV1().Services(meta_v1.NamespaceAll).List(listOps)
if err != nil {
return err
}
for _, svc := range svcList.Items {
id, ok := svc.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
id, ok := svc.ObjectMeta.Annotations[types.EXECUTOR_INSTANCEID_LABEL]
if !ok {
// Backward compatibility with older label name
id, ok = svc.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
}
if ok && id != instanceId {
logger.Debug("cleaning up service", zap.String("service", svc.ObjectMeta.Name))
logger.Info("cleaning up service", zap.String("service", svc.ObjectMeta.Name))
err := client.CoreV1().Services(svc.ObjectMeta.Namespace).Delete(svc.ObjectMeta.Name, nil)
if err != nil {
logger.Error("error cleaning up service",
@@ -205,16 +147,20 @@ func cleanupServices(logger *zap.Logger, client *kubernetes.Clientset, instanceI
return nil
}
func cleanupHpa(logger *zap.Logger, client *kubernetes.Clientset, instanceId string) error {
hpaList, err := client.AutoscalingV1().HorizontalPodAutoscalers(meta_v1.NamespaceAll).List(meta_v1.ListOptions{})
func CleanupHpa(logger *zap.Logger, client *kubernetes.Clientset, instanceId string, listOps meta_v1.ListOptions) error {
hpaList, err := client.AutoscalingV1().HorizontalPodAutoscalers(meta_v1.NamespaceAll).List(listOps)
if err != nil {
return err
}
for _, hpa := range hpaList.Items {
id, ok := hpa.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
id, ok := hpa.ObjectMeta.Annotations[types.EXECUTOR_INSTANCEID_LABEL]
if !ok {
// Backward compatibility with older label name
id, ok = hpa.ObjectMeta.Labels[types.EXECUTOR_INSTANCEID_LABEL]
}
if ok && id != instanceId {
logger.Debug("cleaning up HPA", zap.String("hpa", hpa.ObjectMeta.Name))
logger.Info("cleaning up HPA", zap.String("hpa", hpa.ObjectMeta.Name))
err := client.AutoscalingV1().HorizontalPodAutoscalers(hpa.ObjectMeta.Namespace).Delete(hpa.ObjectMeta.Name, nil)
if err != nil {
logger.Error("error cleaning up HPA",
@@ -224,7 +170,6 @@ func cleanupHpa(logger *zap.Logger, client *kubernetes.Clientset, instanceId str
}
// ignore err
}
}
return nil
@@ -234,6 +179,9 @@ func cleanupHpa(logger *zap.Logger, client *kubernetes.Clientset, instanceId str
// deletes the rolebindings completely if there are no Service Accounts in a rolebinding object.
func CleanupRoleBindings(logger *zap.Logger, client *kubernetes.Clientset, fissionClient *crd.FissionClient, functionNs, envBuilderNs string, cleanupRoleBindingInterval time.Duration) {
for {
// some sleep before the next reaper iteration
time.Sleep(cleanupRoleBindingInterval)
logger.Debug("starting cleanupRoleBindings cycle")
// get all rolebindings ( just to be efficient, one call to kubernetes )
rbList, err := client.RbacV1beta1().RoleBindings(meta_v1.NamespaceAll).List(meta_v1.ListOptions{})
@@ -294,7 +242,7 @@ func CleanupRoleBindings(logger *zap.Logger, client *kubernetes.Clientset, fissi
break
}
if fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType == types.ExecutorTypeNewdeploy {
if fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType == fv1.ExecutorTypeNewdeploy {
ndmFunc = true
break
}
@@ -362,8 +310,5 @@ func CleanupRoleBindings(logger *zap.Logger, client *kubernetes.Clientset, fissi
}
}
}
// some sleep before the next reaper iteration
time.Sleep(cleanupRoleBindingInterval)
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2019 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"sync"
"time"
apiv1 "k8s.io/api/core/v1"
)
// ApplyImagePullSecret applies image pull secret to the give pod spec.
// It's intentional not to check the existence of secret here.
// First, Kubernetes will set Pod status to "ImagePullBackOff" once
// kubelet failed to pull image so that users will know what's happening.
// Second, Fission no longer need to handle "secret not found" error
// when creating the environment deployment since kubelet will retry to
// pull image until successes.
func ApplyImagePullSecret(secret string, podspec apiv1.PodSpec) *apiv1.PodSpec {
if len(secret) > 0 {
podspec.ImagePullSecrets = []apiv1.LocalObjectReference{{Name: secret}}
}
return &podspec
}
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) {
waitCh := make(chan struct{})
go func() {
defer close(waitCh)
wg.Wait()
}()
select {
case <-waitCh:
case <-time.After(timeout):
}
}
+11 -3
View File
@@ -9,9 +9,9 @@ import (
"strings"
"time"
"go.uber.org/zap"
"github.com/pkg/errors"
"go.opencensus.io/plugin/ochttp"
"go.uber.org/zap"
"golang.org/x/net/context/ctxhttp"
ferror "github.com/fission/fission/pkg/error"
@@ -91,12 +91,20 @@ func sendRequest(logger *zap.Logger, ctx context.Context, httpClient *http.Clien
if err != nil {
logger.Error("error reading response body", zap.Error(err))
}
resp.Body.Close()
defer resp.Body.Close()
return body, err
}
err = ferror.MakeErrorFromHTTP(resp)
}
// skip retry and return directly due to context deadline exceeded
if err == context.DeadlineExceeded {
msg := "error specializing function pod, either increase the specialization timeout for function or check function pod log would help."
err = errors.Wrap(err, msg)
logger.Error(msg, zap.Error(err), zap.String("url", url))
return nil, err
}
if i < maxRetries-1 {
time.Sleep(50 * time.Duration(2*i) * time.Millisecond)
logger.Error("error specializing/fetching/uploading package, retrying", zap.Error(err), zap.String("url", url))
+3 -37
View File
@@ -21,7 +21,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
@@ -33,7 +32,6 @@ import (
uuid "github.com/satori/go.uuid"
"go.opencensus.io/plugin/ochttp"
"go.uber.org/zap"
"golang.org/x/net/context/ctxhttp"
k8serr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
@@ -96,38 +94,6 @@ func MakeFetcher(logger *zap.Logger, sharedVolumePath string, sharedSecretPath s
}, nil
}
func downloadUrl(ctx context.Context, httpClient *http.Client, url string, localPath string) error {
resp, err := ctxhttp.Get(ctx, httpClient, url)
if err != nil {
return err
}
defer resp.Body.Close()
w, err := os.Create(localPath)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
return err
}
// flushing write buffer to file
err = w.Sync()
if err != nil {
return err
}
err = os.Chmod(localPath, 0600)
if err != nil {
return err
}
return nil
}
func verifyChecksum(fileChecksum, checksum *fv1.Checksum) error {
if checksum.Type != fv1.ChecksumTypeSHA256 {
return ferror.MakeError(ferror.ErrorInvalidArgument, "Unsupported checksum type")
@@ -232,7 +198,7 @@ func (fetcher *Fetcher) SpecializeHandler(w http.ResponseWriter, r *http.Request
err = fetcher.SpecializePod(r.Context(), req.FetchReq, req.LoadReq)
if err != nil {
fetcher.logger.Error("error specializing pod", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -263,7 +229,7 @@ func (fetcher *Fetcher) Fetch(ctx context.Context, pkg *fv1.Package, req types.F
if req.FetchType == types.FETCH_URL {
// fetch the file and save it to the tmp path
err := downloadUrl(ctx, fetcher.httpClient, req.Url, tmpPath)
err := utils.DownloadUrl(ctx, fetcher.httpClient, req.Url, tmpPath)
if err != nil {
e := "failed to download url"
fetcher.logger.Error(e, zap.Error(err), zap.String("url", req.Url))
@@ -302,7 +268,7 @@ func (fetcher *Fetcher) Fetch(ctx context.Context, pkg *fv1.Package, req types.F
}
} else {
// download and verify
err := downloadUrl(ctx, fetcher.httpClient, archive.URL, tmpPath)
err := utils.DownloadUrl(ctx, fetcher.httpClient, archive.URL, tmpPath)
if err != nil {
e := "failed to download url"
fetcher.logger.Error(e, zap.Error(err), zap.String("url", req.Url))
@@ -53,6 +53,14 @@ func Wrapper(action cmd.CommandAction) func(*cobra.Command, []string) error {
func SetFlags(cmd *cobra.Command, flagSet flag.FlagSet) {
aliases := make(map[string]string)
// set global flags
for _, f := range flagSet.Global {
globalFlags(cmd, f)
for _, alias := range f.Aliases {
aliases[alias] = f.Name
}
}
// set required flags
for _, f := range flagSet.Required {
requiredFlags(cmd, f)
@@ -84,7 +92,7 @@ func SetFlags(cmd *cobra.Command, flagSet flag.FlagSet) {
func optionalFlags(cmd *cobra.Command, flags ...flag.Flag) {
for _, f := range flags {
toCobraFlag(cmd, f)
toCobraFlag(cmd, f, false)
if f.Deprecated {
usage := fmt.Sprintf("Use --%v instead. The flag still works for now and will be removed in future", f.Substitute)
cmd.Flags().MarkDeprecated(f.Name, usage)
@@ -96,12 +104,18 @@ func optionalFlags(cmd *cobra.Command, flags ...flag.Flag) {
func requiredFlags(cmd *cobra.Command, flags ...flag.Flag) {
for _, f := range flags {
toCobraFlag(cmd, f)
toCobraFlag(cmd, f, false)
cmd.MarkFlagRequired(f.Name)
}
}
func toCobraFlag(cmd *cobra.Command, f flag.Flag) {
func globalFlags(cmd *cobra.Command, flags ...flag.Flag) {
for _, f := range flags {
toCobraFlag(cmd, f, true)
}
}
func toCobraFlag(cmd *cobra.Command, f flag.Flag, global bool) {
// Workaround to pass aliases to templater for generating flag aliases.
if len(f.Aliases) > 0 {
var aliases []string
@@ -119,67 +133,72 @@ func toCobraFlag(cmd *cobra.Command, f flag.Flag) {
helptemplate.AliasSeparator, f.Usage)
}
flagset := cmd.Flags()
if global {
flagset = cmd.PersistentFlags()
}
switch f.Type {
case flag.Bool:
val, ok := f.DefaultValue.(bool)
if !ok {
val = false
}
cmd.Flags().BoolP(f.Name, f.Short, val, f.Usage)
flagset.BoolP(f.Name, f.Short, val, f.Usage)
case flag.String:
val, ok := f.DefaultValue.(string)
if !ok {
val = ""
}
cmd.Flags().StringP(f.Name, f.Short, val, f.Usage)
flagset.StringP(f.Name, f.Short, val, f.Usage)
case flag.StringSlice:
val, ok := f.DefaultValue.([]string)
if !ok {
val = []string{}
}
cmd.Flags().StringArrayP(f.Name, f.Short, val, f.Usage)
flagset.StringArrayP(f.Name, f.Short, val, f.Usage)
case flag.Int:
val, ok := f.DefaultValue.(int)
if !ok {
val = 0
}
cmd.Flags().IntP(f.Name, f.Short, val, f.Usage)
flagset.IntP(f.Name, f.Short, val, f.Usage)
case flag.IntSlice:
val, ok := f.DefaultValue.([]int)
if !ok {
val = []int{}
}
cmd.Flags().IntSliceP(f.Name, f.Short, val, f.Usage)
flagset.IntSliceP(f.Name, f.Short, val, f.Usage)
case flag.Int64:
val, ok := f.DefaultValue.(int64)
if !ok {
val = 0
}
cmd.Flags().Int64P(f.Name, f.Short, val, f.Usage)
flagset.Int64P(f.Name, f.Short, val, f.Usage)
case flag.Int64Slice:
val, ok := f.DefaultValue.([]int64)
if !ok {
val = []int64{}
}
cmd.Flags().Int64SliceP(f.Name, f.Short, val, f.Usage)
flagset.Int64SliceP(f.Name, f.Short, val, f.Usage)
case flag.Float32:
val, ok := f.DefaultValue.(float32)
if !ok {
val = 0
}
cmd.Flags().Float32P(f.Name, f.Short, val, f.Usage)
flagset.Float32P(f.Name, f.Short, val, f.Usage)
case flag.Float64:
val, ok := f.DefaultValue.(float64)
if !ok {
val = 0
}
cmd.Flags().Float64P(f.Name, f.Short, val, f.Usage)
flagset.Float64P(f.Name, f.Short, val, f.Usage)
case flag.Duration:
val, ok := f.DefaultValue.(time.Duration)
if !ok {
val = 0
}
cmd.Flags().DurationP(f.Name, f.Short, val, f.Usage)
flagset.DurationP(f.Name, f.Short, val, f.Usage)
}
}
@@ -50,7 +50,7 @@ const (
{{end}}`
// SectionFlags is the help template section that displays the command's flags.
SectionFlags = `{{ if or $visibleFlags.HasFlags $explicitlyExposedFlags.HasFlags}}Options:
SectionFlags = `{{ if or $visibleFlags.HasFlags $explicitlyExposedFlags.HasFlags}}Options
{{ if $visibleFlags.HasFlags}}{{trimRight (flagsUsages $visibleFlags)}}{{end}}{{ if $explicitlyExposedFlags.HasFlags}}{{ if $visibleFlags.HasFlags}}
{{end}}{{trimRight (flagsUsages $explicitlyExposedFlags)}}{{end}}
+4 -4
View File
@@ -33,8 +33,8 @@ func Commands() *cobra.Command {
Required: []flag.Flag{flag.EnvName, flag.EnvImage},
Optional: []flag.Flag{flag.EnvPoolsize, flag.EnvBuilderImage, flag.EnvBuildCmd,
flag.RunTimeMinCPU, flag.RunTimeMaxCPU, flag.RunTimeMinMemory, flag.RunTimeMaxMemory,
flag.EnvTerminationGracePeriod, flag.EnvVersion, flag.EnvExternalNetwork, flag.EnvKeepArchive,
flag.NamespaceEnvironment, flag.SpecSave},
flag.EnvTerminationGracePeriod, flag.EnvVersion, flag.EnvImagePullSecret,
flag.EnvExternalNetwork, flag.EnvKeepArchive, flag.NamespaceEnvironment, flag.SpecSave},
})
getCmd := &cobra.Command{
@@ -55,8 +55,8 @@ func Commands() *cobra.Command {
wrapper.SetFlags(updateCmd, flag.FlagSet{
Required: []flag.Flag{flag.EnvName},
Optional: []flag.Flag{flag.EnvImage, flag.EnvPoolsize,
flag.EnvBuilderImage, flag.EnvBuildCmd, flag.EnvExternalNetwork,
flag.EnvTerminationGracePeriod, flag.EnvKeepArchive, flag.NamespaceEnvironment},
flag.EnvBuilderImage, flag.EnvBuildCmd, flag.EnvImagePullSecret, flag.EnvTerminationGracePeriod,
flag.EnvKeepArchive, flag.NamespaceEnvironment, flag.EnvExternalNetwork},
})
deleteCmd := &cobra.Command{
@@ -112,6 +112,7 @@ func createEnvironmentFromCmd(input cli.Input) (*fv1.Environment, error) {
envExternalNetwork := input.Bool(flagkey.EnvExternalNetwork)
keepArchive := input.Bool(flagkey.EnvKeeparchive)
envGracePeriod := input.Int64(flagkey.EnvGracePeriod)
pullSecret := input.String(flagkey.EnvImagePullSecret)
envVersion := input.Int(flagkey.EnvVersion)
// Environment API interface version is not specified and
@@ -169,6 +170,7 @@ func createEnvironmentFromCmd(input cli.Input) (*fv1.Environment, error) {
AllowAccessToExternalNetwork: envExternalNetwork,
TerminationGracePeriod: envGracePeriod,
KeepArchive: keepArchive,
ImagePullSecret: pullSecret,
},
}
@@ -123,6 +123,10 @@ func updateExistingEnvironmentWithCmd(env *fv1.Environment, input cli.Input) (*f
env.Spec.KeepArchive = input.Bool(flagkey.EnvKeeparchive)
}
if input.IsSet(flagkey.EnvImagePullSecret) {
env.Spec.ImagePullSecret = input.String(flagkey.EnvImagePullSecret)
}
env.Spec.AllowAccessToExternalNetwork = envExternalNetwork
// TODO: allow to update resource.
+6 -2
View File
@@ -36,13 +36,16 @@ func Commands() *cobra.Command {
flag.FnExecutorType, flag.FnCfgMap, flag.FnSecret,
flag.FnSpecializationTimeout, flag.FnExecutionTimeout,
// TODO retired pkg related flag from function cmd
// TODO retired pkg & trigger related flags from function cmd
flag.PkgCode, flag.PkgSrcArchive, flag.PkgDeployArchive,
flag.PkgSrcChecksum, flag.PkgDeployChecksum, flag.PkgInsecure,
flag.FnBuildCmd,
flag.HtUrl, flag.HtMethod,
// flag for newdeploy to use.
flag.RunTimeMinCPU, flag.RunTimeMaxCPU, flag.RunTimeMinMemory,
flag.RunTimeMaxMemory, flag.FnBuildCmd, flag.ReplicasMin,
flag.RunTimeMaxMemory, flag.ReplicasMin,
flag.ReplicasMax, flag.RunTimeTargetCPU,
flag.NamespaceFunction, flag.NamespaceEnvironment, flag.SpecSave},
@@ -84,6 +87,7 @@ func Commands() *cobra.Command {
flag.FnSpecializationTimeout, flag.FnExecutionTimeout,
flag.PkgCode, flag.PkgSrcArchive, flag.PkgDeployArchive,
flag.PkgSrcChecksum, flag.PkgDeployChecksum, flag.PkgInsecure,
flag.FnBuildCmd, flag.PkgForce,
flag.RunTimeMinCPU, flag.RunTimeMaxCPU, flag.RunTimeMinMemory,
+135 -48
View File
@@ -36,7 +36,6 @@ import (
"github.com/fission/fission/pkg/fission-cli/console"
flagkey "github.com/fission/fission/pkg/fission-cli/flag/key"
"github.com/fission/fission/pkg/fission-cli/util"
"github.com/fission/fission/pkg/types"
)
const (
@@ -363,36 +362,48 @@ func (opts *CreateSubCommand) run(input cli.Input) error {
}
func getInvokeStrategy(input cli.Input, existingInvokeStrategy *fv1.InvokeStrategy) (strategy *fv1.InvokeStrategy, err error) {
var es *fv1.ExecutionStrategy
var fnExecutor, newFnExecutor fv1.ExecutorType
if existingInvokeStrategy == nil {
es, err = getExecutionStrategy(input)
} else {
es, err = updateExecutionStrategy(input, &existingInvokeStrategy.ExecutionStrategy)
}
if err != nil {
return nil, err
}
return &fv1.InvokeStrategy{
ExecutionStrategy: *es,
StrategyType: fv1.StrategyTypeExecution,
}, nil
}
func getExecutionStrategy(input cli.Input) (strategy *fv1.ExecutionStrategy, err error) {
var fnExecutor fv1.ExecutorType
switch input.String(flagkey.FnExecutorType) {
case "":
fallthrough
case types.ExecutorTypePoolmgr:
newFnExecutor = types.ExecutorTypePoolmgr
case types.ExecutorTypeNewdeploy:
newFnExecutor = types.ExecutorTypeNewdeploy
case string(fv1.ExecutorTypePoolmgr):
fnExecutor = fv1.ExecutorTypePoolmgr
case string(fv1.ExecutorTypeNewdeploy):
fnExecutor = fv1.ExecutorTypeNewdeploy
default:
return nil, errors.New("executor type must be one of 'poolmgr' or 'newdeploy', defaults to 'poolmgr'")
return nil, errors.Errorf("executor type must be one of '%v' or '%v'", fv1.ExecutorTypePoolmgr, fv1.ExecutorTypeNewdeploy)
}
if existingInvokeStrategy != nil {
fnExecutor = existingInvokeStrategy.ExecutionStrategy.ExecutorType
specializationTimeout := fv1.DefaultSpecializationTimeOut
// override the executor type if user specified a new executor type
if input.IsSet(flagkey.FnExecutorType) {
fnExecutor = newFnExecutor
if input.IsSet(flagkey.FnSpecializationTimeout) {
specializationTimeout = input.Int(flagkey.FnSpecializationTimeout)
if specializationTimeout < fv1.DefaultSpecializationTimeOut {
return nil, errors.Errorf("%v must be greater than or equal to 120 seconds", flagkey.FnSpecializationTimeout)
}
} else {
fnExecutor = newFnExecutor
}
if input.IsSet(flagkey.FnSpecializationTimeout) && fnExecutor != types.ExecutorTypeNewdeploy {
return nil, errors.Errorf("%v flag is only applicable for newdeploy type of executor", flagkey.FnSpecializationTimeout)
}
if fnExecutor == types.ExecutorTypePoolmgr {
if fnExecutor == fv1.ExecutorTypePoolmgr {
if input.IsSet(flagkey.RuntimeTargetcpu) || input.IsSet(flagkey.ReplicasMinscale) || input.IsSet(flagkey.ReplicasMaxscale) {
return nil, errors.New("to set target CPU or min/max scale for function, please specify \"--executortype newdeploy\"")
}
@@ -400,24 +411,102 @@ func getInvokeStrategy(input cli.Input, existingInvokeStrategy *fv1.InvokeStrate
if input.IsSet(flagkey.RuntimeMincpu) || input.IsSet(flagkey.RuntimeMaxcpu) || input.IsSet(flagkey.RuntimeMinmemory) || input.IsSet(flagkey.RuntimeMaxmemory) {
console.Warn("To limit CPU/Memory for function with executor type \"poolmgr\", please specify resources limits when creating environment")
}
strategy = &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: types.ExecutorTypePoolmgr,
},
strategy = &fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypePoolmgr,
SpecializationTimeout: specializationTimeout,
}
} else {
// set default value
targetCPU := DEFAULT_TARGET_CPU_PERCENTAGE
minScale := DEFAULT_MIN_SCALE
maxScale := minScale
specializationTimeout := fv1.DefaultSpecializationTimeOut
if input.IsSet(flagkey.RuntimeTargetcpu) {
targetCPU, err = getTargetCPU(input)
if err != nil {
return nil, err
}
}
if existingInvokeStrategy != nil && existingInvokeStrategy.ExecutionStrategy.ExecutorType == types.ExecutorTypeNewdeploy {
minScale = existingInvokeStrategy.ExecutionStrategy.MinScale
maxScale = existingInvokeStrategy.ExecutionStrategy.MaxScale
targetCPU = existingInvokeStrategy.ExecutionStrategy.TargetCPUPercent
specializationTimeout = existingInvokeStrategy.ExecutionStrategy.SpecializationTimeout
minScale := DEFAULT_MIN_SCALE
if input.IsSet(flagkey.ReplicasMinscale) {
minScale = input.Int(flagkey.ReplicasMinscale)
}
maxScale := minScale
if input.IsSet(flagkey.ReplicasMaxscale) {
maxScale = input.Int(flagkey.ReplicasMaxscale)
if maxScale <= 0 {
return nil, errors.Errorf("%v must be greater than 0", flagkey.ReplicasMaxscale)
}
}
if minScale > maxScale {
return nil, fmt.Errorf("minscale (%v) can not be greater than maxscale (%v)", minScale, maxScale)
}
// Right now a simple single case strategy implementation
// This will potentially get more sophisticated once we have more strategies in place
strategy = &fv1.ExecutionStrategy{
ExecutorType: fnExecutor,
MinScale: minScale,
MaxScale: maxScale,
TargetCPUPercent: targetCPU,
SpecializationTimeout: specializationTimeout,
}
}
return strategy, nil
}
func updateExecutionStrategy(input cli.Input, existingExecutionStrategy *fv1.ExecutionStrategy) (strategy *fv1.ExecutionStrategy, err error) {
fnExecutor := existingExecutionStrategy.ExecutorType
oldExecutor := existingExecutionStrategy.ExecutorType
if input.IsSet(flagkey.FnExecutorType) {
switch input.String(flagkey.FnExecutorType) {
case "":
fallthrough
case string(fv1.ExecutorTypePoolmgr):
fnExecutor = fv1.ExecutorTypePoolmgr
case string(fv1.ExecutorTypeNewdeploy):
fnExecutor = fv1.ExecutorTypeNewdeploy
default:
return nil, errors.Errorf("executor type must be one of '%v' or '%v'", fv1.ExecutorTypePoolmgr, fv1.ExecutorTypeNewdeploy)
}
}
specializationTimeout := existingExecutionStrategy.SpecializationTimeout
if input.IsSet(flagkey.FnSpecializationTimeout) {
specializationTimeout = input.Int(flagkey.FnSpecializationTimeout)
if specializationTimeout < fv1.DefaultSpecializationTimeOut {
return nil, errors.Errorf("%v must be greater than or equal to 120 seconds", flagkey.FnSpecializationTimeout)
}
} else {
if specializationTimeout < fv1.DefaultSpecializationTimeOut {
specializationTimeout = fv1.DefaultSpecializationTimeOut
}
}
if fnExecutor == fv1.ExecutorTypePoolmgr {
if input.IsSet(flagkey.RuntimeTargetcpu) || input.IsSet(flagkey.ReplicasMinscale) || input.IsSet(flagkey.ReplicasMaxscale) {
return nil, errors.New("to set target CPU or min/max scale for function, please specify \"--executortype newdeploy\"")
}
if input.IsSet(flagkey.RuntimeMincpu) || input.IsSet(flagkey.RuntimeMaxcpu) || input.IsSet(flagkey.RuntimeMinmemory) || input.IsSet(flagkey.RuntimeMaxmemory) {
console.Warn("To limit CPU/Memory for function with executor type \"poolmgr\", please specify resources limits when creating environment")
}
strategy = &fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypePoolmgr,
SpecializationTimeout: specializationTimeout,
}
} else {
targetCPU := existingExecutionStrategy.TargetCPUPercent
minScale := existingExecutionStrategy.MinScale
maxScale := existingExecutionStrategy.MaxScale
if fnExecutor != oldExecutor { // from poolmanager to newdeploy
targetCPU = DEFAULT_TARGET_CPU_PERCENTAGE
minScale = DEFAULT_MIN_SCALE
maxScale = minScale
}
if input.IsSet(flagkey.RuntimeTargetcpu) {
@@ -425,6 +514,10 @@ func getInvokeStrategy(input cli.Input, existingInvokeStrategy *fv1.InvokeStrate
if err != nil {
return nil, err
}
} else {
if targetCPU <= 0 || targetCPU > 100 {
targetCPU = DEFAULT_TARGET_CPU_PERCENTAGE
}
}
if input.IsSet(flagkey.ReplicasMinscale) {
@@ -436,12 +529,9 @@ func getInvokeStrategy(input cli.Input, existingInvokeStrategy *fv1.InvokeStrate
if maxScale <= 0 {
return nil, errors.Errorf("%v must be greater than 0", flagkey.ReplicasMaxscale)
}
}
if input.IsSet(flagkey.FnSpecializationTimeout) {
specializationTimeout = input.Int(flagkey.FnSpecializationTimeout)
if specializationTimeout < fv1.DefaultSpecializationTimeOut {
return nil, errors.Errorf("%v must be greater than or equal to 120 seconds", flagkey.FnSpecializationTimeout)
} else {
if maxScale <= 0 {
maxScale = 1
}
}
@@ -451,15 +541,12 @@ func getInvokeStrategy(input cli.Input, existingInvokeStrategy *fv1.InvokeStrate
// Right now a simple single case strategy implementation
// This will potentially get more sophisticated once we have more strategies in place
strategy = &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: fnExecutor,
MinScale: minScale,
MaxScale: maxScale,
TargetCPUPercent: targetCPU,
SpecializationTimeout: specializationTimeout,
},
strategy = &fv1.ExecutionStrategy{
ExecutorType: fnExecutor,
MinScale: minScale,
MaxScale: maxScale,
TargetCPUPercent: targetCPU,
SpecializationTimeout: specializationTimeout,
}
}
+88 -64
View File
@@ -29,38 +29,41 @@ import (
func TestGetInvokeStrategy(t *testing.T) {
cases := []struct {
name string
testArgs map[string]interface{}
existingInvokeStrategy *fv1.InvokeStrategy
expectedResult *fv1.InvokeStrategy
expectError bool
}{
{
// case: use default executor poolmgr
name: "use default executor poolmgr",
testArgs: map[string]interface{}{},
existingInvokeStrategy: nil,
expectedResult: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypePoolmgr,
ExecutorType: fv1.ExecutorTypePoolmgr,
SpecializationTimeout: 120,
},
},
expectError: false,
},
{
// case: executor type set to poolmgr
testArgs: map[string]interface{}{flagkey.FnExecutorType: fv1.ExecutorTypePoolmgr},
name: "executor type set to poolmgr",
testArgs: map[string]interface{}{flagkey.FnExecutorType: string(fv1.ExecutorTypePoolmgr)},
existingInvokeStrategy: nil,
expectedResult: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypePoolmgr,
ExecutorType: fv1.ExecutorTypePoolmgr,
SpecializationTimeout: 120,
},
},
expectError: false,
},
{
// case: executor type set to newdeploy
testArgs: map[string]interface{}{flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy},
name: "executor type set to newdeploy",
testArgs: map[string]interface{}{flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy)},
existingInvokeStrategy: nil,
expectedResult: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
@@ -75,8 +78,8 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: executor type change from poolmgr to newdeploy
testArgs: map[string]interface{}{flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy},
name: "executor type change from poolmgr to newdeploy",
testArgs: map[string]interface{}{flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy)},
existingInvokeStrategy: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
@@ -96,8 +99,8 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: executor type change from newdeploy to poolmgr
testArgs: map[string]interface{}{flagkey.FnExecutorType: fv1.ExecutorTypePoolmgr},
name: "executor type change from newdeploy to poolmgr",
testArgs: map[string]interface{}{flagkey.FnExecutorType: string(fv1.ExecutorTypePoolmgr)},
existingInvokeStrategy: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
@@ -111,15 +114,16 @@ func TestGetInvokeStrategy(t *testing.T) {
expectedResult: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypePoolmgr,
ExecutorType: fv1.ExecutorTypePoolmgr,
SpecializationTimeout: fv1.DefaultSpecializationTimeOut,
},
},
expectError: false,
},
{
// case: minscale < maxscale
name: "minscale < maxscale",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMinscale: 2,
flagkey.ReplicasMaxscale: 3,
},
@@ -137,9 +141,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: minscale > maxscale
name: "minscale > maxscale",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMinscale: 5,
flagkey.ReplicasMaxscale: 3,
},
@@ -148,19 +152,28 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: true,
},
{
// case: maxscale not specified
name: "maxscale not specified",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMinscale: 5,
},
existingInvokeStrategy: nil,
expectedResult: nil,
expectError: true,
expectedResult: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypeNewdeploy,
MinScale: 5,
MaxScale: 5,
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
SpecializationTimeout: fv1.DefaultSpecializationTimeOut,
},
},
expectError: false,
},
{
// case: minscale not specified
name: "minscale not specified",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMaxscale: 3,
},
existingInvokeStrategy: nil,
@@ -177,9 +190,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: maxscale set to 0
name: "maxscale set to 0",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMaxscale: 0,
},
existingInvokeStrategy: nil,
@@ -187,9 +200,28 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: true,
},
{
// case: maxscale set to 9 when existing is 5
name: "update minscale with value larger than existing maxScale",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMinscale: 9,
},
existingInvokeStrategy: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
ExecutionStrategy: fv1.ExecutionStrategy{
ExecutorType: fv1.ExecutorTypeNewdeploy,
MinScale: 2,
MaxScale: 5,
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
SpecializationTimeout: fv1.DefaultSpecializationTimeOut,
},
},
expectedResult: nil,
expectError: true,
},
{
name: "maxscale set to 9 when existing is 5",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.ReplicasMaxscale: 9,
},
existingInvokeStrategy: &fv1.InvokeStrategy{
@@ -215,9 +247,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: change nothing for existing strategy
name: "change nothing for existing strategy",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
},
existingInvokeStrategy: &fv1.InvokeStrategy{
StrategyType: fv1.StrategyTypeExecution,
@@ -242,9 +274,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: set target cpu percentage
name: "set target cpu percentage",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.RuntimeTargetcpu: 50,
},
existingInvokeStrategy: nil,
@@ -261,9 +293,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: change target cpu percentage
name: "change target cpu percentage",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.RuntimeTargetcpu: 20,
},
existingInvokeStrategy: &fv1.InvokeStrategy{
@@ -289,9 +321,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: change specializationtimeout
name: "change specializationtimeout",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.FnSpecializationTimeout: 200,
},
existingInvokeStrategy: &fv1.InvokeStrategy{
@@ -316,19 +348,9 @@ func TestGetInvokeStrategy(t *testing.T) {
expectError: false,
},
{
// case: specializationtimeout should not work for poolmgr
name: "specializationtimeout should not be less than 120",
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypePoolmgr,
flagkey.FnSpecializationTimeout: 10,
},
existingInvokeStrategy: nil,
expectedResult: nil,
expectError: true,
},
{
// case: specializationtimeout should not be less than 120
testArgs: map[string]interface{}{
flagkey.FnExecutorType: fv1.ExecutorTypeNewdeploy,
flagkey.FnExecutorType: string(fv1.ExecutorTypeNewdeploy),
flagkey.FnSpecializationTimeout: 90,
},
existingInvokeStrategy: nil,
@@ -337,25 +359,27 @@ func TestGetInvokeStrategy(t *testing.T) {
},
}
for i, c := range cases {
fmt.Printf("=== Test Case %v ===\n", i)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
flags := dummy.TestFlagSet()
flags := dummy.TestFlagSet()
for k, v := range c.testArgs {
flags.Set(k, v)
}
strategy, err := getInvokeStrategy(flags, c.existingInvokeStrategy)
if c.expectError {
assert.NotNil(t, err)
if err != nil {
fmt.Println(err)
for k, v := range c.testArgs {
flags.Set(k, v)
}
} else {
assert.Nil(t, err)
assert.NoError(t, strategy.Validate(), fmt.Sprintf("Failed at test case %v", i))
assert.Equal(t, *c.expectedResult, *strategy)
}
strategy, err := getInvokeStrategy(flags, c.existingInvokeStrategy)
if c.expectError {
assert.NotNil(t, err)
if err != nil {
fmt.Println(err)
}
} else {
assert.Nil(t, err)
if err == nil {
assert.NoError(t, strategy.Validate())
assert.Equal(t, *c.expectedResult, *strategy)
}
}
})
}
}
+32 -46
View File
@@ -31,6 +31,7 @@ import (
"github.com/fission/fission/pkg/controller/client"
"github.com/fission/fission/pkg/fission-cli/cliwrapper/cli"
"github.com/fission/fission/pkg/fission-cli/cmd/httptrigger"
"github.com/fission/fission/pkg/fission-cli/console"
flagkey "github.com/fission/fission/pkg/fission-cli/flag/key"
"github.com/fission/fission/pkg/fission-cli/util"
)
@@ -65,6 +66,7 @@ func (opts *TestSubCommand) do(input cli.Input) error {
}
routerURL = "127.0.0.1:" + localRouterPort
} else {
console.Verbose(2, "Env FISSION_ROUTER: %v", routerURL)
routerURL = strings.TrimPrefix(routerURL, "http://")
}
@@ -77,6 +79,9 @@ func (opts *TestSubCommand) do(input cli.Input) error {
if err != nil {
return err
}
console.Verbose(2, "Function test url: %v", functionUrl.String())
queryParams := input.StringSlice(flagkey.FnTestQuery)
if len(queryParams) > 0 {
query := url.Values{}
@@ -97,16 +102,13 @@ func (opts *TestSubCommand) do(input cli.Input) error {
functionUrl.RawQuery = query.Encode()
}
ctx := context.Background()
if deadline := input.Duration(flagkey.FnTestTimeout); deadline > 0 {
var closeCtx func()
ctx, closeCtx = context.WithTimeout(ctx, deadline)
defer closeCtx()
}
ctx, closeCtx := context.WithTimeout(context.Background(), input.Duration(flagkey.FnTestTimeout))
defer closeCtx()
headers := input.StringSlice(flagkey.FnTestHeader)
resp, err := doHTTPRequest(ctx, input.String(flagkey.HtMethod), functionUrl.String(), input.String(flagkey.FnTestBody), headers)
resp, err := doHTTPRequest(ctx, functionUrl.String(),
input.StringSlice(flagkey.FnTestHeader),
input.String(flagkey.HtMethod),
input.String(flagkey.FnTestBody))
if err != nil {
return err
}
@@ -118,21 +120,25 @@ func (opts *TestSubCommand) do(input cli.Input) error {
}
if resp.StatusCode < 400 {
fmt.Print(string(body))
os.Stdout.Write(body)
return nil
}
fmt.Printf("Error calling function %s: %d; Please try again or fix the error: %s", m.Name, resp.StatusCode, string(body))
err = printPodLogs(input)
console.Errorf("Error calling function %s: %d; Please try again or fix the error: %s\n", m.Name, resp.StatusCode, string(body))
log, err := printPodLogs(opts.client, m)
if err != nil {
fmt.Printf("Error getting function logs from pod: %v. Try to get logs from log database", err)
return Log(input)
console.Errorf("Error getting function logs from controller: %v. Try to get logs from log database.", err)
err = Log(input)
if err != nil {
return errors.Wrapf(err, "error retrieving function log from log database")
}
} else {
console.Info(log)
}
return nil
return errors.New("error getting function response")
}
func doHTTPRequest(ctx context.Context, method, url, body string, headers []string) (*http.Response, error) {
func doHTTPRequest(ctx context.Context, url string, headers []string, method, body string) (*http.Response, error) {
method, err := httptrigger.GetMethod(method)
if err != nil {
return nil, err
@@ -158,41 +164,21 @@ func doHTTPRequest(ctx context.Context, method, url, body string, headers []stri
return resp, nil
}
func printPodLogs(input cli.Input) error {
fnName := input.String(flagkey.FnName)
u, err := util.GetApplicationUrl("application=fission-api")
func printPodLogs(client *client.Client, fnMeta *metav1.ObjectMeta) (string, error) {
reader, statusCode, err := client.FunctionPodLogs(fnMeta)
if err != nil {
return err
return "", errors.Wrap(err, "error executing get logs request")
}
defer reader.Close()
queryURL, err := url.Parse(u)
body, err := ioutil.ReadAll(reader)
if err != nil {
return errors.Wrap(err, "error parsing the base URL")
}
queryURL.Path = fmt.Sprintf("/proxy/logs/%s", fnName)
req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil)
if err != nil {
return errors.Wrap(err, "error creating logs request")
return "", errors.Wrap(err, "error reading the response body")
}
httpClient := http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "execute get logs request")
if statusCode != http.StatusOK {
return string(body), errors.Errorf("error getting logs from controller, status code: '%v'", statusCode)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("get logs from pod directly")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "read the response body")
}
fmt.Println(string(body))
return nil
return string(body), nil
}
+29 -59
View File
@@ -30,7 +30,6 @@ import (
"github.com/fission/fission/pkg/fission-cli/console"
flagkey "github.com/fission/fission/pkg/fission-cli/flag/key"
"github.com/fission/fission/pkg/fission-cli/util"
"github.com/fission/fission/pkg/types"
)
type UpdateSubCommand struct {
@@ -82,31 +81,12 @@ func (opts *UpdateSubCommand) complete(input cli.Input) error {
envNamespace = ""
}
var deployArchiveFiles []string
codeFlag := false
code := input.String(flagkey.PkgCode)
if len(code) == 0 {
deployArchiveFiles = input.StringSlice(flagkey.PkgDeployArchive)
} else {
deployArchiveFiles = append(deployArchiveFiles, input.String(flagkey.PkgCode))
codeFlag = true
}
srcArchiveFiles := input.StringSlice(flagkey.PkgSrcArchive)
pkgName := input.String(flagkey.FnPackageName)
entrypoint := input.String(flagkey.FnEntrypoint)
buildcmd := input.String(flagkey.PkgBuildCmd)
force := input.Bool(flagkey.PkgForce)
secretNames := input.StringSlice(flagkey.FnSecret)
cfgMapNames := input.StringSlice(flagkey.FnCfgMap)
specializationTimeout := input.Int(flagkey.FnSpecializationTimeout)
if len(srcArchiveFiles) > 0 && len(deployArchiveFiles) > 0 {
return errors.Errorf("need either of --%v or --%v and not both arguments", flagkey.PkgSrcArchive, flagkey.PkgDeployArchive)
}
var secrets []fv1.SecretReference
var configMaps []fv1.ConfigMapReference
@@ -187,18 +167,6 @@ func (opts *UpdateSubCommand) complete(input cli.Input) error {
}
function.Spec.InvokeStrategy = *strategy
if input.IsSet(flagkey.FnSpecializationTimeout) {
if strategy.ExecutionStrategy.ExecutorType != types.ExecutorTypeNewdeploy {
return errors.Errorf("--%v flag is only applicable for newdeploy type of executor", flagkey.FnSpecializationTimeout)
}
if specializationTimeout < fv1.DefaultSpecializationTimeOut {
return errors.Errorf("--%v must be greater than or equal to 120 seconds", flagkey.FnSpecializationTimeout)
} else {
function.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout = specializationTimeout
}
}
resReqs, err := util.GetResourceReqs(input, &function.Spec.Resources)
if err != nil {
return err
@@ -214,36 +182,38 @@ func (opts *UpdateSubCommand) complete(input cli.Input) error {
return errors.Wrap(err, fmt.Sprintf("read package '%v.%v'. Pkg should be present in the same ns as the function", pkgName, fnNamespace))
}
pkgMetadata := &pkg.Metadata
forceUpdate := input.Bool(flagkey.PkgForce)
if len(deployArchiveFiles) != 0 || len(srcArchiveFiles) != 0 || len(buildcmd) != 0 || len(envName) != 0 || len(envNamespace) != 0 {
fnList, err := _package.GetFunctionsByPackage(opts.client, pkg.Metadata.Name, pkg.Metadata.Namespace)
if err != nil {
return errors.Wrap(err, "error getting function list")
}
fnList, err := _package.GetFunctionsByPackage(opts.client, pkg.Metadata.Name, pkg.Metadata.Namespace)
if err != nil {
return errors.Wrap(err, "error getting function list")
}
if !force && len(fnList) > 1 {
return errors.New("package is used by multiple functions, use --force to force update")
}
if !forceUpdate && len(fnList) > 1 {
return errors.Errorf("Package is used by multiple functions, use --%v to force update", flagkey.PkgForce)
}
pkgMetadata, err = _package.UpdatePackage(opts.client, pkg, envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, false, codeFlag)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error updating package '%v'", pkgName))
}
newPkgMeta, err := _package.UpdatePackage(input, opts.client, pkg)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error updating package '%v'", pkgName))
}
fmt.Printf("package '%v' updated\n", pkgMetadata.GetName())
// update resource version of package reference of functions that shared the same package
// the package resource version of function has been changed,
// we need to update function resource version to prevent conflict.
// TODO: remove this block when deprecating pkg flags of function command.
if pkg.Metadata.ResourceVersion != newPkgMeta.ResourceVersion {
var fns []fv1.Function
// don't update the package resource version of the function we are currently
// updating to prevent update conflict.
for _, fn := range fnList {
// ignore the update for current function here, it will be updated later.
if fn.Metadata.Name != fnName {
fn.Spec.Package.PackageRef.ResourceVersion = pkgMetadata.ResourceVersion
_, err := opts.client.FunctionUpdate(&fn)
if err != nil {
return errors.Wrap(err, "error updating function")
}
if fn.Metadata.UID != function.Metadata.UID {
fns = append(fns, fn)
}
}
err = _package.UpdateFunctionPackageResourceVersion(opts.client, newPkgMeta, fns...)
if err != nil {
return errors.Wrap(err, "error updating function package reference resource version")
}
}
// TODO : One corner case where user just updates the pkg reference with fnUpdate, but internally this new pkg reference
@@ -251,9 +221,9 @@ func (opts *UpdateSubCommand) complete(input cli.Input) error {
// update function spec with new package metadata
function.Spec.Package.PackageRef = fv1.PackageRef{
Namespace: pkgMetadata.Namespace,
Name: pkgMetadata.Name,
ResourceVersion: pkgMetadata.ResourceVersion,
Namespace: newPkgMeta.Namespace,
Name: newPkgMeta.Name,
ResourceVersion: newPkgMeta.ResourceVersion,
}
if function.Spec.Environment.Name != pkg.Spec.Environment.Name {
@@ -273,6 +243,6 @@ func (opts *UpdateSubCommand) run(input cli.Input) error {
return errors.Wrap(err, "error updating function")
}
fmt.Printf("function '%v' updated\n", opts.function.Metadata.Name)
fmt.Printf("Function '%v' updated\n", opts.function.Metadata.Name)
return nil
}
+5 -3
View File
@@ -32,7 +32,8 @@ func Commands() *cobra.Command {
wrapper.SetFlags(createCmd, flag.FlagSet{
Required: []flag.Flag{flag.PkgEnvironment},
Optional: []flag.Flag{flag.PkgName, flag.PkgCode, flag.PkgSrcArchive, flag.PkgDeployArchive,
flag.PkgBuildCmd, flag.NamespacePackage, flag.NamespaceEnvironment, flag.SpecSave},
flag.PkgSrcChecksum, flag.PkgDeployChecksum, flag.PkgInsecure, flag.PkgBuildCmd,
flag.NamespacePackage, flag.NamespaceEnvironment, flag.SpecSave},
})
getSrcCmd := &cobra.Command{
@@ -62,8 +63,9 @@ func Commands() *cobra.Command {
}
wrapper.SetFlags(updateCmd, flag.FlagSet{
Required: []flag.Flag{flag.PkgName},
Optional: []flag.Flag{flag.PkgEnvironment, flag.PkgSrcArchive, flag.PkgDeployArchive,
flag.PkgBuildCmd, flag.PkgForce, flag.NamespacePackage, flag.NamespaceEnvironment},
Optional: []flag.Flag{flag.PkgEnvironment, flag.PkgCode, flag.PkgSrcArchive, flag.PkgDeployArchive,
flag.PkgSrcChecksum, flag.PkgDeployChecksum, flag.PkgInsecure, flag.PkgBuildCmd, flag.PkgForce,
flag.NamespacePackage, flag.NamespaceEnvironment},
})
deleteCmd := &cobra.Command{
+9 -4
View File
@@ -124,6 +124,10 @@ func (opts *CreateSubCommand) run(input cli.Input) error {
func CreatePackage(input cli.Input, client *client.Client, pkgName string, pkgNamespace string, envName string, envNamespace string,
srcArchiveFiles []string, deployArchiveFiles []string, buildcmd string, specDir string, specFile string, noZip bool) (*metav1.ObjectMeta, error) {
insecure := input.Bool(flagkey.PkgInsecure)
deployChecksum := input.String(flagkey.PkgDeployChecksum)
srcChecksum := input.String(flagkey.PkgSrcChecksum)
pkgSpec := fv1.PackageSpec{
Environment: fv1.EnvironmentReference{
Namespace: envNamespace,
@@ -136,9 +140,9 @@ func CreatePackage(input cli.Input, client *client.Client, pkgName string, pkgNa
if len(specFile) > 0 { // we should do this in all cases, i think
pkgStatus = fv1.BuildStatusNone
}
deployment, err := CreateArchive(client, deployArchiveFiles, noZip, specDir, specFile)
deployment, err := CreateArchive(client, deployArchiveFiles, noZip, insecure, deployChecksum, specDir, specFile)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error creating source archive")
}
pkgSpec.Deployment = *deployment
if len(pkgName) == 0 {
@@ -146,9 +150,9 @@ func CreatePackage(input cli.Input, client *client.Client, pkgName string, pkgNa
}
}
if len(srcArchiveFiles) > 0 {
source, err := CreateArchive(client, srcArchiveFiles, false, specDir, specFile)
source, err := CreateArchive(client, srcArchiveFiles, false, insecure, srcChecksum, specDir, specFile)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error creating deploy archive")
}
pkgSpec.Source = *source
pkgStatus = fv1.BuildStatusPending // set package build status to pending
@@ -164,6 +168,7 @@ func CreatePackage(input cli.Input, client *client.Client, pkgName string, pkgNa
if len(pkgName) == 0 {
pkgName = strings.ToLower(uuid.NewV4().String())
}
pkg := &fv1.Package{
Metadata: metav1.ObjectMeta{
Name: pkgName,
+73 -40
View File
@@ -19,6 +19,7 @@ package _package
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
@@ -33,15 +34,18 @@ import (
pkgutil "github.com/fission/fission/pkg/fission-cli/cmd/package/util"
"github.com/fission/fission/pkg/fission-cli/cmd/spec"
spectypes "github.com/fission/fission/pkg/fission-cli/cmd/spec/types"
"github.com/fission/fission/pkg/fission-cli/console"
flagkey "github.com/fission/fission/pkg/fission-cli/flag/key"
"github.com/fission/fission/pkg/fission-cli/util"
"github.com/fission/fission/pkg/utils"
uuid "github.com/satori/go.uuid"
)
// CreateArchive returns a fv1.Archive made from an archive . If specFile, then
// create an archive upload spec in the specs directory; otherwise
// upload the archive using client. noZip avoids zipping the
// includeFiles, but is ignored if there's more than one includeFile.
func CreateArchive(client *client.Client, includeFiles []string, noZip bool, specDir string, specFile string) (*fv1.Archive, error) {
func CreateArchive(client *client.Client, includeFiles []string, noZip bool, insecure bool, checksum string, specDir string, specFile string) (*fv1.Archive, error) {
// get root dir
var rootDir string
var err error
@@ -81,7 +85,7 @@ func CreateArchive(client *client.Client, includeFiles []string, noZip bool, spe
}
path := filepath.Join(rootDir, path)
files, err := utils.FindAllGlobs([]string{path})
files, err := utils.FindAllGlobs(path)
if err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "error finding all globs"))
continue
@@ -96,51 +100,80 @@ func CreateArchive(client *client.Client, includeFiles []string, noZip bool, spe
return nil, errs.ErrorOrNil()
}
if len(specFile) > 0 {
if len(fileURL) > 0 {
if len(fileURL) > 0 {
if insecure {
return &fv1.Archive{
Type: fv1.ArchiveTypeUrl,
URL: fileURL,
}, nil
} else {
// create an ArchiveUploadSpec and reference it from the archive
aus := &spectypes.ArchiveUploadSpec{
Name: archiveName("", includeFiles),
IncludeGlobs: includeFiles,
}
// check if this AUS exists in the specs; if so, don't create a new one
fr, err := spec.ReadSpecs(specDir)
if err != nil {
return nil, errors.Wrap(err, "error reading specs")
}
obj := fr.SpecExists(aus, true, true)
if obj != nil {
oldAus := obj.(*spectypes.ArchiveUploadSpec)
fmt.Printf("Re-using previously created archive %v\n", oldAus.Name)
aus.Name = oldAus.Name
} else {
// save the uploadspec
err := spec.SpecSave(*aus, specFile)
if err != nil {
return nil, errors.Wrapf(err, "write spec file %v", specFile)
}
}
// create the archive object
archive := fv1.Archive{
Type: fv1.ArchiveTypeUrl,
URL: fmt.Sprintf("%v%v", spec.ARCHIVE_URL_PREFIX, aus.Name),
}
return &archive, nil
}
var csum *fv1.Checksum
if len(checksum) > 0 {
csum = &fv1.Checksum{
Type: fv1.ChecksumTypeSHA256,
Sum: checksum,
}
} else {
console.Info(fmt.Sprintf("Downloading file to generate SHA256 checksum. To skip this step, please use --%v / --%v / --%v",
flagkey.PkgSrcChecksum, flagkey.PkgDeployChecksum, flagkey.PkgInsecure))
tmpDir, err := utils.GetTempDir()
if err != nil {
return nil, err
}
file := filepath.Join(tmpDir, uuid.NewV4().String())
err = utils.DownloadUrl(context.Background(), http.DefaultClient, fileURL, file)
if err != nil {
return nil, errors.Wrap(err, "error downloading file from the given URL")
}
csum, err = utils.GetFileChecksum(file)
if err != nil {
return nil, errors.Wrap(err, "error generating file SHA256 checksum")
}
}
return &fv1.Archive{
Type: fv1.ArchiveTypeUrl,
URL: fileURL,
Checksum: *csum,
}, nil
}
if len(fileURL) > 0 {
return &fv1.Archive{
if len(specFile) > 0 {
// create an ArchiveUploadSpec and reference it from the archive
aus := &spectypes.ArchiveUploadSpec{
Name: archiveName("", includeFiles),
IncludeGlobs: includeFiles,
}
// check if this AUS exists in the specs; if so, don't create a new one
fr, err := spec.ReadSpecs(specDir)
if err != nil {
return nil, errors.Wrap(err, "error reading specs")
}
obj := fr.SpecExists(aus, true, true)
if obj != nil {
oldAus := obj.(*spectypes.ArchiveUploadSpec)
fmt.Printf("Re-using previously created archive %v\n", oldAus.Name)
aus.Name = oldAus.Name
} else {
// save the uploadspec
err := spec.SpecSave(*aus, specFile)
if err != nil {
return nil, errors.Wrapf(err, "write spec file %v", specFile)
}
}
// create the archive object
archive := fv1.Archive{
Type: fv1.ArchiveTypeUrl,
URL: fileURL,
}, nil
URL: fmt.Sprintf("%v%v", spec.ARCHIVE_URL_PREFIX, aus.Name),
}
return &archive, nil
}
archivePath, err := makeArchiveFile("", includeFiles, noZip)
@@ -165,7 +198,7 @@ func makeArchiveFile(archiveNameHint string, archiveInput []string, noZip bool)
archiveName := archiveName(archiveNameHint, archiveInput)
// Get files from inputs as number of files decide next steps
files, err := utils.FindAllGlobs(archiveInput)
files, err := utils.FindAllGlobs(archiveInput...)
if err != nil {
return "", errors.Wrap(err, "error finding all globs")
}
+86 -55
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"time"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -31,15 +32,10 @@ import (
)
type UpdateSubCommand struct {
client *client.Client
pkgName string
pkgNamespace string
force bool
envName string
envNamespace string
srcArchiveFiles []string
deployArchiveFiles []string
buildcmd string
client *client.Client
pkgName string
pkgNamespace string
force bool
}
func Update(input cli.Input) error {
@@ -65,11 +61,6 @@ func (opts *UpdateSubCommand) complete(input cli.Input) error {
opts.pkgName = input.String(flagkey.PkgName)
opts.pkgNamespace = input.String(flagkey.NamespacePackage)
opts.force = input.Bool(flagkey.PkgForce)
opts.envName = input.String(flagkey.PkgEnvironment)
opts.envNamespace = input.String(flagkey.NamespaceEnvironment)
opts.srcArchiveFiles = input.StringSlice(flagkey.PkgSrcArchive)
opts.deployArchiveFiles = input.StringSlice(flagkey.PkgDeployArchive)
opts.buildcmd = input.String(flagkey.PkgBuildCmd)
return nil
}
@@ -82,88 +73,111 @@ func (opts *UpdateSubCommand) run(input cli.Input) error {
return errors.Wrap(err, "get package")
}
// if the new env specified is the same as the old one, no need to update package
// same is true for all update parameters, but, for now, we dont check all of them - because, its ok to
// re-write the object with same old values, we just end up getting a new resource version for the object.
if len(opts.envName) > 0 && opts.envName == pkg.Spec.Environment.Name {
opts.envName = ""
}
if opts.envNamespace == pkg.Spec.Environment.Namespace {
opts.envNamespace = ""
}
forceUpdate := input.Bool(flagkey.PkgForce)
fnList, err := GetFunctionsByPackage(opts.client, pkg.Metadata.Name, pkg.Metadata.Namespace)
if err != nil {
return errors.Wrap(err, "get function list")
return errors.Wrap(err, "error getting function list")
}
if !opts.force && len(fnList) > 1 {
return errors.New("Package is used by multiple functions, use --force to force update")
if !forceUpdate && len(fnList) > 1 {
return errors.Errorf("package is used by multiple functions, use --%v to force update", flagkey.PkgForce)
}
newPkgMeta, err := UpdatePackage(opts.client, pkg,
opts.envName, opts.envNamespace, opts.srcArchiveFiles,
opts.deployArchiveFiles, opts.buildcmd, false, false)
newPkgMeta, err := UpdatePackage(input, opts.client, pkg)
if err != nil {
return errors.Wrap(err, "update package")
return errors.Wrap(err, "error updating package")
}
// update resource version of package reference of functions that shared the same package
for _, fn := range fnList {
fn.Spec.Package.PackageRef.ResourceVersion = newPkgMeta.ResourceVersion
_, err := opts.client.FunctionUpdate(&fn)
if pkg.Metadata.ResourceVersion != newPkgMeta.ResourceVersion {
err = UpdateFunctionPackageResourceVersion(opts.client, newPkgMeta, fnList...)
if err != nil {
return errors.Wrap(err, "update function")
return errors.Wrap(err, "error updating function package reference resource version")
}
}
fmt.Printf("Package '%v' updated\n", newPkgMeta.GetName())
return nil
}
func UpdatePackage(client *client.Client, pkg *fv1.Package, envName, envNamespace string,
srcArchiveFiles []string, deployArchiveFiles []string, buildcmd string, forceRebuild bool, noZip bool) (*metav1.ObjectMeta, error) {
func UpdatePackage(input cli.Input, client *client.Client, pkg *fv1.Package) (*metav1.ObjectMeta, error) {
envName := input.String(flagkey.PkgEnvironment)
envNamespace := input.String(flagkey.NamespaceEnvironment)
srcArchiveFiles := input.StringSlice(flagkey.PkgSrcArchive)
deployArchiveFiles := input.StringSlice(flagkey.PkgDeployArchive)
buildcmd := input.String(flagkey.PkgBuildCmd)
insecure := input.Bool(flagkey.PkgInsecure)
deployChecksum := input.String(flagkey.PkgDeployChecksum)
srcChecksum := input.String(flagkey.PkgSrcChecksum)
code := input.String(flagkey.PkgCode)
needToBuild := false
noZip := false
needToRebuild := false
needToUpdate := false
if len(envName) > 0 {
if input.IsSet(flagkey.PkgCode) {
deployArchiveFiles = append(deployArchiveFiles, code)
noZip = true
needToUpdate = true
}
if input.IsSet(flagkey.PkgEnvironment) {
pkg.Spec.Environment.Name = envName
needToBuild = true
needToRebuild = true
needToUpdate = true
}
if len(envNamespace) > 0 {
if input.IsSet(flagkey.NamespaceEnvironment) {
pkg.Spec.Environment.Namespace = envNamespace
needToBuild = true
needToRebuild = true
needToUpdate = true
}
if len(buildcmd) > 0 {
if input.IsSet(flagkey.PkgBuildCmd) {
pkg.Spec.BuildCommand = buildcmd
needToBuild = true
needToRebuild = true
needToUpdate = true
}
if len(srcArchiveFiles) > 0 {
srcArchive, err := CreateArchive(client, srcArchiveFiles, false, "", "")
if input.IsSet(flagkey.PkgSrcArchive) {
srcArchive, err := CreateArchive(client, srcArchiveFiles, noZip, insecure, srcChecksum, "", "")
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error creating source archive")
}
pkg.Spec.Source = *srcArchive
needToBuild = true
needToRebuild = true
needToUpdate = true
} else if input.IsSet(flagkey.PkgSrcChecksum) {
pkg.Spec.Source.Checksum = fv1.Checksum{
Type: fv1.ChecksumTypeSHA256,
Sum: srcChecksum,
}
needToUpdate = true
}
if len(deployArchiveFiles) > 0 {
deployArchive, err := CreateArchive(client, deployArchiveFiles, noZip, "", "")
if input.IsSet(flagkey.PkgDeployArchive) || input.IsSet(flagkey.PkgCode) {
deployArchive, err := CreateArchive(client, deployArchiveFiles, noZip, insecure, deployChecksum, "", "")
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error creating deploy archive")
}
pkg.Spec.Deployment = *deployArchive
// Users may update the env, envNS and deploy archive at the same time,
// but without the source archive. In this case, we should set needToBuild to false
needToBuild = false
needToRebuild = false
needToUpdate = true
} else if input.IsSet(flagkey.PkgDeployChecksum) {
pkg.Spec.Deployment.Checksum = fv1.Checksum{
Type: fv1.ChecksumTypeSHA256,
Sum: srcChecksum,
}
needToUpdate = true
}
if !needToUpdate {
return &pkg.Metadata, nil
}
// Set package as pending status when needToBuild is true
if needToBuild || forceRebuild {
if needToRebuild {
// change into pending state to trigger package build
pkg.Status = fv1.PackageStatus{
BuildStatus: fv1.BuildStatusPending,
@@ -176,9 +190,26 @@ func UpdatePackage(client *client.Client, pkg *fv1.Package, envName, envNamespac
return nil, errors.Wrap(err, "update package")
}
fmt.Printf("Package '%v' updated\n", newPkgMeta.GetName())
return newPkgMeta, err
}
func UpdateFunctionPackageResourceVersion(client *client.Client, pkgMeta *metav1.ObjectMeta, fnList ...fv1.Function) error {
errs := &multierror.Error{}
// update resource version of package reference of functions that shared the same package
for _, fn := range fnList {
fn.Spec.Package.PackageRef.ResourceVersion = pkgMeta.ResourceVersion
_, err := client.FunctionUpdate(&fn)
if err != nil {
errs = multierror.Append(errs, errors.Wrapf(err, "error updating package resource version of function '%v'", fn.Metadata.Name))
}
}
return errs.ErrorOrNil()
}
func updatePackageStatus(client *client.Client, pkg *fv1.Package, status fv1.BuildStatus) (*metav1.ObjectMeta, error) {
switch status {
case fv1.BuildStatusNone, fv1.BuildStatusPending, fv1.BuildStatusRunning, fv1.BuildStatusSucceeded, fv1.CanaryConfigStatusAborted:
+4 -4
View File
@@ -427,13 +427,13 @@ func localArchiveFromSpec(specDir string, aus *spectypes.ArchiveUploadSpec) (*fv
files = append(files, aus.IncludeGlobs[0])
} else {
for _, relativeGlob := range aus.IncludeGlobs {
absGlob := rootDir + "/" + relativeGlob
f, err := filepath.Glob(absGlob)
absGlob := filepath.Join(rootDir, relativeGlob)
console.Verbose(2, "try to find globs in path '%v'", absGlob)
fs, err := utils.FindAllGlobs(absGlob)
if err != nil {
return nil, errors.Wrapf(err, "Invalid glob in archive %v: %v", aus.Name, relativeGlob)
}
files = append(files, f...)
// xxx handle excludeGlobs here
files = append(files, fs...)
}
}
+11 -4
View File
@@ -105,13 +105,20 @@ func (opts *InitSubCommand) complete(input cli.Input) error {
func (opts *InitSubCommand) run(input cli.Input) error {
specDir := util.GetSpecDir(input)
readme := filepath.Join(specDir, "README")
config := filepath.Join(specDir, "fission-deployment-config.yaml")
if _, err := os.Stat(config); err == nil {
return errors.Errorf("Spec DeploymentConfig already exists in directory '%v'", specDir)
}
// Add a bit of documentation to the spec dir here
err := ioutil.WriteFile(filepath.Join(specDir, "README"), []byte(SPEC_README), 0644)
err := ioutil.WriteFile(readme, []byte(SPEC_README), 0644)
if err != nil {
return err
}
err = writeDeploymentConfig(specDir, opts.deployConfig)
err = writeDeploymentConfig(config, opts.deployConfig)
if err != nil {
return errors.Wrap(err, "error writing deployment config")
}
@@ -124,7 +131,7 @@ func (opts *InitSubCommand) run(input cli.Input) error {
// writeDeploymentConfig serializes the DeploymentConfig to YAML and writes it to a new
// fission-config.yaml in specDir.
func writeDeploymentConfig(specDir string, dc *spectypes.DeploymentConfig) error {
func writeDeploymentConfig(file string, dc *spectypes.DeploymentConfig) error {
y, err := yaml.Marshal(dc)
if err != nil {
return err
@@ -134,7 +141,7 @@ func writeDeploymentConfig(specDir string, dc *spectypes.DeploymentConfig) error
"# See the README in this directory for background and usage information.\n" +
"# Do not edit the UID below: that will break 'fission spec apply'\n")
err = ioutil.WriteFile(filepath.Join(specDir, "fission-deployment-config.yaml"), append(msg, y...), 0644)
err = ioutil.WriteFile(file, append(msg, y...), 0644)
if err != nil {
return err
}
+18 -27
View File
@@ -297,35 +297,26 @@ func (fr *FissionResources) Validate(input cli.Input) error {
for _, p := range fr.Packages {
packages[MapKey(&p.Metadata)] = false
if strings.HasPrefix(p.Spec.Deployment.URL, ARCHIVE_URL_PREFIX) {
// check archive refs from package
aname := strings.TrimPrefix(p.Spec.Source.URL, ARCHIVE_URL_PREFIX)
if len(aname) > 0 {
if _, ok := archives[aname]; !ok {
result = multierror.Append(result, fmt.Errorf(
"%v: package '%v' references unknown source archive %v%v",
fr.SourceMap.Locations["Package"][p.Metadata.Namespace][p.Metadata.Name],
p.Metadata.Name,
ARCHIVE_URL_PREFIX,
aname))
} else {
archives[aname] = true
}
}
as := map[string]string{
"source": p.Spec.Source.URL,
"deployment": p.Spec.Deployment.URL,
}
if strings.HasPrefix(p.Spec.Deployment.URL, ARCHIVE_URL_PREFIX) {
aname := strings.TrimPrefix(p.Spec.Deployment.URL, ARCHIVE_URL_PREFIX)
if len(aname) > 0 {
if _, ok := archives[aname]; !ok {
result = multierror.Append(result, fmt.Errorf(
"%v: package '%v' references unknown deployment archive %v%v",
fr.SourceMap.Locations["Package"][p.Metadata.Namespace][p.Metadata.Name],
p.Metadata.Name,
ARCHIVE_URL_PREFIX,
aname))
} else {
archives[aname] = true
for archiveType, u := range as {
if strings.HasPrefix(u, ARCHIVE_URL_PREFIX) {
aname := strings.TrimPrefix(u, ARCHIVE_URL_PREFIX)
if len(aname) > 0 {
if _, ok := archives[aname]; !ok {
result = multierror.Append(result, fmt.Errorf(
"%v: package '%v' references unknown %v archive '%v%v'",
fr.SourceMap.Locations["Package"][p.Metadata.Namespace][p.Metadata.Name],
p.Metadata.Name,
archiveType,
ARCHIVE_URL_PREFIX,
aname))
} else {
archives[aname] = true
}
}
}
}
+5
View File
@@ -33,6 +33,11 @@ func Error(msg interface{}) {
os.Stderr.WriteString(fmt.Sprintf("%v: %v\n", color.RedString("Error"), trimNewline(msg)))
}
func Errorf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
os.Stderr.WriteString(fmt.Sprintf("%v: %v\n", color.RedString("Error"), trimNewline(msg)))
}
func Warn(msg interface{}) {
os.Stdout.WriteString(fmt.Sprintf("%v: %v\n", color.YellowString("Warning"), trimNewline(msg)))
}
+16 -12
View File
@@ -24,13 +24,13 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
flagkey "github.com/fission/fission/pkg/fission-cli/flag/key"
"github.com/fission/fission/pkg/types"
)
type (
FlagType = int
FlagSet struct {
Global []Flag
Required []Flag
Optional []Flag
}
@@ -93,7 +93,7 @@ var (
FnBuildCmd = Flag{Type: String, Name: flagkey.FnBuildCmd, Usage: "Package build command for builder to run with"}
FnSecret = Flag{Type: StringSlice, Name: flagkey.FnSecret, Usage: "Function access to secret, should be present in the same namespace as the function. You can provide multiple secrets using multiple --secrets flags. In the case of fn update the the secrets will be replaced by the provided list of secrets."}
FnCfgMap = Flag{Type: StringSlice, Name: flagkey.FnCfgMap, Usage: "Function access to configmap, should be present in the same namespace as the function. You can provide multiple configmaps using multiple --configmap flags. In case of fn update the configmaps will be replaced by the provided list of configmaps."}
FnExecutorType = Flag{Type: String, Name: flagkey.FnExecutorType, Usage: "Executor type for execution; one of 'poolmgr', 'newdeploy'", DefaultValue: types.ExecutorTypePoolmgr}
FnExecutorType = Flag{Type: String, Name: flagkey.FnExecutorType, Usage: "Executor type for execution; one of 'poolmgr', 'newdeploy'", DefaultValue: string(fv1.ExecutorTypePoolmgr)}
FnExecutionTimeout = Flag{Type: Int, Name: flagkey.FnExecutionTimeout, Aliases: []string{"ft"}, Usage: "Maximum time for a request to wait for the response from the function", DefaultValue: 60}
FnLogPod = Flag{Type: String, Name: flagkey.FnLogPod, Usage: "Function pod name (use the latest pod name if unspecified)"}
FnLogFollow = Flag{Type: Bool, Name: flagkey.FnLogFollow, Short: "f", Usage: "Specify if the logs should be streamed"}
@@ -141,6 +141,7 @@ var (
EnvExternalNetwork = Flag{Type: Bool, Name: flagkey.EnvExternalNetwork, Usage: "Allow pod to access external network (only works when istio feature is enabled)"}
EnvTerminationGracePeriod = Flag{Type: Int, Name: flagkey.EnvGracePeriod, Aliases: []string{"period"}, Usage: "Grace time (in seconds) for pod to perform connection draining before termination", DefaultValue: 360}
EnvVersion = Flag{Type: Int, Name: flagkey.EnvVersion, Usage: "Environment API version (1 means v1 interface)", DefaultValue: 1}
EnvImagePullSecret = Flag{Type: String, Name: flagkey.EnvImagePullSecret, Usage: "Secret for Kubernetes to pull an image from a private registry"}
KwName = Flag{Type: String, Name: flagkey.KwName, Usage: "Watch name"}
KwFnName = Flag{Type: String, Name: flagkey.KwFnName, Usage: "Function name"}
@@ -148,16 +149,19 @@ var (
KwObjType = Flag{Type: String, Name: flagkey.KwObjType, Usage: "Type of resource to watch (Pod, Service, etc.)", DefaultValue: "pod"}
KwLabels = Flag{Type: String, Name: flagkey.KwLabels, Usage: "Label selector of the form a=b,c=d"}
PkgName = Flag{Type: String, Name: flagkey.PkgName, Usage: "Package name"}
PkgForce = Flag{Type: Bool, Name: flagkey.PkgForce, Short: "f", Usage: "Force update a package even if it is used by one or more functions"}
PkgEnvironment = Flag{Type: String, Name: flagkey.PkgEnvironment, Usage: "Environment name"}
PkgBuildCmd = Flag{Type: String, Name: flagkey.PkgBuildCmd, Usage: "Build command for builder to run with"}
PkgOutput = Flag{Type: String, Name: flagkey.PkgOutput, Short: "o", Usage: "Output filename to save archive content"}
PkgStatus = Flag{Type: String, Name: flagkey.PkgStatus, Usage: `Filter packages by status`}
PkgOrphan = Flag{Type: Bool, Name: flagkey.PkgOrphan, Usage: "Orphan packages that are not referenced by any function"}
PkgCode = Flag{Type: String, Name: flagkey.PkgCode, Usage: "URL or local path for single file source code"}
PkgDeployArchive = Flag{Type: StringSlice, Name: flagkey.PkgDeployArchive, Aliases: []string{"deploy"}, Usage: "URL or local paths for binary archive"}
PkgSrcArchive = Flag{Type: StringSlice, Name: flagkey.PkgSrcArchive, Aliases: []string{"source", "src"}, Usage: "URL or local paths for source archive"}
PkgName = Flag{Type: String, Name: flagkey.PkgName, Usage: "Package name"}
PkgForce = Flag{Type: Bool, Name: flagkey.PkgForce, Short: "f", Usage: "Force update a package even if it is used by one or more functions"}
PkgEnvironment = Flag{Type: String, Name: flagkey.PkgEnvironment, Usage: "Environment name"}
PkgBuildCmd = Flag{Type: String, Name: flagkey.PkgBuildCmd, Usage: "Build command for builder to run with"}
PkgOutput = Flag{Type: String, Name: flagkey.PkgOutput, Short: "o", Usage: "Output filename to save archive content"}
PkgStatus = Flag{Type: String, Name: flagkey.PkgStatus, Usage: `Filter packages by status`}
PkgOrphan = Flag{Type: Bool, Name: flagkey.PkgOrphan, Usage: "Orphan packages that are not referenced by any function"}
PkgCode = Flag{Type: String, Name: flagkey.PkgCode, Usage: "URL or local path for single file source code"}
PkgDeployArchive = Flag{Type: StringSlice, Name: flagkey.PkgDeployArchive, Aliases: []string{"deploy"}, Usage: "URL or local paths for binary archive"}
PkgDeployChecksum = Flag{Type: String, Name: flagkey.PkgDeployChecksum, Usage: "SHA256 checksum of deploy archive when providing URL"}
PkgSrcArchive = Flag{Type: StringSlice, Name: flagkey.PkgSrcArchive, Aliases: []string{"source", "src"}, Usage: "URL or local paths for source archive"}
PkgSrcChecksum = Flag{Type: String, Name: flagkey.PkgSrcChecksum, Usage: "SHA256 checksum of source archive when providing URL"}
PkgInsecure = Flag{Type: Bool, Name: flagkey.PkgInsecure, Usage: "Skip generating SHA256 checksum for file integrity validation"}
SpecSave = Flag{Type: Bool, Name: flagkey.SpecSave, Usage: "Save to the spec directory instead of creating on cluster"}
SpecDir = Flag{Type: String, Name: flagkey.SpecDir, Usage: "Directory to store specs, defaults to ./specs"}
+14 -10
View File
@@ -96,6 +96,7 @@ const (
EnvExternalNetwork = "externalnetwork"
EnvGracePeriod = "graceperiod"
EnvVersion = "version"
EnvImagePullSecret = "imagepullsecret"
KwName = resourceName
KwFnName = "function"
@@ -103,16 +104,19 @@ const (
KwObjType = "type"
KwLabels = "labels"
PkgName = resourceName
PkgForce = force
PkgEnvironment = "env"
PkgCode = "code"
PkgSrcArchive = "sourcearchive"
PkgDeployArchive = "deployarchive"
PkgBuildCmd = "buildcmd"
PkgOutput = Output
PkgStatus = "status"
PkgOrphan = "orphan"
PkgName = resourceName
PkgForce = force
PkgEnvironment = "env"
PkgCode = "code"
PkgSrcArchive = "sourcearchive"
PkgDeployArchive = "deployarchive"
PkgSrcChecksum = "srcchecksum"
PkgDeployChecksum = "deploychecksum"
PkgInsecure = "insecure"
PkgBuildCmd = "buildcmd"
PkgOutput = Output
PkgStatus = "status"
PkgOrphan = "orphan"
SpecSave = "spec"
SpecDir = "specdir"
+181 -131
View File
@@ -32,7 +32,6 @@ import (
"github.com/pkg/errors"
"go.opencensus.io/plugin/ochttp"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
@@ -54,9 +53,9 @@ type (
logger *zap.Logger
fmap *functionServiceMap
executor *executorClient.Client
function *metav1.ObjectMeta
function *fv1.Function
httpTrigger *fv1.HTTPTrigger
functionMetadataMap map[string]*metav1.ObjectMeta
functionMap map[string]*fv1.Function
fnWeightDistributionList []FunctionWeightDistribution
tsRoundTripperParams *tsRoundTripperParams
isDebugEnv bool
@@ -86,9 +85,13 @@ type (
// A layer on top of http.DefaultTransport, with retries.
RetryingRoundTripper struct {
logger *zap.Logger
funcHandler *functionHandler
timeout int
logger *zap.Logger
funcHandler *functionHandler
funcTimeout time.Duration
closeContextFunc *context.CancelFunc
serviceUrl *url.URL
urlFromCache bool
totalRetry int
}
// To keep the request body open during retries, we create an interface with Close operation being a no-op.
@@ -145,26 +148,10 @@ func (w *fakeCloseReadCloser) RealClose() error {
// inside ServeHttp function of the reverseProxy.
// Earlier, GetServiceForFunction was called inside handler function and fission explicitly set http status code to 500
// if it returned an error.
func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
func (roundTripper *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// Set forwarded host header if not exists
roundTripper.addForwardedHostHeader(req)
fnMeta := roundTripper.funcHandler.function
// Metrics stuff
startTime := time.Now()
funcMetricLabels := &functionLabels{
namespace: fnMeta.Namespace,
name: fnMeta.Name,
}
httpMetricLabels := &httpLabels{
method: req.Method,
}
if roundTripper.funcHandler.httpTrigger != nil {
httpMetricLabels.host = roundTripper.funcHandler.httpTrigger.Spec.Host
httpMetricLabels.path = roundTripper.funcHandler.httpTrigger.Spec.RelativeURL
}
// set the timeout for transport context
transport := roundTripper.getDefaultTransport()
ocRoundTripper := &ochttp.Transport{Base: transport}
@@ -183,6 +170,8 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
}
}()
roundTripper.logger.Debug("request headers", zap.Any("headers", req.Header))
// The reason for request failure may vary from case to case.
// After some investigation, found most of the failure are due to
// network timeout or target function is under heavy workload. In
@@ -194,19 +183,15 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
// than the predefined threshold, reset retryCounter and remove service
// cache, then retry to get new svc record from executor again.
var retryCounter int
var serviceUrl *url.URL
var serviceUrlFromCache bool
var err error
var resp *http.Response
var fnMeta = &roundTripper.funcHandler.function.Metadata
for i := 0; i < roundTripper.funcHandler.tsRoundTripperParams.maxRetries; i++ {
// set service url of target service of request only when
// trying to get new service url from cache/executor.
if retryCounter == 0 {
// get function service url from cache or executor
serviceUrl, serviceUrlFromCache, err = roundTripper.funcHandler.getServiceEntry()
roundTripper.serviceUrl, roundTripper.urlFromCache, err = roundTripper.funcHandler.getServiceEntry()
if err != nil {
// We might want a specific error code or header for fission failures as opposed to
// user function bugs.
@@ -228,21 +213,16 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
// service url maybe nil if router cannot find one in cache,
// so here we retry to get service url again
if serviceUrl == nil {
if roundTripper.serviceUrl == nil {
time.Sleep(executingTimeout)
executingTimeout = executingTimeout * time.Duration(roundTripper.funcHandler.tsRoundTripperParams.timeoutExponent)
continue
}
// tapService before invoking roundTrip for the serviceUrl
if serviceUrlFromCache {
go roundTripper.funcHandler.tapService(serviceUrl)
}
// modify the request to reflect the service url
// this service url may have come from the cache lookup or from executor response
req.URL.Scheme = serviceUrl.Scheme
req.URL.Host = serviceUrl.Host
req.URL.Scheme = roundTripper.serviceUrl.Scheme
req.URL.Host = roundTripper.serviceUrl.Host
// To keep the function run container simple, it
// doesn't do any routing. In the future if we have
@@ -254,7 +234,7 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
// Overwrite request host with internal host,
// or request will be blocked in some situations
// (e.g. istio-proxy)
req.Host = serviceUrl.Host
req.Host = roundTripper.serviceUrl.Host
}
// over-riding default settings.
@@ -263,37 +243,21 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
KeepAlive: roundTripper.funcHandler.tsRoundTripperParams.keepAliveTime,
}).DialContext
overhead := time.Since(startTime)
roundTripper.logger.Debug("request headers", zap.Any("headers", req.Header))
// Creating context for client
if roundTripper.timeout <= 0 {
roundTripper.timeout = fv1.DEFAULT_FUNCTION_TIMEOUT
}
roundTripper.logger.Debug("Creating context for request for ", zap.Any("time", roundTripper.timeout))
// pass request context as parent context for the case
// that user aborts connection before timeout. Otherwise,
// the request won't be canceled until the deadline exceeded
// which may be a potential security issue.
ctx, closeCtx := context.WithTimeout(req.Context(), time.Duration(roundTripper.timeout)*time.Second)
// Do NOT assign returned request to "req"
// because the request used in the last round
// will be canceled when calling setContext.
newReq := roundTripper.setContext(req)
// forward the request to the function service
resp, err = ocRoundTripper.RoundTrip(req.WithContext(ctx))
closeCtx()
resp, err := ocRoundTripper.RoundTrip(newReq)
if err == nil {
// Track metrics
httpMetricLabels.code = resp.StatusCode
funcMetricLabels.cached = serviceUrlFromCache
functionCallCompleted(funcMetricLabels, httpMetricLabels,
overhead, time.Since(startTime), resp.ContentLength)
// return response back to user
return resp, nil
} else if i >= roundTripper.funcHandler.tsRoundTripperParams.maxRetries-1 {
}
roundTripper.totalRetry += 1
if i >= roundTripper.funcHandler.tsRoundTripperParams.maxRetries-1 {
// return here if we are in the last round
roundTripper.logger.Error("error getting response from function",
zap.String("function_name", fnMeta.Name),
@@ -316,11 +280,16 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
return resp, err
}
// close response body before entering next loop
if resp != nil {
resp.Body.Close()
}
// Check whether an error is an timeout error ("dial tcp i/o timeout").
// If it's not a timeout error or retryCounter exceeded pre-defined threshold,
// we assume the entry in router cache is stale, invalidate it.
if !isNetTimeoutErr || retryCounter >= roundTripper.funcHandler.tsRoundTripperParams.svcAddrRetryCount {
if serviceUrlFromCache {
if roundTripper.urlFromCache {
// if transport.RoundTrip returns a network dial error and serviceUrl was from cache,
// it means, the entry in router cache is stale, so invalidate it.
roundTripper.logger.Debug("request errored out - removing function from router's cache and requesting a new service for function",
@@ -342,11 +311,6 @@ func (roundTripper RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Res
roundTripper.logger.Debug("Backing off before retrying", zap.Any("backoff_time", executingTimeout), zap.Error(err))
time.Sleep(executingTimeout)
executingTimeout = executingTimeout * time.Duration(roundTripper.funcHandler.tsRoundTripperParams.timeoutExponent)
// close response body before entering next loop
if resp != nil {
resp.Body.Close()
}
}
e := errors.New("Unable to get service url for connection")
@@ -373,25 +337,47 @@ func (roundTripper RetryingRoundTripper) getDefaultTransport() *http.Transport {
}
}
func (fh *functionHandler) tapService(serviceUrl *url.URL) {
// setContext returns a shallow copy of request with a new timeout context.
func (roundTripper *RetryingRoundTripper) setContext(req *http.Request) *http.Request {
if roundTripper.closeContextFunc != nil {
(*roundTripper.closeContextFunc)()
}
// pass request context as parent context for the case
// that user aborts connection before timeout. Otherwise,
// the request won't be canceled until the deadline exceeded
// which may be a potential security issue.
ctx, closeCtx := context.WithTimeout(req.Context(), roundTripper.funcTimeout)
roundTripper.closeContextFunc = &closeCtx
return req.WithContext(ctx)
}
// closeContext closes the context to release resources.
func (roundTripper *RetryingRoundTripper) closeContext() {
if roundTripper.closeContextFunc != nil {
(*roundTripper.closeContextFunc)()
}
}
func (fh *functionHandler) tapService(fn *fv1.Function, serviceUrl *url.URL) {
if fh.executor == nil {
return
}
fh.executor.TapService(serviceUrl)
fh.executor.TapService(fn.Metadata, fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType, serviceUrl)
}
func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *http.Request) {
if fh.httpTrigger != nil && fh.httpTrigger.Spec.FunctionReference.Type == types.FunctionReferenceTypeFunctionWeights {
// canary deployment. need to determine the function to send request to now
fnMetadata := getCanaryBackend(fh.functionMetadataMap, fh.fnWeightDistributionList)
if fnMetadata == nil {
fn := getCanaryBackend(fh.functionMap, fh.fnWeightDistributionList)
if fn == nil {
fh.logger.Error("could not get canary backend",
zap.Any("metadataMap", fh.functionMetadataMap),
zap.Any("fnMap", fh.functionMap),
zap.Any("distributionList", fh.fnWeightDistributionList))
// TODO : write error to responseWrite and return response
return
}
fh.function = fnMetadata
fh.function = fn
fh.logger.Debug("chosen function backend's metadata", zap.Any("metadata", fh.function))
}
@@ -399,7 +385,7 @@ func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *h
setPathInfoToHeader(request)
// system params
setFunctionMetadataToHeader(fh.function, request)
setFunctionMetadataToHeader(&fh.function.Metadata, request)
director := func(req *http.Request) {
if _, ok := req.Header["User-Agent"]; !ok {
@@ -408,21 +394,42 @@ func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *h
}
}
var timeout int = fv1.DEFAULT_FUNCTION_TIMEOUT
if fh.functionTimeoutMap != nil {
timeout = fh.functionTimeoutMap[fh.function.GetUID()]
fnTimeout := fh.functionTimeoutMap[fh.function.Metadata.GetUID()]
if fnTimeout == 0 {
fnTimeout = fv1.DEFAULT_FUNCTION_TIMEOUT
}
proxy := &httputil.ReverseProxy{
Director: director,
Transport: &RetryingRoundTripper{
logger: fh.logger.Named("roundtripper"),
funcHandler: &fh,
timeout: timeout,
},
ErrorHandler: getProxyErrorHandler(fh.logger, fh.function),
rrt := &RetryingRoundTripper{
logger: fh.logger.Named("roundtripper"),
funcHandler: &fh,
funcTimeout: time.Duration(fnTimeout) * time.Second,
}
start := time.Now()
proxy := &httputil.ReverseProxy{
Director: director,
Transport: rrt,
ErrorHandler: fh.getProxyErrorHandler(start, rrt),
ModifyResponse: func(resp *http.Response) error {
go fh.collectFunctionMetric(start, rrt, request, resp)
return nil
},
}
defer func() {
// If the context is closed when RoundTrip returns, client may receive
// truncated response body due to "context canceled" error. To avoid
// this, we need to close request context after proxy.ServeHTTP finished.
//
// NOTE: rrt.closeContext() must be put in the defer function; otherwise,
// reverseProxy may panic when failed to write response and the context
// will not be closed.
//
// ref: https://github.com/golang/go/issues/28239
rrt.closeContext()
}()
proxy.ServeHTTP(responseWriter, request)
}
@@ -453,38 +460,10 @@ func findCeil(randomNumber int, wtDistrList []FunctionWeightDistribution) string
}
// picks a function to route to based on a random number generated
func getCanaryBackend(fnMetadatamap map[string]*metav1.ObjectMeta, fnWtDistributionList []FunctionWeightDistribution) *metav1.ObjectMeta {
func getCanaryBackend(fnMap map[string]*fv1.Function, fnWtDistributionList []FunctionWeightDistribution) *fv1.Function {
randomNumber := rand.Intn(fnWtDistributionList[len(fnWtDistributionList)-1].sumPrefix + 1)
fnName := findCeil(randomNumber, fnWtDistributionList)
return fnMetadatamap[fnName]
}
// getProxyErrorHandler returns a reverse proxy error handler
func getProxyErrorHandler(logger *zap.Logger, fnMeta *metav1.ObjectMeta) func(rw http.ResponseWriter, req *http.Request, err error) {
return func(rw http.ResponseWriter, req *http.Request, err error) {
status := http.StatusBadGateway
switch err {
case context.Canceled:
// 499 CLIENT CLOSED REQUEST
// A non-standard status code introduced by nginx for the case
// when a client closes the connection while nginx is processing the request.
// Reference: https://httpstatuses.com/499
status = 499
logger.Debug("client closes the connection",
zap.Any("function", fnMeta), zap.Any("request_header", req.Header))
case context.DeadlineExceeded:
status = http.StatusGatewayTimeout
logger.Error("function not responses before the timeout",
zap.Any("function", fnMeta), zap.Any("request_header", req.Header))
default:
logger.Error("error sending request to function",
zap.Error(err), zap.Any("function", fnMeta), zap.Any("request_header", req.Header))
}
// TODO: return error message that contains traceable UUID back to user. Issue #693
rw.WriteHeader(status)
}
return fnMap[fnName]
}
// addForwardedHostHeader add "forwarded host" to request header
@@ -544,28 +523,30 @@ func (fh *functionHandler) getServiceEntry() (serviceUrl *url.URL, serviceUrlFro
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fnMeta := &fh.function.Metadata
// Use throttle to limit the total amount of requests sent
// to the executor to prevent it from overloaded.
recordObj, err := fh.svcAddrUpdateThrottler.RunOnce(
crd.CacheKey(fh.function),
crd.CacheKey(fnMeta),
func(firstToTheLock bool) (interface{}, error) {
var u *url.URL
// Get service entry from executor and update cache if its the first goroutine
if firstToTheLock { // first to the service url
fh.logger.Debug("calling getServiceForFunction",
zap.String("function_name", fh.function.Name))
zap.String("function_name", fnMeta.Name))
u, err = fh.getServiceEntryFromExecutor(ctx)
if err != nil {
fh.logger.Error("error getting service url from executor",
zap.Error(err),
zap.String("function_name", fh.function.Name))
zap.String("function_name", fnMeta.Name))
return nil, err
}
// add the address in router's cache
fh.logger.Info("assigning service url for function",
zap.String("url", u.String()),
zap.String("function_name", fh.function.Name))
fh.fmap.assign(fh.function, u)
zap.String("function_name", fnMeta.Name))
fh.fmap.assign(fnMeta, u)
} else {
u, err = fh.getServiceEntryFromCache()
if err != nil {
@@ -583,9 +564,9 @@ func (fh *functionHandler) getServiceEntry() (serviceUrl *url.URL, serviceUrlFro
e := "error updating service address entry for function"
fh.logger.Error(e,
zap.Error(err),
zap.String("function_name", fh.function.Name),
zap.String("function_namespace", fh.function.Namespace))
return nil, false, errors.Wrapf(err, "%s %s_%s", e, fh.function.Name, fh.function.Namespace)
zap.String("function_name", fnMeta.Name),
zap.String("function_namespace", fnMeta.Namespace))
return nil, false, errors.Wrapf(err, "%s %s_%s", e, fnMeta.Name, fnMeta.Namespace)
}
record, ok := recordObj.(svcEntryRecord)
@@ -597,9 +578,9 @@ func (fh *functionHandler) getServiceEntry() (serviceUrl *url.URL, serviceUrlFro
}
// getServiceEntryFromCache returns service url entry returns from cache
func (fh *functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err error) {
func (fh functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err error) {
// cache lookup to get serviceUrl
serviceUrl, err = fh.fmap.lookup(fh.function)
serviceUrl, err = fh.fmap.lookup(&fh.function.Metadata)
if err != nil {
var errMsg string
@@ -612,7 +593,7 @@ func (fh *functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err
if e.Code == ferror.ErrorNotFound {
return nil, nil
}
errMsg = fmt.Sprintf("Error getting function %v;s service entry from cache: %v", fh.function.Name, err)
errMsg = fmt.Sprintf("Error getting function %v;s service entry from cache: %v", fh.function.Metadata.Name, err)
}
return nil, ferror.MakeError(http.StatusInternalServerError, errMsg)
}
@@ -620,9 +601,9 @@ func (fh *functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err
}
// getServiceEntryFromExecutor returns service url entry returns from executor
func (fh *functionHandler) getServiceEntryFromExecutor(ctx context.Context) (*url.URL, error) {
func (fh functionHandler) getServiceEntryFromExecutor(ctx context.Context) (*url.URL, error) {
// send a request to executor to specialize a new pod
service, err := fh.executor.GetServiceForFunction(ctx, fh.function)
service, err := fh.executor.GetServiceForFunction(ctx, &fh.function.Metadata)
if err != nil {
statusCode, errMsg := ferror.GetHTTPError(err)
fh.logger.Error("error from GetServiceForFunction",
@@ -644,3 +625,72 @@ func (fh *functionHandler) getServiceEntryFromExecutor(ctx context.Context) (*ur
return serviceUrl, nil
}
// getProxyErrorHandler returns a reverse proxy error handler
func (fh functionHandler) getProxyErrorHandler(start time.Time, rrt *RetryingRoundTripper) func(rw http.ResponseWriter, req *http.Request, err error) {
return func(rw http.ResponseWriter, req *http.Request, err error) {
var status int
var msg string
switch err {
case context.Canceled:
// 499 CLIENT CLOSED REQUEST
// A non-standard status code introduced by nginx for the case
// when a client closes the connection while nginx is processing the request.
// Reference: https://httpstatuses.com/499
status = 499
msg = "client closes the connection"
fh.logger.Debug(msg, zap.Any("function", fh.function), zap.Any("request_header", req.Header))
case context.DeadlineExceeded:
status = http.StatusGatewayTimeout
msg = "function not responses before the timeout"
fh.logger.Error(msg, zap.Any("function", fh.function), zap.Any("request_header", req.Header))
default:
status = http.StatusBadGateway
msg = "error sending request to function"
fh.logger.Error(msg, zap.Error(err), zap.Any("function", fh.function), zap.Any("request_header", req.Header))
}
go fh.collectFunctionMetric(start, rrt, req, &http.Response{
StatusCode: status,
ContentLength: 0,
})
// TODO: return error message that contains traceable UUID back to user. Issue #693
rw.WriteHeader(status)
rw.Write([]byte(msg))
}
}
func (fh functionHandler) collectFunctionMetric(start time.Time, rrt *RetryingRoundTripper, req *http.Request, resp *http.Response) {
duration := time.Since(start)
// Metrics stuff
funcMetricLabels := &functionLabels{
namespace: fh.function.Metadata.Namespace,
name: fh.function.Metadata.Name,
}
httpMetricLabels := &httpLabels{
method: req.Method,
}
if fh.httpTrigger != nil {
httpMetricLabels.host = fh.httpTrigger.Spec.Host
httpMetricLabels.path = fh.httpTrigger.Spec.RelativeURL
}
// Track metrics
httpMetricLabels.code = resp.StatusCode
funcMetricLabels.cached = rrt.urlFromCache
functionCallCompleted(funcMetricLabels, httpMetricLabels,
duration, duration, resp.ContentLength)
// tapService before invoking roundTrip for the serviceUrl
if rrt.urlFromCache {
fh.tapService(fh.function, rrt.serviceUrl)
}
fh.logger.Debug("Request complete", zap.String("function", fh.function.Metadata.Name),
zap.Int("retry", rrt.totalRetry), zap.Duration("total-time", duration),
zap.Int64("content-length", resp.ContentLength))
}
+19 -6
View File
@@ -57,12 +57,12 @@ func TestFunctionProxying(t *testing.T) {
backendURL := createBackendService(testResponseString)
log.Printf("Created backend svc at %v", backendURL)
fn := &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
fnMeta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
logger, err := zap.NewDevelopment()
panicIf(err)
fmap := makeFunctionServiceMap(logger, 0)
fmap.assign(fn, backendURL)
fmap.assign(&fnMeta, backendURL)
httpTrigger := &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
@@ -78,9 +78,11 @@ func TestFunctionProxying(t *testing.T) {
}
fh := &functionHandler{
logger: logger,
fmap: fmap,
function: fn,
logger: logger,
fmap: fmap,
function: &fv1.Function{
Metadata: metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
},
tsRoundTripperParams: &tsRoundTripperParams{
timeout: 50 * time.Millisecond,
timeoutExponent: 2,
@@ -97,7 +99,18 @@ func TestFunctionProxying(t *testing.T) {
func TestProxyErrorHandler(t *testing.T) {
logger, err := zap.NewDevelopment()
assert.Nil(t, err)
errHandler := getProxyErrorHandler(logger, nil)
fh := &functionHandler{
logger: logger,
function: &fv1.Function{
Metadata: metav1.ObjectMeta{
Name: "dummy",
Namespace: "dummy-bar",
},
},
}
errHandler := fh.getProxyErrorHandler(time.Now(), &RetryingRoundTripper{})
req, err := http.NewRequest("GET", "http://foobar.com", nil)
assert.Nil(t, err)
+9 -9
View File
@@ -50,7 +50,7 @@ type (
// a distribution of requests across two functions.
resolveResult struct {
resolveResultType
functionMetadataMap map[string]*metav1.ObjectMeta
functionMap map[string]*fv1.Function
functionWtDistributionList []FunctionWeightDistribution
}
@@ -134,12 +134,13 @@ func (frr *functionReferenceResolver) resolveByName(namespace, name string) (*re
}
f := obj.(*fv1.Function)
functionMetadataMap := make(map[string]*metav1.ObjectMeta, 1)
functionMetadataMap[f.Metadata.Name] = &f.Metadata
functionMap := map[string]*fv1.Function{
f.Metadata.Name: f,
}
rr := resolveResult{
resolveResultType: resolveResultSingleFunction,
functionMetadataMap: functionMetadataMap,
resolveResultType: resolveResultSingleFunction,
functionMap: functionMap,
}
return &rr, nil
@@ -147,7 +148,7 @@ func (frr *functionReferenceResolver) resolveByName(namespace, name string) (*re
func (frr *functionReferenceResolver) resolveByFunctionWeights(namespace string, fr *fv1.FunctionReference) (*resolveResult, error) {
functionMetadataMap := make(map[string]*metav1.ObjectMeta)
functionMap := make(map[string]*fv1.Function)
fnWtDistrList := make([]FunctionWeightDistribution, 0)
sumPrefix := 0
@@ -167,19 +168,18 @@ func (frr *functionReferenceResolver) resolveByFunctionWeights(namespace string,
}
f := obj.(*fv1.Function)
functionMetadataMap[f.Metadata.Name] = &f.Metadata
functionMap[f.Metadata.Name] = f
sumPrefix = sumPrefix + functionWeight
fnWtDistrList = append(fnWtDistrList, FunctionWeightDistribution{
name: functionName,
weight: functionWeight,
sumPrefix: sumPrefix,
})
}
rr := resolveResult{
resolveResultType: resolveResultMultipleFunctions,
functionMetadataMap: functionMetadataMap,
functionMap: functionMap,
functionWtDistributionList: fnWtDistrList,
}
+10 -11
View File
@@ -142,7 +142,7 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router
fmap: ts.functionServiceMap,
executor: ts.executor,
httpTrigger: &trigger,
functionMetadataMap: rr.functionMetadataMap,
functionMap: rr.functionMap,
fnWeightDistributionList: rr.functionWtDistributionList,
tsRoundTripperParams: ts.tsRoundTripperParams,
isDebugEnv: ts.isDebugEnv,
@@ -158,8 +158,8 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router
// deployment. For more details, please check "handler" function of functionHandler.
if rr.resolveResultType == resolveResultSingleFunction {
for _, metadata := range fh.functionMetadataMap {
fh.function = metadata
for _, fn := range fh.functionMap {
fh.function = fn
}
}
@@ -185,20 +185,19 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router
// Internal triggers for each function by name. Non-http
// triggers route into these.
for _, function := range ts.functions {
m := function.Metadata
for i := range ts.functions {
fn := ts.functions[i]
fh := &functionHandler{
logger: ts.logger.Named(m.Name),
logger: ts.logger.Named(fn.Metadata.Name),
fmap: ts.functionServiceMap,
function: &m,
function: &fn,
executor: ts.executor,
tsRoundTripperParams: ts.tsRoundTripperParams,
isDebugEnv: ts.isDebugEnv,
svcAddrUpdateThrottler: ts.svcAddrUpdateThrottler,
functionTimeoutMap: fnTimeoutMap,
}
muxRouter.HandleFunc(utils.UrlForFunction(function.Metadata.Name, function.Metadata.Namespace), fh.handler)
muxRouter.HandleFunc(utils.UrlForFunction(fn.Metadata.Name, fn.Metadata.Namespace), fh.handler)
}
// Healthz endpoint for the router.
@@ -263,8 +262,8 @@ func (ts *HTTPTriggerSet) initFunctionController() (k8sCache.Store, k8sCache.Con
// update resolver function reference cache
for key, rr := range ts.resolver.copy() {
if key.namespace == fn.Metadata.Namespace &&
rr.functionMetadataMap[fn.Metadata.Name] != nil &&
rr.functionMetadataMap[fn.Metadata.Name].ResourceVersion != fn.Metadata.ResourceVersion {
rr.functionMap[fn.Metadata.Name] != nil &&
rr.functionMap[fn.Metadata.Name].Metadata.ResourceVersion != fn.Metadata.ResourceVersion {
// invalidate resolver cache
ts.logger.Debug("invalidating resolver cache")
err := ts.resolver.delete(key.namespace, key.triggerName, key.triggerResourceVersion)
+3
View File
@@ -71,6 +71,9 @@ func deleteIngress(logger *zap.Logger, trigger *fv1.HTTPTrigger, kubeClient *kub
}
func updateIngress(logger *zap.Logger, oldT *fv1.HTTPTrigger, newT *fv1.HTTPTrigger, kubeClient *kubernetes.Clientset) {
if !oldT.Spec.CreateIngress && !newT.Spec.CreateIngress {
return
}
if !oldT.Spec.CreateIngress && newT.Spec.CreateIngress {
createIngress(logger, newT, kubeClient)
+9 -7
View File
@@ -32,12 +32,12 @@ import (
func TestRouter(t *testing.T) {
// metadata for a fake function
fn := &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
fnMeta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
// and a reference to it
fr := fv1.FunctionReference{
Type: types.FunctionReferenceTypeFunctionName,
Name: fn.Name,
Name: fnMeta.Name,
}
// start a fake service
@@ -49,7 +49,7 @@ func TestRouter(t *testing.T) {
// set up the cache with this fake service
fmap := makeFunctionServiceMap(logger, 0)
fmap.assign(fn, testServiceUrl)
fmap.assign(&fnMeta, testServiceUrl)
// HTTP trigger set with a trigger for this function
triggers, _, _ := makeHTTPTriggerSet(logger, fmap, nil, nil, nil, nil,
@@ -81,12 +81,14 @@ func TestRouter(t *testing.T) {
triggerResourceVersion: "1234",
}
fnMetaMap := make(map[string]*metav1.ObjectMeta, 1)
fnMetaMap[fn.Name] = fn
fnMetaMap := make(map[string]*fv1.Function, 1)
fnMetaMap[fnMeta.Name] = &fv1.Function{
Metadata: fnMeta,
}
rr := resolveResult{
resolveResultType: resolveResultSingleFunction,
functionMetadataMap: fnMetaMap,
resolveResultType: resolveResultSingleFunction,
functionMap: fnMetaMap,
}
frr.refCache.Set(nfr, rr)
+9 -14
View File
@@ -91,7 +91,6 @@ const (
)
const EXECUTOR_INSTANCEID_LABEL = fv1.EXECUTOR_INSTANCEID_LABEL
const POOLMGR_INSTANCEID_LABEL = fv1.POOLMGR_INSTANCEID_LABEL
const (
ChecksumTypeSHA256 = fv1.ChecksumTypeSHA256
@@ -121,22 +120,18 @@ const (
// executor kubernetes object label key
const (
ENVIRONMENT_NAMESPACE = "environmentNamespace"
ENVIRONMENT_NAME = "environmentName"
ENVIRONMENT_UID = "environmentUid"
FUNCTION_NAMESPACE = "functionNamespace"
FUNCTION_NAME = "functionName"
FUNCTION_UID = "functionUid"
EXECUTOR_TYPE = "executorType"
ENVIRONMENT_NAMESPACE = "environmentNamespace"
ENVIRONMENT_NAME = "environmentName"
ENVIRONMENT_UID = "environmentUid"
FUNCTION_NAMESPACE = "functionNamespace"
FUNCTION_NAME = "functionName"
FUNCTION_UID = "functionUid"
FUNCTION_RESOURCE_VERSION = "functionResourceVersion"
EXECUTOR_TYPE = "executorType"
)
const (
ExecutorTypePoolmgr = fv1.ExecutorTypePoolmgr
ExecutorTypeNewdeploy = fv1.ExecutorTypeNewdeploy
)
const (
StrategyTypeExecution = fv1.StrategyTypeExecution
ANNOTATION_SVC_HOST = "svcHost"
)
const (
+56 -7
View File
@@ -17,12 +17,14 @@ limitations under the License.
package utils
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strings"
@@ -30,10 +32,12 @@ import (
"github.com/mholt/archiver"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"golang.org/x/net/context/ctxhttp"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/fission-cli/console"
)
func UrlForFunction(name, namespace string) string {
@@ -99,21 +103,34 @@ func GetTempDir() (string, error) {
return dir, err
}
// FindAllGlobs returns a list of globs of input list.
func FindAllGlobs(inputList []string) ([]string, error) {
// FindAllGlobs ignores all hidden files and returns a list of globs of input list.
func FindAllGlobs(paths ...string) ([]string, error) {
files := make([]string, 0)
for _, glob := range inputList {
f, err := filepath.Glob(glob)
for _, p := range paths {
// use absolute path to find files
path, err := filepath.Abs(p)
if err != nil {
return nil, errors.Errorf("invalid glob %v: %v", glob, err)
return nil, errors.Wrapf(err, "error getting absolute path of path '%v'", p)
}
globs, err := filepath.Glob(path)
if err != nil {
return nil, errors.Errorf("invalid glob %v: %v", path, err)
}
for _, f := range globs {
// ignore hidden file.
if strings.HasPrefix(filepath.Base(f), ".") {
console.Verbose(2, "Ignore hidden file '%v'", f)
continue
}
files = append(files, f)
// xxx handle excludeGlobs here
}
files = append(files, f...)
}
return files, nil
}
func MakeZipArchive(targetName string, globs ...string) (string, error) {
files, err := FindAllGlobs(globs)
files, err := FindAllGlobs(globs...)
if err != nil {
return "", err
}
@@ -198,3 +215,35 @@ func GetChecksum(src io.Reader) (*fv1.Checksum, error) {
func IsURL(str string) bool {
return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://")
}
func DownloadUrl(ctx context.Context, httpClient *http.Client, url string, localPath string) error {
resp, err := ctxhttp.Get(ctx, httpClient, url)
if err != nil {
return err
}
defer resp.Body.Close()
w, err := os.Create(localPath)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
return err
}
// flushing write buffer to file
err = w.Sync()
if err != nil {
return err
}
err = os.Chmod(localPath, 0600)
if err != nil {
return err
}
return nil
}
+7 -4
View File
@@ -512,6 +512,7 @@ run_all_tests() {
export JOBS=6
$ROOT/test/run_test.sh \
$ROOT/test/tests/test_canary.sh \
$ROOT/test/tests/test_fn_update/test_idle_objects_reaper.sh \
$ROOT/test/tests/mqtrigger/kafka/test_kafka.sh \
$ROOT/test/tests/test_annotations.sh \
$ROOT/test/tests/test_archive_pruner.sh \
@@ -519,7 +520,6 @@ run_all_tests() {
$ROOT/test/tests/test_buildermgr.sh \
$ROOT/test/tests/test_env_vars.sh \
$ROOT/test/tests/test_environments/test_python_env.sh \
$ROOT/test/tests/test_fn_update/test_idle_objects_reaper.sh \
$ROOT/test/tests/test_function_test/test_fn_test.sh \
$ROOT/test/tests/test_function_update.sh \
$ROOT/test/tests/test_ingress.sh \
@@ -527,19 +527,22 @@ run_all_tests() {
$ROOT/test/tests/test_logging/test_function_logs.sh \
$ROOT/test/tests/test_node_hello_http.sh \
$ROOT/test/tests/test_package_command.sh \
$ROOT/test/tests/test_package_checksum.sh \
$ROOT/test/tests/test_pass.sh \
$ROOT/test/tests/test_router_cache_invalidation.sh \
$ROOT/test/tests/test_specs/test_spec.sh \
$ROOT/test/tests/test_specs/test_spec_multifile.sh \
$ROOT/test/tests/test_specs/test_ignore_hidden_file.sh \
$ROOT/test/tests/test_specs/test_spec_merge/test_spec_merge.sh \
$ROOT/test/tests/test_specs/test_spec_archive/test_spec_archive.sh \
$ROOT/test/tests/test_environments/test_tensorflow_serving_env.sh \
$ROOT/test/tests/test_environments/test_go_env.sh \
$ROOT/test/tests/mqtrigger/nats/test_mqtrigger.sh \
$ROOT/test/tests/mqtrigger/nats/test_mqtrigger_error.sh
$ROOT/test/tests/mqtrigger/nats/test_mqtrigger_error.sh \
$ROOT/test/tests/test_huge_response/test_huge_response.sh
FAILURES=$?
# FIXME: run tests with newdeploy one by one.
export JOBS=2
export JOBS=3
$ROOT/test/run_test.sh \
$ROOT/test/tests/test_backend_newdeploy.sh \
$ROOT/test/tests/test_environments/test_java_builder.sh \
@@ -41,7 +41,7 @@ timeout 60 bash -c "test_fn ${fn}-gpm 'world'"
log "Waiting for idle pod reaper to recycle resources"
# the LIST_OLD function list fsvc older than 2 mins
# so in worst case, we need to wait for up to 4 mins + some buffer
sleep 260
sleep 300
# The replicas of function deployment should be 0 due to minScale = 0
ndDeployReplicas=$(kubectl -n $FUNCTION_NAMESPACE get deploy -l functionName=${fn}-nd -ojsonpath='{.items[0].spec.replicas}')
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"io/ioutil"
"net/http"
)
// Handler is the entry point for this fission function
func Handler(w http.ResponseWriter, r *http.Request) {
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
set -euo pipefail
source $(dirname $0)/../../utils.sh
TEST_ID=$(generate_test_id)
echo "TEST_ID = $TEST_ID"
ROOT=$(dirname $0)/../../..
pushd $ROOT/test/tests/test_huge_response
cleanup() {
clean_resource_by_id $TEST_ID
rm -rf response.json
popd
}
retryPost() {
local fn=$1
log "Send huge JSON request body"
set +e
while true; do
curl -X POST http://$FISSION_ROUTER/$fn \
-H "Content-Type: application/json" \
--data-binary "@generated.json" > response.json
difftext=$(diff generated.json response.json)
if [ -z "$difftext" ]; then
break
else
echo "Receive truncated body"
sleep 1
fi
done
set -e
}
export -f retryPost
if [ -z "${TEST_NOCLEANUP:-}" ]; then
trap cleanup EXIT
else
log "TEST_NOCLEANUP is set; not cleaning up test artifacts afterwards."
fi
env=go-$TEST_ID
fn_poolmgr=hello-go-poolmgr-$TEST_ID
log "Creating environment for Golang"
fission env create --name $env --image $GO_RUNTIME_IMAGE --builder $GO_BUILDER_IMAGE --period 5
timeout 90 bash -c "wait_for_builder $env"
pkgName=$(generate_test_id)
fission package create --name $pkgName --src hello.go --env $env
# wait for build to finish at most 90s
timeout 90 bash -c "waitBuild $pkgName"
log "Creating function for Golang"
fission fn create --name $fn_poolmgr --env $env --pkg $pkgName --entrypoint Handler
log "Creating route for function"
fission route create --name $fn_poolmgr --function $fn_poolmgr --url /$fn_poolmgr --method POST
log "Waiting for router & pools to catch up"
sleep 5
log "Testing function"
timeout 20 bash -c "retryPost $fn_poolmgr"
log "Test PASSED"
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -euo pipefail
source $(dirname $0)/../utils.sh
TEST_ID=$(generate_test_id)
echo "TEST_ID = $TEST_ID"
tmp_dir="/tmp/test-$TEST_ID"
mkdir -p $tmp_dir
ROOT=$(dirname $0)/../..
checkpkgsum() {
local pkg=$1
local sum=$2
pkgsum=$(kubectl -n default get packages ${pkg} -o jsonpath='{.spec.deployment.checksum.sum}')
if [ "${sum}" != "${pkgsum}" ]; then
log "have different sha256 checksum: ${sum} vs. ${pkgsum}"
kubectl -n default get packages ${pkg} -o yaml
exit 1
fi
}
cleanup() {
log "Cleaning up..."
clean_resource_by_id $TEST_ID
rm -rf $tmp_dir
}
if [ -z "${TEST_NOCLEANUP:-}" ]; then
trap cleanup EXIT
else
log "TEST_NOCLEANUP is set; not cleaning up test artifacts afterwards."
fi
log "Download test script"
url1="https://raw.githubusercontent.com/fission/fission/master/examples/nodejs/hello.js"
url2="https://raw.githubusercontent.com/fission/fission/master/examples/nodejs/hello-callback.js"
wget ${url1}
sum=$(shasum -a 256 hello.js|cut -d' ' -f 1)
wget ${url2}
sum2=$(shasum -a 256 hello-callback.js|cut -d' ' -f 1)
env="nodejs-${TEST_ID}"
log "Function with file URL"
fn1="fn1-$TEST_ID"
fission env create --name ${env} --image $NODE_RUNTIME_IMAGE --period 5
fission fn create --name ${fn1} --env ${env} --code ${url1}
pkgname=$(kubectl -n default get functions ${fn1} -o jsonpath="{.spec.package.packageref.name}")
checkpkgsum ${pkgname} ${sum}
log "Creating route"
fission route create --name ${fn1} --function ${fn1} --url /${fn1} --method GET
sleep 3
timeout 60 bash -c "test_fn ${fn1} 'hello, world'"
log "Update function with file URL"
fission fn update --name ${fn1} --env ${env} --code ${url2}
pkgname=$(kubectl -n default get functions ${fn1} -o jsonpath="{.spec.package.packageref.name}")
checkpkgsum ${pkgname} ${sum2}
sleep 3
timeout 60 bash -c "test_fn ${fn1} 'Hello, world callback!'"
log "Function with file URL & checksum"
fn2="fn2-$TEST_ID"
fission fn create --name ${fn2} --env ${env} --code ${url1} --deploychecksum ${sum}
pkgname=$(kubectl -n default get functions ${fn2} -o jsonpath="{.spec.package.packageref.name}")
checkpkgsum ${pkgname} ${sum}
log "Creating route"
fission route create --name ${fn2} --function ${fn2} --url /${fn2} --method GET
timeout 60 bash -c "test_fn ${fn2} 'hello, world'"
log "Function with file URL & insecure"
fn3="fn3-$TEST_ID"
fission fn create --name ${fn3} --env ${env} --code ${url1} --insecure
pkgname=$(kubectl -n default get functions ${fn3} -o jsonpath="{.spec.package.packageref.name}")
checkpkgsum ${pkgname} ""
log "Creating route"
fission route create --name ${fn3} --function ${fn3} --url /${fn3} --method GET
sleep 3
timeout 60 bash -c "test_fn ${fn3} 'hello, world'"
pkg1="pkg1-$TEST_ID"
pkg2="pkg2-$TEST_ID"
pkg3="pkg3-$TEST_ID"
fission pkg create --name ${pkg1} --env ${env} --code ${url1}
checkpkgsum ${pkg1} ${sum}
fission pkg update --name ${pkg1} --env ${env} --code ${url2}
checkpkgsum ${pkg1} ${sum2}
fission pkg create --name ${pkg2} --env ${env} --code ${url1} --deploychecksum ${sum}
checkpkgsum ${pkg2} ${sum}
fission pkg create --name ${pkg3} --env ${env} --code ${url1} --insecure
checkpkgsum ${pkg3} ""
log "Test PASSED"
exit 0
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
set -euo pipefail
source $(dirname $0)/../../utils.sh
ROOT=` realpath $(dirname $0)/../../../`
TEST_ID=$(generate_test_id)
cleanup() {
log "Cleaning up..."
fission spec destroy || true
rm -rf document specs
rm -rf ${TEST_ID}
popd
}
if [ -z "${TEST_NOCLEANUP:-}" ]; then
trap cleanup EXIT
else
log "TEST_NOCLEANUP is set; not cleaning up test artifacts afterwards."
fi
tmp_dir="/tmp/test-$TEST_ID"
mkdir -p $tmp_dir
pushd $tmp_dir
mkdir -p document
cp $ROOT/examples/nodejs/hello.js document/h1.js
cp $ROOT/examples/nodejs/hello.js document/h2.js
log "Create hidden file"
touch document/.im_invisible
log "Create specs"
fission spec init
fission pkg list
#fission env create --name nodejs --image fission/node-env --period 5 --version 2 --spec
fission pkg create --name nodejs --env nodejs --deploy "document/*" --spec
log "Apply specs"
fission --verbosity 2 spec apply
mkdir ${TEST_ID}
fission pkg getdeploy --name nodejs > ${TEST_ID}/a.zip
unzip ${TEST_ID}/a.zip -d ${TEST_ID}/
log "Check whether hidden file exists"
if [ -f ${TEST_ID}/.im_invisible ];
then
log "Found hidden file"
ls -al ${TEST_ID}
exit 1
fi
log "Check file amount"
fileamount=$(ls -al ${TEST_ID} | grep -v total | wc -l)
if [ ! ${fileamount} -eq 5 ];
then
log "File amount incorrect, expect 5"
ls -al ${TEST_ID}
exit 1
fi
log "Test PASSED"
@@ -0,0 +1,42 @@
Fission Specs
=============
This is a set of specifications for a Fission app. This includes functions,
environments, and triggers; we collectively call these things "resources".
How to use these specs
----------------------
These specs are handled with the 'fission spec' command. See 'fission spec --help'.
'fission spec apply' will "apply" all resources specified in this directory to your
cluster. That means it checks what resources exist on your cluster, what resources are
specified in the specs directory, and reconciles the difference by creating, updating or
deleting resources on the cluster.
'fission spec apply' will also package up your source code (or compiled binaries) and
upload the archives to the cluster if needed. It uses 'ArchiveUploadSpec' resources in
this directory to figure out which files to archive.
You can use 'fission spec apply --watch' to watch for file changes and continuously keep
the cluster updated.
You can add YAMLs to this directory by writing them manually, but it's easier to generate
them. Use 'fission function create --spec' to generate a function spec,
'fission environment create --spec' to generate an environment spec, and so on.
You can edit any of the files in this directory, except 'fission-deployment-config.yaml',
which contains a UID that you should never change. To apply your changes simply use
'fission spec apply'.
fission-deployment-config.yaml
------------------------------
fission-deployment-config.yaml contains a UID. This UID is what fission uses to correlate
resources on the cluster to resources in this directory.
All resources created by 'fission spec apply' are annotated with this UID. Resources on
the cluster that are _not_ annotated with this UID are never modified or deleted by
fission.
@@ -0,0 +1,16 @@
apiVersion: fission.io/v1
kind: Environment
metadata:
creationTimestamp: null
name: dummyfoobarnode
namespace: default
spec:
builder:
command: build
image: fission/node-builder:1.6.0
keeparchive: false
poolsize: 3
runtime:
image: fission/node-env:1.6.0
terminationGracePeriod: 20
version: 2
@@ -0,0 +1,7 @@
# This file is generated by the 'fission spec init' command.
# See the README in this directory for background and usage information.
# Do not edit the UID below: that will break 'fission spec apply'
apiVersion: fission.io/v1
kind: DeploymentConfig
name: test-spec-archive
uid: 04b21526-8873-4dc2-b897-e87ed5347670
@@ -0,0 +1,56 @@
apiVersion: fission.io/v1
kind: Function
metadata:
creationTimestamp: null
name: sourcearchive
namespace: default
spec:
InvokeStrategy:
ExecutionStrategy:
ExecutorType: poolmgr
MaxScale: 0
MinScale: 0
SpecializationTimeout: 0
TargetCPUPercent: 0
StrategyType: execution
configmaps: null
environment:
name: dummyfoobarnode
namespace: default
functionTimeout: 60
package:
functionName: source
packageref:
name: sourcearchive
namespace: default
resources: {}
secrets: null
---
apiVersion: fission.io/v1
kind: Function
metadata:
creationTimestamp: null
name: deployarchive
namespace: default
spec:
InvokeStrategy:
ExecutionStrategy:
ExecutorType: poolmgr
MaxScale: 0
MinScale: 0
SpecializationTimeout: 0
TargetCPUPercent: 0
StrategyType: execution
configmaps: null
environment:
name: dummyfoobarnode
namespace: default
functionTimeout: 60
package:
functionName: deploy
packageref:
name: deployarchive
namespace: default
resources: {}
secrets: null
@@ -0,0 +1,24 @@
include:
- func/*
kind: ArchiveUploadSpec
name: functions-deploy-archive
---
apiVersion: fission.io/v1
kind: Package
metadata:
creationTimestamp: null
name: deployarchive
namespace: default
spec:
deployment:
checksum: {}
type: url
url: archive://functions-source-archive
environment:
name: dummyfoobarnode
namespace: default
source:
checksum: {}
status:
buildstatus: none
@@ -0,0 +1,24 @@
include:
- func/*
kind: ArchiveUploadSpec
name: functions-source-archive
---
apiVersion: fission.io/v1
kind: Package
metadata:
creationTimestamp: null
name: sourcearchive
namespace: default
spec:
deployment:
checksum: {}
environment:
name: dummyfoobarnode
namespace: default
source:
checksum: {}
type: url
url: archive://functions-deploy-archive
status:
buildstatus: pending
@@ -0,0 +1,43 @@
#!/bin/bash
set -euo pipefail
source $(dirname $0)/../../../utils.sh
ROOT=` realpath $(dirname $0)/../../../../`
cleanup() {
log "Cleaning up..."
fission spec destroy
rm -rf func
popd
}
if [ -z "${TEST_NOCLEANUP:-}" ]; then
trap cleanup EXIT
else
log "TEST_NOCLEANUP is set; not cleaning up test artifacts afterwards."
fi
pushd $(dirname $0)
[ -d specs ]
[ -f specs/README ]
[ -f specs/fission-deployment-config.yaml ]
mkdir -p func
cp $ROOT/examples/nodejs/hello.js func/deploy.js
cp $ROOT/examples/nodejs/hello.js func/source.js
fission spec destroy || true
log "Apply specs"
fission --verbosity 2 spec apply
log "verify deployarchive function works"
fission fn test --name deployarchive
timeout 60s bash -c "waitBuild sourcearchive"
log "verify sourcearchive function works"
fission fn test --name sourcearchive
log "Test PASSED"
@@ -20,7 +20,6 @@ pushd $(dirname $0)
fission spec apply
fission fn test --name $fn_p
fission fn test --name $fn_nd
hnd=$(kubectl -n $FUNCTION_NAMESPACE get deployment -l=functionName=$fn_nd -ojsonpath='{.items[0].spec.template.spec.hostname}')
+6 -2
View File
@@ -44,6 +44,10 @@ clean_resource_by_id() {
}
test_fn() {
if [ -z $FISSION_ROUTER ]; then
log "Environment FISSION_ROUTER not set"
exit 1
fi
url="http://$FISSION_ROUTER/$1"
expect=$2
test_response $url $expect
@@ -162,8 +166,8 @@ export FISSION_NATS_STREAMING_URL="http://defaultFissionAuthToken@$(kubectl -n $
## To change the environment image setting for CI test, please refer run_all_tests() in test_utils.sh.
export PYTHON_RUNTIME_IMAGE=${PYTHON_RUNTIME_IMAGE:-fission/python-env}
export PYTHON_BUILDER_IMAGE=${PYTHON_BUILDER_IMAGE:-fission/python-builder}
export GO_RUNTIME_IMAGE=${GO_RUNTIME_IMAGE:-fission/go-env}
export GO_BUILDER_IMAGE=${GO_BUILDER_IMAGE:-fission/go-builder}
export GO_RUNTIME_IMAGE=${GO_RUNTIME_IMAGE:-fission/go-env-1.12}
export GO_BUILDER_IMAGE=${GO_BUILDER_IMAGE:-fission/go-builder-1.12}
export JVM_RUNTIME_IMAGE=${JVM_RUNTIME_IMAGE:-fission/jvm-env}
export JVM_BUILDER_IMAGE=${JVM_BUILDER_IMAGE:-fission/jvm-builder}
export NODE_RUNTIME_IMAGE=${NODE_RUNTIME_IMAGE:-fission/node-env}