diff --git a/go.mod b/go.mod index 12f246cd8e..f32836b23d 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,12 @@ module knative.dev/client go 1.22.0 require ( - github.com/google/go-cmp v0.6.0 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect - github.com/hashicorp/hcl v1.0.1-vault-5 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cobra v1.7.0 - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.16.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/term v0.23.0 // indirect gotest.tools/v3 v3.3.0 k8s.io/api v0.30.3 - k8s.io/apiextensions-apiserver v0.30.3 // indirect k8s.io/apimachinery v0.30.3 - k8s.io/cli-runtime v0.29.2 // indirect - k8s.io/client-go v0.30.3 // indirect k8s.io/code-generator v0.30.3 knative.dev/client/pkg v0.0.0 - knative.dev/eventing v0.42.1-0.20240820132224-5fc4c0fcd118 // indirect knative.dev/hack v0.0.0-20240814130635-06f7aff93954 knative.dev/networking v0.0.0-20240815142417-37fdbdd0854b knative.dev/pkg v0.0.0-20240815051656-89743d9bbf7c @@ -31,8 +18,6 @@ require ( replace knative.dev/client/pkg => ./pkg -require k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect - require ( contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect @@ -62,12 +47,15 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-containerregistry v0.13.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -76,6 +64,7 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -96,8 +85,11 @@ require ( github.com/rickb777/plural v1.4.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.16.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -105,10 +97,12 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect @@ -122,12 +116,17 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.30.3 // indirect k8s.io/apiserver v0.30.3 // indirect + k8s.io/cli-runtime v0.29.2 // indirect + k8s.io/client-go v0.30.3 // indirect k8s.io/gengo v0.0.0-20240404160639-a0386bf69313 // indirect k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240808142205-8e686545bdb8 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect knative.dev/client-pkg v0.0.0-20240808015000-22f598931483 // indirect + knative.dev/eventing v0.42.1-0.20240820132224-5fc4c0fcd118 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect diff --git a/go.work.sum b/go.work.sum index a46e941bbd..ceffe76b7c 100644 --- a/go.work.sum +++ b/go.work.sum @@ -21,8 +21,6 @@ cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5 contrib.go.opencensus.io/exporter/zipkin v0.1.2 h1:YqE293IZrKtqPnpwDPH/lOqTWD/s3Iwabycam74JV3g= contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj3gsxcuytSqtH0dxSWW1RE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= -emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= -emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= @@ -64,8 +62,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= github.com/aws/aws-sdk-go-v2/config v1.17.8 h1:b9LGqNnOdg9vR4Q43tBTVWk4J6F+W774MSchvKJsqnE= @@ -96,8 +92,6 @@ github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20221004211355-a250ad2ca1e3 h1:Ted/bR1N6ltMrASdwRhX1BrGYSFg3aeGMlK8GlgkGh4= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20221004211355-a250ad2ca1e3/go.mod h1:m06KtrZgOloUaePAQMv+Ha8kRmTnKdozTHZrweepIrw= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4= @@ -106,14 +100,6 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cert-manager/cert-manager v1.13.3 h1:3R4G0RI7K0OkTZhWlVOC5SGZMYa2NwqmQJoyKydrz/M= github.com/cert-manager/cert-manager v1.13.3/go.mod h1:BM2+Pt/NmSv1Zr25/MHv6BgIEF9IUxA1xAjp80qkxgc= -github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= -github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= -github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chrismellard/docker-credential-acr-env v0.0.0-20221002210726-e883f69e0206 h1:lG6Usi/kX/JBZzGz1H+nV+KwM97vThQeKunCbS6PutU= github.com/chrismellard/docker-credential-acr-env v0.0.0-20221002210726-e883f69e0206/go.mod h1:1UmFRnmMnVsHwD+ZntmLkoVBB1ZLa6V+XXEbF6hZCxU= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= @@ -129,8 +115,6 @@ github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 v2.0.0-20240508060731-1ed947 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0= github.com/containerd/stargz-snapshotter/estargz v0.12.1/go.mod h1:12VUuCq3qPq4y8yUW+l5w3+oXV3cx2Po3KSe/SmPGqw= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -168,8 +152,6 @@ github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjl github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= -github.com/erikgeiser/promptkit v0.9.0 h1:3qL1mS/ntCrXdb8sTP/ka82CJ9kEQaGuYXNrYJkWYBc= -github.com/erikgeiser/promptkit v0.9.0/go.mod h1:pU9dtogSe3Jlc2AY77EP7R4WFP/vgD4v+iImC83KsCo= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -267,16 +249,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= -github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0 h1:rBhB9Rls+yb8kA4x5a/cWxOufWfXt24E+kq4YlbGj3g= @@ -285,14 +261,6 @@ github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8 github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= @@ -313,13 +281,13 @@ github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= github.com/sagikazarmark/crypt v0.10.0 h1:96E1qrToLBU6fGzo+PRRz7KGOc9FkYFiPnR3/zf8Smg= github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 6abd729916..0d387b28bf 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -166,7 +166,7 @@ func setupConfig(t *testing.T, configContent string) (string, func()) { // Save old args backupArgs := os.Args - // WriteCache out a temporary configContent file + // Write out a temporary configContent file var cfgFile string if configContent != "" { cfgFile = filepath.Join(tmpDir, "config.yaml") diff --git a/pkg/config/dir/local.go b/pkg/config/dir/local.go new file mode 100644 index 0000000000..ee4f9cf72e --- /dev/null +++ b/pkg/config/dir/local.go @@ -0,0 +1,88 @@ +/* + 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 dir + +import ( + "context" + "log" + "os" + "path" + + "emperror.dev/errors" +) + +const ( + // ConfigDirEnvName is the name of the environment variable that can be used + // to override the config directory. + ConfigDirEnvName = "KN_CONFIG_DIR" + + // CacheDirEnvName is the name of the environment variable that can be used + // to override the cache directory. + CacheDirEnvName = "KN_CACHE_DIR" +) + +var ( + configDirKey = struct{}{} //nolint:gochecknoglobals + cacheDirKey = struct{}{} //nolint:gochecknoglobals +) + +// Config returns the path to the config directory. It will be created if it +// does not exist. +func Config(ctx context.Context) string { + return userPath(ctx, configDirKey, ConfigDirEnvName, localConfig) +} + +// Cache returns the path to the cache directory. It will be created if it +// does not exist. +func Cache(ctx context.Context) string { + return userPath(ctx, cacheDirKey, CacheDirEnvName, localCache) +} + +func localConfig() string { + cd, err := os.UserConfigDir() + if err != nil { + log.Fatal(errors.WithStack(err)) + } + return path.Join(cd, "kn") +} + +func localCache() string { + cd, err := os.UserCacheDir() + if err != nil { + log.Fatal(errors.WithStack(err)) + } + return path.Join(cd, "kn") +} + +func userPath(ctx context.Context, key interface{}, envKey string, fn func() string) string { + if p, ok := ctx.Value(key).(string); ok { + return ensurePathExists(p) + } + p := os.Getenv(envKey) + if p == "" { + p = fn() + } + return ensurePathExists(p) +} + +func ensurePathExists(p string) string { + fileMode := os.FileMode(0o750) + if err := os.MkdirAll(p, fileMode); err != nil { + log.Fatal(errors.WithStack(err)) + } + return p +} diff --git a/pkg/config/dir/local_test.go b/pkg/config/dir/local_test.go new file mode 100644 index 0000000000..fbfc9a0742 --- /dev/null +++ b/pkg/config/dir/local_test.go @@ -0,0 +1,69 @@ +/* + 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 dir_test + +import ( + "strings" + "testing" + + "knative.dev/client/pkg/config/dir" + "knative.dev/client/pkg/context" +) + +func TestConfig(t *testing.T) { + ctx := context.TestContext(t) + want := t.TempDir() + ctx = dir.WithConfigDir(ctx, want) + + got := dir.Config(ctx) + if got != want { + t.Errorf("want %s,\n got %s", want, got) + } +} + +func TestConfigDefaults(t *testing.T) { + ctx := context.TestContext(t) + got := dir.Config(ctx) + if got == "" { + t.Errorf("want non-empty config dir") + } + if !strings.Contains(got, "kn") { + t.Errorf("want config dir to contain 'kn'") + } +} + +func TestCache(t *testing.T) { + ctx := context.TestContext(t) + want := t.TempDir() + ctx = dir.WithCacheDir(ctx, want) + + got := dir.Cache(ctx) + if got != want { + t.Errorf("want %s,\n got %s", want, got) + } +} + +func TestCacheDefaults(t *testing.T) { + ctx := context.TestContext(t) + got := dir.Cache(ctx) + if got == "" { + t.Errorf("want non-empty cache dir") + } + if !strings.Contains(got, "kn") { + t.Errorf("want cache dir to contain 'kn'") + } +} diff --git a/pkg/config/dir/testing.go b/pkg/config/dir/testing.go new file mode 100644 index 0000000000..b69ea727ab --- /dev/null +++ b/pkg/config/dir/testing.go @@ -0,0 +1,40 @@ +/* + 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 dir + +import ( + "context" + "testing" +) + +// WithConfigDir returns a new context with the given config directory +// configured. It should be used for testing only. +func WithConfigDir(ctx context.Context, p string) context.Context { + if !testing.Testing() { + panic("WithConfigDir should be used only in tests") + } + return context.WithValue(ctx, configDirKey, p) +} + +// WithCacheDir returns a new context with the given cache directory +// configured. It should be used for testing only. +func WithCacheDir(ctx context.Context, p string) context.Context { + if !testing.Testing() { + panic("WithCacheDir should be used only in tests") + } + return context.WithValue(ctx, cacheDirKey, p) +} diff --git a/pkg/context/testing.go b/pkg/context/testing.go new file mode 100644 index 0000000000..a17da3da54 --- /dev/null +++ b/pkg/context/testing.go @@ -0,0 +1,57 @@ +/* + 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 context + +import ( + sysctx "context" + + "go.uber.org/zap/zaptest" +) + +type testingTKey struct{} + +// TestingT is a subset of the API provided by all *testing.T and *testing.B +// objects. It is also compatible with zaptest.TestingT. This allows us to +// write tests that use zaptest and our own context package. +type TestingT interface { + Log(args ...any) + Cleanup(func()) + TempDir() string + Setenv(key, value string) + + zaptest.TestingT +} + +// TestContext returns a new context with the given testing object configured. +func TestContext(t TestingT) sysctx.Context { + return WithTestingT(sysctx.TODO(), t) +} + +// WithTestingT returns a context with the given testing object configured. +func WithTestingT(ctx sysctx.Context, t TestingT) sysctx.Context { + return sysctx.WithValue(ctx, testingTKey{}, t) +} + +// TestingTFromContext returns the testing object configured in the given +// context. If no testing object is configured, it returns nil. +func TestingTFromContext(ctx sysctx.Context) TestingT { + t, ok := ctx.Value(testingTKey{}).(TestingT) + if !ok { + return nil + } + return t +} diff --git a/pkg/context/testing_test.go b/pkg/context/testing_test.go new file mode 100644 index 0000000000..ca77f82528 --- /dev/null +++ b/pkg/context/testing_test.go @@ -0,0 +1,31 @@ +/* + 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 context_test + +import ( + "testing" + + "knative.dev/client/pkg/context" +) + +func TestTestingTFromContext(t *testing.T) { + ctx := context.TestContext(t) + got := context.TestingTFromContext(ctx) + if got != t { + t.Errorf("want %v,\n got %v", t, got) + } +} diff --git a/pkg/context/wrapper.go b/pkg/context/wrapper.go new file mode 100644 index 0000000000..db2ae90b14 --- /dev/null +++ b/pkg/context/wrapper.go @@ -0,0 +1,59 @@ +/* + 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 context + +import ( + sysctx "context" + "time" +) + +type Context = sysctx.Context + +// TODO is a wrapper for sysctx.TODO for ease of use (single import). +func TODO() sysctx.Context { + return sysctx.TODO() +} + +// Background is a wrapper for sysctx.Background for ease of use (single import). +func Background() sysctx.Context { + return sysctx.Background() +} + +// WithCancel is a wrapper for sysctx.WithCancel for ease of use (single import). +func WithCancel(parent sysctx.Context) (sysctx.Context, sysctx.CancelFunc) { + return sysctx.WithCancel(parent) +} + +// WithDeadline is a wrapper for sysctx.WithDeadline for ease of use (single import). +func WithDeadline(parent sysctx.Context, deadline time.Time) (sysctx.Context, sysctx.CancelFunc) { + return sysctx.WithDeadline(parent, deadline) +} + +// WithTimeout is a wrapper for sysctx.WithTimeout for ease of use (single import). +func WithTimeout(parent sysctx.Context, timeout time.Duration) (sysctx.Context, sysctx.CancelFunc) { + return sysctx.WithTimeout(parent, timeout) +} + +// WithValue is a wrapper for sysctx.WithValue for ease of use (single import). +func WithValue(parent sysctx.Context, key, val interface{}) sysctx.Context { + return sysctx.WithValue(parent, key, val) +} + +// Value is a wrapper for sysctx.Value for ease of use (single import). +func Value(ctx sysctx.Context, key interface{}) interface{} { + return ctx.Value(key) +} diff --git a/pkg/go.mod b/pkg/go.mod index 15a3b27017..66bb5644b9 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -3,6 +3,11 @@ module knative.dev/client/pkg go 1.22.0 require ( + emperror.dev/errors v0.8.1 + github.com/charmbracelet/bubbles v0.17.1 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/erikgeiser/promptkit v0.9.0 github.com/google/go-cmp v0.6.0 github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.1-vault-5 // indirect @@ -11,6 +16,8 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 + go.uber.org/multierr v1.11.0 + go.uber.org/zap v1.27.0 golang.org/x/mod v0.20.0 golang.org/x/term v0.23.0 gotest.tools/v3 v3.3.0 @@ -33,12 +40,16 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.4.2 // 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/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/cloudevents/sdk-go/sql/v2 v2.15.2 // indirect github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect @@ -68,13 +79,21 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect @@ -89,6 +108,7 @@ require ( github.com/prometheus/statsd_exporter v0.22.8 // indirect github.com/rickb777/date v1.20.0 // indirect github.com/rickb777/plural v1.4.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/spf13/cast v1.5.1 // indirect @@ -98,8 +118,6 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect go.opencensus.io v0.24.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/pkg/go.sum b/pkg/go.sum index 75aa751a47..d3fc36ded6 100644 --- a/pkg/go.sum +++ b/pkg/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= @@ -53,6 +55,10 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8V github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -66,6 +72,14 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= +github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -77,6 +91,8 @@ github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= @@ -95,6 +111,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/promptkit v0.9.0 h1:3qL1mS/ntCrXdb8sTP/ka82CJ9kEQaGuYXNrYJkWYBc= +github.com/erikgeiser/promptkit v0.9.0/go.mod h1:pU9dtogSe3Jlc2AY77EP7R4WFP/vgD4v+iImC83KsCo= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= @@ -268,10 +286,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -288,6 +315,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -350,6 +385,10 @@ github.com/rickb777/date v1.20.0 h1:oRGcq4b+ba12N/HnsVZuWSK/QJb/o/hnjOJEyRMGUT0= github.com/rickb777/date v1.20.0/go.mod h1:8AR0TBrjDGUjwKToBI8L+RafzNg7gqlT0ox0cERCwEo= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -416,11 +455,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= @@ -589,6 +630,8 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/output/context.go b/pkg/output/context.go new file mode 100644 index 0000000000..0341fc6409 --- /dev/null +++ b/pkg/output/context.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 output + +import "context" + +type printerKey struct{} + +func PrinterFrom(ctx context.Context) Printer { + p, ok := ctx.Value(printerKey{}).(Printer) + if !ok { + return defaultPrinter() + } + return p +} + +func WithContext(ctx context.Context, p Printer) context.Context { + return context.WithValue(ctx, printerKey{}, p) +} + +func defaultPrinter() Printer { + return OsPrinter +} diff --git a/pkg/output/environment/color.go b/pkg/output/environment/color.go new file mode 100644 index 0000000000..61ed1d7e06 --- /dev/null +++ b/pkg/output/environment/color.go @@ -0,0 +1,61 @@ +/* + 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 environment + +import ( + "os" + "strings" +) + +// SupportsColor returns true if the environment supports color output. +// +// See NonColorRequested and ColorIsForced functions for more information. +func SupportsColor() bool { + color := true + if NonColorRequested() { + color = false + } + if ColorIsForced() { + color = true + } + return color +} + +// NonColorRequested returns true if the NO_COLOR environment variable is set to a +// truthy value. +// +// See https://no-color.org/ for more information. +func NonColorRequested() bool { + return settingToBool(os.Getenv("NO_COLOR")) +} + +// ColorIsForced returns true if the FORCE_COLOR environment variable is set to +// a truthy value. +// +// See https://force-color.org/ for more information. +func ColorIsForced() bool { + return settingToBool(os.Getenv("FORCE_COLOR")) +} + +func settingToBool(s string) bool { + s = strings.ToLower(s) + return len(s) != 0 && + s != "0" && + s != "false" && + s != "no" && + s != "off" +} diff --git a/pkg/output/logging/context.go b/pkg/output/logging/context.go new file mode 100644 index 0000000000..c726db6700 --- /dev/null +++ b/pkg/output/logging/context.go @@ -0,0 +1,168 @@ +/* + 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" + "fmt" + "os" + "time" + + "emperror.dev/errors" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + pkgcontext "knative.dev/client/pkg/context" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/environment" + "knative.dev/client/pkg/output/term" + "knative.dev/pkg/logging" +) + +// ErrCallEnsureLoggerFirst is returned when LoggerFrom() is called before EnsureLogger(). +var ErrCallEnsureLoggerFirst = errors.New("call EnsureLogger() before LoggerFrom() method") + +// EnsureLogger ensures that a logger is attached to the context. The returned +// 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) + if errors.Is(err, ErrCallEnsureLoggerFirst) { + ctx = EnsureLogFile(ctx) + z = setupLogging(ctx) + } + l := &zapLogger{SugaredLogger: z} + for _, f := range fields { + l = l.WithFields(f).(*zapLogger) + } + return WithLogger(ctx, l) +} + +// 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 err != nil { + fatal(err) + } + + 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 { + return logging.WithLogger(ctx, z.SugaredLogger) + } + fatal("unsupported logger type: " + fmt.Sprintf("%#v", l)) + return nil +} + +func loggerFrom(ctx context.Context) (*zap.SugaredLogger, error) { + l := logging.FromContext(ctx) + if l.Desugar().Name() == "fallback" { + return nil, ErrCallEnsureLoggerFirst + } + return l, nil +} + +func setupLogging(ctx context.Context) *zap.SugaredLogger { + var logger *zap.Logger + if t := pkgcontext.TestingTFromContext(ctx); t != nil { + logger = createTestingLogger(t) + } else { + logger = teeLoggers( + createDefaultLogger(ctx), + createFileLogger(ctx), + ) + } + return logger.Sugar() +} + +func teeLoggers(logger1 *zap.Logger, logger2 *zap.Logger) *zap.Logger { + return zap.New(zapcore.NewTee( + logger1.Core(), + logger2.Core(), + )) +} + +func createFileLogger(ctx context.Context) *zap.Logger { + ec := zap.NewProductionEncoderConfig() + ec.EncodeTime = zapcore.ISO8601TimeEncoder + + logFile := LogFileFrom(ctx) + if logFile == nil { + fatal(errors.New("no log file in context")) + } + return zap.New(zapcore.NewCore( + zapcore.NewJSONEncoder(ec), + zapcore.AddSync(logFile), + zapcore.DebugLevel, + )) +} + +func createDefaultLogger(ctx context.Context) *zap.Logger { + prtr := output.PrinterFrom(ctx) + errout := prtr.ErrOrStderr() + ec := zap.NewDevelopmentEncoderConfig() + ec.EncodeLevel = zapcore.CapitalLevelEncoder + if environment.SupportsColor() { + ec.EncodeLevel = zapcore.CapitalColorLevelEncoder + } + ec.EncodeTime = ElapsedMillisTimeEncoder(time.Now()) + ec.ConsoleSeparator = " " + + lvl := activeLogLevel(zapcore.WarnLevel) + logger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(ec), + zapcore.AddSync(errout), + lvl, + )) + if !term.IsFancy(errout) { + ec = zap.NewProductionEncoderConfig() + logger = zap.New(zapcore.NewCore( + zapcore.NewJSONEncoder(ec), + zapcore.AddSync(errout), + lvl, + )) + } + return logger +} + +func createTestingLogger(t pkgcontext.TestingT) *zap.Logger { + lvl := activeLogLevel(zapcore.DebugLevel) + return zaptest.NewLogger(t, zaptest.WrapOptions( + zap.AddCaller(), + ), zaptest.Level(lvl)) +} + +func activeLogLevel(defaultLevel zapcore.Level) zapcore.Level { + if lvl := os.Getenv("LOG_LEVEL"); lvl != "" { + l, err := zapcore.ParseLevel(lvl) + if err != nil { + fatal(errors.WithStack(err)) + } + return l + } + return defaultLevel +} + +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/context_test.go b/pkg/output/logging/context_test.go new file mode 100644 index 0000000000..f928c8853b --- /dev/null +++ b/pkg/output/logging/context_test.go @@ -0,0 +1,71 @@ +/* + 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 ( + "errors" + "testing" + + "knative.dev/client/pkg/context" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/logging" +) + +func TestEnsureLogger(t *testing.T) { + ctx := context.TestContext(t) + ctx = logging.EnsureLogger(ctx) + got := logging.LoggerFrom(ctx) + if got == nil { + t.Errorf("want logger, got nil") + } + got.Debug("test") +} + +func TestEnsureLoggerWithoutTestingT(t *testing.T) { + t.Setenv("LOG_LEVEL", "debug") + t.Setenv("FORCE_COLOR", "true") + ctx := context.TODO() + printer := output.NewTestPrinter() + ctx = output.WithContext(ctx, printer) + ctx = logging.EnsureLogger(ctx) + + l := logging.LoggerFrom(ctx) + if l == nil { + t.Errorf("want logger, got nil") + } + l.Debug("test") + out := printer.Outputs() + got := out.Err.String() + want := "0 \x1b[35mDEBUG\x1b[0m test\n" + if got != want { + t.Errorf("\nwant %q,\n got %q", want, got) + } +} + +func TestLoggerFrom(t *testing.T) { + ctx := context.TestContext(t) + args := logging.WithFatalCaptured(func() { + logging.LoggerFrom(ctx) + }) + if len(args) != 1 { + t.Errorf("want 1 arg, got %d", len(args)) + } + err := args[0].(error) + if !errors.Is(err, logging.ErrCallEnsureLoggerFirst) { + t.Errorf("want %v, got %v", logging.ErrCallEnsureLoggerFirst, err) + } +} diff --git a/pkg/output/logging/logfile.go b/pkg/output/logging/logfile.go new file mode 100644 index 0000000000..a0d9df03a6 --- /dev/null +++ b/pkg/output/logging/logfile.go @@ -0,0 +1,81 @@ +/* + 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" + "log" + "os" + "path" + + configdir "knative.dev/client/pkg/config/dir" +) + +type logFileKey struct{} +type logFileCloserKey struct{} + +type Closer func() error + +// EnsureLogFile ensures that a log file is present in the context. If not, it +// creates one and adds it to the context. The file's Closer is also added to +// the context if not already present. +func EnsureLogFile(ctx context.Context) context.Context { + var file *os.File + if f := LogFileFrom(ctx); f == nil { + file = createLogFile(ctx) + ctx = WithLogFile(ctx, file) + } + var closer Closer + if c := LogFileCloserFrom(ctx); c == nil { + closer = file.Close + ctx = WithLogFileCloser(ctx, closer) + } + return ctx +} + +func LogFileFrom(ctx context.Context) *os.File { + if f, ok := ctx.Value(logFileKey{}).(*os.File); ok { + return f + } + return nil +} + +func WithLogFile(ctx context.Context, f *os.File) context.Context { + return context.WithValue(ctx, logFileKey{}, f) +} + +func LogFileCloserFrom(ctx context.Context) Closer { + if closer, ok := ctx.Value(logFileCloserKey{}).(Closer); ok { + return closer + } + return nil +} + +func WithLogFileCloser(ctx context.Context, closer Closer) context.Context { + return context.WithValue(ctx, logFileCloserKey{}, closer) +} + +func createLogFile(ctx context.Context) *os.File { + cachePath := configdir.Cache(ctx) + logPath := path.Join(cachePath, "last-exec.log.jsonl") + if logFile, err := os.Create(logPath); err != nil { + log.Fatal(err) + return nil + } else { + return logFile + } +} diff --git a/pkg/output/logging/logger.go b/pkg/output/logging/logger.go new file mode 100644 index 0000000000..8f49cb4898 --- /dev/null +++ b/pkg/output/logging/logger.go @@ -0,0 +1,52 @@ +/* + 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 + +// Logger is the interface for logging, similar to the Uber's zap.Logger. +type Logger interface { + // WithName returns a new Logger with the given name. + WithName(name string) Logger + + // WithFields returns a new Logger with the given fields. + WithFields(fields Fields) Logger + + // Debug logs a message at the debug level. + Debug(args ...any) + // Info logs a message at the info level. + Info(args ...any) + // Warn logs a message at the warn level. + Warn(args ...any) + // Error logs a message at the error level. + Error(args ...any) + // Fatal logs a message at the fatal level and then exit the program. + Fatal(args ...any) + + // Debugf logs a message at the debug level using given format. + Debugf(format string, args ...any) + // Infof logs a message at the info level using given format. + Infof(format string, args ...any) + // Warnf logs a message at the warn level using given format. + Warnf(format string, args ...any) + // Errorf logs a message at the error level using given format. + Errorf(format string, args ...any) + // Fatalf logs a message at the fatal level using given format and then + // exit the program. + Fatalf(format string, args ...any) +} + +// Fields is a map. It is used to add structured context to the logging output. +type Fields map[string]any diff --git a/pkg/output/logging/testing.go b/pkg/output/logging/testing.go new file mode 100644 index 0000000000..0b62a3c765 --- /dev/null +++ b/pkg/output/logging/testing.go @@ -0,0 +1,42 @@ +/* + 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 ( + "log" + "testing" +) + +var fatal = log.Fatal + +// WithFatalCaptured runs the given function and captures the arguments passed +// to Fatal function. It is useful in tests to verify that the expected error +// was reported. +func WithFatalCaptured(fn func()) []any { + if !testing.Testing() { + panic("WithFatalCaptured should be used only in tests") + } + var captured []any + fatal = func(args ...any) { + captured = args + } + defer func() { + fatal = log.Fatal + }() + fn() + return captured +} diff --git a/pkg/output/logging/zap.go b/pkg/output/logging/zap.go new file mode 100644 index 0000000000..5aece2c253 --- /dev/null +++ b/pkg/output/logging/zap.go @@ -0,0 +1,40 @@ +/* + 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 "go.uber.org/zap" + +type zapLogger struct { + *zap.SugaredLogger +} + +func (z zapLogger) WithName(name string) Logger { + return &zapLogger{ + SugaredLogger: z.SugaredLogger.Named(name), + } +} + +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{ + SugaredLogger: z.SugaredLogger.With(a...), + } +} diff --git a/pkg/output/os.go b/pkg/output/os.go new file mode 100644 index 0000000000..64cab36db9 --- /dev/null +++ b/pkg/output/os.go @@ -0,0 +1,40 @@ +/* + 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 output + +import ( + "io" + "os" +) + +type OsInOut struct{} + +var OsPrinter = stdPrinter{OsInOut{}} //nolint:gochecknoglobals + +func (o OsInOut) InOrStdin() io.Reader { + return os.Stdin +} + +func (o OsInOut) OutOrStdout() io.Writer { + return os.Stdout +} + +func (o OsInOut) ErrOrStderr() io.Writer { + return os.Stderr +} + +var _ InputOutput = OsInOut{} diff --git a/pkg/output/printer.go b/pkg/output/printer.go new file mode 100644 index 0000000000..b22ead1c4f --- /dev/null +++ b/pkg/output/printer.go @@ -0,0 +1,61 @@ +/* + 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 output + +import "fmt" + +type Printer interface { + Print(i ...any) + Println(i ...any) + Printf(format string, i ...any) + + PrintErr(i ...any) + PrintErrln(i ...any) + PrintErrf(format string, i ...any) + + InputOutput +} + +type stdPrinter struct { + InputOutput +} + +func (p stdPrinter) Print(i ...any) { + _, _ = fmt.Fprint(p.OutOrStdout(), i...) +} + +func (p stdPrinter) Println(i ...any) { + p.Print(fmt.Sprintln(i...)) +} + +func (p stdPrinter) Printf(format string, i ...any) { + p.Print(fmt.Sprintf(format, i...)) +} + +func (p stdPrinter) PrintErr(i ...any) { + _, _ = fmt.Fprint(p.ErrOrStderr(), i...) +} + +func (p stdPrinter) PrintErrln(i ...any) { + p.PrintErr(fmt.Sprintln(i...)) +} + +func (p stdPrinter) PrintErrf(format string, i ...any) { + p.PrintErr(fmt.Sprintf(format, i...)) +} + +var _ Printer = stdPrinter{} diff --git a/pkg/output/std.go b/pkg/output/std.go new file mode 100644 index 0000000000..7d609e7de7 --- /dev/null +++ b/pkg/output/std.go @@ -0,0 +1,27 @@ +/* + 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 output + +import ( + "io" +) + +type InputOutput interface { + InOrStdin() io.Reader + OutOrStdout() io.Writer + ErrOrStderr() io.Writer +} diff --git a/pkg/output/term/io.go b/pkg/output/term/io.go new file mode 100644 index 0000000000..5285c45bc5 --- /dev/null +++ b/pkg/output/term/io.go @@ -0,0 +1,46 @@ +/* + 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 term + +import ( + "io" + "os" + + "golang.org/x/term" + "knative.dev/client/pkg/output/environment" +) + +// IsReaderTerminal returns true if the given reader is a real terminal. +func IsReaderTerminal(r io.Reader) bool { + f, ok := r.(*os.File) + return ok && term.IsTerminal(int(f.Fd())) +} + +// IsWriterTerminal returns true if the given writer is a real terminal. +func IsWriterTerminal(w io.Writer) bool { + f, ok := w.(*os.File) + return ok && term.IsTerminal(int(f.Fd())) +} + +// IsFancy returns true if the given writer is a real terminal, or the color +// is forced by the environment variables. +func IsFancy(w io.Writer) bool { + if environment.ColorIsForced() { + return true + } + return !environment.NonColorRequested() && IsWriterTerminal(w) +} diff --git a/pkg/output/testing.go b/pkg/output/testing.go new file mode 100644 index 0000000000..3dbc524715 --- /dev/null +++ b/pkg/output/testing.go @@ -0,0 +1,84 @@ +/* + 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 output + +import ( + "bytes" + "io" + "strings" + "testing" +) + +func NewTestPrinter() TestPrinter { + if !testing.Testing() { + panic("NewTestPrinter() can only be used in tests") + } + buf := bytes.NewBufferString("") + return NewTestPrinterWithInput(buf) +} + +func NewTestPrinterWithInput(input io.Reader) TestPrinter { + if !testing.Testing() { + panic("NewTestPrinterWithInput() can only be used in tests") + } + return TestPrinter{ + stdPrinter{ + testInOut{ + in: input, + TestOutputs: TestOutputs{ + Out: bytes.NewBufferString(""), + Err: bytes.NewBufferString(""), + }, + }, + }, + } +} + +func NewTestPrinterWithAnswers(answers []string) TestPrinter { + return NewTestPrinterWithInput(bytes.NewBufferString(strings.Join(answers, "\n"))) +} + +type TestPrinter struct { + stdPrinter +} + +func (p TestPrinter) Outputs() TestOutputs { + return p.InputOutput.(testInOut).TestOutputs //nolint:forcetypeassert +} + +type TestOutputs struct { + Out, Err *bytes.Buffer +} + +func (t TestOutputs) OutOrStdout() io.Writer { + return t.Out +} + +func (t TestOutputs) ErrOrStderr() io.Writer { + return t.Err +} + +type testInOut struct { + in io.Reader + TestOutputs +} + +func (t testInOut) InOrStdin() io.Reader { + return t.in +} + +var _ InputOutput = testInOut{} diff --git a/pkg/output/tui/bubbletea964.go b/pkg/output/tui/bubbletea964.go new file mode 100644 index 0000000000..cda1614879 --- /dev/null +++ b/pkg/output/tui/bubbletea964.go @@ -0,0 +1,57 @@ +/* + 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 tui + +import ( + "io" + "log" + "os" +) + +// safeguardBubbletea964 will safeguard the io.Reader by returning a new +// io.Reader that will prevent the +// https://github.com/charmbracelet/bubbletea/issues/964 issue. +// +// TODO: Remove this function once the issue is resolved. +func safeguardBubbletea964(in io.Reader) io.Reader { + if in == nil { + return in + } + if in == os.Stdin { + // this is not a *os.File, so it will not try to do the epoll stuff + return bubbletea964Input{Reader: in} + } + if f, ok := in.(*os.File); ok { + if st, err := f.Stat(); err != nil { + log.Fatal("unexpected: ", err) + } else { + if !st.Mode().IsRegular() { + if st.Name() != os.DevNull { + log.Println("WARN: non-regular file given as input: ", + st.Name(), " (mode: ", st.Mode(), + "). Using `nil` as input.") + } + return nil + } + } + } + return in +} + +type bubbletea964Input struct { + io.Reader +} diff --git a/pkg/output/tui/bubbletea964_test.go b/pkg/output/tui/bubbletea964_test.go new file mode 100644 index 0000000000..b651fc50a6 --- /dev/null +++ b/pkg/output/tui/bubbletea964_test.go @@ -0,0 +1,79 @@ +/* + 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 tui + +import ( + "io" + "os" + "testing" + + "gotest.tools/v3/assert" +) + +func TestSafeguardBubbletea964(t *testing.T) { + t.Parallel() + tmp := t.TempDir() + assert.NilError(t, os.WriteFile(tmp+"/file", []byte("test"), 0o600)) + td := openFile(t, tmp) + tf := openFile(t, tmp+"/file") + tcs := []safeguardBubbletea964TestCase{{ + name: "nil input", + in: nil, + want: nil, + }, { + name: "non-regular file", + in: os.NewFile(td.Fd(), "/"), + want: nil, + }, { + name: "regular file", + in: tf, + want: tf, + }, { + name: "dev null", + in: openFile(t, os.DevNull), + want: nil, + }, { + name: "stdin", + in: os.Stdin, + want: bubbletea964Input{Reader: os.Stdin}, + }} + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := safeguardBubbletea964(tc.in) + assert.Equal(t, got, tc.want) + }) + + } +} + +type safeguardBubbletea964TestCase struct { + name string + in io.Reader + want io.Reader +} + +func openFile(tb testing.TB, name string) *os.File { + tb.Helper() + f, err := os.Open(name) + assert.NilError(tb, err) + tb.Cleanup(func() { + assert.NilError(tb, f.Close()) + }) + return f +} diff --git a/pkg/output/tui/choose.go b/pkg/output/tui/choose.go new file mode 100644 index 0000000000..3fbfa82932 --- /dev/null +++ b/pkg/output/tui/choose.go @@ -0,0 +1,54 @@ +/* + 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 tui + +import ( + "context" + "fmt" + + "github.com/erikgeiser/promptkit/selection" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/logging" +) + +// NewChooser returns a new Chooser. +func NewChooser[T any](iw *InteractiveWidgets) Chooser[T] { + return &bubbleChooser[T]{iw.ctx} +} + +type Chooser[T any] interface { + Choose(options []T, format string, a ...any) T +} + +type bubbleChooser[T any] struct { + ctx context.Context +} + +func (c *bubbleChooser[T]) Choose(options []T, format string, a ...any) T { + ctx := c.ctx + prt := output.PrinterFrom(ctx) + l := logging.LoggerFrom(ctx) + sel := selection.New(fmt.Sprintf(format, a...), options) + sel.PageSize = 3 + sel.Input = prt.InOrStdin() + sel.Output = prt.OutOrStdout() + chosen, err := sel.RunPrompt() + if err != nil { + l.Fatal(err) + } + return chosen +} diff --git a/pkg/output/tui/io.go b/pkg/output/tui/io.go new file mode 100644 index 0000000000..33f70e554f --- /dev/null +++ b/pkg/output/tui/io.go @@ -0,0 +1,33 @@ +/* + 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 tui + +import ( + "os" + + tea "github.com/charmbracelet/bubbletea" + "knative.dev/client/pkg/output" +) + +func ioProgramOptions(io output.InputOutput) []tea.ProgramOption { + opts := make([]tea.ProgramOption, 0, 2) + opts = append(opts, tea.WithInput(safeguardBubbletea964(io.InOrStdin()))) + if io.OutOrStdout() != nil && io.OutOrStdout() != os.Stdout { + opts = append(opts, tea.WithOutput(io.OutOrStdout())) + } + return opts +} diff --git a/pkg/output/tui/print.go b/pkg/output/tui/print.go new file mode 100644 index 0000000000..e36bb90b9d --- /dev/null +++ b/pkg/output/tui/print.go @@ -0,0 +1,31 @@ +/* + 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 tui + +import ( + "strings" + + "knative.dev/client/pkg/output" +) + +func (w *widgets) Printf(format string, a ...any) { + printer := output.PrinterFrom(w.ctx) + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + printer.Printf(format, a...) +} diff --git a/pkg/output/tui/progress.go b/pkg/output/tui/progress.go new file mode 100644 index 0000000000..120949dc32 --- /dev/null +++ b/pkg/output/tui/progress.go @@ -0,0 +1,309 @@ +/* + 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 tui + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + "github.com/charmbracelet/bubbles/progress" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "go.uber.org/multierr" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/term" +) + +const speedInterval = time.Second / 5 + +type Progress interface { + Runnable[ProgressControl] +} + +type ProgressControl interface { + io.Writer + Error(err error) +} + +func (w *widgets) NewProgress(totalSize int, message Message) Progress { + return &BubbleProgress{ + InputOutput: output.PrinterFrom(w.ctx), + TotalSize: totalSize, + Message: message, + } +} + +type Message struct { + Text string + PaddingSize int +} + +func (m Message) BoundingBoxSize() int { + mSize := m.TextSize() + if mSize < m.PaddingSize { + mSize = m.PaddingSize + } + return mSize +} + +func (m Message) TextSize() int { + return len(m.Text) +} + +type BubbleProgress struct { + output.InputOutput + Message + TotalSize int + FinalPause time.Duration + + prog progress.Model + tea *tea.Program + downloaded int + speed int + prevSpeed []int + err error + quitChan chan struct{} + teaErr error +} + +func (b *BubbleProgress) With(fn func(ProgressControl) error) error { + b.start() + err := func() error { + defer b.stop() + return fn(b) + }() + return multierr.Combine(err, b.err, b.teaErr) +} + +func (b *BubbleProgress) Error(err error) { + b.err = err + b.tea.Send(tea.Quit()) +} + +func (b *BubbleProgress) Write(bytes []byte) (int, error) { + if b.err != nil { + return 0, b.err + } + noOfBytes := len(bytes) + b.downloaded += noOfBytes + b.speed += noOfBytes + if b.TotalSize > 0 { + percent := float64(b.downloaded) / float64(b.TotalSize) + b.onProgress(percent) + } + return noOfBytes, nil +} + +func (b *BubbleProgress) Init() tea.Cmd { + return b.tickSpeed() +} + +func (b *BubbleProgress) View() string { + return b.display(b.prog.View()) + + "\n" + helpStyle("Press Ctrl+C to cancel") +} + +func helpStyle(str string) string { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render(str) +} + +func (b *BubbleProgress) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + handle := bubbleProgressHandler{b} + switch event := msg.(type) { + case tea.WindowSizeMsg: + return handle.windowSize(event) + + case tea.KeyMsg: + return handle.keyPressed(event) + + case speedChange: + return handle.speedChange() + + case percentChange: + return handle.percentChange(event) + + // FrameMsg is sent when the progress bar wants to animate itself + case progress.FrameMsg: + return handle.progressFrame(event) + + default: + return b, nil + } +} + +type bubbleProgressHandler struct { + *BubbleProgress +} + +func (b bubbleProgressHandler) windowSize(event tea.WindowSizeMsg) (tea.Model, tea.Cmd) { + const percentLen = 4 + b.prog.Width = event.Width - len(b.display("")) + percentLen + return b, nil +} + +func (b bubbleProgressHandler) keyPressed(event tea.KeyMsg) (tea.Model, tea.Cmd) { + if event.Type == tea.KeyCtrlC { + b.err = context.Canceled + return b, tea.Quit + } + return b, nil +} + +func (b bubbleProgressHandler) speedChange() (tea.Model, tea.Cmd) { + b.prevSpeed = append(b.prevSpeed, b.speed) + const speedAvgAmount = 4 + if len(b.prevSpeed) > speedAvgAmount { + b.prevSpeed = b.prevSpeed[1:] + } + b.speed = 0 + if b.downloaded < b.TotalSize { + return b, b.tickSpeed() + } + return b, nil +} + +func (b bubbleProgressHandler) percentChange(event percentChange) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0, 1) + cmds = append(cmds, b.prog.SetPercent(float64(event))) + + if event >= 1.0 { + cmds = append(cmds, b.quitSignal()) + } + + return b, tea.Batch(cmds...) +} + +func (b *BubbleProgress) quitSignal() tea.Cmd { + // The final pause is to give the progress bar a chance to finish its + // animation before quitting. Otherwise, it ends abruptly, and the user + // might not see the progress bar at 100%. + return tea.Sequence(b.finalPause(), tea.Quit) +} + +func (b bubbleProgressHandler) progressFrame(event progress.FrameMsg) (tea.Model, tea.Cmd) { + progressModel, cmd := b.prog.Update(event) + if m, ok := progressModel.(progress.Model); ok { + b.prog = m + } + return b, cmd +} + +func (b *BubbleProgress) display(bar string) string { + const padding = 2 + const pad = " ⋮ " + paddingLen := padding + b.Message.BoundingBoxSize() - b.Message.TextSize() + titlePad := strings.Repeat(" ", paddingLen) + total := humanizeBytes(float64(b.TotalSize), "") + totalFmt := fmt.Sprintf("%6.2f %-3s", total.num, total.unit) + return b.Message.Text + titlePad + bar + pad + b.speedFormatted() + + pad + totalFmt +} + +func (b *BubbleProgress) speedFormatted() string { + s := humanizeBytes(b.speedPerSecond(), "/s") + return fmt.Sprintf("%6.2f %-5s", s.num, s.unit) +} + +func (b *BubbleProgress) speedPerSecond() float64 { + speed := 0. + for _, s := range b.prevSpeed { + speed += float64(s) + } + if len(b.prevSpeed) > 0 { + speed /= float64(len(b.prevSpeed)) + } + return speed / float64(speedInterval.Microseconds()) * + float64(time.Second.Microseconds()) +} + +func (b *BubbleProgress) tickSpeed() tea.Cmd { + return tea.Every(speedInterval, func(ti time.Time) tea.Msg { + return speedChange{} + }) +} + +func (b *BubbleProgress) start() { + b.prog = progress.New(progress.WithDefaultGradient()) + b.tea = tea.NewProgram(b, ioProgramOptions(b.InputOutput)...) + b.quitChan = make(chan struct{}) + go func() { + t := b.tea + if _, err := t.Run(); err != nil { + b.teaErr = err + } + close(b.quitChan) + }() +} + +func (b *BubbleProgress) stop() { + if b.tea == nil { + return + } + + b.tea.Send(b.quitSignal()) + <-b.quitChan + + if term.IsWriterTerminal(b.OutOrStdout()) && b.teaErr == nil { + b.teaErr = b.tea.ReleaseTerminal() + } + + b.tea = nil + b.quitChan = nil +} + +func (b *BubbleProgress) onProgress(percent float64) { + b.tea.Send(percentChange(percent)) +} + +func (b *BubbleProgress) finalPause() tea.Cmd { + pause := b.FinalPause + if pause == 0 { + pause = speedInterval * 3 + } + return tea.Tick(pause, func(_ time.Time) tea.Msg { + return nil + }) +} + +type percentChange float64 + +type speedChange struct{} + +type humanByteSize struct { + num float64 + unit string +} + +func humanizeBytes(bytes float64, unitSuffix string) humanByteSize { + num := bytes + units := []string{ + "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", + } + i := 0 + const kilo = 1024 + for num > kilo && i < len(units)-1 { + num /= kilo + i++ + } + return humanByteSize{ + num: num, + unit: units[i] + unitSuffix, + } +} diff --git a/pkg/output/tui/progress_test.go b/pkg/output/tui/progress_test.go new file mode 100644 index 0000000000..d1540fc610 --- /dev/null +++ b/pkg/output/tui/progress_test.go @@ -0,0 +1,69 @@ +//go:build !race + +// TODO: there is a race condition in the progress code that needs to be fixed +// somehow + +/* + 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 tui_test + +import ( + "crypto/rand" + "strings" + "testing" + "time" + + "knative.dev/client/pkg/context" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/tui" +) + +func TestProgress(t *testing.T) { + ctx := context.TestContext(t) + prt := output.NewTestPrinter() + ctx = output.WithContext(ctx, prt) + w := tui.NewWidgets(ctx) + p := w.NewProgress(42_000, tui.Message{Text: "message"}) + // This is a hack to make the test run faster + p.(*tui.BubbleProgress).FinalPause = 50 * time.Millisecond + if p == nil { + t.Errorf("want progress, got nil") + } + if err := p.With(func(pc tui.ProgressControl) error { + time.Sleep(20 * time.Millisecond) + for i := 0; i < 42; i++ { + buf := make([]byte, 1000) + if _, err := rand.Read(buf); err != nil { + return err + } + if _, err := pc.Write(buf); err != nil { + return err + } + } + + return nil + }); err != nil { + t.Errorf("want nil, got %v", err) + } + + got := prt.Outputs().Out.String() + want := "\x1b[?25lmessage ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% ⋮ 0.00 B/s ⋮ 41.02 KiB\r\n" + + "Press Ctrl+C to cancel" + if !strings.HasPrefix(got, want) { + t.Errorf("prefix missmatch\nwant %q,\n got %q", want, got) + } +} diff --git a/pkg/output/tui/runnable.go b/pkg/output/tui/runnable.go new file mode 100644 index 0000000000..d79637ebf8 --- /dev/null +++ b/pkg/output/tui/runnable.go @@ -0,0 +1,21 @@ +/* + 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 tui + +type Runnable[T any] interface { + With(fn func(T) error) error +} diff --git a/pkg/output/tui/spinner.go b/pkg/output/tui/spinner.go new file mode 100644 index 0000000000..c55bbb51d7 --- /dev/null +++ b/pkg/output/tui/spinner.go @@ -0,0 +1,113 @@ +/* + 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 tui + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "go.uber.org/multierr" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/term" +) + +const spinnerColor = lipgloss.Color("205") + +type Spinner interface { + Runnable[Spinner] +} + +func (w *widgets) NewSpinner(message string) Spinner { + return &BubbleSpinner{ + InputOutput: output.PrinterFrom(w.ctx), + Message: Message{Text: message}, + } +} + +type BubbleSpinner struct { + output.InputOutput + Message + + spin spinner.Model + tea *tea.Program + quitChan chan struct{} + teaErr error +} + +func (b *BubbleSpinner) With(fn func(Spinner) error) error { + b.start() + err := func() error { + defer b.stop() + return fn(b) + }() + return multierr.Combine(err, b.teaErr) +} + +func (b *BubbleSpinner) Init() tea.Cmd { + return b.spin.Tick +} + +func (b *BubbleSpinner) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + m, c := b.spin.Update(msg) + b.spin = m + return b, c +} + +func (b *BubbleSpinner) View() string { + return fmt.Sprintf("%s %s", b.Message.Text, b.spin.View()) +} + +func (b *BubbleSpinner) start() { + b.spin = spinner.New( + spinner.WithSpinner(spinner.Meter), + spinner.WithStyle(spinnerStyle()), + ) + b.tea = tea.NewProgram(b, ioProgramOptions(b.InputOutput)...) + b.quitChan = make(chan struct{}) + go func() { + t := b.tea + if _, err := t.Run(); err != nil { + b.teaErr = err + } + close(b.quitChan) + }() +} + +func (b *BubbleSpinner) stop() { + if b.tea == nil { + return + } + + b.tea.Quit() + <-b.quitChan + + if term.IsWriterTerminal(b.OutOrStdout()) && b.teaErr == nil { + b.teaErr = b.tea.ReleaseTerminal() + } + + b.tea = nil + b.quitChan = nil + endMsg := fmt.Sprintf("%s %s\n", + b.Message.Text, spinnerStyle().Render("Done")) + _, _ = b.OutOrStdout().Write([]byte(endMsg)) +} + +func spinnerStyle() lipgloss.Style { + return lipgloss.NewStyle().Foreground(spinnerColor) +} diff --git a/pkg/output/tui/spinner_test.go b/pkg/output/tui/spinner_test.go new file mode 100644 index 0000000000..a0f6a1a4fe --- /dev/null +++ b/pkg/output/tui/spinner_test.go @@ -0,0 +1,49 @@ +/* + 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 tui_test + +import ( + "testing" + "time" + + "knative.dev/client/pkg/context" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/tui" +) + +func TestSpinner(t *testing.T) { + ctx := context.TestContext(t) + prt := output.NewTestPrinter() + ctx = output.WithContext(ctx, prt) + w := tui.NewWidgets(ctx) + s := w.NewSpinner("message") + + if s == nil { + t.Errorf("want spinner, got nil") + } + if err := s.With(func(spinner tui.Spinner) error { + time.Sleep(20 * time.Millisecond) + return nil + }); err != nil { + t.Errorf("want nil, got %v", err) + } + got := prt.Outputs().Out.String() + want := "\x1b[?25lmessage ▰▱▱\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006lmessage Done\n" + if got != want { + t.Errorf("text missmatch\nwant %q,\n got %q", want, got) + } +} diff --git a/pkg/output/tui/widgets.go b/pkg/output/tui/widgets.go new file mode 100644 index 0000000000..3a8ceb353d --- /dev/null +++ b/pkg/output/tui/widgets.go @@ -0,0 +1,68 @@ +/* + 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 tui + +import ( + "context" + + "emperror.dev/errors" + "knative.dev/client/pkg/output" + "knative.dev/client/pkg/output/term" +) + +// ErrNotInteractive is returned when the user is not in an interactive session. +var ErrNotInteractive = errors.New("not interactive session") + +// Widgets is a set of widgets that can be used to display progress, spinners, +// and other interactive elements. +type Widgets interface { + // Printf prints a formatted string to the output. + Printf(format string, a ...any) + // NewSpinner returns a new spinner with the given message. The spinner will + // be started when the With method is called and stopped when the function + // returns. + NewSpinner(message string) Spinner + // NewProgress returns a new progress bar with the given total size and + // message. The progress bar will be started when the With method is called + // and stopped when the function returns. The progress bar can be updated + // with the Write method. + NewProgress(totalSize int, message Message) Progress +} + +func NewWidgets(ctx context.Context) Widgets { + return &widgets{ctx: ctx} +} + +type widgets struct { + ctx context.Context +} + +// NewInteractiveWidgets returns a set of interactive widgets if the user is +// in an interactive session. If the user is not in an interactive session, +// ErrNotInteractive error is returned. +func NewInteractiveWidgets(ctx context.Context) (*InteractiveWidgets, error) { + prt := output.PrinterFrom(ctx) + if !term.IsReaderTerminal(prt.InOrStdin()) { + return nil, errors.WithStack(ErrNotInteractive) + } + + return &InteractiveWidgets{ctx: ctx}, nil +} + +type InteractiveWidgets struct { + ctx context.Context +} diff --git a/pkg/output/tui/widgets_test.go b/pkg/output/tui/widgets_test.go new file mode 100644 index 0000000000..52911b25c3 --- /dev/null +++ b/pkg/output/tui/widgets_test.go @@ -0,0 +1,45 @@ +/* + 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 tui_test + +import ( + "testing" + + "knative.dev/client/pkg/context" + "knative.dev/client/pkg/output/tui" +) + +func TestNewWidgets(t *testing.T) { + ctx := context.TestContext(t) + w := tui.NewWidgets(ctx) + + if w == nil { + t.Errorf("want widgets, got nil") + } +} + +func TestNewInteractiveWidgets(t *testing.T) { + ctx := context.TestContext(t) + w, err := tui.NewInteractiveWidgets(ctx) + + if err == nil { + t.Error("want error, got nil") + } + if w != nil { + t.Errorf("want nil, got %v", w) + } +}