From 8ca97c7920e0d084158a6483a3f0d17559d4ed97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Mon, 2 Nov 2020 09:36:35 +0100 Subject: [PATCH] Feature: "kn service apply" (#964) * feat: "kn service apply" This commit introduces a client-side apply with a plain JsonPatchMerge. This is more limited than a StrategicPatchMerg as it does not allow to merge lists (they are just overwritten). Also is not a real 3-way merger that would lead to a conflict when both the, the server-side and the provide update overlapp in fields that updated, compared to the shared original configuration. This is a problem of JsonThreeWayMerger itself, as pointed out in https://github.com/kubernetes/kubernetes/pull/40666#pullrequestreview-502804243. This limitation is shared with kubectl, which suffers from the same issue if using `kubectl apply` with a custom resource (i.e. with everything that has schema that is not registered within kubectl). Tests are missing, too, but will come soon * chore: Add tests for 'kn apply' * refactor: Removed PatchService from pulic API interface * fix: Display of service URL at the end, when no changes apply * chore: Add initial E2E test * chore: Implemented review suggestions * More tests * Example for kn service apply * Remove commented-out code * lint fixes * fix formatting of kn service apply doc * fixing go.sum * chore: Update deps --- docs/cmd/kn_service.md | 1 + docs/cmd/kn_service_apply.md | 93 ++++++ go.mod | 2 +- go.sum | 58 +++- pkg/kn/commands/service/apply.go | 123 ++++++++ pkg/kn/commands/service/apply_mock_test.go | 187 ++++++++++++ pkg/kn/commands/service/create.go | 18 +- pkg/kn/commands/service/create_mock_test.go | 8 +- pkg/kn/commands/service/create_test.go | 118 +++----- pkg/kn/commands/service/service.go | 3 +- pkg/serving/v1/apply.go | 268 ++++++++++++++++++ pkg/serving/v1/apply_test.go | 233 +++++++++++++++ pkg/serving/v1/client.go | 38 +++ pkg/serving/v1/client_mock.go | 10 + pkg/serving/v1/client_mock_test.go | 2 + pkg/util/unstructured.go | 8 +- test/e2e/service_apply_test.go | 62 ++++ .../pkg/util/jsonmergepatch/patch.go | 160 +++++++++++ vendor/modules.txt | 3 +- 19 files changed, 1303 insertions(+), 92 deletions(-) create mode 100644 docs/cmd/kn_service_apply.md create mode 100644 pkg/kn/commands/service/apply.go create mode 100644 pkg/kn/commands/service/apply_mock_test.go create mode 100644 pkg/serving/v1/apply.go create mode 100644 pkg/serving/v1/apply_test.go create mode 100644 test/e2e/service_apply_test.go create mode 100644 vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go diff --git a/docs/cmd/kn_service.md b/docs/cmd/kn_service.md index 7243d7e9d..27ff8f534 100644 --- a/docs/cmd/kn_service.md +++ b/docs/cmd/kn_service.md @@ -27,6 +27,7 @@ kn service ### SEE ALSO * [kn](kn.md) - kn manages Knative Serving and Eventing resources +* [kn service apply](kn_service_apply.md) - Apply a service declaration * [kn service create](kn_service_create.md) - Create a service * [kn service delete](kn_service_delete.md) - Delete services * [kn service describe](kn_service_describe.md) - Show details of a service diff --git a/docs/cmd/kn_service_apply.md b/docs/cmd/kn_service_apply.md new file mode 100644 index 000000000..8aa6c4cc5 --- /dev/null +++ b/docs/cmd/kn_service_apply.md @@ -0,0 +1,93 @@ +## kn service apply + +Apply a service declaration + +### Synopsis + +Apply a service declaration + +``` +kn service apply NAME +``` + +### Examples + +``` + +# Create an initial service with using 'kn service apply', if the service has not +# been already created +kn service apply s0 --image knativesamples/helloworld + +# Apply the service again which is a no-operation if none of the options changed +kn service apply s0 --image knativesamples/helloworld + +# Add an environment variable to your service. Note, that you have to always fully +# specify all parameters (in contrast to 'kn service update') +kn service apply s0 --image knativesamples/helloworld --env foo=bar + +# Read the service declaration from a file +kn service apply s0 --filename my-svc.yml + +``` + +### Options + +``` + -a, --annotation stringArray Annotations to set for both Service and Revision. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-). + --annotation-revision stringArray Revision annotation to set. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-). This flag takes precedence over the "annotation" flag. + --annotation-service stringArray Service annotation to set. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-). This flag takes precedence over the "annotation" flag. + --arg stringArray Add argument to the container command. Example: --arg myArg1 --arg --myArg2 --arg myArg3=3. You can use this flag multiple times. + --async DEPRECATED: please use --no-wait instead. Do not wait for 'service apply' operation to be completed. + --autoscale-window string Duration to look back for making auto-scaling decisions. The service is scaled to zero if no request was received in during that time. (eg: 10s) + --cluster-local Specify that the service be private. (--no-cluster-local will make the service publicly available) + --cmd string Specify command to be used as entrypoint instead of default one. Example: --cmd /app/start or --cmd /app/start --arg myArg to pass additional arguments. + --concurrency-limit int Hard Limit of concurrent requests to be processed by a single replica. + --concurrency-target int Recommendation for when to scale up based on the concurrent number of incoming request. Defaults to --concurrency-limit when given. + --concurrency-utilization int Percentage of concurrent requests utilization before scaling up. (default 70) + -e, --env stringArray Environment variable to set. NAME=value; you may provide this flag any number of times to set multiple environment variables. To unset, specify the environment variable name followed by a "-" (e.g., NAME-). + --env-from stringArray Add environment variables from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret:). Example: --env-from cm:myconfigmap or --env-from secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --env-from cm:myconfigmap-. + -f, --filename string Create a service from file. The created service can be further modified by combining with other options. For example, -f /path/to/file --env NAME=value adds also an environment variable. + --force Create service forcefully, replaces existing service if any. + -h, --help help for apply + --image string Image to run. + -l, --label stringArray Labels to set for both Service and Revision. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). + --label-revision stringArray Revision label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag. + --label-service stringArray Service label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag. + --limit strings The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'. + --limits-cpu string DEPRECATED: please use --limit instead. The limits on the requested CPU (e.g., 1000m). + --limits-memory string DEPRECATED: please use --limit instead. The limits on the requested memory (e.g., 1024Mi). + --lock-to-digest Keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true) + --mount stringArray Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume. + -n, --namespace string Specify the namespace to operate in. + --no-cluster-local Do not specify that the service be private. (--no-cluster-local will make the service publicly available) (default true) + --no-lock-to-digest Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) + --no-wait Do not wait for 'service apply' operation to be completed. + -p, --port string The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'. + --pull-secret string Image pull secret to set. An empty argument ("") clears the pull secret. The referenced secret must exist in the service's namespace. + --request strings The resource requirement requests for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource request, append "-" to the resource name, e.g. '--request cpu-'. + --requests-cpu string DEPRECATED: please use --request instead. The requested CPU (e.g., 250m). + --requests-memory string DEPRECATED: please use --request instead. The requested memory (e.g., 64Mi). + --revision-name string The revision name to set. Must start with the service name and a dash as a prefix. Empty revision name will result in the server generating a name for the revision. Accepts golang templates, allowing {{.Service}} for the service name, {{.Generation}} for the generation, and {{.Random [n]}} for n random consonants. (default "{{.Service}}-{{.Random 5}}-{{.Generation}}") + --scale int Minimum and maximum number of replicas. + --scale-init int Initial number of replicas with which a service starts. Can be 0 or a positive integer. + --scale-max int Maximum number of replicas. + --scale-min int Minimum number of replicas. + --service-account string Service account name to set. An empty argument ("") clears the service account. The referenced service account must exist in the service's namespace. + --user int The user ID to run the container (e.g., 1001). + --volume stringArray Add a volume from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret: or sc:). Example: --volume myvolume=cm:myconfigmap or --volume myvolume=secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-. + --wait Wait for 'service apply' operation to be completed. (default true) + --wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 600) +``` + +### Options inherited from parent commands + +``` + --config string kn configuration file (default: ~/.config/kn/config.yaml) + --kubeconfig string kubectl configuration file (default: ~/.kube/config) + --log-http log http traffic +``` + +### SEE ALSO + +* [kn service](kn_service.md) - Manage Knative services + diff --git a/go.mod b/go.mod index 49832dca8..f5efc238a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module knative.dev/client go 1.14 require ( + github.com/google/go-cmp v0.5.2 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.0.1-0.20200715031239-b95db644ed1c @@ -27,7 +28,6 @@ require ( // ---------------------------------------------------------------------------------------------- replace ( k8s.io/api => k8s.io/api v0.18.8 - k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.18.8 k8s.io/apimachinery => k8s.io/apimachinery v0.18.8 k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.8 k8s.io/client-go => k8s.io/client-go v0.18.8 diff --git a/go.sum b/go.sum index a52bd094a..d946011ca 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,7 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -296,11 +297,16 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v0.0.0-20180117170138-065b426bd416/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.0.0-20180108230905-e214231b295a/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -375,6 +381,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= @@ -397,6 +404,7 @@ github.com/fsouza/fake-gcs-server v0.0.0-20180612165233-e85be23bdaa8/go.mod h1:1 github.com/fsouza/fake-gcs-server v1.19.4/go.mod h1:I0/88nHCASqJJ5M7zVF0zKODkYTcuXFW5J5yajsNJnE= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -425,34 +433,41 @@ github.com/go-logr/zapr v0.1.1/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aA github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.17.2/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= @@ -465,11 +480,13 @@ github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+Z github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.7 h1:VRuXN2EnMSsZdauzdss6JBC29YotDqG59BZ+tdlIL1s= github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= @@ -503,6 +520,7 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6 github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -682,9 +700,12 @@ github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20170330212424-2500245aa611/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -775,6 +796,7 @@ github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeY github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -919,6 +941,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ= github.com/nats-io/go-nats v1.7.0/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= @@ -1147,6 +1170,7 @@ github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:s github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= @@ -1158,6 +1182,7 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -1293,6 +1318,7 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opentelemetry.io/otel v0.2.3/go.mod h1:OgNpQOjrlt33Ew6Ds0mGjmcTQg/rhUctsbkRdk/g1fw= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1303,6 +1329,7 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.3.0/go.mod h1:9CWT6lKIep8U41DDaPiH6eFscnTyjfTANNQNx6LrIcA= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= @@ -1312,6 +1339,7 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.9.2-0.20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -1326,6 +1354,7 @@ golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1419,6 +1448,7 @@ golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1711,6 +1741,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180608181217-32ee49c4dd80/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -1762,6 +1793,7 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1829,9 +1861,11 @@ gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eR gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= gopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0-20150622162204-20b71e5b60d7/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5/go.mod h1:hiOFpYm0ZJbusNj2ywpbrXowU3G8U6GIQzqn2mw1UIE= +gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= @@ -1840,6 +1874,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1868,11 +1903,21 @@ honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75 honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4= k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY= +k8s.io/apiextensions-apiserver v0.0.0-20190918201827-3de75813f604/go.mod h1:7H8sjDlWQu89yWB3FhZfsLyRCRLuoXoCoY5qtwW1q6I= +k8s.io/apiextensions-apiserver v0.16.4/go.mod h1:HYQwjujEkXmQNhap2C9YDdIVOSskGZ3et0Mvjcyjbto= +k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs= +k8s.io/apiextensions-apiserver v0.17.6/go.mod h1:Z3CHLP3Tha+Rbav7JR3S+ye427UaJkHBomK2c4XtZ3A= +k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio= k8s.io/apiextensions-apiserver v0.18.8/go.mod h1:7f4ySEkkvifIr4+BRrRWriKKIJjPyg9mb/p63dJKnlM= k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0= k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig= +k8s.io/apiserver v0.0.0-20190918200908-1e17798da8c1/go.mod h1:4FuDU+iKPjdsdQSN3GsEKZLB/feQsj1y9dhhBDVV2Ns= +k8s.io/apiserver v0.16.4/go.mod h1:kbLJOak655g6W7C+muqu1F76u9wnEycfKMqbVaXIdAc= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= +k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I= +k8s.io/apiserver v0.17.6/go.mod h1:sAYqm8hUDNA9aj/TzqwsJoExWrxprKv0tqs/z88qym0= +k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8= k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM= k8s.io/cli-runtime v0.18.8 h1:ycmbN3hs7CfkJIYxJAOB10iW7BVPmXGXkfEyiV9NJ+k= k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M= @@ -1883,9 +1928,13 @@ k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KN k8s.io/cloud-provider v0.18.8/go.mod h1:cn9AlzMPVIXA4HHLVbgGUigaQlZyHSZ7WAwDEFNrQSs= k8s.io/code-generator v0.18.8 h1:lgO1P1wjikEtzNvj7ia+x1VC4svJ28a/r0wnOLhhOTU= k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= +k8s.io/component-base v0.0.0-20190918200425-ed2f0867c778/go.mod h1:DFWQCXgXVLiWtzFaS17KxHdlUeUymP7FLxZSkmL9/jU= +k8s.io/component-base v0.16.4/go.mod h1:GYQ+4hlkEwdlpAp59Ztc4gYuFhdoZqiAJD1unYDJ3FM= k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs= k8s.io/component-base v0.17.4/go.mod h1:5BRqHMbbQPm2kKu35v3G+CpVq4K0RJKC7TRioF0I9lE= +k8s.io/component-base v0.17.6/go.mod h1:jgRLWl0B0rOzFNtxQ9E4BphPmDqoMafujdau6AdG2Xo= +k8s.io/component-base v0.18.4/go.mod h1:7jr/Ef5PGmKwQhyAz/pjByxJbC58mhKAhiaDu0vXfPk= k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU= k8s.io/csi-translation-lib v0.17.0/go.mod h1:HEF7MEz7pOLJCnxabi45IPkhSsE/KmxPQksuCrHKWls= k8s.io/csi-translation-lib v0.17.4/go.mod h1:CsxmjwxEI0tTNMzffIAcgR9lX4wOh6AKHdxQrT7L0oo= @@ -1898,12 +1947,15 @@ k8s.io/gengo v0.0.0-20200205140755-e0e292d8aa12 h1:pZzawYyz6VRNPVYpqGv61LWCimQv1 k8s.io/gengo v0.0.0-20200205140755-e0e292d8aa12/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.3/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= @@ -1925,7 +1977,9 @@ k8s.io/test-infra v0.0.0-20200617221206-ea73eaeab7ff/go.mod h1:L3+cRvwftUq8IW1Tr k8s.io/test-infra v0.0.0-20200630233406-1dca6122872e/go.mod h1:L3+cRvwftUq8IW1TrHji5m3msnc4uck/7LsE/GR/aZk= k8s.io/test-infra v0.0.0-20200803112140-d8aa4e063646/go.mod h1:rtUd2cOFwT0aBma1ld6W40F7PuVVw4ELLSFlz9ZEmv8= k8s.io/utils v0.0.0-20181019225348-5e321f9a457c/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20190907131718-3d4f5b7dea0b/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200124190032-861946025e34/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= @@ -1985,9 +2039,11 @@ sigs.k8s.io/controller-runtime v0.5.4/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynR sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= +sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/structured-merge-diff v1.0.1 h1:LOs1LZWMsz1xs77Phr/pkB4LFaavH7IVq/3+WTN9XTA= +sigs.k8s.io/structured-merge-diff v1.0.1/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= diff --git a/pkg/kn/commands/service/apply.go b/pkg/kn/commands/service/apply.go new file mode 100644 index 000000000..aaf0354c4 --- /dev/null +++ b/pkg/kn/commands/service/apply.go @@ -0,0 +1,123 @@ +// Copyright © 2020 The Knative 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 service + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" + + "knative.dev/client/pkg/kn/commands" + clientservingv1 "knative.dev/client/pkg/serving/v1" +) + +var applyExample = ` +# Create an initial service with using 'kn service apply', if the service has not +# been already created +kn service apply s0 --image knativesamples/helloworld + +# Apply the service again which is a no-operation if none of the options changed +kn service apply s0 --image knativesamples/helloworld + +# Add an environment variable to your service. Note, that you have to always fully +# specify all parameters (in contrast to 'kn service update') +kn service apply s0 --image knativesamples/helloworld --env foo=bar + +# Read the service declaration from a file +kn service apply s0 --filename my-svc.yml +` + +func NewServiceApplyCommand(p *commands.KnParams) *cobra.Command { + var applyFlags ConfigurationEditFlags + var waitFlags commands.WaitFlags + + serviceApplyCommand := &cobra.Command{ + Use: "apply NAME", + Short: "Apply a service declaration", + Example: applyExample, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 1 && applyFlags.Filename == "" { + return errors.New("'service apply' requires the service name given as single argument") + } + name := "" + if len(args) == 1 { + name = args[0] + } + + namespace, err := p.GetNamespace(cmd) + if err != nil { + return err + } + + var service *servingv1.Service + applyFlags.RevisionName = "" + if applyFlags.Filename == "" { + service, err = constructService(cmd, applyFlags, name, namespace) + } else { + service, err = constructServiceFromFile(cmd, applyFlags, name, namespace) + } + if err != nil { + return err + } + + client, err := p.NewServingClient(namespace) + if err != nil { + return err + } + + waitDoing, waitVerb, err := examineServiceForApply(cmd, client, service.Name) + if err != nil { + return err + } + + hasChanged, err := client.ApplyService(service) + if err != nil { + return err + } + if !hasChanged { + fmt.Fprintf(cmd.OutOrStdout(), "No changes to apply to service '%s'.\n", service.Name) + + return showUrl(client, service.Name, "unchanged", "", cmd.OutOrStdout()) + } + return waitIfRequested(client, service.Name, waitFlags, waitDoing, waitVerb, cmd.OutOrStdout()) + }, + } + commands.AddNamespaceFlags(serviceApplyCommand.Flags(), false) + applyFlags.AddCreateFlags(serviceApplyCommand) + waitFlags.AddConditionWaitFlags(serviceApplyCommand, commands.WaitDefaultTimeout, "apply", "service", "ready") + return serviceApplyCommand +} + +func examineServiceForApply(cmd *cobra.Command, client clientservingv1.KnServingClient, serviceName string) (string, string, error) { + currentService, err := client.GetService(serviceName) + if err != nil { + if apierrors.IsNotFound(err) { + return "Creating", "created", nil + } + return "", "", err + } + + annotationMap := currentService.Annotations + if annotationMap != nil { + if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok { + fmt.Fprintf(cmd.OutOrStdout(), "Warning: 'kn service apply' should be used only for services created by 'kn service apply'\n") + } + } + return "Applying", "applied", nil +} diff --git a/pkg/kn/commands/service/apply_mock_test.go b/pkg/kn/commands/service/apply_mock_test.go new file mode 100644 index 000000000..1f1dcd10a --- /dev/null +++ b/pkg/kn/commands/service/apply_mock_test.go @@ -0,0 +1,187 @@ +// Copyright © 2019 The Knative 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 service + +import ( + "fmt" + "testing" + "time" + + "github.com/pkg/errors" + "gotest.tools/assert" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" + + knclient "knative.dev/client/pkg/serving/v1" + "knative.dev/client/pkg/util/mock" + "knative.dev/client/pkg/wait" + + "knative.dev/client/pkg/util" +) + +func TestServiceApplyCreateMock(t *testing.T) { + // New mock client + client := knclient.NewMockKnServiceClient(t) + + r := setupServiceApplyRecorder(client, "foo", nil, apierrors.NewNotFound(servingv1.Resource("service"), "foo"), true) + + // Testing: + output, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", "foo", "http://foo.example.com", "Ready")) + + // Validate that all recorded API methods have been called + r.Validate() +} + +func TestServiceApplyCreateFromFileMock(t *testing.T) { + testWithServiceFiles(t, func(t *testing.T, file string) { + for _, testArgs := range [][]string{ + {"apply", "foo", "--filename", file}, + {"apply", "--filename", file}, + } { + client := knclient.NewMockKnServiceClient(t) + + r := setupServiceApplyRecorder(client, "foo", nil, apierrors.NewNotFound(servingv1.Resource("service"), "foo"), true) + + // Testing: + output, err := executeServiceCommand(client, testArgs...) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", "foo", "http://foo.example.com", "Ready")) + + // Validate that all recorded API methods have been called + r.Validate() + } + }) +} + +func TestServiceApplyCreateFromFileMockWithoutName(t *testing.T) { + testWithServiceFiles(t, func(t *testing.T, file string) { + client := knclient.NewMockKnServiceClient(t) + + r := setupServiceApplyRecorder(client, "foo", nil, apierrors.NewNotFound(servingv1.Resource("service"), "foo"), true) + + // Testing: + output, err := executeServiceCommand(client, "apply", "foo", "--filename", file) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", "foo", "http://foo.example.com", "Ready")) + + // Validate that all recorded API methods have been called + r.Validate() + }) +} + +func TestServiceApplyUpdateMock(t *testing.T) { + // New mock client + client := knclient.NewMockKnServiceClient(t) + + service := createServiceWithImage("foo", "gcr.io/foo/bar:baz") + + r := setupServiceApplyRecorder(client, "foo", service, nil, true) + + // Testing: + output, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "applied", "foo", "http://foo.example.com", "Ready")) + + // Validate that all recorded API methods have been called + r.Validate() +} + +func TestServiceApplyUpdateUnchanged(t *testing.T) { + // New mock client + client := knclient.NewMockKnServiceClient(t) + + service := createServiceWithImage("foo", "gcr.io/foo/bar:baz") + + r := setupServiceApplyRecorder(client, "foo", service, nil, false) + + // Testing: + output, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "No changes", "apply", "foo", "http://foo.example.com")) + + // Validate that all recorded API methods have been called + r.Validate() +} + +func TestServiceApplyWithGetError(t *testing.T) { + // New mock client + client := knclient.NewMockKnServiceClient(t) + + errThrown := errors.New("boom!") + r := setupServiceApplyRecorder(client, "foo", nil, errThrown, true) + + _, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz") + assert.Equal(t, err, errThrown) + + // Validate that all recorded API methods have been called + r.Validate() +} + +func setupServiceApplyRecorder(client *knclient.MockKnServingClient, name string, service *servingv1.Service, err error, hasChanged bool) *knclient.ServingRecorder { + // Recording: + r := client.Recorder() + // Check for existing service --> no + r.GetService(name, service, err) + // Error test + if err != nil && !apierrors.IsNotFound(err) { + return r + } + + // Create service (don't validate given service --> "Any()" arg is allowed) + r.ApplyService(func(t *testing.T, a interface{}) { + svc := a.(*servingv1.Service) + assert.Equal(t, svc.Name, name) + setUrl(svc, fmt.Sprintf("http://%s.example.com", name)) + }, hasChanged, nil) + + // Fetch service for URL + r.GetService(name, getServiceWithUrl(name, fmt.Sprintf("http://%s.example.com", name)), nil) + + if !hasChanged { + return r + } + // Wait for service to become ready + r.WaitForService(name, mock.Any(), wait.NoopMessageCallback(), nil, time.Second) + + return r +} + +func createServiceWithImage(name string, image string) *servingv1.Service { + return &servingv1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: servingv1.ServiceSpec{ + ConfigurationSpec: servingv1.ConfigurationSpec{ + Template: servingv1.RevisionTemplateSpec{ + Spec: servingv1.RevisionSpec{ + PodSpec: v1.PodSpec{ + Containers: []v1.Container{ + { + Image: image, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/pkg/kn/commands/service/create.go b/pkg/kn/commands/service/create.go index 1e6524596..74c66b9da 100644 --- a/pkg/kn/commands/service/create.go +++ b/pkg/kn/commands/service/create.go @@ -145,7 +145,7 @@ func createService(client clientservingv1.KnServingClient, service *servingv1.Se return err } - return waitIfRequested(client, service, waitFlags, "Creating", "created", out) + return waitIfRequested(client, service.Name, waitFlags, "Creating", "created", out) } func replaceService(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, out io.Writer) error { @@ -153,23 +153,23 @@ func replaceService(client clientservingv1.KnServingClient, service *servingv1.S if err != nil { return err } - return waitIfRequested(client, service, waitFlags, "Replacing", "replaced", out) + return waitIfRequested(client, service.Name, waitFlags, "Replacing", "replaced", out) } -func waitIfRequested(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, verbDoing string, verbDone string, out io.Writer) error { +func waitIfRequested(client clientservingv1.KnServingClient, serviceName string, waitFlags commands.WaitFlags, verbDoing string, verbDone string, out io.Writer) error { //TODO: deprecated condition should be removed with --async flag if waitFlags.Async { fmt.Fprintf(out, "\nWARNING: flag --async is deprecated and going to be removed in future release, please use --no-wait instead.\n\n") - fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", service.Name, verbDone, client.Namespace()) + fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", serviceName, verbDone, client.Namespace()) return nil } if !waitFlags.Wait { - fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", service.Name, verbDone, client.Namespace()) + fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", serviceName, verbDone, client.Namespace()) return nil } - fmt.Fprintf(out, "%s service '%s' in namespace '%s':\n", verbDoing, service.Name, client.Namespace()) - return waitForServiceToGetReady(client, service.Name, waitFlags.TimeoutInSeconds, verbDone, out) + fmt.Fprintf(out, "%s service '%s' in namespace '%s':\n", verbDoing, serviceName, client.Namespace()) + return waitForServiceToGetReady(client, serviceName, waitFlags.TimeoutInSeconds, verbDone, out) } func prepareAndUpdateService(client clientservingv1.KnServingClient, service *servingv1.Service) error { @@ -239,6 +239,10 @@ func serviceExists(client clientservingv1.KnServingClient, name string) (bool, e func constructService(cmd *cobra.Command, editFlags ConfigurationEditFlags, name string, namespace string) (*servingv1.Service, error) { + if name == "" || namespace == "" { + return nil, errors.New("internal: no name or namespace provided when constructing a service") + } + service := servingv1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, diff --git a/pkg/kn/commands/service/create_mock_test.go b/pkg/kn/commands/service/create_mock_test.go index 277e563e9..bbcbf12cf 100644 --- a/pkg/kn/commands/service/create_mock_test.go +++ b/pkg/kn/commands/service/create_mock_test.go @@ -681,8 +681,12 @@ func TestServiceCreateWithAutoScaleServiceAnnotationsError(t *testing.T) { func getServiceWithUrl(name string, urlName string) *servingv1.Service { service := servingv1.Service{} - url, _ := apis.ParseURL(urlName) - service.Status.URL = url service.Name = name + setUrl(&service, urlName) return &service } + +func setUrl(service *servingv1.Service, urlName string) { + url, _ := apis.ParseURL(urlName) + service.Status.URL = url +} diff --git a/pkg/kn/commands/service/create_test.go b/pkg/kn/commands/service/create_test.go index 6ebd0a894..d155454cc 100644 --- a/pkg/kn/commands/service/create_test.go +++ b/pkg/kn/commands/service/create_test.go @@ -822,93 +822,61 @@ var serviceJSON = ` } }` -func TestServiceCreateFromYAML(t *testing.T) { - tempDir, err := ioutil.TempDir("", "kn-file") - defer os.RemoveAll(tempDir) - assert.NilError(t, err) +func TestServiceCreateFromFile(t *testing.T) { + testWithServiceFiles(t, func(t *testing.T, tempFile string) { + for _, testArgs := range [][]string{ + { + "service", "create", "foo", "--filename", tempFile}, { + "service", "create", "--filename", tempFile}, + } { + action, created, _, err := fakeServiceCreate(testArgs, false) + assert.NilError(t, err) + assert.Assert(t, action.Matches("create", "services")) - tempFile := filepath.Join(tempDir, "service.yaml") - err = ioutil.WriteFile(tempFile, []byte(serviceYAML), os.FileMode(0666)) - assert.NilError(t, err) - - action, created, _, err := fakeServiceCreate([]string{ - "service", "create", "foo", "--filename", tempFile}, false) - assert.NilError(t, err) - assert.Assert(t, action.Matches("create", "services")) - - assert.Equal(t, created.Name, "foo") - assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz") + assert.Equal(t, created.Name, "foo") + assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz") + } + }) } -func TestServiceCreateFromJSON(t *testing.T) { +func testWithServiceFiles(t *testing.T, testFunction func(t *testing.T, file string)) { tempDir, err := ioutil.TempDir("", "kn-file") defer os.RemoveAll(tempDir) assert.NilError(t, err) - tempFile := filepath.Join(tempDir, "service.json") - err = ioutil.WriteFile(tempFile, []byte(serviceJSON), os.FileMode(0666)) - assert.NilError(t, err) - - action, created, _, err := fakeServiceCreate([]string{ - "service", "create", "foo", "--filename", tempFile}, false) - assert.NilError(t, err) - assert.Assert(t, action.Matches("create", "services")) - - assert.Equal(t, created.Name, "foo") - assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz") -} - -func TestServiceCreateFromFileWithName(t *testing.T) { - tempDir, err := ioutil.TempDir("", "kn-file") - defer os.RemoveAll(tempDir) - assert.NilError(t, err) - - tempFile := filepath.Join(tempDir, "service.yaml") - err = ioutil.WriteFile(tempFile, []byte(serviceYAML), os.FileMode(0666)) - assert.NilError(t, err) - - t.Log("no NAME param provided") - action, created, _, err := fakeServiceCreate([]string{ - "service", "create", "--filename", tempFile}, false) - assert.NilError(t, err) - assert.Assert(t, action.Matches("create", "services")) - - assert.Equal(t, created.Name, "foo") - assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz") - - t.Log("no service.Name provided in file") - err = ioutil.WriteFile(tempFile, []byte(strings.ReplaceAll(serviceYAML, "name: foo", "")), os.FileMode(0666)) - assert.NilError(t, err) - action, created, _, err = fakeServiceCreate([]string{ - "service", "create", "cli-foo", "--filename", tempFile}, false) - assert.NilError(t, err) - assert.Assert(t, action.Matches("create", "services")) - - assert.Equal(t, created.Name, "cli-foo") - assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz") + for _, d := range []struct { + filename string + content string + }{ + {"service.yaml", + serviceYAML, + }, + {"service.json", + serviceJSON, + }, + } { + tempFile := filepath.Join(tempDir, d.filename) + err = ioutil.WriteFile(tempFile, []byte(d.content), os.FileMode(0666)) + assert.NilError(t, err) + testFunction(t, tempFile) + } } func TestServiceCreateFileNameMismatch(t *testing.T) { - tempDir, err := ioutil.TempDir("", "kn-file") - assert.NilError(t, err) + testWithServiceFiles(t, func(t *testing.T, tempFile string) { + _, _, _, err := fakeServiceCreate([]string{ + "service", "create", "anotherFoo", "--filename", tempFile}, false) + assert.Assert(t, err != nil) + assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "provided", "'anotherFoo'", "name", "match", "from", "file", "'foo'")) - tempFile := filepath.Join(tempDir, "service.json") - err = ioutil.WriteFile(tempFile, []byte(serviceJSON), os.FileMode(0666)) - assert.NilError(t, err) + err = ioutil.WriteFile(tempFile, []byte(strings.ReplaceAll(serviceYAML, "name: foo", "")), os.FileMode(0666)) + assert.NilError(t, err) + _, _, _, err = fakeServiceCreate([]string{ + "service", "create", "--filename", tempFile}, false) + assert.Assert(t, err != nil) + assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "no", "service", "name", "provided", "parameter", "file")) - t.Log("NAME param nad service.Name differ") - _, _, _, err = fakeServiceCreate([]string{ - "service", "create", "anotherFoo", "--filename", tempFile}, false) - assert.Assert(t, err != nil) - assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "provided", "'anotherFoo'", "name", "match", "from", "file", "'foo'")) - - t.Log("no NAME param & no service.Name provided in file") - err = ioutil.WriteFile(tempFile, []byte(strings.ReplaceAll(serviceYAML, "name: foo", "")), os.FileMode(0666)) - assert.NilError(t, err) - _, _, _, err = fakeServiceCreate([]string{ - "service", "create", "--filename", tempFile}, false) - assert.Assert(t, err != nil) - assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "no", "service", "name", "provided", "parameter", "file")) + }) } func TestServiceCreateFileError(t *testing.T) { diff --git a/pkg/kn/commands/service/service.go b/pkg/kn/commands/service/service.go index 3a4a273b0..72c73a042 100644 --- a/pkg/kn/commands/service/service.go +++ b/pkg/kn/commands/service/service.go @@ -42,6 +42,7 @@ func NewServiceCommand(p *commands.KnParams) *cobra.Command { serviceCmd.AddCommand(NewServiceCreateCommand(p)) serviceCmd.AddCommand(NewServiceDeleteCommand(p)) serviceCmd.AddCommand(NewServiceUpdateCommand(p)) + serviceCmd.AddCommand(NewServiceApplyCommand(p)) serviceCmd.AddCommand(NewServiceExportCommand(p)) return serviceCmd } @@ -64,7 +65,7 @@ func showUrl(client clientservingv1.KnServingClient, serviceName string, origina url := service.Status.URL.String() newRevision := service.Status.LatestReadyRevisionName - if originalRevision != "" && originalRevision == newRevision { + if (originalRevision != "" && originalRevision == newRevision) || originalRevision == "unchanged" { fmt.Fprintf(out, "Service '%s' with latest revision '%s' (unchanged) is available at URL:\n%s\n", serviceName, newRevision, url) } else { fmt.Fprintf(out, "Service '%s' %s to latest revision '%s' is available at URL:\n%s\n", serviceName, what, newRevision, url) diff --git a/pkg/serving/v1/apply.go b/pkg/serving/v1/apply.go new file mode 100644 index 000000000..616c43773 --- /dev/null +++ b/pkg/serving/v1/apply.go @@ -0,0 +1,268 @@ +package v1 + +import ( + "context" + "strings" + "time" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/jsonmergepatch" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" + + "knative.dev/client/pkg/util" +) + +// Copyright © 2020 The Knative 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. + +// Helper methods supporting Apply() + +// patch performs a 3-way merge and returns whether the original service has been changed +// This method uses a simple JSON 3-way merge which has some severe limitations, like that arrays +// can't be merged. Ideally a strategicpatch merge should be used, which allows a more fine grained +// way for performing the merge (but this is not supported for custom resources) +// See issue https://github.com/knative/client/issues/1073 for more details how this method should be +// improved for a better merge strategy. +func (cl *knServingClient) patch(modifiedService *servingv1.Service, currentService *servingv1.Service, uOriginalService []byte) (bool, error) { + uModifiedService, err := getModifiedConfiguration(modifiedService, true) + if err != nil { + return false, err + } + hasChanged, err := cl.patchSimple(currentService, uModifiedService, uOriginalService) + for i := 1; i <= 5 && apierrors.IsConflict(err); i++ { + if i > 1 { + time.Sleep(1 * time.Second) + } + currentService, err = cl.GetService(currentService.Name) + if err != nil { + return false, err + } + hasChanged, err = cl.patchSimple(currentService, uModifiedService, uOriginalService) + } + return hasChanged, err +} + +func (cl *knServingClient) patchSimple(currentService *servingv1.Service, uModifiedService []byte, uOriginalService []byte) (bool, error) { + // Serialize the current configuration of the object from the server. + uCurrentService, err := encodeService(currentService) + if err != nil { + return false, err + } + + patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(uOriginalService, uModifiedService, uCurrentService) + if err != nil { + return false, err + } + + if string(patch) == "{}" { + return false, nil + } + + // Check if the generation has been counted up, only then the backend detected a change + savedService, err := cl.patchService(currentService.Name, types.MergePatchType, patch) + if err != nil { + return false, err + } + return savedService.Generation != savedService.Status.ObservedGeneration, nil +} + +// patchService patches the given service +func (cl *knServingClient) patchService(name string, patchType types.PatchType, patch []byte) (*servingv1.Service, error) { + service, err := cl.client.Services(cl.namespace).Patch(context.TODO(), name, patchType, patch, metav1.PatchOptions{}) + if err != nil { + return nil, err + } + err = updateServingGvk(service) + + return service, err +} + +func getOriginalConfiguration(service *servingv1.Service) []byte { + annots := service.Annotations + if annots == nil { + return nil + } + original, ok := annots[v1.LastAppliedConfigAnnotation] + if !ok { + return nil + } + return []byte(original) +} + +func getModifiedConfiguration(service *servingv1.Service, annotate bool) ([]byte, error) { + + // First serialize the object without the annotation to prevent recursion, + // then add that serialization to it as the annotation and serialize it again. + var uModifiedService []byte + + // Otherwise, use the server side version of the object. + // Get the current annotations from the object. + annots := service.Annotations + if annots == nil { + annots = map[string]string{} + } + + original := annots[v1.LastAppliedConfigAnnotation] + delete(annots, v1.LastAppliedConfigAnnotation) + service.Annotations = annots + + uModifiedService, err := encodeService(service) + if err != nil { + return nil, err + } + + if annotate { + annots[v1.LastAppliedConfigAnnotation] = strings.TrimRight(string(uModifiedService), "\n") + + service.Annotations = annots + uModifiedService, err = encodeService(service) + if err != nil { + return nil, err + } + } + + // Restore the object to its original condition. + annots[v1.LastAppliedConfigAnnotation] = original + service.Annotations = annots + return uModifiedService, nil +} + +func updateLastAppliedAnnotation(service *servingv1.Service) error { + annots := service.Annotations + if annots == nil { + annots = map[string]string{} + } + lastApplied, err := encodeService(service) + if err != nil { + return err + } + + // Cleanup any trailing newlines + annots[v1.LastAppliedConfigAnnotation] = strings.TrimRight(string(lastApplied), "\n") + + service.Annotations = annots + return nil +} + +func encodeService(service *servingv1.Service) ([]byte, error) { + scheme := runtime.NewScheme() + err := servingv1.AddToScheme(scheme) + if err != nil { + return nil, err + } + factory := serializer.NewCodecFactory(scheme) + encoder := factory.EncoderForVersion(unstructured.UnstructuredJSONScheme, servingv1.SchemeGroupVersion) + err = util.UpdateGroupVersionKindWithScheme(service, servingv1.SchemeGroupVersion, scheme) + if err != nil { + return nil, err + } + + serviceUnstructured, err := util.ToUnstructured(service) + if err != nil { + return nil, err + } + + // Remove/adapt service so that it can be used in the apply-annotation + cleanupServiceUnstructured(serviceUnstructured) + + return runtime.Encode(encoder, serviceUnstructured) +} + +func cleanupServiceUnstructured(uService *unstructured.Unstructured) { + clearCreationTimestamps(uService.Object) + removeStatus(uService.Object) + removeContainerNameAndResourcesIfNotSet(uService.Object) + +} + +func removeContainerNameAndResourcesIfNotSet(uService map[string]interface{}) { + uContainer := extractUserContainer(uService) + if uContainer == nil { + return + } + name, ok := uContainer["name"] + if ok && name != "" { + delete(uContainer, "name") + } + + resources := uContainer["resources"] + if resources == nil { + return + } + resourcesMap := resources.(map[string]interface{}) + if len(resourcesMap) == 0 { + delete(uContainer, "resources") + } +} + +func extractUserContainer(uService map[string]interface{}) map[string]interface{} { + tSpec := extractTemplateSpec(uService) + if tSpec == nil { + return nil + } + containers := tSpec["containers"] + if len(containers.([]interface{})) == 0 { + return nil + } + return containers.([]interface{})[0].(map[string]interface{}) +} + +func removeStatus(uService map[string]interface{}) { + delete(uService, "status") +} + +func clearCreationTimestamps(uService map[string]interface{}) { + meta := uService["metadata"] + if meta != nil { + delete(meta.(map[string]interface{}), "creationTimestamp") + } + template := extractTemplate(uService) + if template != nil { + meta = template["metadata"] + if meta != nil { + delete(meta.(map[string]interface{}), "creationTimestamp") + } + } +} + +func extractTemplateSpec(uService map[string]interface{}) map[string]interface{} { + templ := extractTemplate(uService) + if templ == nil { + return nil + } + templSpec := templ["spec"] + if templSpec == nil { + return nil + } + + return templSpec.(map[string]interface{}) +} + +func extractTemplate(uService map[string]interface{}) map[string]interface{} { + spec := uService["spec"] + if spec == nil { + return nil + } + templ := spec.(map[string]interface{})["template"] + if templ == nil { + return nil + } + return templ.(map[string]interface{}) +} diff --git a/pkg/serving/v1/apply_test.go b/pkg/serving/v1/apply_test.go new file mode 100644 index 000000000..d64093b93 --- /dev/null +++ b/pkg/serving/v1/apply_test.go @@ -0,0 +1,233 @@ +// Copyright © 2020 The Knative 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 v1 + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "gotest.tools/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + clienttesting "k8s.io/client-go/testing" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" + "sigs.k8s.io/yaml" + + "knative.dev/client/pkg/util" +) + +func TestApplyServiceWithNoImage(t *testing.T) { + _, client := setup() + serviceFaulty := newService("faulty-service") + _, err := client.ApplyService(serviceFaulty) + assert.Assert(t, err != nil) + assert.Assert(t, util.ContainsAll(err.Error(), "image name")) +} + +func TestApplyServiceCreate(t *testing.T) { + serving, client := setup() + + serviceNew := newServiceWithImage("new-service", "test/image") + serving.AddReactor("get", "services", + func(a clienttesting.Action) (bool, runtime.Object, error) { + name := a.(clienttesting.GetAction).GetName() + return true, nil, errors.NewNotFound(servingv1.Resource("service"), name) + }) + + serving.AddReactor("create", "services", + func(a clienttesting.Action) (bool, runtime.Object, error) { + assert.Equal(t, testNamespace, a.GetNamespace()) + return true, serviceNew, nil + }) + + hasChanged, err := client.ApplyService(serviceNew) + assert.NilError(t, err) + assert.Assert(t, hasChanged, "service has changed") +} + +func TestApplyServiceUpdate(t *testing.T) { + serving, client := setup() + + serviceOld := newServiceWithImage("my-service", "test/image") + serviceNew := newServiceWithImage("my-service", "test/new-image") + serving.AddReactor("get", "services", + func(a clienttesting.Action) (bool, runtime.Object, error) { + name := a.(clienttesting.GetAction).GetName() + assert.Equal(t, name, "my-service") + return true, serviceOld, nil + }) + + serving.AddReactor("patch", "services", + func(a clienttesting.Action) (bool, runtime.Object, error) { + serviceNew.Generation = 2 + serviceNew.Status.ObservedGeneration = 1 + return true, serviceNew, nil + }) + + hasChanged, err := client.ApplyService(serviceNew) + assert.NilError(t, err) + assert.Assert(t, hasChanged, "service has changed") +} + +func newServiceWithImage(name string, image string) *servingv1.Service { + svc := newService(name) + svc.Spec = servingv1.ServiceSpec{ + ConfigurationSpec: servingv1.ConfigurationSpec{ + Template: servingv1.RevisionTemplateSpec{ + Spec: servingv1.RevisionSpec{ + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: image, + }, + }, + }, + }, + }, + }, + } + return svc +} + +func TestExtractUserContainer(t *testing.T) { + tests := []struct { + name string + service string + want string + }{ + {"Simple Service", + ` +spec: + template: + spec: + containers: + - image: gcr.io/foo/bar:baz +`, + ` +image: gcr.io/foo/bar:baz +`, + }, + { + "No template", + ` +spec: +`, + "", + }, { + "No template spec", + ` +spec: + template: +`, + "", + }, + { + "No template spec containers", + ` +spec: + template: + spec: +`, + "", + }, + { + "Empty template spec containers", + ` +spec: + template: + spec: + containers: [] +`, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serviceMap map[string]interface{} + yaml.Unmarshal([]byte(tt.service), &serviceMap) + + got := extractUserContainer(serviceMap) + + if tt.want == "" { + assert.Assert(t, got == nil) + } else { + var expectedMap map[string]interface{} + yaml.Unmarshal([]byte(tt.want), &expectedMap) + if !reflect.DeepEqual(got, expectedMap) { + t.Errorf("extractUserContainer() = %v, want %v", got, expectedMap) + } + } + }) + } +} + +func TestCleanupServiceUnstructured(t *testing.T) { + tests := []struct { + name string + service string + want string + }{ + {"Simple Service with fields to remove", + ` +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: foo + creationTimestamp: "2020-10-22T08:16:37Z" +spec: + template: + metadata: + name: "bar" + creationTimestamp: null + spec: + containers: + - image: gcr.io/foo/bar:baz + name: "bla" + resources: {} +status: + observedGeneration: 1 +`, + ` +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: foo +spec: + template: + metadata: + name: "bar" + spec: + containers: + - image: gcr.io/foo/bar:baz +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ud := &unstructured.Unstructured{} + assert.NilError(t, yaml.Unmarshal([]byte(tt.service), ud)) + cleanupServiceUnstructured(ud) + + expectedMap := &unstructured.Unstructured{} + yaml.Unmarshal([]byte(tt.want), &expectedMap) + if !reflect.DeepEqual(ud, expectedMap) { + t.Errorf("cleanupServiceUnstructured(): " + cmp.Diff(ud, expectedMap)) + } + }) + } +} diff --git a/pkg/serving/v1/client.go b/pkg/serving/v1/client.go index b464eebfe..aa60f6bd8 100644 --- a/pkg/serving/v1/client.go +++ b/pkg/serving/v1/client.go @@ -16,6 +16,7 @@ package v1 import ( "context" + "errors" "fmt" "time" @@ -68,6 +69,17 @@ type KnServingClient interface { // place. UpdateServiceWithRetry(name string, updateFunc ServiceUpdateFunc, nrRetries int) error + // Apply a service's definition to the cluster. The full service declaration needs to be provided, + // which is different to UpdateService which can also do a partial update. If the given + // service does not already exists (identified by name) then the service is create. + // If the service exists, then a three-way merge will be performed between the original + // configuration given (from the last "apply" operation), the new configuration as given ] + // here and the current configuration as found on the cluster. + // The returned bool indicates whether the service has been changed or whether this operation + // was a no-op + // An error can indicate a general error or a conflict that occurred during the three way merge. + ApplyService(service *servingv1.Service) (bool, error) + // Delete a service by name DeleteService(name string, timeout time.Duration) error @@ -267,6 +279,32 @@ func updateServiceWithRetry(cl KnServingClient, name string, updateFunc ServiceU } } +// ApplyService applies a service definition that contains the service's targer state +func (cl *knServingClient) ApplyService(modifiedService *servingv1.Service) (bool, error) { + currentService, err := cl.GetService(modifiedService.Name) + if err != nil && !apierrors.IsNotFound(err) { + return false, err + } + + containers := modifiedService.Spec.Template.Spec.Containers + if len(containers) == 0 || containers[0].Image == "" && currentService != nil { + return false, errors.New("'service apply' requires the image name to run provided with the --image option") + } + + // No current service --> create a new service + if currentService == nil { + err := updateLastAppliedAnnotation(modifiedService) + if err != nil { + return false, err + } + return true, cl.CreateService(modifiedService) + } + + // Merge with existing service + uOriginalService := getOriginalConfiguration(currentService) + return cl.patch(modifiedService, currentService, uOriginalService) +} + // Delete a service by name // Param `timeout` represents a duration to wait for a delete op to finish. // For `timeout == 0` delete is performed async without any wait. diff --git a/pkg/serving/v1/client_mock.go b/pkg/serving/v1/client_mock.go index 8c6d57424..2586b269a 100644 --- a/pkg/serving/v1/client_mock.go +++ b/pkg/serving/v1/client_mock.go @@ -104,6 +104,16 @@ func (c *MockKnServingClient) UpdateServiceWithRetry(name string, updateFunc Ser return updateServiceWithRetry(c, name, updateFunc, maxRetry) } +// Update the given service +func (sr *ServingRecorder) ApplyService(service interface{}, hasChanged bool, err error) { + sr.r.Add("ApplyService", []interface{}{service}, []interface{}{hasChanged, err}) +} + +func (c *MockKnServingClient) ApplyService(service *servingv1.Service) (bool, error) { + call := c.recorder.r.VerifyCall("ApplyService", service) + return call.Result[0].(bool), mock.ErrorOrNil(call.Result[1]) +} + // Delete a service by name func (sr *ServingRecorder) DeleteService(name, timeout interface{}, err error) { sr.r.Add("DeleteService", []interface{}{name, timeout}, []interface{}{err}) diff --git a/pkg/serving/v1/client_mock_test.go b/pkg/serving/v1/client_mock_test.go index 10f20b70c..7ec9738af 100644 --- a/pkg/serving/v1/client_mock_test.go +++ b/pkg/serving/v1/client_mock_test.go @@ -37,6 +37,7 @@ func TestMockKnClient(t *testing.T) { recorder.ListServices(mock.Any(), nil, nil) recorder.CreateService(&servingv1.Service{}, nil) recorder.UpdateService(&servingv1.Service{}, nil) + recorder.ApplyService(&servingv1.Service{}, true, nil) recorder.DeleteService("hello", time.Duration(10)*time.Second, nil) recorder.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback(), nil, 10*time.Second) recorder.GetRevision("hello", nil, nil) @@ -52,6 +53,7 @@ func TestMockKnClient(t *testing.T) { client.ListServices(WithLabel("foo", "bar")) client.CreateService(&servingv1.Service{}) client.UpdateService(&servingv1.Service{}) + client.ApplyService(&servingv1.Service{}) client.DeleteService("hello", time.Duration(10)*time.Second) client.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback()) client.GetRevision("hello") diff --git a/pkg/util/unstructured.go b/pkg/util/unstructured.go index 77f2e7a32..ccaa6dc79 100644 --- a/pkg/util/unstructured.go +++ b/pkg/util/unstructured.go @@ -32,8 +32,8 @@ func ToUnstructuredList(obj runtime.Object) (*unstructured.UnstructuredList, err if err != nil { return nil, err } - for _, item := range items { - ud, err := toUnstructured(item) + for _, obji := range items { + ud, err := ToUnstructured(obji) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func ToUnstructuredList(obj runtime.Object) (*unstructured.UnstructuredList, err } } else { - ud, err := toUnstructured(obj) + ud, err := ToUnstructured(obj) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func ToUnstructuredList(obj runtime.Object) (*unstructured.UnstructuredList, err } -func toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) { +func ToUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) { b, err := json.Marshal(obj) if err != nil { return nil, err diff --git a/test/e2e/service_apply_test.go b/test/e2e/service_apply_test.go new file mode 100644 index 000000000..5c68ed17b --- /dev/null +++ b/test/e2e/service_apply_test.go @@ -0,0 +1,62 @@ +// Copyright 2019 The Knative 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. + +// +build e2e +// +build !eventing + +package e2e + +import ( + "testing" + + "gotest.tools/assert" + + pkgtest "knative.dev/pkg/test" + + "knative.dev/client/lib/test" + "knative.dev/client/pkg/util" +) + +func TestServiceApply(t *testing.T) { + t.Parallel() + it, err := test.NewKnTest() + assert.NilError(t, err) + defer func() { + assert.NilError(t, it.Teardown()) + }() + + r := test.NewKnRunResultCollector(t, it) + defer r.DumpIfFailed() + + t.Log("apply hello service (initially)") + result := serviceApply(r, "hello-apply") + r.AssertNoError(result) + assert.Check(r.T(), util.ContainsAllIgnoreCase(result.Stdout, "creating", "service", "hello-apply", "ready", "http")) + t.Log("apply hello service (unchanged)") + result = serviceApply(r, "hello-apply") + r.AssertNoError(result) + assert.Check(r.T(), util.ContainsAllIgnoreCase(result.Stdout, "no changes", "service", "hello-apply", "http")) + + t.Log("apply hello service (update env)") + result = serviceApply(r, "hello-apply", "--env", "tik=tok") + r.AssertNoError(result) + assert.Check(r.T(), util.ContainsAllIgnoreCase(result.Stdout, "applying", "service", "hello-apply", "ready", "http")) +} + +// ServiceApply applies a test service and returns the output +func serviceApply(r *test.KnRunResultCollector, serviceName string, args ...string) test.KnRunResult { + fullArgs := append([]string{}, "service", "apply", serviceName, "--image", pkgtest.ImagePath("helloworld")) + fullArgs = append(fullArgs, args...) + return r.KnTest().Kn().Run(fullArgs...) +} diff --git a/vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go b/vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go new file mode 100644 index 000000000..e56e17734 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go @@ -0,0 +1,160 @@ +/* +Copyright 2017 The Kubernetes 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 jsonmergepatch + +import ( + "fmt" + "reflect" + + "github.com/evanphx/json-patch" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/mergepatch" +) + +// Create a 3-way merge patch based-on JSON merge patch. +// Calculate addition-and-change patch between current and modified. +// Calculate deletion patch between original and modified. +func CreateThreeWayJSONMergePatch(original, modified, current []byte, fns ...mergepatch.PreconditionFunc) ([]byte, error) { + if len(original) == 0 { + original = []byte(`{}`) + } + if len(modified) == 0 { + modified = []byte(`{}`) + } + if len(current) == 0 { + current = []byte(`{}`) + } + + addAndChangePatch, err := jsonpatch.CreateMergePatch(current, modified) + if err != nil { + return nil, err + } + // Only keep addition and changes + addAndChangePatch, addAndChangePatchObj, err := keepOrDeleteNullInJsonPatch(addAndChangePatch, false) + if err != nil { + return nil, err + } + + deletePatch, err := jsonpatch.CreateMergePatch(original, modified) + if err != nil { + return nil, err + } + // Only keep deletion + deletePatch, deletePatchObj, err := keepOrDeleteNullInJsonPatch(deletePatch, true) + if err != nil { + return nil, err + } + + hasConflicts, err := mergepatch.HasConflicts(addAndChangePatchObj, deletePatchObj) + if err != nil { + return nil, err + } + if hasConflicts { + return nil, mergepatch.NewErrConflict(mergepatch.ToYAMLOrError(addAndChangePatchObj), mergepatch.ToYAMLOrError(deletePatchObj)) + } + patch, err := jsonpatch.MergePatch(deletePatch, addAndChangePatch) + if err != nil { + return nil, err + } + + var patchMap map[string]interface{} + err = json.Unmarshal(patch, &patchMap) + if err != nil { + return nil, fmt.Errorf("Failed to unmarshal patch for precondition check: %s", patch) + } + meetPreconditions, err := meetPreconditions(patchMap, fns...) + if err != nil { + return nil, err + } + if !meetPreconditions { + return nil, mergepatch.NewErrPreconditionFailed(patchMap) + } + + return patch, nil +} + +// keepOrDeleteNullInJsonPatch takes a json-encoded byte array and a boolean. +// It returns a filtered object and its corresponding json-encoded byte array. +// It is a wrapper of func keepOrDeleteNullInObj +func keepOrDeleteNullInJsonPatch(patch []byte, keepNull bool) ([]byte, map[string]interface{}, error) { + var patchMap map[string]interface{} + err := json.Unmarshal(patch, &patchMap) + if err != nil { + return nil, nil, err + } + filteredMap, err := keepOrDeleteNullInObj(patchMap, keepNull) + if err != nil { + return nil, nil, err + } + o, err := json.Marshal(filteredMap) + return o, filteredMap, err +} + +// keepOrDeleteNullInObj will keep only the null value and delete all the others, +// if keepNull is true. Otherwise, it will delete all the null value and keep the others. +func keepOrDeleteNullInObj(m map[string]interface{}, keepNull bool) (map[string]interface{}, error) { + filteredMap := make(map[string]interface{}) + var err error + for key, val := range m { + switch { + case keepNull && val == nil: + filteredMap[key] = nil + case val != nil: + switch typedVal := val.(type) { + case map[string]interface{}: + // Explicitly-set empty maps are treated as values instead of empty patches + if len(typedVal) == 0 { + if !keepNull { + filteredMap[key] = typedVal + } + continue + } + + var filteredSubMap map[string]interface{} + filteredSubMap, err = keepOrDeleteNullInObj(typedVal, keepNull) + if err != nil { + return nil, err + } + + // If the returned filtered submap was empty, this is an empty patch for the entire subdict, so the key + // should not be set + if len(filteredSubMap) != 0 { + filteredMap[key] = filteredSubMap + } + + case []interface{}, string, float64, bool, int64, nil: + // Lists are always replaced in Json, no need to check each entry in the list. + if !keepNull { + filteredMap[key] = val + } + default: + return nil, fmt.Errorf("unknown type: %v", reflect.TypeOf(typedVal)) + } + } + } + return filteredMap, nil +} + +func meetPreconditions(patchObj map[string]interface{}, fns ...mergepatch.PreconditionFunc) (bool, error) { + // Apply the preconditions to the patch, and return an error if any of them fail. + for _, fn := range fns { + if !fn(patchObj) { + return false, fmt.Errorf("precondition failed for: %v", patchObj) + } + } + return true, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 11beda38c..ca2416af8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -109,6 +109,7 @@ github.com/golang/protobuf/ptypes/wrappers # github.com/google/btree v1.0.0 github.com/google/btree # github.com/google/go-cmp v0.5.2 +## explicit github.com/google/go-cmp/cmp github.com/google/go-cmp/cmp/cmpopts github.com/google/go-cmp/cmp/internal/diff @@ -549,6 +550,7 @@ k8s.io/apimachinery/pkg/util/errors k8s.io/apimachinery/pkg/util/framer k8s.io/apimachinery/pkg/util/intstr k8s.io/apimachinery/pkg/util/json +k8s.io/apimachinery/pkg/util/jsonmergepatch k8s.io/apimachinery/pkg/util/mergepatch k8s.io/apimachinery/pkg/util/naming k8s.io/apimachinery/pkg/util/net @@ -936,7 +938,6 @@ sigs.k8s.io/structured-merge-diff/v3/value ## explicit sigs.k8s.io/yaml # k8s.io/api => k8s.io/api v0.18.8 -# k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.18.8 # k8s.io/apimachinery => k8s.io/apimachinery v0.18.8 # k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.8 # k8s.io/client-go => k8s.io/client-go v0.18.8