From c9f128423b587a81a920806aa009bc6913edcd5f Mon Sep 17 00:00:00 2001 From: Chris Suszynski Date: Wed, 25 Sep 2024 12:46:31 +0200 Subject: [PATCH] :gift_heart: Extract reusable part of sink flag (#1968) * Extract reuseable part of sink flag * Return nil on empty sink * Extract kube params * Compute with default mappings * Allow to change the default log level * Publicate Zap based logger, and allow custom loggers to be used. --- docs/cmd/kn_source_apiserver_create.md | 2 +- docs/cmd/kn_source_apiserver_update.md | 2 +- docs/cmd/kn_source_binding_create.md | 2 +- docs/cmd/kn_source_binding_update.md | 2 +- docs/cmd/kn_source_container_create.md | 2 +- docs/cmd/kn_source_container_update.md | 2 +- docs/cmd/kn_source_ping_create.md | 2 +- docs/cmd/kn_source_ping_update.md | 2 +- docs/cmd/kn_subscription_create.md | 6 +- docs/cmd/kn_subscription_update.md | 6 +- docs/cmd/kn_trigger_create.md | 2 +- docs/cmd/kn_trigger_update.md | 2 +- go.mod | 1 + go.sum | 4 + pkg/commands/flags/sink.go | 186 ++++----------- pkg/commands/flags/sink_test.go | 186 +++++++++------ pkg/commands/namespaced_test.go | 33 +-- pkg/commands/subscription/list_test.go | 2 +- pkg/commands/types.go | 56 +---- pkg/commands/types_test.go | 150 +------------ pkg/dynamic/fake/fake.go | 18 +- pkg/flags/sink/commandline.go | 60 +++++ pkg/flags/sink/sink.go | 299 +++++++++++++++++++++++++ pkg/k8s/params.go | 99 ++++++++ pkg/k8s/params_test.go | 190 ++++++++++++++++ pkg/output/logging/context.go | 27 ++- pkg/output/logging/log_level.go | 43 ++++ pkg/output/logging/log_level_test.go | 36 +++ pkg/output/logging/zap.go | 11 +- pkg/root/root.go | 10 +- pkg/util/errors/cause.go | 37 +++ pkg/util/errors/cause_test.go | 36 +++ pkg/util/errors/wrap.go | 32 +++ 33 files changed, 1081 insertions(+), 467 deletions(-) create mode 100644 pkg/flags/sink/commandline.go create mode 100644 pkg/flags/sink/sink.go create mode 100644 pkg/k8s/params.go create mode 100644 pkg/k8s/params_test.go create mode 100644 pkg/output/logging/log_level.go create mode 100644 pkg/output/logging/log_level_test.go create mode 100644 pkg/util/errors/cause.go create mode 100644 pkg/util/errors/cause_test.go create mode 100644 pkg/util/errors/wrap.go diff --git a/docs/cmd/kn_source_apiserver_create.md b/docs/cmd/kn_source_apiserver_create.md index 2c393a3fd3..09eb10c272 100644 --- a/docs/cmd/kn_source_apiserver_create.md +++ b/docs/cmd/kn_source_apiserver_create.md @@ -26,7 +26,7 @@ kn source apiserver create NAME --resource RESOURCE --sink SINK --resource stringArray Specification for which events to listen, in the format Kind:APIVersion:LabelSelector, e.g. "Event:sourcesv1:key=value". "LabelSelector" is a list of comma separated key value pairs. "LabelSelector" can be omitted, e.g. "Event:sourcesv1". --service-account string Name of the service account to use to run this source - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_source_apiserver_update.md b/docs/cmd/kn_source_apiserver_update.md index bfdaed3cb9..62556e926b 100644 --- a/docs/cmd/kn_source_apiserver_update.md +++ b/docs/cmd/kn_source_apiserver_update.md @@ -26,7 +26,7 @@ kn source apiserver update NAME --resource stringArray Specification for which events to listen, in the format Kind:APIVersion:LabelSelector, e.g. "Event:sourcesv1:key=value". "LabelSelector" is a list of comma separated key value pairs. "LabelSelector" can be omitted, e.g. "Event:sourcesv1". --service-account string Name of the service account to use to run this source - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_source_binding_create.md b/docs/cmd/kn_source_binding_create.md index 6cf513547e..8b32d137d7 100644 --- a/docs/cmd/kn_source_binding_create.md +++ b/docs/cmd/kn_source_binding_create.md @@ -20,7 +20,7 @@ kn source binding create NAME --subject SUBJECT --sink SINK --ce-override stringArray Cloud Event overrides to apply before sending event to sink. Example: '--ce-override key=value' You may be provide this flag multiple times. To unset, append "-" to the key (e.g. --ce-override key-). -h, --help help for create -n, --namespace string Specify the namespace to operate in. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. --subject string Subject which emits cloud events. This argument takes format kind:apiVersion:name for named resources or kind:apiVersion:labelKey1=value1,labelKey2=value2 for matching via a label selector ``` diff --git a/docs/cmd/kn_source_binding_update.md b/docs/cmd/kn_source_binding_update.md index 1808da2e16..ff14eb58fd 100644 --- a/docs/cmd/kn_source_binding_update.md +++ b/docs/cmd/kn_source_binding_update.md @@ -20,7 +20,7 @@ kn source binding update NAME --ce-override stringArray Cloud Event overrides to apply before sending event to sink. Example: '--ce-override key=value' You may be provide this flag multiple times. To unset, append "-" to the key (e.g. --ce-override key-). -h, --help help for update -n, --namespace string Specify the namespace to operate in. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. --subject string Subject which emits cloud events. This argument takes format kind:apiVersion:name for named resources or kind:apiVersion:labelKey1=value1,labelKey2=value2 for matching via a label selector ``` diff --git a/docs/cmd/kn_source_container_create.md b/docs/cmd/kn_source_container_create.md index c15e43df14..f28932640c 100644 --- a/docs/cmd/kn_source_container_create.md +++ b/docs/cmd/kn_source_container_create.md @@ -41,7 +41,7 @@ kn source container create NAME --image IMAGE --sink SINK --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-'. --security-context string Predefined security context for the service. Accepted values: 'none' for no security context and 'strict' for dropping all capabilities, running as non-root, and no privilege escalation. (default "none") --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. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. --toleration strings Add toleration to be set, works if the feature gate is enabled in Knative Serving feature flags configuration. Example: --tolerations Key="key1",Operator="Equal",Value="value1",Effect="NoSchedule" --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:) a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:) or a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim). PersistentVolumeClaim only works if the feature gate is enabled in Knative Serving feature flags configuration. Example: --volume myvolume=cm:myconfigmap, --volume myvolume=secret:mysecret or --volume emptyDir:myvol:size=1Gi,type=Memory. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-. diff --git a/docs/cmd/kn_source_container_update.md b/docs/cmd/kn_source_container_update.md index 074424e365..a3f7940c62 100644 --- a/docs/cmd/kn_source_container_update.md +++ b/docs/cmd/kn_source_container_update.md @@ -41,7 +41,7 @@ kn source container update NAME --image IMAGE --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-'. --security-context string Predefined security context for the service. Accepted values: 'none' for no security context and 'strict' for dropping all capabilities, running as non-root, and no privilege escalation. (default "none") --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. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. --toleration strings Add toleration to be set, works if the feature gate is enabled in Knative Serving feature flags configuration. Example: --tolerations Key="key1",Operator="Equal",Value="value1",Effect="NoSchedule" --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:) a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:) or a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim). PersistentVolumeClaim only works if the feature gate is enabled in Knative Serving feature flags configuration. Example: --volume myvolume=cm:myconfigmap, --volume myvolume=secret:mysecret or --volume emptyDir:myvol:size=1Gi,type=Memory. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-. diff --git a/docs/cmd/kn_source_ping_create.md b/docs/cmd/kn_source_ping_create.md index ea2f6b5a18..36b9da0341 100644 --- a/docs/cmd/kn_source_ping_create.md +++ b/docs/cmd/kn_source_ping_create.md @@ -23,7 +23,7 @@ kn source ping create NAME --sink SINK -h, --help help for create -n, --namespace string Specify the namespace to operate in. --schedule string Optional schedule specification in crontab format (e.g. '*/2 * * * *' for every two minutes. By default fire every minute. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_source_ping_update.md b/docs/cmd/kn_source_ping_update.md index ffd3c30cc4..6965bd7a78 100644 --- a/docs/cmd/kn_source_ping_update.md +++ b/docs/cmd/kn_source_ping_update.md @@ -23,7 +23,7 @@ kn source ping update NAME -h, --help help for update -n, --namespace string Specify the namespace to operate in. --schedule string Optional schedule specification in crontab format (e.g. '*/2 * * * *' for every two minutes. By default fire every minute. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_subscription_create.md b/docs/cmd/kn_subscription_create.md index 261c58fda1..ace3fd36b6 100644 --- a/docs/cmd/kn_subscription_create.md +++ b/docs/cmd/kn_subscription_create.md @@ -23,9 +23,9 @@ kn subscription create NAME --channel string Specify the channel to subscribe to. For the default channel, just use the name (e.g. 'mychannel'). A mapped channel type like 'imc' can be used as a prefix (e.g. 'imc:mychannel'). Finally you can specify the full coordinates to the referenced channel with Group:Version:Kind:Name (e.g. 'messaging.knative.dev:v1beta1:KafkaChannel:mychannel'). -h, --help help for create -n, --namespace string Specify the namespace to operate in. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. - --sink-dead-letter string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink-dead-letter broker:nest' for a broker 'nest', '--sink-dead-letter channel:pipe' for a channel 'pipe', '--sink-dead-letter ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-dead-letter https://event.receiver.uri' for an HTTP URI, '--sink-dead-letter ksvc:receiver' or simply '--sink-dead-letter receiver' for a Knative service 'receiver' in the current namespace. '--sink-dead-letter special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. - --sink-reply string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink-reply broker:nest' for a broker 'nest', '--sink-reply channel:pipe' for a channel 'pipe', '--sink-reply ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-reply https://event.receiver.uri' for an HTTP URI, '--sink-reply ksvc:receiver' or simply '--sink-reply receiver' for a Knative service 'receiver' in the current namespace. '--sink-reply special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + --sink-dead-letter string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink-dead-letter broker:nest' for a broker 'nest', '--sink-dead-letter channel:pipe' for a channel 'pipe', '--sink-dead-letter ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-dead-letter https://event.receiver.uri' for an HTTP URI, '--sink-dead-letter ksvc:receiver' or simply '--sink-dead-letter receiver' for a Knative service 'receiver' in the current namespace, '--sink-dead-letter svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink-dead-letter special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + --sink-reply string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink-reply broker:nest' for a broker 'nest', '--sink-reply channel:pipe' for a channel 'pipe', '--sink-reply ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-reply https://event.receiver.uri' for an HTTP URI, '--sink-reply ksvc:receiver' or simply '--sink-reply receiver' for a Knative service 'receiver' in the current namespace, '--sink-reply svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink-reply special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_subscription_update.md b/docs/cmd/kn_subscription_update.md index 4d41708689..c9f7d047f7 100644 --- a/docs/cmd/kn_subscription_update.md +++ b/docs/cmd/kn_subscription_update.md @@ -22,9 +22,9 @@ kn subscription update NAME ``` -h, --help help for update -n, --namespace string Specify the namespace to operate in. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. - --sink-dead-letter string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink-dead-letter broker:nest' for a broker 'nest', '--sink-dead-letter channel:pipe' for a channel 'pipe', '--sink-dead-letter ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-dead-letter https://event.receiver.uri' for an HTTP URI, '--sink-dead-letter ksvc:receiver' or simply '--sink-dead-letter receiver' for a Knative service 'receiver' in the current namespace. '--sink-dead-letter special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. - --sink-reply string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink-reply broker:nest' for a broker 'nest', '--sink-reply channel:pipe' for a channel 'pipe', '--sink-reply ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-reply https://event.receiver.uri' for an HTTP URI, '--sink-reply ksvc:receiver' or simply '--sink-reply receiver' for a Knative service 'receiver' in the current namespace. '--sink-reply special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + --sink-dead-letter string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink-dead-letter broker:nest' for a broker 'nest', '--sink-dead-letter channel:pipe' for a channel 'pipe', '--sink-dead-letter ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-dead-letter https://event.receiver.uri' for an HTTP URI, '--sink-dead-letter ksvc:receiver' or simply '--sink-dead-letter receiver' for a Knative service 'receiver' in the current namespace, '--sink-dead-letter svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink-dead-letter special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + --sink-reply string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink-reply broker:nest' for a broker 'nest', '--sink-reply channel:pipe' for a channel 'pipe', '--sink-reply ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink-reply https://event.receiver.uri' for an HTTP URI, '--sink-reply ksvc:receiver' or simply '--sink-reply receiver' for a Knative service 'receiver' in the current namespace, '--sink-reply svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink-reply special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_trigger_create.md b/docs/cmd/kn_trigger_create.md index 46e58ae3a9..fe1ff53184 100644 --- a/docs/cmd/kn_trigger_create.md +++ b/docs/cmd/kn_trigger_create.md @@ -24,7 +24,7 @@ kn trigger create NAME --sink SINK --filter strings Key-value pair for exact CloudEvent attribute matching against incoming events, e.g type=dev.knative.foo -h, --help help for create -n, --namespace string Specify the namespace to operate in. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/docs/cmd/kn_trigger_update.md b/docs/cmd/kn_trigger_update.md index a8967fe65a..31dd7846cf 100644 --- a/docs/cmd/kn_trigger_update.md +++ b/docs/cmd/kn_trigger_update.md @@ -27,7 +27,7 @@ kn trigger update NAME --filter strings Key-value pair for exact CloudEvent attribute matching against incoming events, e.g type=dev.knative.foo -h, --help help for update -n, --namespace string Specify the namespace to operate in. - -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace. '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. + -s, --sink string Addressable sink for events. You can specify a broker, channel, Knative service, Kubernetes service or URI. Examples: '--sink broker:nest' for a broker 'nest', '--sink channel:pipe' for a channel 'pipe', '--sink ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', '--sink https://event.receiver.uri' for an HTTP URI, '--sink ksvc:receiver' or simply '--sink receiver' for a Knative service 'receiver' in the current namespace, '--sink svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, '--sink special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. If a prefix is not provided, it is considered as a Knative service in the current namespace. ``` ### Options inherited from parent commands diff --git a/go.mod b/go.mod index 43234975cc..0c3d170615 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ replace knative.dev/client/pkg => ./pkg require ( contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect + emperror.dev/errors v0.8.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 6e1989116c..a76ff7c8ce 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d/g contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -421,11 +423,13 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= diff --git a/pkg/commands/flags/sink.go b/pkg/commands/flags/sink.go index cb03edc461..4e5dabf15a 100644 --- a/pkg/commands/flags/sink.go +++ b/pkg/commands/flags/sink.go @@ -16,19 +16,18 @@ package flags import ( "context" - "fmt" - "strings" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime/schema" - "knative.dev/client/pkg/config" - "knative.dev/pkg/apis" - duckv1 "knative.dev/pkg/apis/duck/v1" - clientdynamic "knative.dev/client/pkg/dynamic" + "knative.dev/client/pkg/flags/sink" + "knative.dev/client/pkg/util/errors" + duckv1 "knative.dev/pkg/apis/duck/v1" ) +// SinkFlags holds information about given sink together with optional mappings +// to allow ease of referencing the common types. type SinkFlags struct { Sink string SinkMappings map[string]schema.GroupVersionResource @@ -42,156 +41,69 @@ func NewSinkFlag(mapping map[string]schema.GroupVersionResource) *SinkFlags { } // AddWithFlagName configures Sink flag with given flag name and a short flag name -// pass empty short flag name if you don't want to set one +// pass empty short flag name if you don't want to set one. func (i *SinkFlags) AddWithFlagName(cmd *cobra.Command, fname, short string) { - flag := "--" + fname + i.AddToFlagSet(cmd.Flags(), fname, short) +} + +// AddToFlagSet configures Sink flag with given flag name and a short flag name +// pass empty short flag name if you don't want to set one +func (i *SinkFlags) AddToFlagSet(fs *pflag.FlagSet, fname, short string) { if short == "" { - cmd.Flags().StringVar(&i.Sink, fname, "", "") + fs.StringVar(&i.Sink, fname, "", "") } else { - cmd.Flags().StringVarP(&i.Sink, fname, short, "", "") - } - cmd.Flag(fname).Usage = "Addressable sink for events. " + - "You can specify a broker, channel, Knative service or URI. " + - "Examples: '" + flag + " broker:nest' for a broker 'nest', " + - "'" + flag + " channel:pipe' for a channel 'pipe', " + - "'" + flag + " ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', " + - "'" + flag + " https://event.receiver.uri' for an HTTP URI, " + - "'" + flag + " ksvc:receiver' or simply '" + flag + " receiver' for a Knative service 'receiver' in the current namespace. " + - "'" + flag + " special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. " + - "If a prefix is not provided, it is considered as a Knative service in the current namespace." - // Use default mapping if empty - if i.SinkMappings == nil { - i.SinkMappings = defaultSinkMappings - } - for _, p := range config.GlobalConfig.SinkMappings() { - //user configuration might override the default configuration - i.SinkMappings[p.Prefix] = schema.GroupVersionResource{ - Resource: p.Resource, - Group: p.Group, - Version: p.Version, - } + fs.StringVarP(&i.Sink, fname, short, "", "") } + fs.Lookup(fname).Usage = sink.Usage(fname) } // Add configures Sink flag with name 'Sink' amd short name 's' func (i *SinkFlags) Add(cmd *cobra.Command) { - i.AddWithFlagName(cmd, "sink", "s") + i.AddWithFlagName(cmd, sink.DefaultFlagName, sink.DefaultFlagShorthand) } -// SinkPrefixes maps prefixes used for sinks to their GroupVersionResources. -var defaultSinkMappings = map[string]schema.GroupVersionResource{ - "broker": { - Resource: "brokers", - Group: "eventing.knative.dev", - Version: "v1", - }, - // Shorthand alias for service - "ksvc": { - Resource: "services", - Group: "serving.knative.dev", - Version: "v1", - }, - "channel": { - Resource: "channels", - Group: "messaging.knative.dev", - Version: "v1", - }, +// WithDefaultMappings will return a copy of SinkFlags with provided mappings +// and the default ones. +func (i *SinkFlags) WithDefaultMappings() *SinkFlags { + return &SinkFlags{ + Sink: i.Sink, + SinkMappings: sink.ComputeWithDefaultMappings(i.SinkMappings), + } +} + +// Parse returns the sink reference, which may refer to URL or to Kubernetes +// resource. The namespace given should be the current namespace within the +// context. +func (i *SinkFlags) Parse(namespace string) (*sink.Reference, error) { + // Use default mapping if empty + sf := i.WithDefaultMappings() + return sink.Parse(sf.Sink, namespace, sf.SinkMappings) } // ResolveSink returns the Destination referred to by the flags in the acceptor. // It validates that any object the user is referring to exists. func (i *SinkFlags) ResolveSink(ctx context.Context, knclient clientdynamic.KnDynamicClient, namespace string) (*duckv1.Destination, error) { - client := knclient.RawClient() - if i.Sink == "" { - return nil, nil - } - // Use default mapping if empty - if i.SinkMappings == nil { - i.SinkMappings = defaultSinkMappings - } - prefix, name, ns := parseSink(i.Sink) - if prefix == "" { - // URI target - uri, err := apis.ParseURL(name) - if err != nil { - return nil, err - } - return &duckv1.Destination{URI: uri}, nil - } - gvr, ok := i.SinkMappings[prefix] - if !ok { - if prefix == "svc" || prefix == "service" { - return nil, fmt.Errorf("unsupported Sink prefix: '%s', please use prefix 'ksvc' for knative service", prefix) - } - idx := strings.LastIndex(prefix, "/") - var groupVersion string - var kind string - if idx != -1 && idx < len(prefix)-1 { - groupVersion, kind = prefix[:idx], prefix[idx+1:] - } else { - kind = prefix - } - parsedVersion, err := schema.ParseGroupVersion(groupVersion) - if err != nil { - return nil, err - } - - // For the RAWclient the resource name must be in lower case plural form. - // This is the best effort to sanitize the inputs, but the safest way is to provide - // the appropriate form in user's input. - if !strings.HasSuffix(kind, "s") { - kind = kind + "s" - } - kind = strings.ToLower(kind) - gvr = parsedVersion.WithResource(kind) - } - if ns != "" { - namespace = ns - } - obj, err := client.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + s, err := i.Parse(namespace) if err != nil { + if errors.Is(err, sink.ErrSinkIsRequired) { + // returns nil, if sink isn't provided to keep the current contract + return nil, nil + } return nil, err } - - destination := &duckv1.Destination{ - Ref: &duckv1.KReference{ - Kind: obj.GetKind(), - APIVersion: obj.GetAPIVersion(), - Name: obj.GetName(), - Namespace: namespace, - }, - } - return destination, nil -} - -// parseSink takes the string given by the user into the prefix, name and namespace of -// the object. If the user put a URI instead, the prefix is empty and the name -// is the whole URI. -func parseSink(sink string) (string, string, string) { - parts := strings.SplitN(sink, ":", 3) - switch { - case len(parts) == 1: - return "ksvc", parts[0], "" - case parts[0] == "http" || parts[0] == "https": - return "", sink, "" - case len(parts) == 3: - return parts[0], parts[1], parts[2] - default: - return parts[0], parts[1], "" + var dest *duckv1.Destination + dest, err = s.Resolve(ctx, knclient) + if err != nil { + // Returning original error that caused sink.ErrSinkIsInvalid as it is + // directly presented to the end-user. + return nil, errors.CauseOf(err, sink.ErrSinkIsInvalid) } + return dest, nil } // SinkToString prepares a Sink for list output -func SinkToString(sink duckv1.Destination) string { - if sink.Ref != nil { - if sink.Ref.Kind == "Service" && strings.HasPrefix(sink.Ref.APIVersion, defaultSinkMappings["ksvc"].Group) { - return fmt.Sprintf("ksvc:%s", sink.Ref.Name) - } else { - return fmt.Sprintf("%s:%s", strings.ToLower(sink.Ref.Kind), sink.Ref.Name) - } - } - if sink.URI != nil { - return sink.URI.String() - } - return "" +// Deprecated: use (*sink.Reference).AsText instead. +func SinkToString(dest duckv1.Destination) string { + ref := sink.GuessFromDestination(dest) + return ref.String() } diff --git a/pkg/commands/flags/sink_test.go b/pkg/commands/flags/sink_test.go index 7e2800ca12..654411f14b 100644 --- a/pkg/commands/flags/sink_test.go +++ b/pkg/commands/flags/sink_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package flags +package flags_test import ( "context" @@ -20,7 +20,9 @@ import ( "github.com/spf13/cobra" "gotest.tools/v3/assert" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/client/pkg/commands/flags" eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1" messagingv1 "knative.dev/eventing/pkg/apis/messaging/v1" "knative.dev/eventing/pkg/apis/sources/v1beta2" @@ -44,37 +46,33 @@ type sinkFlagAddTestCases struct { } func TestSinkFlagAdd(t *testing.T) { - cases := []*sinkFlagAddTestCases{ - { - "", - "sink", - "s", - }, - { - "subscriber", - "subscriber", - "", - }, - } + cases := []sinkFlagAddTestCases{{ + "", + "sink", + "s", + }, { + "subscriber", + "subscriber", + "", + }} for _, tc := range cases { - c := &cobra.Command{Use: "sinktest"} - sinkFlags := SinkFlags{} - if tc.flagName == "" { - sinkFlags.Add(c) - assert.Equal(t, tc.expectedFlagName, c.Flag("sink").Name) - assert.Equal(t, tc.expectedShortName, c.Flag("sink").Shorthand) - } else { - sinkFlags.AddWithFlagName(c, tc.flagName, "") - assert.Equal(t, tc.expectedFlagName, c.Flag(tc.flagName).Name) - assert.Equal(t, tc.expectedShortName, c.Flag(tc.flagName).Shorthand) - } + t.Run(tc.flagName, func(t *testing.T) { + c := &cobra.Command{Use: "sinktest"} + sinkFlags := flags.SinkFlags{} + if tc.flagName == "" { + sinkFlags.Add(c) + assert.Equal(t, tc.expectedFlagName, c.Flag("sink").Name) + assert.Equal(t, tc.expectedShortName, c.Flag("sink").Shorthand) + } else { + sinkFlags.AddWithFlagName(c, tc.flagName, "") + assert.Equal(t, tc.expectedFlagName, c.Flag(tc.flagName).Name) + assert.Equal(t, tc.expectedShortName, c.Flag(tc.flagName).Shorthand) + } + }) } } func TestResolve(t *testing.T) { - targetExampleCom, err := apis.ParseURL("http://target.example.com") - assert.NilError(t, err) - mysvc := &servingv1.Service{ TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "serving.knative.dev/v1"}, ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "default"}, @@ -91,6 +89,10 @@ func TestResolve(t *testing.T) { TypeMeta: metav1.TypeMeta{Kind: "PingSource", APIVersion: "sources.knative.dev/v1"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}, } + k8sService := &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}, + } cases := []resolveCase{ {"ksvc:mysvc", &duckv1.Destination{ Ref: &duckv1.KReference{Kind: "Service", @@ -143,24 +145,42 @@ func TestResolve(t *testing.T) { Name: "foo", }}, ""}, {"http://target.example.com", &duckv1.Destination{ - URI: targetExampleCom, + URI: url(t, "http://target.example.com"), }, ""}, {"k8ssvc:foo", nil, "k8ssvcs \"foo\" not found"}, - {"svc:foo", nil, "please use prefix 'ksvc' for knative service"}, - {"service:foo", nil, "please use prefix 'ksvc' for knative service"}, + {"svc:foo", &duckv1.Destination{Ref: &duckv1.KReference{ + APIVersion: "v1", + Kind: "Service", + Namespace: "default", + Name: "foo", + }}, ""}, + {"service:foo", &duckv1.Destination{Ref: &duckv1.KReference{ + APIVersion: "v1", + Kind: "Service", + Namespace: "default", + Name: "foo", + }}, ""}, {"absent:foo", nil, "absents \"foo\" not found"}, + {"", nil, ""}, } - dynamicClient := dynamicfake.CreateFakeKnDynamicClient("default", mysvc, defaultBroker, pipeChannel, pingSource) + dynamicClient := dynamicfake.CreateFakeKnDynamicClient( + "default", + mysvc, defaultBroker, pipeChannel, pingSource, k8sService, + ) for _, c := range cases { - i := &SinkFlags{Sink: c.sink} - result, err := i.ResolveSink(context.Background(), dynamicClient, "default") - if c.destination != nil { - assert.DeepEqual(t, result, c.destination) - assert.NilError(t, err) - } else { - assert.ErrorContains(t, err, c.errContents) - } + t.Run(c.sink, func(t *testing.T) { + sf := &flags.SinkFlags{Sink: c.sink} + result, err := sf.ResolveSink(context.Background(), dynamicClient, "default") + if c.errContents == "" { + assert.DeepEqual(t, result, c.destination) + assert.NilError(t, err) + } else { + assert.Check(t, err != nil && err.Error() != "", + "error ins't empty") + assert.ErrorContains(t, err, c.errContents) + } + }) } } @@ -197,47 +217,73 @@ func TestResolveWithNamespace(t *testing.T) { } dynamicClient := dynamicfake.CreateFakeKnDynamicClient("my-namespace", mysvc, defaultBroker, pipeChannel) for _, c := range cases { - i := &SinkFlags{Sink: c.sink} - result, err := i.ResolveSink(context.Background(), dynamicClient, "default") - if c.destination != nil { - assert.DeepEqual(t, result, c.destination) - assert.Equal(t, c.destination.Ref.Namespace, "my-namespace") - assert.NilError(t, err) - } else { - assert.ErrorContains(t, err, c.errContents) - } + t.Run(c.sink, func(t *testing.T) { + i := &flags.SinkFlags{Sink: c.sink} + result, err := i.ResolveSink(context.Background(), dynamicClient, "default") + if c.destination != nil { + assert.DeepEqual(t, result, c.destination) + assert.Equal(t, c.destination.Ref.Namespace, "my-namespace") + assert.NilError(t, err) + } else { + assert.ErrorContains(t, err, c.errContents) + } + }) } } func TestSinkToString(t *testing.T) { - sink := duckv1.Destination{ - Ref: &duckv1.KReference{Kind: "Service", + tcs := []resolveCase{{ + sink: "ksvc:mysvc", + destination: &duckv1.Destination{Ref: &duckv1.KReference{ + Kind: "Service", APIVersion: "serving.knative.dev/v1", Namespace: "my-namespace", - Name: "mysvc"}} - expected := "ksvc:mysvc" - assert.Equal(t, expected, SinkToString(sink)) - sink = duckv1.Destination{ - Ref: &duckv1.KReference{Kind: "Broker", + Name: "mysvc", + }}, + }, { + sink: "broker:default", + destination: &duckv1.Destination{Ref: &duckv1.KReference{ + Kind: "Broker", APIVersion: "eventing.knative.dev/v1", Namespace: "my-namespace", - Name: "default"}} - expected = "broker:default" - assert.Equal(t, expected, SinkToString(sink)) - sink = duckv1.Destination{ - Ref: &duckv1.KReference{Kind: "Service", + Name: "default", + }}, + }, { + sink: "svc:mysvc", + destination: &duckv1.Destination{Ref: &duckv1.KReference{ + Kind: "Service", APIVersion: "v1", Namespace: "my-namespace", - Name: "mysvc"}} - expected = "service:mysvc" - assert.Equal(t, expected, SinkToString(sink)) + Name: "mysvc", + }}, + }, { + sink: "things.acme.dev/v1alpha1:abc", + destination: &duckv1.Destination{Ref: &duckv1.KReference{ + Kind: "Thing", + APIVersion: "acme.dev/v1alpha1", + Namespace: "my-namespace", + Name: "abc", + }}, + }, { + sink: "http://target.example.com", + destination: &duckv1.Destination{ + URI: url(t, "http://target.example.com"), + }, + }, { + sink: "", + destination: &duckv1.Destination{}, + }} + for _, tc := range tcs { + t.Run(tc.sink, func(t *testing.T) { + got := flags.SinkToString(*tc.destination) + assert.Equal(t, got, tc.sink) + }) + } +} - uri := "http://target.example.com" - targetExampleCom, err := apis.ParseURL(uri) +func url(t testing.TB, uri string) *apis.URL { + t.Helper() + u, err := apis.ParseURL(uri) assert.NilError(t, err) - sink = duckv1.Destination{ - URI: targetExampleCom, - } - assert.Equal(t, uri, SinkToString(sink)) - assert.Equal(t, "", SinkToString(duckv1.Destination{})) + return u } diff --git a/pkg/commands/namespaced_test.go b/pkg/commands/namespaced_test.go index 92a6f893d1..e902be01f5 100644 --- a/pkg/commands/namespaced_test.go +++ b/pkg/commands/namespaced_test.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "gotest.tools/v3/assert" "k8s.io/client-go/tools/clientcmd" + "knative.dev/client/pkg/k8s" "knative.dev/client/pkg/util/test" ) @@ -41,7 +42,7 @@ func TestGetNamespaceSample(t *testing.T) { testCmd := testCommandGenerator(true) expectedNamespace := "test1" testCmd.SetArgs([]string{"--namespace", expectedNamespace}) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) kp := &KnParams{fixedCurrentNamespace: FakeNamespace} actualNamespace, err := kp.GetNamespace(testCmd) if err != nil { @@ -63,7 +64,7 @@ func TestGetNamespaceSample(t *testing.T) { func TestGetNamespaceDefault(t *testing.T) { testCmd := testCommandGenerator(true) expectedNamespace := "current" - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) kp := &KnParams{fixedCurrentNamespace: FakeNamespace} actualNamespace, err := kp.GetNamespace(testCmd) if err != nil { @@ -84,7 +85,7 @@ func TestGetNamespaceAllNamespacesSet(t *testing.T) { // Test both variants of the "all namespaces" flag for _, arg := range []string{"--all-namespaces", "-A"} { testCmd.SetArgs([]string{"--namespace", sampleNamespace, arg}) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) kp := &KnParams{fixedCurrentNamespace: FakeNamespace} actualNamespace, err := kp.GetNamespace(testCmd) if err != nil { @@ -106,7 +107,7 @@ func TestGetNamespaceDefaultAllNamespacesUnset(t *testing.T) { // Test both variants of the "all namespaces" flag for _, arg := range []string{"--all-namespaces", "-A"} { testCmd.SetArgs([]string{arg}) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) kp := &KnParams{fixedCurrentNamespace: FakeNamespace} actualNamespace, err := kp.GetNamespace(testCmd) if err != nil { @@ -124,7 +125,7 @@ func TestGetNamespaceAllNamespacesNotDefined(t *testing.T) { testCmd := testCommandGenerator(false) expectedNamespace := "test1" testCmd.SetArgs([]string{"--namespace", expectedNamespace}) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) kp := &KnParams{fixedCurrentNamespace: FakeNamespace} actualNamespace, err := kp.GetNamespace(testCmd) if err != nil { @@ -143,9 +144,9 @@ func TestGetNamespaceFallback(t *testing.T) { err := os.WriteFile(tempFile, []byte(BASIC_KUBECONFIG), test.FileModeReadWrite) assert.NilError(t, err) - kp := &KnParams{KubeCfgPath: tempFile} + kp := &KnParams{Params: k8s.Params{KubeCfgPath: tempFile}} testCmd := testCommandGenerator(true) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) actual, err := kp.GetNamespace(testCmd) assert.NilError(t, err) if isInCluster() { @@ -161,9 +162,9 @@ func TestGetNamespaceFallback(t *testing.T) { err := os.WriteFile(tempFile, []byte(""), test.FileModeReadWrite) assert.NilError(t, err) - kp := &KnParams{KubeCfgPath: tempFile} + kp := &KnParams{Params: k8s.Params{KubeCfgPath: tempFile}} testCmd := testCommandGenerator(true) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) actual, err := kp.GetNamespace(testCmd) assert.NilError(t, err) if isInCluster() { @@ -175,11 +176,11 @@ func TestGetNamespaceFallback(t *testing.T) { }) t.Run("MissingConfig", func(t *testing.T) { - kp := &KnParams{KubeCfgPath: filepath.Join(tempDir, "missing")} + kp := &KnParams{Params: k8s.Params{KubeCfgPath: filepath.Join(tempDir, "missing")}} testCmd := testCommandGenerator(true) - testCmd.Execute() + assert.NilError(t, testCmd.Execute()) actual, err := kp.GetNamespace(testCmd) - assert.ErrorContains(t, err, "can not be found") + assert.ErrorIs(t, err, k8s.ErrCantFindConfigFile) assert.Equal(t, actual, "") }) } @@ -193,7 +194,7 @@ func TestCurrentNamespace(t *testing.T) { err := os.WriteFile(tempFile, []byte(""), test.FileModeReadWrite) assert.NilError(t, err) - kp := &KnParams{KubeCfgPath: tempFile} + kp := &KnParams{Params: k8s.Params{KubeCfgPath: tempFile}} actual, err := kp.CurrentNamespace() if isInCluster() { // In-cluster config overrides the mocked one in OpenShift CI @@ -209,10 +210,10 @@ func TestCurrentNamespace(t *testing.T) { t.Run("MissingConfig", func(t *testing.T) { // Missing kubeconfig - kp := &KnParams{KubeCfgPath: filepath.Join(tempDir, "missing")} + kp := &KnParams{Params: k8s.Params{KubeCfgPath: filepath.Join(tempDir, "missing")}} actual, err := kp.CurrentNamespace() assert.Assert(t, err != nil) - assert.ErrorContains(t, err, "can not be found") + assert.ErrorIs(t, err, k8s.ErrCantFindConfigFile) assert.Assert(t, actual == "") }) @@ -229,7 +230,7 @@ func TestCurrentNamespace(t *testing.T) { tempFile := filepath.Join(tempDir, "mock") err := os.WriteFile(tempFile, []byte(BASIC_KUBECONFIG), test.FileModeReadWrite) assert.NilError(t, err) - kp := &KnParams{KubeCfgPath: tempFile} + kp := &KnParams{Params: k8s.Params{KubeCfgPath: tempFile}} actual, err := kp.CurrentNamespace() assert.NilError(t, err) if isInCluster() { diff --git a/pkg/commands/subscription/list_test.go b/pkg/commands/subscription/list_test.go index 28f29c2e7d..ac9a8bd673 100644 --- a/pkg/commands/subscription/list_test.go +++ b/pkg/commands/subscription/list_test.go @@ -69,7 +69,7 @@ func TestSubscriptionList(t *testing.T) { assert.NilError(t, err) ol := strings.Split(out, "\n") assert.Check(t, util.ContainsAll(ol[0], "NAME", "CHANNEL", "SUBSCRIBER", "REPLY", "DEAD LETTER SINK", "READY", "REASON")) - assert.Check(t, util.ContainsAll(ol[1], "s0", "InMemoryChannel:imc0", "ksvc:ksvc0", "broker:b00", "broker:b01")) + assert.Check(t, util.ContainsAll(ol[1], "s0", "InMemoryChannel:imc0", "ksvc0", "broker:b00", "broker:b01")) assert.Check(t, util.ContainsAll(ol[2], "s1", "imc1", "ksvc1", "b10", "b11")) assert.Check(t, util.ContainsAll(ol[3], "s2", "imc2", "ksvc2", "b20", "b21")) }) diff --git a/pkg/commands/types.go b/pkg/commands/types.go index 67621e0c18..d0c00672a0 100644 --- a/pkg/commands/types.go +++ b/pkg/commands/types.go @@ -15,12 +15,11 @@ package commands import ( - "fmt" "io" "os" - "path/filepath" "k8s.io/client-go/kubernetes" + "knative.dev/client/pkg/k8s" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" @@ -48,14 +47,8 @@ import ( // KnParams for creating commands. Useful for inserting mocks for testing. type KnParams struct { + k8s.Params Output io.Writer - KubeCfgPath string - KubeContext string - KubeCluster string - KubeAsUser string - KubeAsUID string - KubeAsGroup []string - ClientConfig clientcmd.ClientConfig NewKubeClient func() (kubernetes.Interface, error) NewServingClient func(namespace string) (clientservingv1.KnServingClient, error) NewServingV1beta1Client func(namespace string) (clientservingv1beta1.KnServingClient, error) @@ -72,8 +65,12 @@ type KnParams struct { // Set this if you want to nail down the namespace fixedCurrentNamespace string + + // Memorizes the loaded config + clientcmd.ClientConfig } +// Initialize will initialize the default factories for the clients. func (params *KnParams) Initialize() { if params.NewKubeClient == nil { params.NewKubeClient = params.newKubeClient @@ -246,44 +243,3 @@ func (params *KnParams) RestConfig() (*rest.Config, error) { return config, nil } - -// GetClientConfig gets ClientConfig from KubeCfgPath -func (params *KnParams) GetClientConfig() (clientcmd.ClientConfig, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - configOverrides := &clientcmd.ConfigOverrides{} - if params.KubeContext != "" { - configOverrides.CurrentContext = params.KubeContext - } - if params.KubeCluster != "" { - configOverrides.Context.Cluster = params.KubeCluster - } - if params.KubeAsUser != "" { - configOverrides.AuthInfo.Impersonate = params.KubeAsUser - } - if params.KubeAsUID != "" { - configOverrides.AuthInfo.ImpersonateUID = params.KubeAsUID - } - if len(params.KubeAsGroup) > 0 { - configOverrides.AuthInfo.ImpersonateGroups = params.KubeAsGroup - } - if len(params.KubeCfgPath) == 0 { - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides), nil - } - - _, err := os.Stat(params.KubeCfgPath) - if err == nil { - loadingRules.ExplicitPath = params.KubeCfgPath - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides), nil - } - - if !os.IsNotExist(err) { - return nil, err - } - - paths := filepath.SplitList(params.KubeCfgPath) - if len(paths) > 1 { - return nil, fmt.Errorf("can not find config file. '%s' looks like a path. "+ - "Please use the env var KUBECONFIG if you want to check for multiple configuration files", params.KubeCfgPath) - } - return nil, fmt.Errorf("config file '%s' can not be found", params.KubeCfgPath) -} diff --git a/pkg/commands/types_test.go b/pkg/commands/types_test.go index 7986774b38..bdf1d71f17 100644 --- a/pkg/commands/types_test.go +++ b/pkg/commands/types_test.go @@ -15,18 +15,14 @@ package commands import ( - "fmt" - "os" "path/filepath" "strings" "testing" - "knative.dev/client/pkg/util/test" - "gotest.tools/v3/assert" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "knative.dev/client/pkg/util" + "knative.dev/client/pkg/k8s" ) type configTestCase struct { @@ -106,149 +102,7 @@ func TestPrepareConfig(t *testing.T) { kpEmptyConfig = &KnParams{} kpEmptyConfig.KubeCfgPath = filepath.Join("non", "existing", "file") _, err = kpEmptyConfig.RestConfig() - assert.ErrorContains(t, err, "can not be found") -} - -type typeTestCase struct { - kubeCfgPath string - kubeContext string - kubeAsUser string - kubeAsUID string - kubeAsGroup []string - kubeCluster string - explicitPath string - expectedError string -} - -func TestGetClientConfig(t *testing.T) { - multiConfigs := fmt.Sprintf("%s%s%s", "/testing/assets/kube-config-01.yml", string(os.PathListSeparator), "/testing/assets/kube-config-02.yml") - - tempDir := t.TempDir() - tempFile := filepath.Join(tempDir, "mock") - err := os.WriteFile(tempFile, []byte(BASIC_KUBECONFIG), test.FileModeReadWrite) - assert.NilError(t, err) - - for _, tc := range []typeTestCase{ - { - "", - "", - "", - "", - []string{}, - "", - clientcmd.NewDefaultClientConfigLoadingRules().ExplicitPath, - "", - }, - { - tempFile, - "", - "", - "", - []string{}, - "", - tempFile, - "", - }, - { - "/testing/assets/kube-config-01.yml", - "foo", - "", - "", - []string{}, - "bar", - "", - fmt.Sprintf("config file '%s' can not be found", "/testing/assets/kube-config-01.yml"), - }, - { - multiConfigs, - "", - "", - "", - []string{}, - "", - "", - fmt.Sprintf("can not find config file. '%s' looks like a path. Please use the env var KUBECONFIG if you want to check for multiple configuration files", multiConfigs), - }, - { - tempFile, - "", - "admin", - "", - []string{}, - "", - tempFile, - "", - }, - { - tempFile, - "", - "admin", - "", - []string{"system:authenticated", "system:masters"}, - "", - tempFile, - "", - }, - { - tempFile, - "", - "admin", - "abc123", - []string{}, - "", - tempFile, - "", - }, - } { - p := &KnParams{ - KubeCfgPath: tc.kubeCfgPath, - KubeContext: tc.kubeContext, - KubeAsUser: tc.kubeAsUser, - KubeAsUID: tc.kubeAsUID, - KubeAsGroup: tc.kubeAsGroup, - KubeCluster: tc.kubeCluster, - } - - clientConfig, err := p.GetClientConfig() - if tc.expectedError != "" { - assert.Assert(t, util.ContainsAll(err.Error(), tc.expectedError)) - } else { - assert.Assert(t, err == nil, err) - } - - if clientConfig != nil { - configAccess := clientConfig.ConfigAccess() - assert.Assert(t, configAccess.GetExplicitFile() == tc.explicitPath) - - if tc.kubeContext != "" { - config, err := clientConfig.RawConfig() - assert.NilError(t, err) - assert.Assert(t, config.CurrentContext == tc.kubeContext) - assert.Assert(t, config.Contexts[tc.kubeContext].Cluster == tc.kubeCluster) - } - - if tc.kubeAsUser != "" { - config, err := clientConfig.ClientConfig() - assert.NilError(t, err) - assert.Assert(t, config.Impersonate.UserName == tc.kubeAsUser) - } - - if tc.kubeAsUID != "" { - config, err := clientConfig.ClientConfig() - assert.NilError(t, err) - assert.Assert(t, config.Impersonate.UID == tc.kubeAsUID) - } - - if len(tc.kubeAsGroup) > 0 { - config, err := clientConfig.ClientConfig() - assert.NilError(t, err) - assert.Assert(t, len(config.Impersonate.Groups) == len(tc.kubeAsGroup)) - for i := range tc.kubeAsGroup { - assert.Assert(t, config.Impersonate.Groups[i] == tc.kubeAsGroup[i]) - } - } - } - } + assert.ErrorIs(t, err, k8s.ErrCantFindConfigFile) } func TestNewSourcesClient(t *testing.T) { diff --git a/pkg/dynamic/fake/fake.go b/pkg/dynamic/fake/fake.go index 954cc43ff0..bd09ab381d 100644 --- a/pkg/dynamic/fake/fake.go +++ b/pkg/dynamic/fake/fake.go @@ -16,7 +16,9 @@ package fake import ( "context" + "testing" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" @@ -32,13 +34,17 @@ import ( // CreateFakeKnDynamicClient gives you a dynamic client for testing containing the given objects. func CreateFakeKnDynamicClient(testNamespace string, objects ...runtime.Object) dynamic.KnDynamicClient { + if !testing.Testing() { + panic("For test usage only!") + } scheme := runtime.NewScheme() - servingv1.AddToScheme(scheme) - eventingv1.AddToScheme(scheme) - messagingv1.AddToScheme(scheme) - sourcesv1.AddToScheme(scheme) - sourcesv1beta2.AddToScheme(scheme) - apiextensionsv1.AddToScheme(scheme) + _ = corev1.AddToScheme(scheme) + _ = servingv1.AddToScheme(scheme) + _ = eventingv1.AddToScheme(scheme) + _ = messagingv1.AddToScheme(scheme) + _ = sourcesv1.AddToScheme(scheme) + _ = sourcesv1beta2.AddToScheme(scheme) + _ = apiextensionsv1.AddToScheme(scheme) _, dynamicClient := dynamicclientfake.With(context.TODO(), scheme, objects...) return dynamic.NewKnDynamicClient(dynamicClient, testNamespace) } diff --git a/pkg/flags/sink/commandline.go b/pkg/flags/sink/commandline.go new file mode 100644 index 0000000000..8924d97325 --- /dev/null +++ b/pkg/flags/sink/commandline.go @@ -0,0 +1,60 @@ +/* + Copyright 2024 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 sink + +import ( + "strings" +) + +var ( + // DefaultFlagName is a default command-line flag name. + DefaultFlagName = "sink" + // DefaultFlagShorthand is a default command-line flag shorthand. + DefaultFlagShorthand = "s" +) + +// Usage returns a usage text which can be used to define sink-like flag. +func Usage(fname string) string { + flag := "--" + fname + return "Addressable sink for events. " + + "You can specify a broker, channel, Knative service, Kubernetes service or URI. " + + "Examples: '" + flag + " broker:nest' for a broker 'nest', " + + "'" + flag + " channel:pipe' for a channel 'pipe', " + + "'" + flag + " ksvc:mysvc:mynamespace' for a Knative service 'mysvc' in another namespace 'mynamespace', " + + "'" + flag + " https://event.receiver.uri' for an HTTP URI, " + + "'" + flag + " ksvc:receiver' or simply '" + flag + " receiver' for a Knative service 'receiver' in the current namespace, " + + "'" + flag + " svc:receiver:mynamespace' for a Kubernetes service 'receiver' in the 'mynamespace' namespace, " + + "'" + flag + " special.eventing.dev/v1alpha1/channels:pipe' for GroupVersionResource of v1alpha1 'pipe'. " + + "If a prefix is not provided, it is considered as a Knative service in the current namespace." +} + +// parseSink takes the string given by the user into the prefix, name and namespace of +// the object. If the user put a URI instead, the prefix is empty and the name +// is the whole URI. +func parseSink(sink string) (string, string, string) { + parts := strings.SplitN(sink, ":", 3) + switch { + case len(parts) == 1: + return knativeServiceShorthand, parts[0], "" + case parts[0] == "http" || parts[0] == "https": + return "", sink, "" + case len(parts) == 3: + return parts[0], parts[1], parts[2] + default: + return parts[0], parts[1], "" + } +} diff --git a/pkg/flags/sink/sink.go b/pkg/flags/sink/sink.go new file mode 100644 index 0000000000..fc1c0c272b --- /dev/null +++ b/pkg/flags/sink/sink.go @@ -0,0 +1,299 @@ +/* + Copyright 2024 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 sink + +import ( + "context" + "errors" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/client/pkg/config" + clientdynamic "knative.dev/client/pkg/dynamic" + "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" +) + +// ErrSinkIsRequired is returned when no sink is given. +var ErrSinkIsRequired = errors.New("sink is required") + +// ErrSinkIsInvalid is returned when the sink has invalid format. +var ErrSinkIsInvalid = errors.New("sink has invalid format") + +// Type is a type of Reference. +type Type int + +const ( + // TypeURL is a URL version of the sink. + TypeURL Type = iota + // TypeReference is a Kuberentes version of the sink. + TypeReference +) + +// Reference represents either a URL or Kubernetes resource. +type Reference struct { + *KubeReference + *apis.URL +} + +// KubeReference represents a Kubernetes resource as given by command-line args. +type KubeReference struct { + GVR schema.GroupVersionResource + Name string + Namespace string +} + +// DefaultMappings are used to easily map prefixes for sinks to their +// GroupVersionResources. +var DefaultMappings = withAliasses(map[string]schema.GroupVersionResource{ + "kservice": { + Resource: "services", + Group: "serving.knative.dev", + Version: "v1", + }, + "broker": { + Resource: "brokers", + Group: "eventing.knative.dev", + Version: "v1", + }, + "channel": { + Resource: "channels", + Group: "messaging.knative.dev", + Version: "v1", + }, + "service": { // K8s' service + Resource: "services", + Group: "", + Version: "v1", + }, +}, defaultMappingAliasses) + +var defaultMappingAliasses = map[string]string{ + knativeServiceShorthand: "kservice", + "svc": "service", +} + +const knativeServiceShorthand = "ksvc" + +// Type returns the type of the reference. +func (r *Reference) Type() Type { + if r.KubeReference != nil { + return TypeReference + } + if r.URL != nil { + return TypeURL + } + return Type(-1) // unknown type, unexpected +} + +// Resolve returns the Destination referred to by the sink. It validates that +// any object the user is referring to exists. +func (r *Reference) Resolve(ctx context.Context, knclient clientdynamic.KnDynamicClient) (*duckv1.Destination, error) { + if r.Type() == TypeURL { + return &duckv1.Destination{URI: r.URL}, nil + } + if r.Type() != TypeReference { + return nil, fmt.Errorf("%w: unexpected type %q", + ErrSinkIsInvalid, r.Type()) + } + client := knclient.RawClient() + obj, err := client.Resource(r.GVR). + Namespace(r.Namespace). + Get(ctx, r.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrSinkIsInvalid, err) + } + + destination := &duckv1.Destination{ + Ref: &duckv1.KReference{ + Kind: obj.GetKind(), + APIVersion: obj.GetAPIVersion(), + Name: obj.GetName(), + Namespace: r.Namespace, + }, + } + return destination, nil +} + +// String creates a text representation of the reference +// Deprecated: use AsText instead +func (r *Reference) String() string { + if r == nil { + return "" + } + // unexpected random-like value + ns := "vaizaeso3sheem5ebie5eeh9Aew5eekei3thie4ezooy9geef6iesh9auPhai7na" + if r.KubeReference != nil { + ns = r.Namespace + } + return r.AsText(ns) +} + +// AsText creates a text representation of the resource, and should +// be used by giving a current namespace. +func (r *Reference) AsText(currentNamespace string) string { + if r.Type() == TypeURL { + return r.URL.String() + } + if r.Type() == TypeReference { + repr := r.GvrAsText() + ":" + r.Name + if currentNamespace != r.Namespace { + repr = fmt.Sprintf("%s:%s", repr, r.Namespace) + } + return repr + } + return fmt.Errorf("%w: unexpected type %q", + ErrSinkIsInvalid, r.Type()).Error() +} + +// GvrAsText returns the +func (r *Reference) GvrAsText() string { + if r == nil || r.KubeReference == nil { + return fmt.Errorf("%w: unexpected type %#v", + ErrSinkIsInvalid, r).Error() + } + for alias, as := range defaultMappingAliasses { + if gvr, ok := DefaultMappings[as]; ok && gvr == r.GVR { + return alias + } + } + for alias, gvr := range DefaultMappings { + if r.GVR == gvr { + return alias + } + } + return fmt.Sprintf("%s.%s/%s", + r.GVR.Resource, r.GVR.Group, r.GVR.Version) + +} + +// Parse returns the sink reference of given sink representation, which may +// refer to URL or to the Kubernetes resource. The namespace given should be +// the current namespace within the context. +func Parse(sinkRepr, namespace string, mappings map[string]schema.GroupVersionResource) (*Reference, error) { + if sinkRepr == "" { + return nil, ErrSinkIsRequired + } + prefix, name, ns := parseSink(sinkRepr) + if prefix == "" { + // URI target + uri, err := apis.ParseURL(name) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrSinkIsInvalid, err) + } + return &Reference{URL: uri}, nil + } + gvr, ok := mappings[prefix] + if !ok { + idx := strings.LastIndex(prefix, "/") + var groupVersion string + var kind string + if idx != -1 && idx < len(prefix)-1 { + groupVersion, kind = prefix[:idx], prefix[idx+1:] + } else { + kind = prefix + } + parsedVersion, err := schema.ParseGroupVersion(groupVersion) + if err != nil { + return nil, err + } + + // For the RAWclient the resource name must be in lower case plural form. + // This is the best effort to sanitize the inputs, but the safest way is to provide + // the appropriate form in user's input. + if !strings.HasSuffix(kind, "s") { + kind = kind + "s" + } + kind = strings.ToLower(kind) + gvr = parsedVersion.WithResource(kind) + } + if ns != "" { + namespace = ns + } + return &Reference{KubeReference: &KubeReference{ + GVR: gvr, + Name: name, + Namespace: namespace, + }}, nil +} + +// ComputeWithDefaultMappings will compute mapping by including default mappings +// and mappings provided by the end-user. +func ComputeWithDefaultMappings(mappings map[string]schema.GroupVersionResource) map[string]schema.GroupVersionResource { + sm := make(map[string]schema.GroupVersionResource, + len(mappings)+len(DefaultMappings)) + for k, v := range DefaultMappings { + sm[k] = v + } + for k, v := range mappings { + sm[k] = v + } + for _, p := range config.GlobalConfig.SinkMappings() { + // user configuration might override the default configuration + sm[p.Prefix] = schema.GroupVersionResource{ + Resource: p.Resource, + Group: p.Group, + Version: p.Version, + } + } + return sm +} + +// GuessFromDestination converts the duckv1.Destination to the Reference by guessing +// the type by convention. +// Will return nil, if given empty destination. +func GuessFromDestination(dest duckv1.Destination) *Reference { + if dest.URI != nil { + return &Reference{URL: dest.URI} + } + if dest.Ref == nil { + return nil + } + ref := &corev1.ObjectReference{ + Kind: dest.Ref.Kind, + Namespace: dest.Ref.Namespace, + Name: dest.Ref.Name, + APIVersion: dest.Ref.APIVersion, + } + gvk := ref.GroupVersionKind() + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + return &Reference{KubeReference: &KubeReference{ + GVR: gvr, + Name: ref.Name, + Namespace: ref.Namespace, + }} +} + +func withAliasses( + mappings map[string]schema.GroupVersionResource, + aliases map[string]string, +) map[string]schema.GroupVersionResource { + result := make(map[string]schema.GroupVersionResource, len(aliases)+len(mappings)) + for k, v := range mappings { + result[k] = v + } + for as, alias := range aliases { + if val, ok := result[alias]; ok { + result[as] = val.GroupResource().WithVersion(val.Version) + } + } + return result +} diff --git a/pkg/k8s/params.go b/pkg/k8s/params.go new file mode 100644 index 0000000000..f15313bfd7 --- /dev/null +++ b/pkg/k8s/params.go @@ -0,0 +1,99 @@ +/* + Copyright 2024 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 k8s + +import ( + "fmt" + "os" + "path/filepath" + + "emperror.dev/errors" + "github.com/spf13/pflag" + "k8s.io/client-go/tools/clientcmd" +) + +// ErrCantFindConfigFile is returned when given config file can't be found. +var ErrCantFindConfigFile = errors.New("can not find config file") + +// Params contain Kubernetes specific params, that CLI should comply to. +type Params struct { + KubeCfgPath string + KubeContext string + KubeCluster string + KubeAsUser string + KubeAsUID string + KubeAsGroup []string +} + +// SetFlags is used set flags to the given flagset. +func (kp *Params) SetFlags(flags *pflag.FlagSet) { + flags.StringVar(&kp.KubeCfgPath, "kubeconfig", "", + "kubectl configuration file (default: ~/.kube/config)") + flags.StringVar(&kp.KubeContext, "context", "", + "name of the kubeconfig context to use") + flags.StringVar(&kp.KubeCluster, "cluster", "", + "name of the kubeconfig cluster to use") + flags.StringVar(&kp.KubeAsUser, "as", "", + "username to impersonate for the operation") + flags.StringVar(&kp.KubeAsUID, "as-uid", "", + "uid to impersonate for the operation") + flags.StringArrayVar(&kp.KubeAsGroup, "as-group", + []string{}, "group to impersonate for the operation, this flag can "+ + "be repeated to specify multiple groups") +} + +// GetClientConfig gets ClientConfig from Kube' configuration params. +func (kp *Params) GetClientConfig() (clientcmd.ClientConfig, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + if kp.KubeContext != "" { + configOverrides.CurrentContext = kp.KubeContext + } + if kp.KubeCluster != "" { + configOverrides.Context.Cluster = kp.KubeCluster + } + if kp.KubeAsUser != "" { + configOverrides.AuthInfo.Impersonate = kp.KubeAsUser + } + if kp.KubeAsUID != "" { + configOverrides.AuthInfo.ImpersonateUID = kp.KubeAsUID + } + if len(kp.KubeAsGroup) > 0 { + configOverrides.AuthInfo.ImpersonateGroups = kp.KubeAsGroup + } + if len(kp.KubeCfgPath) == 0 { + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides), nil + } + + _, err := os.Stat(kp.KubeCfgPath) + if err == nil { + loadingRules.ExplicitPath = kp.KubeCfgPath + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides), nil + } + + if !os.IsNotExist(err) { + return nil, err + } + + paths := filepath.SplitList(kp.KubeCfgPath) + if len(paths) > 1 { + return nil, fmt.Errorf("%w. '%s' looks "+ + "like a path. Please use the env var KUBECONFIG if you want to "+ + "check for multiple configuration files", ErrCantFindConfigFile, kp.KubeCfgPath) + } + return nil, fmt.Errorf("%w: '%s'", ErrCantFindConfigFile, kp.KubeCfgPath) +} diff --git a/pkg/k8s/params_test.go b/pkg/k8s/params_test.go new file mode 100644 index 0000000000..1a55b717e9 --- /dev/null +++ b/pkg/k8s/params_test.go @@ -0,0 +1,190 @@ +/* + Copyright 2024 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 k8s_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "k8s.io/client-go/tools/clientcmd" + "knative.dev/client/pkg/k8s" + "knative.dev/client/pkg/util" + "knative.dev/client/pkg/util/test" +) + +func TestGetClientConfig(t *testing.T) { + multiConfigs := fmt.Sprintf("%s%s%s", + "/testing/assets/kube-config-01.yml", + string(os.PathListSeparator), + "/testing/assets/kube-config-02.yml") + + tempDir := t.TempDir() + tempFile := filepath.Join(tempDir, "mock") + assert.NilError(t, os.WriteFile(tempFile, []byte(basicKubeconfig), test.FileModeReadWrite)) + + for _, tc := range []typeTestCase{{ + "", + "", + "", + "", + []string{}, + "", + clientcmd.NewDefaultClientConfigLoadingRules().ExplicitPath, + "", + }, { + tempFile, + "", + "", + "", + []string{}, + "", + tempFile, + "", + }, { + "/testing/assets/kube-config-01.yml", + "foo", + "", + "", + []string{}, + "bar", + "", + fmt.Sprintf("can not find config file: '%s'", "/testing/assets/kube-config-01.yml"), + }, { + multiConfigs, + "", + "", + "", + []string{}, + "", + "", + fmt.Sprintf("can not find config file. '%s' looks like a path. Please use the env var KUBECONFIG if you want to check for multiple configuration files", multiConfigs), + }, { + tempFile, + "", + "admin", + "", + []string{}, + "", + tempFile, + "", + }, { + tempFile, + "", + "admin", + "", + []string{"system:authenticated", "system:masters"}, + "", + tempFile, + "", + }, { + tempFile, + "", + "admin", + "abc123", + []string{}, + "", + tempFile, + "", + }} { + p := &k8s.Params{ + KubeCfgPath: tc.kubeCfgPath, + KubeContext: tc.kubeContext, + KubeAsUser: tc.kubeAsUser, + KubeAsUID: tc.kubeAsUID, + KubeAsGroup: tc.kubeAsGroup, + KubeCluster: tc.kubeCluster, + } + var clientConfig clientcmd.ClientConfig + { + cc, err := p.GetClientConfig() + if tc.expectedError != "" { + assert.Assert(t, util.ContainsAll(err.Error(), tc.expectedError)) + } else { + assert.Assert(t, err == nil, err) + } + clientConfig = cc + } + + if clientConfig != nil { + configAccess := clientConfig.ConfigAccess() + assert.Assert(t, configAccess.GetExplicitFile() == tc.explicitPath) + + if tc.kubeContext != "" { + config, err := clientConfig.RawConfig() + assert.NilError(t, err) + assert.Assert(t, config.CurrentContext == tc.kubeContext) + assert.Assert(t, config.Contexts[tc.kubeContext].Cluster == tc.kubeCluster) + } + + if tc.kubeAsUser != "" { + config, err := clientConfig.ClientConfig() + assert.NilError(t, err) + assert.Assert(t, config.Impersonate.UserName == tc.kubeAsUser) + } + + if tc.kubeAsUID != "" { + config, err := clientConfig.ClientConfig() + assert.NilError(t, err) + assert.Assert(t, config.Impersonate.UID == tc.kubeAsUID) + } + + if len(tc.kubeAsGroup) > 0 { + config, err := clientConfig.ClientConfig() + assert.NilError(t, err) + assert.Assert(t, len(config.Impersonate.Groups) == len(tc.kubeAsGroup)) + for i := range tc.kubeAsGroup { + assert.Assert(t, config.Impersonate.Groups[i] == tc.kubeAsGroup[i]) + } + } + } + } +} + +var basicKubeconfig = `apiVersion: v1 +kind: Config +preferences: {} +users: +- name: a + user: + client-certificate-data: "" + client-key-data: "" +clusters: +- name: a + cluster: + insecure-skip-tls-verify: true + server: https://127.0.0.1:8080 +contexts: +- name: a + context: + cluster: a + user: a +current-context: a +` + +type typeTestCase struct { + kubeCfgPath string + kubeContext string + kubeAsUser string + kubeAsUID string + kubeAsGroup []string + kubeCluster string + explicitPath string + expectedError string +} diff --git a/pkg/output/logging/context.go b/pkg/output/logging/context.go index c726db6700..b73fee878b 100644 --- a/pkg/output/logging/context.go +++ b/pkg/output/logging/context.go @@ -18,7 +18,6 @@ package logging import ( "context" - "fmt" "os" "time" @@ -40,14 +39,14 @@ var ErrCallEnsureLoggerFirst = errors.New("call EnsureLogger() before LoggerFrom // context will have a logger attached to it. Given fields will be added to the // logger, either new or existing. func EnsureLogger(ctx context.Context, fields ...Fields) context.Context { - z, err := loggerFrom(ctx) + z, err := zapLoggerFrom(ctx) if errors.Is(err, ErrCallEnsureLoggerFirst) { ctx = EnsureLogFile(ctx) z = setupLogging(ctx) } - l := &zapLogger{SugaredLogger: z} + l := &ZapLogger{SugaredLogger: z} for _, f := range fields { - l = l.WithFields(f).(*zapLogger) + l = l.WithFields(f).(*ZapLogger) } return WithLogger(ctx, l) } @@ -55,24 +54,28 @@ func EnsureLogger(ctx context.Context, fields ...Fields) context.Context { // LoggerFrom returns the logger from the context. If EnsureLogger() was not // called before, it will panic. func LoggerFrom(ctx context.Context) Logger { - z, err := loggerFrom(ctx) + if l, ok := ctx.Value(loggerKey).(Logger); ok { + return l + } + z, err := zapLoggerFrom(ctx) if err != nil { fatal(err) } - return &zapLogger{z} + return &ZapLogger{z} } // WithLogger attaches the given logger to the context. func WithLogger(ctx context.Context, l Logger) context.Context { - if z, ok := l.(*zapLogger); ok { + if z, ok := l.(*ZapLogger); ok { return logging.WithLogger(ctx, z.SugaredLogger) } - fatal("unsupported logger type: " + fmt.Sprintf("%#v", l)) - return nil + return context.WithValue(ctx, loggerKey, l) } -func loggerFrom(ctx context.Context) (*zap.SugaredLogger, error) { +var loggerKey = struct{}{} + +func zapLoggerFrom(ctx context.Context) (*zap.SugaredLogger, error) { l := logging.FromContext(ctx) if l.Desugar().Name() == "fallback" { return nil, ErrCallEnsureLoggerFirst @@ -126,7 +129,7 @@ func createDefaultLogger(ctx context.Context) *zap.Logger { ec.EncodeTime = ElapsedMillisTimeEncoder(time.Now()) ec.ConsoleSeparator = " " - lvl := activeLogLevel(zapcore.WarnLevel) + lvl := activeLogLevel(LogLevelFromContext(ctx)) logger := zap.New(zapcore.NewCore( zapcore.NewConsoleEncoder(ec), zapcore.AddSync(errout), @@ -161,6 +164,8 @@ func activeLogLevel(defaultLevel zapcore.Level) zapcore.Level { return defaultLevel } +// ElapsedMillisTimeEncoder is a time encoder using elapsed time since the +// logger setup. func ElapsedMillisTimeEncoder(setupTime time.Time) zapcore.TimeEncoder { return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendInt64(t.Sub(setupTime).Milliseconds()) diff --git a/pkg/output/logging/log_level.go b/pkg/output/logging/log_level.go new file mode 100644 index 0000000000..72db8ee75f --- /dev/null +++ b/pkg/output/logging/log_level.go @@ -0,0 +1,43 @@ +/* + Copyright 2024 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 logging + +import ( + "context" + + "go.uber.org/zap/zapcore" + pkgcontext "knative.dev/client/pkg/context" +) + +var logLevelKey = struct{}{} + +// LogLevelFromContext returns the default log level for the logger. +func LogLevelFromContext(ctx context.Context) zapcore.Level { + if val, ok := ctx.Value(logLevelKey).(zapcore.Level); ok { + return val + } else { + if pkgcontext.TestingTFromContext(ctx) != nil { + return zapcore.DebugLevel + } + return zapcore.WarnLevel + } +} + +// WithLogLevel will set given log level as the default one in given context. +func WithLogLevel(ctx context.Context, level zapcore.Level) context.Context { + return context.WithValue(ctx, logLevelKey, level) +} diff --git a/pkg/output/logging/log_level_test.go b/pkg/output/logging/log_level_test.go new file mode 100644 index 0000000000..7c7595e91e --- /dev/null +++ b/pkg/output/logging/log_level_test.go @@ -0,0 +1,36 @@ +/* + Copyright 2024 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 logging_test + +import ( + "context" + "testing" + + "go.uber.org/zap/zapcore" + "gotest.tools/v3/assert" + pkgcontext "knative.dev/client/pkg/context" + "knative.dev/client/pkg/output/logging" +) + +func TestLogLevel(t *testing.T) { + ctx := context.TODO() + assert.Equal(t, zapcore.WarnLevel, logging.LogLevelFromContext(ctx)) + ctx = pkgcontext.WithTestingT(ctx, t) + assert.Equal(t, zapcore.DebugLevel, logging.LogLevelFromContext(ctx)) + ctx = logging.WithLogLevel(ctx, zapcore.InfoLevel) + assert.Equal(t, zapcore.InfoLevel, logging.LogLevelFromContext(ctx)) +} diff --git a/pkg/output/logging/zap.go b/pkg/output/logging/zap.go index 5aece2c253..741194a268 100644 --- a/pkg/output/logging/zap.go +++ b/pkg/output/logging/zap.go @@ -18,23 +18,24 @@ package logging import "go.uber.org/zap" -type zapLogger struct { +// ZapLogger is a Google' zap logger based logger. +type ZapLogger struct { *zap.SugaredLogger } -func (z zapLogger) WithName(name string) Logger { - return &zapLogger{ +func (z ZapLogger) WithName(name string) Logger { + return &ZapLogger{ SugaredLogger: z.SugaredLogger.Named(name), } } -func (z zapLogger) WithFields(fields Fields) Logger { +func (z ZapLogger) WithFields(fields Fields) Logger { a := make([]interface{}, 0, len(fields)*2) for k, v := range fields { a = append(a, k, v) } - return &zapLogger{ + return &ZapLogger{ SugaredLogger: z.SugaredLogger.With(a...), } } diff --git a/pkg/root/root.go b/pkg/root/root.go index 83f7179e21..cebdef0954 100644 --- a/pkg/root/root.go +++ b/pkg/root/root.go @@ -84,13 +84,9 @@ Find more information about Knative at: https://knative.dev`, rootName), // Bootstrap flags (rebinding to avoid errors when parsing the full commands) config.AddBootstrapFlags(rootCmd.PersistentFlags()) - // Global flags - rootCmd.PersistentFlags().StringVar(&p.KubeCfgPath, "kubeconfig", "", "kubectl configuration file (default: ~/.kube/config)") - rootCmd.PersistentFlags().StringVar(&p.KubeContext, "context", "", "name of the kubeconfig context to use") - rootCmd.PersistentFlags().StringVar(&p.KubeCluster, "cluster", "", "name of the kubeconfig cluster to use") - rootCmd.PersistentFlags().StringVar(&p.KubeAsUser, "as", "", "username to impersonate for the operation") - rootCmd.PersistentFlags().StringVar(&p.KubeAsUID, "as-uid", "", "uid to impersonate for the operation") - rootCmd.PersistentFlags().StringArrayVar(&p.KubeAsGroup, "as-group", []string{}, "group to impersonate for the operation, this flag can be repeated to specify multiple groups") + // Global Kube' flags + p.Params.SetFlags(rootCmd.PersistentFlags()) + flags.AddBothBoolFlags(rootCmd.PersistentFlags(), &p.LogHTTP, "log-http", "", false, "log http traffic") // Grouped commands diff --git a/pkg/util/errors/cause.go b/pkg/util/errors/cause.go new file mode 100644 index 0000000000..c589b7ba95 --- /dev/null +++ b/pkg/util/errors/cause.go @@ -0,0 +1,37 @@ +/* + Copyright 2024 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 errors + +import ( + "emperror.dev/errors" +) + +// CauseOf will return the error that caused the returned error. This can be +// used when multierr package is used or fmt.Errorf("%w: %w", ErrFront, cause) +// from standard library is used, and we know the front error. +func CauseOf(err, rootErr error) error { + if errors.Is(err, rootErr) { + for _, cause := range errors.GetErrors(err) { + if errors.Is(cause, rootErr) { + continue + } + return cause + } + return err + } + return err +} diff --git a/pkg/util/errors/cause_test.go b/pkg/util/errors/cause_test.go new file mode 100644 index 0000000000..8f94822788 --- /dev/null +++ b/pkg/util/errors/cause_test.go @@ -0,0 +1,36 @@ +/* + Copyright 2024 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 errors_test + +import ( + "fmt" + "syscall" + "testing" + + "knative.dev/client/pkg/util/errors" +) + +func TestCauseOf(t *testing.T) { + errExample := errors.New("example error") + want := syscall.EINVAL + err := fmt.Errorf("%w: %w", errExample, want) + + got := errors.CauseOf(err, errExample) + if !errors.Is(got, want) { + t.Errorf("got error %v, want %v", got, want) + } +} diff --git a/pkg/util/errors/wrap.go b/pkg/util/errors/wrap.go new file mode 100644 index 0000000000..3bda04767e --- /dev/null +++ b/pkg/util/errors/wrap.go @@ -0,0 +1,32 @@ +/* + Copyright 2024 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 errors + +import "emperror.dev/errors" + +// Is reports whether any error in err's chain matches target. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err, target error) bool { + return errors.Is(err, target) +} + +// New returns a new error annotated with stack trace at the point New is called. +func New(msg string) error { + return errors.New(msg) +}