diff --git a/.circleci/config.yml b/.circleci/config.yml index bd2b9e43..f8d72ab4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,13 +13,13 @@ jobs: arch: type: string docker: - - image: circleci/golang:1.13 + - image: circleci/golang:1.14 steps: - checkout - run: GOOS=<< parameters.os >> GOARCH=<< parameters.arch >> go build ./cmd/secrethub test: docker: - - image: circleci/golang:1.13 + - image: circleci/golang:1.14 steps: - checkout - restore_cache: @@ -33,7 +33,7 @@ jobs: - run: make test verify-goreleaser: docker: - - image: goreleaser/goreleaser:v0.127 + - image: goreleaser/goreleaser:v0.133 steps: - checkout - run: goreleaser check diff --git a/Dockerfile b/Dockerfile index 5a58458c..29753135 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.13-alpine as build_base +FROM golang:1.14-alpine as build_base WORKDIR /build ENV GO111MODULE=on RUN apk add --update git diff --git a/go.mod b/go.mod index fb42264c..a1c0f024 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/secrethub/secrethub-cli -go 1.13 +go 1.14 require ( bitbucket.org/zombiezen/cardcpx v0.0.0-20150417151802-902f68ff43ef + cloud.google.com/go v0.57.0 // indirect github.com/alecthomas/kingpin v1.3.8-0.20200323085623-b6657d9477a6 + github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf github.com/atotto/clipboard v0.1.2 github.com/aws/aws-sdk-go v1.25.49 github.com/docker/go-units v0.3.3 @@ -16,10 +18,12 @@ require ( github.com/mitchellh/mapstructure v1.1.2 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/secrethub/demo-app v0.1.0 - github.com/secrethub/secrethub-go v0.28.0 + github.com/secrethub/secrethub-go v0.29.0 github.com/zalando/go-keyring v0.0.0-20190208082241-fbe81aec3a07 - golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a - golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 - golang.org/x/text v0.3.0 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 + golang.org/x/sys v0.0.0-20200501052902-10377860bb8e + golang.org/x/text v0.3.2 + google.golang.org/api v0.26.0 + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 977a9e18..5db40c7d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,34 @@ bitbucket.org/zombiezen/cardcpx v0.0.0-20150417151802-902f68ff43ef h1:Y5Zf3CYdrdGE7GOuK/MNN98GS1V8mOfeiJlISrKUcEo= bitbucket.org/zombiezen/cardcpx v0.0.0-20150417151802-902f68ff43ef/go.mod h1:ZJR5FpaQx7Bt2bzIV3gBaCInI1+kG949WhNYYlRr8eA= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4 h1:pSm8mp0T2OH2CPmPDPtwHPr3VAQaOwVF/JbllOPP4xA= github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022 h1:y8Gs8CzNfDF5AZvjr+5UyGQvQEBL7pwo+v+wX6q9JI8= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/alecthomas/kingpin v0.0.0-20190930021037-0a108b7f5563 h1:YT8l7Flq7VNXnjqwtjCF9bzffTPGgedBC+xyj88lVe4= @@ -20,6 +47,12 @@ github.com/aws/aws-sdk-go v1.19.38 h1:WKjobgPO4Ua1ww2NJJl2/zQNreUZxvqmEzwMlRjjm9 github.com/aws/aws-sdk-go v1.19.38/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.49 h1:j5R2Ey+g8qaiy2NJ9iH+KWzDWS4SjXRCjhc22EeQVE4= github.com/aws/aws-sdk-go v1.25.49/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= @@ -30,18 +63,72 @@ github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/go-chi/chi v4.0.1+incompatible h1:RSRC5qmFPtO90t7pTL0DBMNpZFsb/sHF3RXVlDgFisA= github.com/go-chi/chi v4.0.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -68,14 +155,19 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0C github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/secrethub/demo-app v0.1.0 h1:HwPPxuiSvx4TBE7Qppzu3A9eHqmsBrIz4Ko8u8pqMqw= github.com/secrethub/demo-app v0.1.0/go.mod h1:ymjm8+WXTSDTFqsGVBNVmHSnwtZMYi7KptHvpo/fLH4= github.com/secrethub/secrethub-cli v0.30.0/go.mod h1:dC0wd40v+iQdV83/0rUrOa01LYq+8Yj2AtJB1vzh2ao= github.com/secrethub/secrethub-go v0.21.0/go.mod h1:rc2IfKKBJ4L0wGec0u4XnF5/pe0FFPE4Q1MWfrFso7s= -github.com/secrethub/secrethub-go v0.28.0 h1:N46plUaOIqeE51X/qpNY9rCKTVL7TIlS7LJHoF3z1fA= -github.com/secrethub/secrethub-go v0.28.0/go.mod h1:Wr4gXWrk8OvBHiCttjLq7wFdKSm07rlEhq5OSYPemtI= +github.com/secrethub/secrethub-go v0.27.1-0.20200603082037-a48b9700bb81 h1:gGp4NCf/2N2epcNdVEqwF025gY0h6DOmS7xS7DciyZI= +github.com/secrethub/secrethub-go v0.27.1-0.20200603082037-a48b9700bb81/go.mod h1:Wr4gXWrk8OvBHiCttjLq7wFdKSm07rlEhq5OSYPemtI= +github.com/secrethub/secrethub-go v0.27.1-0.20200603102047-1a4e50eafb91/go.mod h1:Wr4gXWrk8OvBHiCttjLq7wFdKSm07rlEhq5OSYPemtI= +github.com/secrethub/secrethub-go v0.29.0 h1:BUM7lcxmjJENNF6pxq13dKPXf4sP6iQKWq7cbLbOM0g= +github.com/secrethub/secrethub-go v0.29.0/go.mod h1:tDeBtyjfFQX3UqgaZfY+H4dYkcGfiVzrwLDf0XtfOrw= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= @@ -84,24 +176,233 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.0.0-20190208082241-fbe81aec3a07 h1:U5I57s4ISLpeeLYl8b3MsainSSh9F+mRXauln37b50I= github.com/zalando/go-keyring v0.0.0-20190208082241-fbe81aec3a07/go.mod h1:XlXBIfkGawHNVOHlenOaBW7zlfCh8LovwjOgjamYnkQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e h1:hq86ru83GdWTlfQFZGO4nZJTU4Bs2wfHl8oFHRaXsfc= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.25.0 h1:LodzhlzZEUfhXzNUMIfVlf9Gr6Ua5MMtoFWh7+f47qA= +google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08= +google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84 h1:pSLkPbrjnPyLDYUO2VM9mDLqo2V6CFBY84lFSZAfoi4= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc h1:TnonUr8u3himcMY0vSh23jFOXA+cnucl1gB6EQTReBI= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internals/cli/configuration/configuration.go b/internals/cli/configuration/configuration.go deleted file mode 100644 index c4a1ff04..00000000 --- a/internals/cli/configuration/configuration.go +++ /dev/null @@ -1,193 +0,0 @@ -package configuration - -import ( - "encoding/json" - "io/ioutil" - "os" - "reflect" - "strings" - "time" - - "github.com/secrethub/secrethub-cli/internals/cli/posix" - - "github.com/secrethub/secrethub-go/internals/api/uuid" - "github.com/secrethub/secrethub-go/internals/errio" - - "github.com/mitchellh/mapstructure" - yaml "gopkg.in/yaml.v2" -) - -var ( - errConfig = errio.Namespace("configuration") - - // ErrDecodeFailed is given when the config cannot be decoded. - ErrDecodeFailed = errConfig.Code("decode_fail").Error("failed to decode config") - // ErrEncodeFailed is given when the config cannot be encoded. - ErrEncodeFailed = errConfig.Code("encode_fail").Error("failed to encode config") - // ErrFileNotFound is given when the config file cannot be found. - ErrFileNotFound = errConfig.Code("not_found").Error("config file not found") - - // ErrTypeNotSet is given when a config has no type specified - ErrTypeNotSet = errConfig.Code("type_not_set").Error("field `type` of config is not set") -) - -// ReadFromFile attempts to read a config as a struct from a file. -// It will unmarshal yaml and json into the destination. -func ReadFromFile(path string, destination interface{}) error { - data, err := readFile(path) - if err != nil { - return err - } - - return Read(data, destination) -} - -// Read attempts to read a config in a destination interface. -func Read(data []byte, destination interface{}) error { - err := yaml.Unmarshal(data, destination) - return err -} - -// ReadConfigurationDataFromFile retrieves the data and attempts to read the config as a ConfigMap -// from a file. This simplifies the process of determining to parse the config as a ConfigMap or to -// directly unmarshal into a struct. -func ReadConfigurationDataFromFile(path string) (ConfigMap, []byte, error) { - data, err := readFile(path) - if err != nil { - return nil, nil, err - } - - configMap, err := ReadMap(data) - return configMap, data, err -} - -// ReadMap attempts to unmarshal a []byte it into the dest map. -// Both json and yaml are supported -func ReadMap(data []byte) (ConfigMap, error) { - var dest ConfigMap - - // Supports both json and yaml - if err := yaml.Unmarshal(data, &dest); err != nil { - return nil, ErrDecodeFailed - } - - return dest, nil -} - -// ParseMap uses mapstructure to convert a ConfigMap into a struct -// -// For example, the following JSON: -// { -// "ssh_key": "/home/joris/.ssh/secrethub" -// } -// -// Can be loaded in a struct of type: -// type UserConfig struct { -// SSHKeyPath string `json:"ssh_key,omitempty"` -// } -// -// decodeHook is used to convert non-standard types into the correct format -func ParseMap(src *ConfigMap, dst interface{}) error { - - c := mapstructure.DecoderConfig{ - TagName: "json", - Result: dst, - WeaklyTypedInput: true, - DecodeHook: decodeHook} - decoder, err := mapstructure.NewDecoder(&c) - - if err != nil { - return err - } - - return decoder.Decode(src) -} - -// decodeHook adds extra decoding functionality to parsing the map. -func decodeHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { - if t == reflect.TypeOf(uuid.UUID{}) && f == reflect.TypeOf(string("")) { - return uuid.FromString(data.(string)) - } - - if t == reflect.TypeOf(time.Duration(0)) && f == reflect.TypeOf(string("")) { - return time.ParseDuration(data.(string)) - } - - return data, nil -} - -// WriteToFile attempts to marshal the src arg and write to a file at the given path. -// If the file is of an unsupported extension we cannot determine what type -// of encoding to use, so it defaults to writing encoded json to that file. -// JSON is written in indented 'pretty' format to allow for easy user editing. -func WriteToFile(src interface{}, path string, fileMode os.FileMode) error { - path = strings.ToLower(path) - - var data []byte - var err error - switch { - case strings.HasSuffix(path, ".yml") || strings.HasSuffix(path, ".yaml"): - data, err = yaml.Marshal(src) - if err != nil { - return err - } - case strings.HasSuffix(path, ".json"): - data, err = json.MarshalIndent(src, "", " ") - if err != nil { - return err - } - default: - data, err = json.MarshalIndent(src, "", " ") - if err != nil { - return ErrEncodeFailed - } - } - - return ioutil.WriteFile(path, posix.AddNewLine(data), fileMode) -} - -func readFile(path string) ([]byte, error) { - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, ErrFileNotFound - } - - return ioutil.ReadFile(path) -} - -// ConfigMap is the type used for configurations that are still in a map format -// Map format is to make changes in structure with migrations possible -type ConfigMap map[string]interface{} - -// GetVersion returns the version of the configuration file. -// If it is not set, it is assumed that is configuration version 1. -func (c ConfigMap) GetVersion() (int, error) { - version, ok := c["version"] - if !ok { - // Version not set - return 1, nil - } - - ret, ok := version.(int) - - if !ok { - return 0, errConfig.Code("version_wrong_type").Errorf("config value `version` has wrong type %T (actual) != int (expected)", version) - } - - return ret, nil -} - -// GetType returns the type of the configuration file -// If it is not set, it is assumed to have no type. -func (c ConfigMap) GetType() (string, error) { - t, ok := c["type"] - if !ok { - // Config type not set - return "", ErrTypeNotSet - } - - ret, ok := t.(string) - if !ok { - return "", errConfig.Code("type_wrong_type").Errorf("config value `type` has wrong type %T (actual) != string (expected)", t) - } - return ret, nil -} diff --git a/internals/cli/configuration/migrater.go b/internals/cli/configuration/migrater.go deleted file mode 100644 index c3b973d2..00000000 --- a/internals/cli/configuration/migrater.go +++ /dev/null @@ -1,56 +0,0 @@ -package configuration - -import ( - "github.com/secrethub/secrethub-cli/internals/cli/ui" -) - -var ( - // ErrVersionNotReachable is given when the migrater cannot reach the desired version - ErrVersionNotReachable = errConfig.Code("not_reachable").Error("desired config version not reachable") -) - -// Migration defines a function that is used to convert a configuration in ConfigMap format from VersionFrom to VersionTo -type Migration struct { - VersionFrom int - VersionTo int - UpdateFunc func(ui.IO, ConfigMap) (ConfigMap, error) -} - -// MigrateConfigTo attempts to convert a config in ConfigMap format from versionFrom to versionTo -// A list of Migrations has to be passed that form at least a path from versionFrom to versionTo -// Function may fail if the migration path contains dead ends that do not lead to versionTo -// Setting checkOnly to true will not perform the actual migration and can be used to check whether there is a valid -// migration path from one version to another -func MigrateConfigTo(io ui.IO, config ConfigMap, versionFrom int, versionTo int, migrations []Migration, checkOnly bool) (ConfigMap, error) { - var err error - version := versionFrom - for version != versionTo { - migrated := false - for _, m := range migrations { - if m.VersionFrom == version { - if !checkOnly { - config, err = m.migrate(io, config) - } - version = m.VersionTo - migrated = true - break - } - } - - if err != nil { - return config, err - } - if !migrated { - return config, ErrVersionNotReachable - } - - } - - config["version"] = versionTo - - return config, nil -} - -func (m *Migration) migrate(io ui.IO, src ConfigMap) (ConfigMap, error) { - return m.UpdateFunc(io, src) -} diff --git a/internals/cli/configuration/migrater_test.go b/internals/cli/configuration/migrater_test.go deleted file mode 100644 index c577c30e..00000000 --- a/internals/cli/configuration/migrater_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package configuration - -import ( - "testing" - - "github.com/secrethub/secrethub-cli/internals/cli/ui" -) - -var testMigrations = []Migration{ - {VersionFrom: 1, VersionTo: 2, UpdateFunc: testMigration1}, - {VersionFrom: 2, VersionTo: 3, UpdateFunc: testMigration2}, - {VersionFrom: 3, VersionTo: 4, UpdateFunc: testMigration3}, - {VersionFrom: 4, VersionTo: 5, UpdateFunc: testMigration3}, -} - -func TestConfigMigrater(t *testing.T) { - - config, err := getTestConfig() - if err != nil { - t.Fatal(err) - } - version, err := config.GetVersion() - if err != nil { - t.Error(err) - } - - goalVersion := 4 - - config, err = MigrateConfigTo(ui.NewFakeIO(), config, version, goalVersion, testMigrations, false) - - if err != nil { - t.Error(err) - } - - if !(config["applied_migration_1"] == true && config["applied_migration_2"] == true && config["applied_migration_3"] == true && config["migration_count"] == 3) { - t.Errorf("Not all migrations were applied correctly. This was the result: %s", config) - } - - if config["change"] == "not_changed" { - t.Errorf("Migration did not correctly change `change`") - } - - if config["not_change"] != "not_changed" { - t.Errorf("Migration changed `not_change`") - } - -} - -func TestConfigMigrater_NotReachable(t *testing.T) { - config, err := getTestConfig() - if err != nil { - t.Fatal(err) - } - version, err := config.GetVersion() - if err != nil { - t.Error(err) - } - - goalVersion := 6 - - _, err = MigrateConfigTo(ui.NewFakeIO(), config, version, goalVersion, testMigrations, false) - - if err != ErrVersionNotReachable { - t.Error("Did not throw a ErrVersionNotReachable for an unreachable version") - } -} - -func getTestConfig() (ConfigMap, error) { - - data := []byte(`{` + - `"applied_migration_1": false,` + - `"applied_migration_2": false,` + - `"not_change": "not_changed",` + - `"change": "not_changed",` + - `"migration_count": 0` + - `}`) - - return ReadMap(data) -} - -func testMigration1(_ ui.IO, src ConfigMap) (ConfigMap, error) { - - src["applied_migration_1"] = true - src["change"] = "changed" - src["migration_count"] = src["migration_count"].(int) + 1 - - return src, nil -} - -func testMigration2(_ ui.IO, src ConfigMap) (ConfigMap, error) { - - src["applied_migration_2"] = true - src["migration_count"] = src["migration_count"].(int) + 1 - - return src, nil -} - -func testMigration3(_ ui.IO, src ConfigMap) (ConfigMap, error) { - - src["applied_migration_3"] = true - src["migration_count"] = src["migration_count"].(int) + 1 - - return src, nil -} diff --git a/internals/cli/ui/ask.go b/internals/cli/ui/ask.go index ff314aa1..b20adf2b 100644 --- a/internals/cli/ui/ask.go +++ b/internals/cli/ui/ask.go @@ -52,7 +52,7 @@ func AskWithDefault(io IO, question, defaultValue string) (string, error) { // AskSecret prints out the question and reads back the input, // without echoing it back. Useful for passwords and other sensitive inputs. func AskSecret(io IO, question string) (string, error) { - promptIn, promptOut, err := io.Prompts() + _, promptOut, err := io.Prompts() if err != nil { return "", err } @@ -62,7 +62,7 @@ func AskSecret(io IO, question string) (string, error) { return "", err } - raw, err := promptIn.ReadPassword() + raw, err := io.ReadSecret() if err != nil { return "", ErrReadInput(err) } @@ -272,6 +272,28 @@ func (o Option) String() string { return o.Display } +func ChooseDynamicOptionsValidate(io IO, question string, getOptions func() ([]Option, bool, error), optionName string, validateFunc func(string) error) (string, error) { + r, w, err := io.Prompts() + if err != nil { + return "", err + } + + if optionName == "" { + optionName = "option" + } + + s := selecter{ + r: r, + w: w, + getOptions: getOptions, + question: question, + addOwn: true, + validateFunc: validateFunc, + optionName: optionName, + } + return s.run() +} + func ChooseDynamicOptions(io IO, question string, getOptions func() ([]Option, bool, error), addOwn bool, optionName string) (string, error) { r, w, err := io.Prompts() if err != nil { @@ -294,12 +316,13 @@ func ChooseDynamicOptions(io IO, question string, getOptions func() ([]Option, b } type selecter struct { - r io.Reader - w io.Writer - getOptions func() ([]Option, bool, error) - question string - addOwn bool - optionName string + r io.Reader + w io.Writer + getOptions func() ([]Option, bool, error) + validateFunc func(string) error + question string + addOwn bool + optionName string done bool options []Option @@ -359,6 +382,9 @@ func (s *selecter) process() (string, error) { choice, err := strconv.Atoi(in) if err != nil || choice < 1 || choice > len(s.options) { if s.addOwn { + if s.validateFunc != nil { + return in, s.validateFunc(in) + } return in, nil } diff --git a/internals/cli/ui/ask_test.go b/internals/cli/ui/ask_test.go index 0dd3c44b..8d6c6cff 100644 --- a/internals/cli/ui/ask_test.go +++ b/internals/cli/ui/ask_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/secrethub/secrethub-go/internals/assert" + + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" ) func TestAskWithDefault(t *testing.T) { @@ -33,7 +35,7 @@ func TestAskWithDefault(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Reads = tc.in // Run @@ -94,7 +96,7 @@ func TestConfirmCaseInsensitive(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.promptIn) // Run @@ -239,7 +241,7 @@ func TestAskYesNo(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Reads = tc.in // Run @@ -319,7 +321,7 @@ func TestChoose(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Reads = tc.in // Run @@ -415,7 +417,7 @@ func TestChooseDynamicOptions(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Reads = tc.in if tc.getOptions == nil { diff --git a/internals/cli/ui/fakeui/testing.go b/internals/cli/ui/fakeui/testing.go new file mode 100644 index 00000000..4f620fd1 --- /dev/null +++ b/internals/cli/ui/fakeui/testing.go @@ -0,0 +1,133 @@ +// +build !production + +package fakeui + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/secrethub/secrethub-go/internals/assert" +) + +// FakeIO is a helper type for testing that implements the ui.IO interface +type FakeIO struct { + In *FakeReader + Out *FakeWriter + StdIn *os.File + StdOut *os.File + PromptIn *FakeReader + PromptOut *FakeWriter + PasswordReader *FakeReader + PromptErr error +} + +// NewIO creates a new FakeIO with empty buffers. +func NewIO(t *testing.T) *FakeIO { + tempDir, err := ioutil.TempDir("", "") + assert.OK(t, err) + stdIn, err := ioutil.TempFile(tempDir, "in") + assert.OK(t, err) + stdOut, err := ioutil.TempFile(tempDir, "out") + assert.OK(t, err) + + t.Cleanup(func() { + err := os.RemoveAll(tempDir) + if err != nil { + fmt.Printf("could not remove temp dir: %s", err) + } + }) + + return &FakeIO{ + In: &FakeReader{ + Buffer: &bytes.Buffer{}, + }, + Out: &FakeWriter{ + Buffer: &bytes.Buffer{}, + }, + StdIn: stdIn, + StdOut: stdOut, + PasswordReader: &FakeReader{ + Buffer: &bytes.Buffer{}, + }, + PromptIn: &FakeReader{ + Buffer: &bytes.Buffer{}, + }, + PromptOut: &FakeWriter{ + Buffer: &bytes.Buffer{}, + }, + } +} + +// Stdin returns the mocked In. +func (f *FakeIO) Input() io.Reader { + return f.In +} + +// Stdout returns the mocked Out. +func (f *FakeIO) Output() io.Writer { + return f.Out +} + +func (f *FakeIO) Stdin() *os.File { + return f.StdIn +} + +func (f *FakeIO) Stdout() *os.File { + return f.StdOut +} + +func (f *FakeIO) ReadStdout() ([]byte, error) { + return ioutil.ReadFile(f.StdOut.Name()) +} + +// Prompts returns the mocked prompts and error. +func (f *FakeIO) Prompts() (io.Reader, io.Writer, error) { + return f.PromptIn, f.PromptOut, f.PromptErr +} + +func (f *FakeIO) IsInputPiped() bool { + return f.In.Piped +} + +func (f *FakeIO) IsOutputPiped() bool { + return f.Out.Piped +} + +func (f *FakeIO) ReadSecret() ([]byte, error) { + return ioutil.ReadAll(f.PasswordReader) +} + +// FakeReader implements the Reader interface. +type FakeReader struct { + *bytes.Buffer + Piped bool + i int + Reads []string + ReadErr error +} + +// Read returns the mocked ReadErr or reads from the mocked buffer. +func (f *FakeReader) Read(p []byte) (n int, err error) { + if f.ReadErr != nil { + return 0, f.ReadErr + } + if len(f.Reads) > 0 { + if len(f.Reads) <= f.i { + return 0, errors.New("no more fake lines to read") + } + f.Buffer = bytes.NewBufferString(f.Reads[f.i]) + f.i++ + } + return f.Buffer.Read(p) +} + +// FakeWriter implements the Writer interface. +type FakeWriter struct { + *bytes.Buffer + Piped bool +} diff --git a/internals/cli/ui/io.go b/internals/cli/ui/io.go index 4484049c..380adb36 100644 --- a/internals/cli/ui/io.go +++ b/internals/cli/ui/io.go @@ -18,102 +18,107 @@ var ( // IO is an interface to work with input/output. type IO interface { - Stdin() Reader - Stdout() Writer - Prompts() (Reader, Writer, error) + // Input returns an io,Reader that reads input for the current process. + // If the process's input is piped, this reads from the pipe otherwise it asks input from the user. + Input() io.Reader + // Output returns an io.Writer that writes output for the current process. + // If the process's output is piped, this writes to the pipe otherwise it prints to the terminal. + Output() io.Writer + // Stdin returns the *os.File of the current process's stdin stream. + Stdin() *os.File + // Stdin returns the *os.File of the current process's stdout stream. + Stdout() *os.File + // Prompts returns an io.Reader and io.Writer that read and write directly to/from the terminal, even if the + // input or output of the current process is piped. + // If this is not supported, an error is returned. + Prompts() (io.Reader, io.Writer, error) + // ReadSecret reads a line of input from the terminal while hiding the entered characters. + // Returns an error if secret input is not supported. + ReadSecret() ([]byte, error) + // IsInputPiped returns whether the current process's input is piped from another process. + IsInputPiped() bool + // IsOutputPiped returns whether the current process's output is piped to another process. + IsOutputPiped() bool } -// UserIO is a middleware between input and output to the CLI program. -// It implements userIO.Prompter and can be passed to libraries. -type UserIO struct { - Input Reader - Output Writer - tty file - ttyAvailable bool +// standardIO is a middleware between input and output to the CLI program. +// It implements standardIO.Prompter and can be passed to libraries. +type standardIO struct { + input *os.File + output *os.File } -// NewStdUserIO creates a new UserIO middleware only from os.Stdin and os.Stdout. -func NewStdUserIO() UserIO { - return UserIO{ - Input: file{os.Stdin}, - Output: file{os.Stdout}, +// newStdUserIO creates a new standardIO middleware only from os.Stdin and os.Stdout. +func newStdUserIO() standardIO { + return standardIO{ + input: os.Stdin, + output: os.Stdout, } } -// Stdin returns the UserIO's Input. -func (o UserIO) Stdin() Reader { - return o.Input +func (o standardIO) Stdin() *os.File { + return o.input } -// Stdout returns the UserIO's Output. -func (o UserIO) Stdout() Writer { - return o.Output +func (o standardIO) Stdout() *os.File { + return o.output } -// Prompts simply returns Stdin and Stdout, when both input and output are -// not piped. When either input or output is piped, Prompts attempts to -// bypass stdin and stdout by connecting to /dev/tty on Unix systems when -// available. On systems where tty is not available and when either input -// or output is piped, prompting is not possible so an error is returned. -func (o UserIO) Prompts() (Reader, Writer, error) { - if o.Input.IsPiped() || o.Output.IsPiped() { - if o.ttyAvailable { - return o.tty, o.tty, nil - } - return nil, nil, ErrCannotAsk - } - return o.Input, o.Output, nil +// Stdin returns the standardIO's Input. +func (o standardIO) Input() io.Reader { + return o.input } -// Reader can read input for a CLI program. -type Reader interface { - io.Reader - // ReadPassword reads a line of input from a terminal without local echo. - ReadPassword() ([]byte, error) - IsPiped() bool +// Stdout returns the standardIO's Output. +func (o standardIO) Output() io.Writer { + return o.output } -// Readln reads 1 line of input from a io.Reader. The newline character is not included in the response. -func Readln(r io.Reader) (string, error) { - s := bufio.NewScanner(r) - s.Scan() - err := s.Err() - if err != nil { - return "", ErrReadInput(err) +// Prompts simply returns Stdin and Stdout, when both input and output are +// not piped. When either input or output is piped, it returns an error because standardIO does not have +// access to a tty for prompting. +func (o standardIO) Prompts() (io.Reader, io.Writer, error) { + if o.IsOutputPiped() || o.IsInputPiped() { + return nil, nil, ErrCannotAsk } - return s.Text(), nil + return o.input, o.output, nil } -// Writer can write output for a CLI program. -type Writer interface { - io.Writer - IsPiped() bool +func (o standardIO) IsInputPiped() bool { + return isPiped(o.input) } -// file implements the Reader and Writer interface. -type file struct { - *os.File +func (o standardIO) IsOutputPiped() bool { + return isPiped(o.output) } -// ReadPassword reads from a terminal without echoing back the typed input. -func (f file) ReadPassword() ([]byte, error) { - // this case happens among other things when input is piped and ReadPassword is called. +func (o standardIO) ReadSecret() ([]byte, error) { + return readSecret(o.input) +} + +// readSecret reads one line of input from the terminal without echoing the user input. +func readSecret(f *os.File) ([]byte, error) { + // this case happens among other things when input is piped and ReadSecret is called. if !terminal.IsTerminal(int(f.Fd())) { return nil, ErrCannotAsk } - return terminal.ReadPassword(int(f.Fd())) + password, err := terminal.ReadPassword(int(f.Fd())) + if err != nil { + return nil, err + } + return password, nil } -// IsPiped checks whether the file is a pipe. -// If the file does not exist, it returns false. -func (f file) IsPiped() bool { - stat, err := f.Stat() +// Readln reads 1 line of input from a io.Reader. The newline character is not included in the response. +func Readln(r io.Reader) (string, error) { + s := bufio.NewScanner(r) + s.Scan() + err := s.Err() if err != nil { - return false + return "", ErrReadInput(err) } - - return (stat.Mode() & os.ModeCharDevice) == 0 + return s.Text(), nil } // EOFKey returns the key that should be pressed to enter an EOF. diff --git a/internals/cli/ui/io_unix.go b/internals/cli/ui/io_unix.go index 2a6cec91..e97196ce 100644 --- a/internals/cli/ui/io_unix.go +++ b/internals/cli/ui/io_unix.go @@ -2,23 +2,83 @@ package ui -import "os" +import ( + "io" + "os" +) -// NewUserIO creates a new UserIO middleware from os.Stdin and os.Stdout and adds tty if it is available. -func NewUserIO() UserIO { +// ttyIO is the implementation of the IO interface that can use a TTY. +type ttyIO struct { + input *os.File + output *os.File + tty *os.File +} + +// NewUserIO creates a new ttyIO if a TTY is available, otherwise it returns a standardIO. +func NewUserIO() IO { tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err == nil { - return UserIO{ - Input: file{os.Stdin}, - Output: file{os.Stdout}, - tty: file{tty}, - ttyAvailable: true, + return ttyIO{ + input: os.Stdin, + output: os.Stdout, + tty: tty, } } - return NewStdUserIO() + return newStdUserIO() +} + +// Prompts simply returns Stdin and Stdout, when both input and output are +// not piped. When either input or output is piped, Prompts bypasses stdin and stdout by returning the tty. +func (o ttyIO) Prompts() (io.Reader, io.Writer, error) { + if o.IsOutputPiped() || o.IsInputPiped() { + return o.tty, o.tty, nil + } + return o.input, o.output, nil +} + +func (o ttyIO) IsInputPiped() bool { + return isPiped(o.input) +} + +func (o ttyIO) IsOutputPiped() bool { + return isPiped(o.output) +} + +func (o ttyIO) Stdin() *os.File { + return o.input +} + +func (o ttyIO) Stdout() *os.File { + return o.output +} + +// Stdin returns the standardIO's Input. +func (o ttyIO) Input() io.Reader { + return o.input +} + +// Stdout returns the standardIO's Output. +func (o ttyIO) Output() io.Writer { + return o.output +} + +func (o ttyIO) ReadSecret() ([]byte, error) { + return readSecret(o.tty) +} + +// isPiped checks whether the file is a pipe. +// If the file does not exist, it returns false. +func isPiped(file *os.File) bool { + stat, err := file.Stat() + if err != nil { + return false + } + + return (stat.Mode() & os.ModeCharDevice) == 0 } +// eofKey returns the key(s) that should be pressed to enter an EOF. func eofKey() string { return "CTRL-D" } diff --git a/internals/cli/ui/io_windows.go b/internals/cli/ui/io_windows.go index 7006b01e..b39d9f53 100644 --- a/internals/cli/ui/io_windows.go +++ b/internals/cli/ui/io_windows.go @@ -9,28 +9,41 @@ import ( isatty "github.com/mattn/go-isatty" ) -// NewUserIO creates a new UserIO middleware from os.Stdin and os.Stdout and adds tty if it is available. -func NewUserIO() UserIO { - // Ensure colors are printed correctly on Windows. - if !color.NoColor { - return UserIO{ - Input: file{os.Stdin}, - Output: colorStdout{colorable.NewColorableStdout()}, - } - } - - return NewStdUserIO() +// windowsIO is the Windows-specific implementation of the IO interface. +type windowsIO struct { + standardIO + coloredOutput io.Writer } -type colorStdout struct { - io.Writer +// NewUserIO creates a new windowsIO. +func NewUserIO() IO { + return windowsIO{ + standardIO: newStdUserIO(), + coloredOutput: colorable.NewColorable(os.Stdout), + } } -func (c colorStdout) IsPiped() bool { - return os.Getenv("TERM") == "dumb" || - (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) +// Stdout returns the standardIO's Output. +func (o windowsIO) Output() io.Writer { + if !color.NoColor { + return o.coloredOutput + } + return o.output } +// eofKey returns the key(s) that should be pressed to enter an EOF. func eofKey() string { return "CTRL-Z + ENTER" } + +// isPiped checks whether the file is a pipe. +// If the file does not exist, it returns false. +func isPiped(file *os.File) bool { + _, err := file.Stat() + if err != nil { + return false + } + + return os.Getenv("TERM") == "dumb" || + (!isatty.IsTerminal(file.Fd()) && !isatty.IsCygwinTerminal(file.Fd())) +} diff --git a/internals/cli/ui/testing.go b/internals/cli/ui/testing.go deleted file mode 100644 index fb2b0e29..00000000 --- a/internals/cli/ui/testing.go +++ /dev/null @@ -1,99 +0,0 @@ -// +build !production - -package ui - -import ( - "bytes" - "errors" -) - -// FakeIO is a helper type for testing that implements the ui.IO interface -type FakeIO struct { - StdIn *FakeReader - StdOut *FakeWriter - PromptIn *FakeReader - PromptOut *FakeWriter - PromptErr error -} - -// NewFakeIO creates a new FakeIO with empty buffers. -func NewFakeIO() *FakeIO { - return &FakeIO{ - StdIn: &FakeReader{ - Buffer: &bytes.Buffer{}, - }, - StdOut: &FakeWriter{ - Buffer: &bytes.Buffer{}, - }, - PromptIn: &FakeReader{ - Buffer: &bytes.Buffer{}, - }, - PromptOut: &FakeWriter{ - Buffer: &bytes.Buffer{}, - }, - } -} - -// Stdin returns the mocked StdIn. -func (f *FakeIO) Stdin() Reader { - return f.StdIn -} - -// Stdout returns the mocked StdOut. -func (f *FakeIO) Stdout() Writer { - return f.StdOut -} - -// Prompts returns the mocked prompts and error. -func (f *FakeIO) Prompts() (Reader, Writer, error) { - return f.PromptIn, f.PromptOut, f.PromptErr -} - -// FakeReader implements the Reader interface. -type FakeReader struct { - *bytes.Buffer - Piped bool - i int - Reads []string - ReadErr error -} - -// ReadPassword reads a line from the mocked buffer. -func (f *FakeReader) ReadPassword() ([]byte, error) { - pass, err := Readln(f) - if err != nil { - return nil, err - } - return []byte(pass), nil -} - -// IsPiped returns the mocked Piped. -func (f *FakeReader) IsPiped() bool { - return f.Piped -} - -// Read returns the mocked ReadErr or reads from the mocked buffer. -func (f *FakeReader) Read(p []byte) (n int, err error) { - if f.ReadErr != nil { - return 0, f.ReadErr - } - if len(f.Reads) > 0 { - if len(f.Reads) <= f.i { - return 0, errors.New("no more fake lines to read") - } - f.Buffer = bytes.NewBufferString(f.Reads[f.i]) - f.i++ - } - return f.Buffer.Read(p) -} - -// FakeWriter implements the Writer interface. -type FakeWriter struct { - *bytes.Buffer - Piped bool -} - -// IsPiped returns the mocked Piped. -func (f *FakeWriter) IsPiped() bool { - return f.Piped -} diff --git a/internals/secrethub/account_email_verify.go b/internals/secrethub/account_email_verify.go index 8dc6fa70..0181274c 100644 --- a/internals/secrethub/account_email_verify.go +++ b/internals/secrethub/account_email_verify.go @@ -44,7 +44,7 @@ func (cmd *AccountEmailVerifyCommand) Run() error { } if user.EmailVerified { - fmt.Fprintln(cmd.io.Stdout(), "Your email address is already verified.") + fmt.Fprintln(cmd.io.Output(), "Your email address is already verified.") return nil } @@ -53,9 +53,9 @@ func (cmd *AccountEmailVerifyCommand) Run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "An email has been sent to %s with an email verification link. Please check your mail and click the link.\n\n", user.Email) + fmt.Fprintf(cmd.io.Output(), "An email has been sent to %s with an email verification link. Please check your mail and click the link.\n\n", user.Email) - fmt.Fprintf(cmd.io.Stdout(), "Please contact support@secrethub.io if the problem persists.\n\n") + fmt.Fprintf(cmd.io.Output(), "Please contact support@secrethub.io if the problem persists.\n\n") return nil } diff --git a/internals/secrethub/account_init.go b/internals/secrethub/account_init.go index c39f086a..7b187b12 100644 --- a/internals/secrethub/account_init.go +++ b/internals/secrethub/account_init.go @@ -52,7 +52,7 @@ func NewAccountInitCommand(io ui.IO, newClient newClientFunc, credentialStore Cr io: io, credentialStore: credentialStore, clipper: clip.NewClipboard(), - progressPrinter: progress.NewPrinter(io.Stdout(), 500*time.Millisecond), + progressPrinter: progress.NewPrinter(io.Output(), 500*time.Millisecond), newClient: newClient, } } @@ -105,7 +105,7 @@ func (cmd *AccountInitCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -138,14 +138,14 @@ func (cmd *AccountInitCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } } fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "An account credential will be generated and stored at %s. "+ "Losing this credential means you lose the ability to decrypt your secrets. "+ "So keep it safe.\n", @@ -166,13 +166,13 @@ func (cmd *AccountInitCommand) Run() error { } } - fmt.Fprint(cmd.io.Stdout(), "Generating credential...") + fmt.Fprint(cmd.io.Output(), "Generating credential...") err := credential.Create() if err != nil { return err } - fmt.Fprintln(cmd.io.Stdout(), " Done") + fmt.Fprintln(cmd.io.Output(), " Done") exportKey := credential.Key if passphrase != "" { @@ -216,11 +216,11 @@ func (cmd *AccountInitCommand) Run() error { if err != nil { return err } - fmt.Fprintln(cmd.io.Stdout(), "The credential's public component has been copied to the clipboard. To add the credential to your account, paste the clipboard contents in https://dashboard.secrethub.io/account-init") + fmt.Fprintln(cmd.io.Output(), "The credential's public component has been copied to the clipboard. To add the credential to your account, paste the clipboard contents in https://dashboard.secrethub.io/account-init") } else { - fmt.Fprintln(cmd.io.Stdout(), "To add the credential to your account, paste the public component shown below in https://dashboard.secrethub.io/account-init") + fmt.Fprintln(cmd.io.Output(), "To add the credential to your account, paste the public component shown below in https://dashboard.secrethub.io/account-init") - fmt.Fprintf(cmd.io.Stdout(), "\n%s\n", out) + fmt.Fprintf(cmd.io.Output(), "\n%s\n", out) } } else { if !cmd.credentialStore.ConfigDir().Credential().Exists() { @@ -249,21 +249,21 @@ func (cmd *AccountInitCommand) createAccountKey() error { if !isAuthenticated { if cmd.noWait { - fmt.Fprintln(cmd.io.Stdout(), "Not waiting for credential to be added. To continue initializing your account after you have added the credential, run again with --continue.") + fmt.Fprintln(cmd.io.Output(), "Not waiting for credential to be added. To continue initializing your account after you have added the credential, run again with --continue.") return nil } - fmt.Fprint(cmd.io.Stdout(), "Waiting for credential to be added...") + fmt.Fprint(cmd.io.Output(), "Waiting for credential to be added...") authenticatedC, errC := cmd.waitForCredentialToBeAdded(client) select { case <-authenticatedC: - fmt.Fprintln(cmd.io.Stdout(), " Done") + fmt.Fprintln(cmd.io.Output(), " Done") case err := <-errC: - fmt.Fprintln(cmd.io.Stdout(), " Failed") + fmt.Fprintln(cmd.io.Output(), " Failed") return err case <-time.After(WaitTimeout): - fmt.Fprintln(cmd.io.Stdout(), " Failed") + fmt.Fprintln(cmd.io.Output(), " Failed") return ErrAddCredentialTimeout } } @@ -286,7 +286,7 @@ func (cmd *AccountInitCommand) createAccountKey() error { return ErrAccountAlreadyInitialized } - fmt.Fprint(cmd.io.Stdout(), "Finishing setup of your account...") + fmt.Fprint(cmd.io.Output(), "Finishing setup of your account...") key, err := cmd.credentialStore.Import() if err != nil { @@ -295,7 +295,7 @@ func (cmd *AccountInitCommand) createAccountKey() error { _, err = client.Accounts().Keys().Create(key.Verifier(), key.Encrypter()) if err != nil { - fmt.Fprintln(cmd.io.Stdout(), " Failed") + fmt.Fprintln(cmd.io.Output(), " Failed") return err } @@ -313,7 +313,7 @@ func (cmd *AccountInitCommand) createAccountKey() error { return err } - fmt.Fprintln(cmd.io.Stdout(), " Done") + fmt.Fprintln(cmd.io.Output(), " Done") return nil } diff --git a/internals/secrethub/account_inspect.go b/internals/secrethub/account_inspect.go index 27fc7114..bb81543c 100644 --- a/internals/secrethub/account_inspect.go +++ b/internals/secrethub/account_inspect.go @@ -50,7 +50,7 @@ func (cmd *AccountInspectCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), output) + fmt.Fprintln(cmd.io.Output(), output) return nil } diff --git a/internals/secrethub/account_inspect_test.go b/internals/secrethub/account_inspect_test.go index 63f226c7..c6d8f62a 100644 --- a/internals/secrethub/account_inspect_test.go +++ b/internals/secrethub/account_inspect_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -85,7 +85,7 @@ func TestAccountInspect(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Act @@ -93,7 +93,7 @@ func TestAccountInspect(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/acl_check.go b/internals/secrethub/acl_check.go index 27459ac2..0e9df30a 100644 --- a/internals/secrethub/acl_check.go +++ b/internals/secrethub/acl_check.go @@ -48,18 +48,18 @@ func (cmd *ACLCheckCommand) Run() error { if cmd.accountName != "" { for _, level := range levels { if level.Account.Name == cmd.accountName { - fmt.Fprintf(cmd.io.Stdout(), "%s\n", level.Permission.String()) + fmt.Fprintf(cmd.io.Output(), "%s\n", level.Permission.String()) return nil } } - fmt.Fprintln(cmd.io.Stdout(), api.PermissionNone.String()) + fmt.Fprintln(cmd.io.Output(), api.PermissionNone.String()) return nil } sort.Sort(api.SortAccessLevels(levels)) - tabWriter := tabwriter.NewWriter(cmd.io.Stdout(), 0, 4, 4, ' ', 0) + tabWriter := tabwriter.NewWriter(cmd.io.Output(), 0, 4, 4, ' ', 0) fmt.Fprintf(tabWriter, "%s\t%s\n", "PERMISSIONS", "ACCOUNT") for _, level := range levels { diff --git a/internals/secrethub/acl_check_test.go b/internals/secrethub/acl_check_test.go index 2a60d09a..93868a26 100644 --- a/internals/secrethub/acl_check_test.go +++ b/internals/secrethub/acl_check_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -105,7 +105,7 @@ func TestACLCheckCommand_Run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io lister := tc.lister @@ -126,7 +126,7 @@ func TestACLCheckCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, argPath, tc.listerArgPath) }) } diff --git a/internals/secrethub/acl_list.go b/internals/secrethub/acl_list.go index 5fa96b24..76f8ee8a 100644 --- a/internals/secrethub/acl_list.go +++ b/internals/secrethub/acl_list.go @@ -103,7 +103,7 @@ func (cmd *ACLListCommand) run() error { sort.Sort(api.SortDirPaths(paths)) - tabWriter := tabwriter.NewWriter(cmd.io.Stdout(), 0, 4, 4, ' ', 0) + tabWriter := tabwriter.NewWriter(cmd.io.Output(), 0, 4, 4, ' ', 0) fmt.Fprintf(tabWriter, "%s\t%s\t%s\t%s\n", "PATH", "PERMISSIONS", "LAST EDITED", "ACCOUNT") for _, p := range paths { diff --git a/internals/secrethub/acl_list_test.go b/internals/secrethub/acl_list_test.go index 1b993330..ef94ee7b 100644 --- a/internals/secrethub/acl_list_test.go +++ b/internals/secrethub/acl_list_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" faketimeformatter "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -149,7 +149,7 @@ func TestACLListCommand_run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io tc.cmd.newClient = func() (secrethub.ClientInterface, error) { @@ -164,7 +164,7 @@ func TestACLListCommand_run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/acl_rm.go b/internals/secrethub/acl_rm.go index 2fefd241..675eec7a 100644 --- a/internals/secrethub/acl_rm.go +++ b/internals/secrethub/acl_rm.go @@ -54,7 +54,7 @@ func (cmd *ACLRmCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -64,14 +64,14 @@ func (cmd *ACLRmCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Removing access rule...") + fmt.Fprintln(cmd.io.Output(), "Removing access rule...") err = client.AccessRules().Delete(cmd.path.Value(), cmd.accountName.Value()) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Removal complete! The access rule for %s on %s has been removed.\n", cmd.accountName, cmd.path) + fmt.Fprintf(cmd.io.Output(), "Removal complete! The access rule for %s on %s has been removed.\n", cmd.accountName, cmd.path) return nil } diff --git a/internals/secrethub/acl_rm_test.go b/internals/secrethub/acl_rm_test.go index f75dbe84..aae8f9ee 100644 --- a/internals/secrethub/acl_rm_test.go +++ b/internals/secrethub/acl_rm_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -86,7 +86,7 @@ func TestACLRmCommand_Run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.in) io.PromptErr = tc.promptErr tc.cmd.io = io @@ -110,7 +110,7 @@ func TestACLRmCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, io.PromptOut.String(), tc.promptOut) assert.Equal(t, argPath, tc.argPath) assert.Equal(t, argAccountName, tc.argAccountName) diff --git a/internals/secrethub/acl_set.go b/internals/secrethub/acl_set.go index ea401dd4..7d68866e 100644 --- a/internals/secrethub/acl_set.go +++ b/internals/secrethub/acl_set.go @@ -58,12 +58,12 @@ func (cmd *ACLSetCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } - fmt.Fprintf(cmd.io.Stdout(), "Setting access rule for %s at %s with %s\n", cmd.accountName, cmd.path, cmd.permission) + fmt.Fprintf(cmd.io.Output(), "Setting access rule for %s at %s with %s\n", cmd.accountName, cmd.path, cmd.permission) client, err := cmd.newClient() if err != nil { @@ -75,7 +75,7 @@ func (cmd *ACLSetCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Access rule set!") + fmt.Fprintln(cmd.io.Output(), "Access rule set!") return nil diff --git a/internals/secrethub/acl_set_test.go b/internals/secrethub/acl_set_test.go index 9033c10f..5f801a7f 100644 --- a/internals/secrethub/acl_set_test.go +++ b/internals/secrethub/acl_set_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -106,7 +107,7 @@ func TestACLSetCommand_Run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.in) io.PromptErr = tc.askErr tc.cmd.io = io @@ -116,7 +117,7 @@ func TestACLSetCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.stdout) + assert.Equal(t, io.Out.String(), tc.stdout) assert.Equal(t, io.PromptOut.String(), tc.promptOut) }) diff --git a/internals/secrethub/audit.go b/internals/secrethub/audit.go index c913bcc8..3871585d 100644 --- a/internals/secrethub/audit.go +++ b/internals/secrethub/audit.go @@ -2,40 +2,75 @@ package secrethub import ( "fmt" - "strings" - "text/tabwriter" + "io" + "strconv" + + "github.com/secrethub/secrethub-go/internals/errio" + + "github.com/secrethub/secrethub-cli/internals/secrethub/pager" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" "github.com/secrethub/secrethub-cli/internals/cli/ui" "github.com/secrethub/secrethub-cli/internals/secrethub/command" "github.com/secrethub/secrethub-go/pkg/secrethub" - "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" "github.com/secrethub/secrethub-go/internals/api" ) +var ( + errAudit = errio.Namespace("audit") + errNoSuchFormat = errAudit.Code("invalid_format").ErrorPref("invalid format: %s") +) + +const ( + defaultTerminalWidth = 80 + formatTable = "table" + formatJSON = "json" + pipedOutputLineLimit = 1000 +) + // AuditCommand is a command to audit a repo or a secret. type AuditCommand struct { - io ui.IO - path api.Path - useTimestamps bool - timeFormatter TimeFormatter - newClient newClientFunc - perPage int + io ui.IO + newPaginatedWriter func(io.Writer) (io.WriteCloser, error) + path api.Path + useTimestamps bool + timeFormatter TimeFormatter + newClient newClientFunc + terminalWidth func(int) (int, error) + perPage int + maxResults int + format string } // NewAuditCommand creates a new audit command. func NewAuditCommand(io ui.IO, newClient newClientFunc) *AuditCommand { return &AuditCommand{ - io: io, - newClient: newClient, + io: io, + newPaginatedWriter: pager.NewWithFallback, + newClient: newClient, + terminalWidth: func(fd int) (int, error) { + w, _, err := terminal.GetSize(fd) + return w, err + }, } } // Register registers the command, arguments and flags on the provided Registerer. func (cmd *AuditCommand) Register(r command.Registerer) { + defaultLimit := -1 + if cmd.io.IsOutputPiped() { + defaultLimit = pipedOutputLineLimit + } + clause := r.Command("audit", "Show the audit log.") clause.Arg("repo-path or secret-path", "Path to the repository or the secret to audit "+repoPathPlaceHolder+" or "+secretPathPlaceHolder).SetValue(&cmd.path) - clause.Flag("per-page", "number of audit events shown per page").Default("20").IntVar(&cmd.perPage) + clause.Flag("per-page", "Number of audit events shown per page").Default("20").Hidden().IntVar(&cmd.perPage) + clause.Flag("output-format", "Specify the format in which to output the log. Options are: table and json. If the output of the command is parsed by a script an alternative of the table format must be used.").HintOptions("table", "json").Default("table").StringVar(&cmd.format) + clause.Flag("max-results", "Specify the number of entries to list. If maxResults < 0 all entries are displayed. If the output of the command is piped, maxResults defaults to 1000.").Default(strconv.Itoa(defaultLimit)).IntVar(&cmd.maxResults) registerTimestampFlag(clause).BoolVar(&cmd.useTimestamps) command.BindAction(clause, cmd.Run) @@ -49,7 +84,11 @@ func (cmd *AuditCommand) Run() error { // beforeRun configures the command using the flag values. func (cmd *AuditCommand) beforeRun() { - cmd.timeFormatter = NewTimeFormatter(cmd.useTimestamps) + if cmd.format == formatJSON { + cmd.timeFormatter = NewTimeFormatter(true) + } else { + cmd.timeFormatter = NewTimeFormatter(cmd.useTimestamps) + } } // Run prints all audit events for the given repository or secret. @@ -63,13 +102,28 @@ func (cmd *AuditCommand) run() error { return err } - tabWriter := tabwriter.NewWriter(cmd.io.Stdout(), 0, 4, 4, ' ', 0) - header := strings.Join(auditTable.header(), "\t") + "\n" - fmt.Fprint(tabWriter, header) + paginatedWriter, err := cmd.newPaginatedWriter(cmd.io.Output()) + if err != nil { + return err + } + defer paginatedWriter.Close() + + var formatter listFormatter + if cmd.format == formatJSON { + formatter = newJSONFormatter(paginatedWriter, auditTable.header()) + } else if cmd.format == formatTable && cmd.io.IsOutputPiped() { + formatter = newLineFormatter(paginatedWriter) + } else if cmd.format == formatTable { + terminalWidth, err := cmd.terminalWidth(int(cmd.io.Stdout().Fd())) + if err != nil { + terminalWidth = defaultTerminalWidth + } + formatter = newTableFormatter(paginatedWriter, terminalWidth, auditTable.columns()) + } else { + return errNoSuchFormat(cmd.format) + } - i := 0 - for { - i++ + for lineCount := 0; lineCount != cmd.maxResults; lineCount++ { event, err := iter.Next() if err == iterator.Done { break @@ -82,29 +136,13 @@ func (cmd *AuditCommand) run() error { return err } - fmt.Fprint(tabWriter, strings.Join(row, "\t")+"\n") - - if i == cmd.perPage { - err = tabWriter.Flush() - if err != nil { - return err - } - i = 0 - - // wait for to continue. - _, err := ui.Ask(cmd.io, "Press to show more results. Press to exit.") - if err != nil { - return err - } - fmt.Fprint(tabWriter, header) + err = formatter.Write(row) + if err == pager.ErrPagerClosed { + break + } else if err != nil { + return err } } - - err = tabWriter.Flush() - if err != nil { - return err - } - return nil } @@ -150,24 +188,44 @@ func (cmd *AuditCommand) iterAndAuditTable() (secrethub.AuditEventIterator, audi return nil, nil, ErrNoValidRepoOrSecretPath } +type tableColumn struct { + name string + maxWidth int +} + type auditTable interface { header() []string row(event api.Audit) ([]string, error) + columns() []tableColumn } -func newBaseAuditTable(timeFormatter TimeFormatter) baseAuditTable { +func newBaseAuditTable(timeFormatter TimeFormatter, midColumns ...tableColumn) baseAuditTable { + columns := append([]tableColumn{ + {name: "author", maxWidth: 32}, + {name: "event", maxWidth: 22}, + }, midColumns...) + columns = append(columns, []tableColumn{ + {name: "IP address", maxWidth: 45}, + {name: "date", maxWidth: 22}, + }...) + return baseAuditTable{ + tableColumns: columns, timeFormatter: timeFormatter, } } type baseAuditTable struct { + tableColumns []tableColumn timeFormatter TimeFormatter } -func (table baseAuditTable) header(content ...string) []string { - res := append([]string{"AUTHOR", "EVENT"}, content...) - return append(res, "IP ADDRESS", "DATE") +func (table baseAuditTable) header() []string { + res := make([]string, len(table.tableColumns)) + for i, col := range table.tableColumns { + res[i] = col.name + } + return res } func (table baseAuditTable) row(event api.Audit, content ...string) ([]string, error) { @@ -180,6 +238,10 @@ func (table baseAuditTable) row(event api.Audit, content ...string) ([]string, e return append(res, event.IPAddress, table.timeFormatter.Format(event.LoggedAt)), nil } +func (table baseAuditTable) columns() []tableColumn { + return table.tableColumns +} + func newSecretAuditTable(timeFormatter TimeFormatter) secretAuditTable { return secretAuditTable{ baseAuditTable: newBaseAuditTable(timeFormatter), @@ -200,7 +262,7 @@ func (table secretAuditTable) row(event api.Audit) ([]string, error) { func newRepoAuditTable(tree *api.Tree, timeFormatter TimeFormatter) repoAuditTable { return repoAuditTable{ - baseAuditTable: newBaseAuditTable(timeFormatter), + baseAuditTable: newBaseAuditTable(timeFormatter, tableColumn{name: "event subject"}), tree: tree, } } @@ -210,10 +272,6 @@ type repoAuditTable struct { tree *api.Tree } -func (table repoAuditTable) header() []string { - return table.baseAuditTable.header("EVENT SUBJECT") -} - func (table repoAuditTable) row(event api.Audit) ([]string, error) { subject, err := getAuditSubject(event, table.tree) if err != nil { diff --git a/internals/secrethub/audit_repo_test.go b/internals/secrethub/audit_repo_test.go index f874d3f1..6d5bad37 100644 --- a/internals/secrethub/audit_repo_test.go +++ b/internals/secrethub/audit_repo_test.go @@ -1,16 +1,19 @@ package secrethub import ( + "bytes" "errors" + "io" "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" - "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" "github.com/secrethub/secrethub-go/pkg/secrethub" "github.com/secrethub/secrethub-go/pkg/secrethub/fakeclient" + + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" + "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" ) func TestAuditRepoCommand_run(t *testing.T) { @@ -38,9 +41,14 @@ func TestAuditRepoCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + terminalWidth: func(int) (int, error) { + return 83, nil + }, + format: formatTable, + perPage: 20, + maxResults: -1, }, - out: "AUTHOR EVENT EVENT SUBJECT IP ADDRESS DATE\n", + out: "", }, "create repo event": { cmd: AuditCommand{ @@ -77,13 +85,19 @@ func TestAuditRepoCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(_ int) (int, error) { + return 83, nil + }, timeFormatter: &fakes.TimeFormatter{ Response: "2018-01-01T01:01:01+01:00", }, }, - out: "AUTHOR EVENT EVENT SUBJECT IP ADDRESS DATE\n" + - "developer create.repo repo 127.0.0.1 2018-01-01T01:01:01+01:00\n", + out: "AUTHOR EVENT EVENT SUBJECT IP ADDRESS DATE \n" + + "developer create.repo repo 127.0.0.1 2018-01-01T01:0\n" + + " 1:01+01:00 \n", }, "client creation error": { cmd: AuditCommand{ @@ -91,6 +105,7 @@ func TestAuditRepoCommand_run(t *testing.T) { newClient: func() (secrethub.ClientInterface, error) { return nil, ErrCannotFindHomeDir() }, + format: formatTable, perPage: 20, }, err: ErrCannotFindHomeDir(), @@ -112,9 +127,15 @@ func TestAuditRepoCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(int) (int, error) { + return 83, nil + }, }, err: testError, + out: "", }, "get dir error": { cmd: AuditCommand{ @@ -133,6 +154,7 @@ func TestAuditRepoCommand_run(t *testing.T) { }, }, nil }, + format: formatTable, perPage: 20, }, err: testError, @@ -163,7 +185,12 @@ func TestAuditRepoCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(int) (int, error) { + return 83, nil + }, }, err: ErrInvalidAuditActor, out: "", @@ -194,7 +221,12 @@ func TestAuditRepoCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(int) (int, error) { + return 83, nil + }, }, err: ErrInvalidAuditSubject, out: "", @@ -204,15 +236,18 @@ func TestAuditRepoCommand_run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() - tc.cmd.io = io + buffer := bytes.Buffer{} + tc.cmd.newPaginatedWriter = func(_ io.Writer) (io.WriteCloser, error) { + return &fakes.Pager{Buffer: &buffer}, nil + } + tc.cmd.io = fakeui.NewIO(t) // Act err := tc.cmd.run() // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, buffer.String(), tc.out) }) } } diff --git a/internals/secrethub/audit_secret_test.go b/internals/secrethub/audit_secret_test.go index 6d644ead..4bffd59d 100644 --- a/internals/secrethub/audit_secret_test.go +++ b/internals/secrethub/audit_secret_test.go @@ -1,11 +1,14 @@ package secrethub import ( + "bytes" "errors" + "io" "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" + "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -54,13 +57,20 @@ func TestAuditSecretCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(_ int) (int, error) { + return 46, nil + }, timeFormatter: &fakes.TimeFormatter{ Response: "2018-01-01T01:01:01+01:00", }, }, - out: "AUTHOR EVENT IP ADDRESS DATE\n" + - "developer create.secret 127.0.0.1 2018-01-01T01:01:01+01:00\n", + out: "AUTHOR EVENT IP ADDRESS DATE \n" + + "developer create.sec 127.0.0.1 2018-01-01\n" + + " ret T01:01:01+\n" + + " 01:00 \n", }, "0 events": { cmd: AuditCommand{ @@ -79,13 +89,19 @@ func TestAuditSecretCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(_ int) (int, error) { + return 46, nil + }, }, - out: "AUTHOR EVENT IP ADDRESS DATE\n", + out: "", }, "error secret version": { cmd: AuditCommand{ path: "namespace/repo/secret:1", + format: formatTable, perPage: 20, }, err: ErrCannotAuditSecretVersion, @@ -96,6 +112,7 @@ func TestAuditSecretCommand_run(t *testing.T) { newClient: func() (secrethub.ClientInterface, error) { return nil, ErrCannotFindHomeDir() }, + format: formatTable, perPage: 20, }, err: ErrCannotFindHomeDir(), @@ -117,6 +134,7 @@ func TestAuditSecretCommand_run(t *testing.T) { }, }, nil }, + format: formatTable, perPage: 20, }, err: ErrCannotAuditDir, @@ -138,9 +156,15 @@ func TestAuditSecretCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(_ int) (int, error) { + return 46, nil + }, }, err: testError, + out: "", }, "invalid audit actor": { cmd: AuditCommand{ @@ -161,7 +185,12 @@ func TestAuditSecretCommand_run(t *testing.T) { }, }, nil }, - perPage: 20, + format: formatTable, + perPage: 20, + maxResults: -1, + terminalWidth: func(int) (int, error) { + return 83, nil + }, }, err: ErrInvalidAuditActor, out: "", @@ -171,15 +200,18 @@ func TestAuditSecretCommand_run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() - tc.cmd.io = io + buffer := bytes.Buffer{} + tc.cmd.newPaginatedWriter = func(_ io.Writer) (io.WriteCloser, error) { + return &fakes.Pager{Buffer: &buffer}, nil + } + tc.cmd.io = fakeui.NewIO(t) // Act err := tc.cmd.run() // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, buffer.String(), tc.out) }) } } diff --git a/internals/secrethub/clear.go b/internals/secrethub/clear.go index 30565103..83b1c33a 100644 --- a/internals/secrethub/clear.go +++ b/internals/secrethub/clear.go @@ -53,14 +53,14 @@ func (cmd *ClearCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Clearing secrets...") + fmt.Fprintln(cmd.io.Output(), "Clearing secrets...") err = presenter.Clear() if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Clear complete! The secrets are no longer available on the system.\n") + fmt.Fprintf(cmd.io.Output(), "Clear complete! The secrets are no longer available on the system.\n") return nil } diff --git a/internals/secrethub/client_factory.go b/internals/secrethub/client_factory.go index 9e17ff83..64ce19ff 100644 --- a/internals/secrethub/client_factory.go +++ b/internals/secrethub/client_factory.go @@ -1,6 +1,7 @@ package secrethub import ( + "net/http" "net/url" "strings" @@ -34,6 +35,7 @@ type clientFactory struct { client *secrethub.Client ServerURL *url.URL identityProvider string + proxyAddress *url.URL store CredentialConfig } @@ -41,6 +43,7 @@ type clientFactory struct { func (f *clientFactory) Register(r FlagRegisterer) { r.Flag("api-remote", "The SecretHub API address, don't set this unless you know what you're doing.").Hidden().URLVar(&f.ServerURL) r.Flag("identity-provider", "Enable native authentication with a trusted identity provider. Options are `aws` (IAM + KMS) and `key`. When you run the CLI on one of the platforms, you can leverage their respective identity providers to do native keyless authentication. Defaults to key, which uses the default credential sourced from a file, command-line flag, or environment variable. ").Default("key").StringVar(&f.identityProvider) + r.Flag("proxy-address", "Set to the address of a proxy to connect to the API through a proxy. The prepended scheme determines the proxy type (http, https and socks5 are supported). For example: `--proxy-address http://my-proxy:1234`").URLVar(&f.proxyAddress) } // NewClient returns a new client that is configured to use the remote that @@ -51,6 +54,8 @@ func (f *clientFactory) NewClient() (secrethub.ClientInterface, error) { switch strings.ToLower(f.identityProvider) { case "aws": credentialProvider = credentials.UseAWS() + case "gcp": + credentialProvider = credentials.UseGCPServiceAccount() case "key": credentialProvider = f.store.Provider() default: @@ -103,6 +108,14 @@ func (f *clientFactory) baseClientOptions() []secrethub.ClientOption { }), } + if f.proxyAddress != nil { + transport := http.DefaultTransport.(*http.Transport) + transport.Proxy = func(request *http.Request) (*url.URL, error) { + return f.proxyAddress, nil + } + options = append(options, secrethub.WithTransport(transport)) + } + if f.ServerURL != nil { options = append(options, secrethub.WithServerURL(f.ServerURL.String())) } diff --git a/internals/secrethub/client_factory_test.go b/internals/secrethub/client_factory_test.go new file mode 100644 index 00000000..b4899d91 --- /dev/null +++ b/internals/secrethub/client_factory_test.go @@ -0,0 +1,51 @@ +package secrethub + +import ( + "net/http" + "net/url" + "os" + "testing" + + "github.com/secrethub/secrethub-go/internals/assert" + "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" + + "github.com/secrethub/secrethub-cli/internals/cli/ui" +) + +func TestNewClientFactory_ProxyAddress(t *testing.T) { + proxyAddress, err := url.Parse("http://127.0.0.1:15555") + assert.OK(t, err) + + proxyReceivedRequest := false + go func() { + err := http.ListenAndServe(proxyAddress.Hostname()+":"+proxyAddress.Port(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proxyReceivedRequest = true + })) + if err != http.ErrServerClosed && err != nil { + t.Errorf("http server error: %s", err) + } + }() + + // Check if the configuration option takes precedence over the global HTTP_PROXY environment variable + os.Setenv("HTTP_PROXY", "http://test.unknown") + + // Make sure the actual API is not reached if proxying fails + serverAddress, err := url.Parse("http://test.unknown") + assert.OK(t, err) + + io := ui.NewUserIO() + store := NewCredentialConfig(io) + factory := clientFactory{ + identityProvider: "key", + store: store, + ServerURL: serverAddress, + proxyAddress: proxyAddress, + } + + client, err := factory.NewUnauthenticatedClient() + assert.OK(t, err) + + _, _ = client.Users().Create("test", "test@test.test", "test", credentials.CreateKey()) + assert.OK(t, err) + assert.Equal(t, proxyReceivedRequest, true) +} diff --git a/internals/secrethub/config_update_passphrase.go b/internals/secrethub/config_update_passphrase.go index a4c8be9f..c890b715 100644 --- a/internals/secrethub/config_update_passphrase.go +++ b/internals/secrethub/config_update_passphrase.go @@ -49,7 +49,7 @@ func (cmd *ConfigUpdatePassphraseCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } @@ -75,7 +75,7 @@ func (cmd *ConfigUpdatePassphraseCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Successfully updated passphrase!") + fmt.Fprintln(cmd.io.Output(), "Successfully updated passphrase!") return nil } diff --git a/internals/secrethub/credential_backup.go b/internals/secrethub/credential_backup.go index 4d09eb7a..d4673edd 100644 --- a/internals/secrethub/credential_backup.go +++ b/internals/secrethub/credential_backup.go @@ -50,7 +50,7 @@ func (cmd *CredentialBackupCommand) Run() error { return err } if !ok { - fmt.Fprintln(cmd.io.Stdout(), "Aborting") + fmt.Fprintln(cmd.io.Output(), "Aborting") return nil } @@ -66,8 +66,8 @@ func (cmd *CredentialBackupCommand) Run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "This is your backup code: \n%s\n", code) - fmt.Fprintln(cmd.io.Stdout(), "Write it down and store it in a safe location! "+ + fmt.Fprintf(cmd.io.Output(), "This is your backup code: \n%s\n", code) + fmt.Fprintln(cmd.io.Output(), "Write it down and store it in a safe location! "+ "You can restore your account by running `secrethub init`.") return nil diff --git a/internals/secrethub/credential_disable.go b/internals/secrethub/credential_disable.go index 0bfeba0f..dfc6b247 100644 --- a/internals/secrethub/credential_disable.go +++ b/internals/secrethub/credential_disable.go @@ -60,7 +60,7 @@ func (cmd *CredentialDisableCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), + fmt.Fprintln(cmd.io.Output(), "A disabled credential can no longer be used to access SecretHub. "+ "This process can currently not be reversed.") @@ -70,7 +70,7 @@ func (cmd *CredentialDisableCommand) Run() error { return err } if !ok { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -80,7 +80,7 @@ func (cmd *CredentialDisableCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Credential disabled.") + fmt.Fprintln(cmd.io.Output(), "Credential disabled.") return nil } diff --git a/internals/secrethub/credential_list.go b/internals/secrethub/credential_list.go index 8d97cef8..31801f11 100644 --- a/internals/secrethub/credential_list.go +++ b/internals/secrethub/credential_list.go @@ -46,7 +46,7 @@ func (cmd *CredentialListCommand) Run() error { timeFormatter := NewTimeFormatter(cmd.useTimestamps) - w := tabwriter.NewWriter(cmd.io.Stdout(), 0, 2, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.io.Output(), 0, 2, 2, ' ', 0) fmt.Fprintln(w, "FINGERPRINT\t"+ "TYPE\t"+ diff --git a/internals/secrethub/env_ls.go b/internals/secrethub/env_ls.go index 2f4908be..e4835701 100644 --- a/internals/secrethub/env_ls.go +++ b/internals/secrethub/env_ls.go @@ -43,7 +43,7 @@ func (cmd *EnvListCommand) Run() error { // For now only environment variables in which a secret is loaded are printed. // TODO: Make this behavior configurable. if value.containsSecret() { - fmt.Fprintln(cmd.io.Stdout(), key) + fmt.Fprintln(cmd.io.Output(), key) } } diff --git a/internals/secrethub/env_read.go b/internals/secrethub/env_read.go index 3f1059bb..028b2f69 100644 --- a/internals/secrethub/env_read.go +++ b/internals/secrethub/env_read.go @@ -54,7 +54,7 @@ func (cmd *EnvReadCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), res) + fmt.Fprintln(cmd.io.Output(), res) return nil } diff --git a/internals/secrethub/fakes/pager.go b/internals/secrethub/fakes/pager.go new file mode 100644 index 00000000..9d3b3f5a --- /dev/null +++ b/internals/secrethub/fakes/pager.go @@ -0,0 +1,17 @@ +package fakes + +import "bytes" + +// Pager is a mock pager that remembers what is written to it. +// It can also be used as a fake io.WriteCloser. +type Pager struct { + Buffer *bytes.Buffer +} + +func (f *Pager) Write(p []byte) (n int, err error) { + return f.Buffer.Write(p) +} + +func (f *Pager) Close() error { + return nil +} diff --git a/internals/secrethub/generate.go b/internals/secrethub/generate.go index c5ce7452..67fa949e 100644 --- a/internals/secrethub/generate.go +++ b/internals/secrethub/generate.go @@ -133,7 +133,7 @@ func (cmd *GenerateSecretCommand) run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "A randomly generated secret has been written to %s:%d.\n", path, version.Version) + fmt.Fprintf(cmd.io.Output(), "A randomly generated secret has been written to %s:%d.\n", path, version.Version) if cmd.copyToClipboard { err = WriteClipboardAutoClear(data, cmd.clearClipboardAfter, cmd.clipper) @@ -142,7 +142,7 @@ func (cmd *GenerateSecretCommand) run() error { } fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "The generated value has been copied to the clipboard. It will be cleared after %s.\n", units.HumanDuration(cmd.clearClipboardAfter), ) diff --git a/internals/secrethub/generate_test.go b/internals/secrethub/generate_test.go index 27aa4d92..086d41d4 100644 --- a/internals/secrethub/generate_test.go +++ b/internals/secrethub/generate_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -184,7 +184,7 @@ func TestGenerateSecretCommand_run(t *testing.T) { }, tc.clientCreationErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Act @@ -194,7 +194,7 @@ func TestGenerateSecretCommand_run(t *testing.T) { assert.Equal(t, err, tc.err) assert.Equal(t, argPath, tc.path) assert.Equal(t, argData, tc.data) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/init.go b/internals/secrethub/init.go index 51c77f3c..dce0c9ac 100644 --- a/internals/secrethub/init.go +++ b/internals/secrethub/init.go @@ -32,7 +32,7 @@ func NewInitCommand(io ui.IO, newClient newClientFunc, newClientWithoutCredentia newClient: newClient, newClientWithoutCredentials: newClientWithoutCredentials, credentialStore: credentialStore, - progressPrinter: progress.NewPrinter(io.Stdout(), 500*time.Millisecond), + progressPrinter: progress.NewPrinter(io.Output(), 500*time.Millisecond), } } @@ -70,7 +70,7 @@ func (cmd *InitCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -136,13 +136,13 @@ func (cmd *InitCommand) Run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "This backup code can be used to recover the account `%s`\n", me.Username) + fmt.Fprintf(cmd.io.Output(), "This backup code can be used to recover the account `%s`\n", me.Username) ok, err := ui.AskYesNo(cmd.io, "Do you want to continue?", ui.DefaultYes) if err != nil { return err } if !ok { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } diff --git a/internals/secrethub/inject.go b/internals/secrethub/inject.go index 117fb581..e8718bd9 100644 --- a/internals/secrethub/inject.go +++ b/internals/secrethub/inject.go @@ -91,11 +91,11 @@ func (cmd *InjectCommand) Run() error { return ErrReadFile(cmd.inFile, err) } } else { - if !cmd.io.Stdin().IsPiped() { + if !cmd.io.IsInputPiped() { return ErrNoDataOnStdin } - raw, err = ioutil.ReadAll(cmd.io.Stdin()) + raw, err = ioutil.ReadAll(cmd.io.Input()) if err != nil { return err } @@ -135,11 +135,11 @@ func (cmd *InjectCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), fmt.Sprintf("Copied injected template to clipboard. It will be cleared after %s.", units.HumanDuration(cmd.clearClipboardAfter))) + fmt.Fprintln(cmd.io.Output(), fmt.Sprintf("Copied injected template to clipboard. It will be cleared after %s.", units.HumanDuration(cmd.clearClipboardAfter))) } else if cmd.outFile != "" { _, err := os.Stat(cmd.outFile) if err == nil && !cmd.force { - if cmd.io.Stdout().IsPiped() { + if cmd.io.IsOutputPiped() { return ErrFileAlreadyExists } @@ -156,7 +156,7 @@ func (cmd *InjectCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -171,9 +171,9 @@ func (cmd *InjectCommand) Run() error { return ErrCannotWrite(err) } - fmt.Fprintf(cmd.io.Stdout(), "%s\n", absPath) + fmt.Fprintf(cmd.io.Output(), "%s\n", absPath) } else { - fmt.Fprintf(cmd.io.Stdout(), "%s", posix.AddNewLine(out)) + fmt.Fprintf(cmd.io.Output(), "%s", posix.AddNewLine(out)) } return nil diff --git a/internals/secrethub/inspect_secret.go b/internals/secrethub/inspect_secret.go index eb6f212d..fc1ea195 100644 --- a/internals/secrethub/inspect_secret.go +++ b/internals/secrethub/inspect_secret.go @@ -49,7 +49,7 @@ func (cmd *InspectSecretCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), output) + fmt.Fprintln(cmd.io.Output(), output) return nil } diff --git a/internals/secrethub/inspect_secret_test.go b/internals/secrethub/inspect_secret_test.go index be699c74..42edbaa8 100644 --- a/internals/secrethub/inspect_secret_test.go +++ b/internals/secrethub/inspect_secret_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -104,7 +104,7 @@ func TestInspectSecret_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Act @@ -112,7 +112,7 @@ func TestInspectSecret_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } diff --git a/internals/secrethub/inspect_secret_version.go b/internals/secrethub/inspect_secret_version.go index 3e9f5c1a..a372e3f2 100644 --- a/internals/secrethub/inspect_secret_version.go +++ b/internals/secrethub/inspect_secret_version.go @@ -44,7 +44,7 @@ func (cmd *InspectSecretVersionCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), output) + fmt.Fprintln(cmd.io.Output(), output) return nil } diff --git a/internals/secrethub/inspect_secret_version_test.go b/internals/secrethub/inspect_secret_version_test.go index c453926d..dfd0653a 100644 --- a/internals/secrethub/inspect_secret_version_test.go +++ b/internals/secrethub/inspect_secret_version_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -79,7 +79,7 @@ func TestInspectSecretVersion_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Act @@ -87,7 +87,7 @@ func TestInspectSecretVersion_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } diff --git a/internals/secrethub/list.go b/internals/secrethub/list.go index c99d0a63..8a51f99e 100644 --- a/internals/secrethub/list.go +++ b/internals/secrethub/list.go @@ -71,7 +71,7 @@ func (cmd *LsCommand) Run() error { return err } - err = printVersions(cmd.io.Stdout(), cmd.quiet, timeFormatter, version) + err = printVersions(cmd.io.Output(), cmd.quiet, timeFormatter, version) if err != nil { return err } @@ -88,7 +88,7 @@ func (cmd *LsCommand) Run() error { } else if err != nil && !api.IsErrNotFound(err) { return err } else if err == nil { - err = printDir(cmd.io.Stdout(), cmd.quiet, dirFS.RootDir, timeFormatter) + err = printDir(cmd.io.Output(), cmd.quiet, dirFS.RootDir, timeFormatter) if err != nil { return err } @@ -106,7 +106,7 @@ func (cmd *LsCommand) Run() error { return err } - err = printVersions(cmd.io.Stdout(), cmd.quiet, timeFormatter, versions...) + err = printVersions(cmd.io.Output(), cmd.quiet, timeFormatter, versions...) if err != nil { return err } diff --git a/internals/secrethub/list_formatters.go b/internals/secrethub/list_formatters.go new file mode 100644 index 00000000..7ebb425f --- /dev/null +++ b/internals/secrethub/list_formatters.go @@ -0,0 +1,208 @@ +package secrethub + +import ( + "encoding/json" + "fmt" + "io" + "strings" +) + +type listFormatter interface { + Write([]string) error +} + +func newLineFormatter(writer io.Writer) lineFormatter { + return lineFormatter{writer: writer} +} + +// lineFormatter returns a formatter that formats the given table into lines of text with unaligned columns. +type lineFormatter struct { + writer io.Writer +} + +// Write writes a the given row entries separated by '\t' characters. +func (l lineFormatter) Write(line []string) error { + _, err := l.writer.Write([]byte(strings.Join(line, "\t") + "\n")) + return err +} + +// newJSONFormatter returns a table formatter that formats the given table rows as json. +func newJSONFormatter(writer io.Writer, fieldNames []string) *jsonFormatter { + for i := range fieldNames { + fieldNames[i] = toPascalCase(fieldNames[i]) + } + return &jsonFormatter{ + encoder: json.NewEncoder(writer), + fields: fieldNames, + } +} + +func toPascalCase(s string) string { + return strings.ReplaceAll(strings.Title(s), " ", "") +} + +type jsonFormatter struct { + encoder *json.Encoder + fields []string +} + +// Write writes the json representation of the given row +// with the configured field names as keys and the provided values +func (f *jsonFormatter) Write(values []string) error { + if len(f.fields) != len(values) { + return fmt.Errorf("unexpected number of json fields") + } + + jsonMap := make(map[string]string) + for i, element := range values { + jsonMap[f.fields[i]] = element + } + + return f.encoder.Encode(jsonMap) +} + +// newTableFormatter returns a list formatter that formats entries in a table. +func newTableFormatter(writer io.Writer, tableWidth int, columns []tableColumn) *tableFormatter { + return &tableFormatter{ + writer: writer, + tableWidth: tableWidth, + columns: columns, + } +} + +type tableFormatter struct { + tableWidth int + writer io.Writer + computedColumnWidths []int + columns []tableColumn + headerPrinted bool +} + +// Write writes the given values formatted in a table with the configured column widths and names. +// The header of the table is printed on the first call, before any other value. +func (f *tableFormatter) Write(values []string) error { + if !f.headerPrinted { + header := make([]string, len(f.columns)) + for i, col := range f.columns { + header[i] = strings.ToUpper(col.name) + } + formattedHeader := f.formatRow(header) + _, err := f.writer.Write(formattedHeader) + if err != nil { + return err + } + f.headerPrinted = true + } + + formattedRow := f.formatRow(values) + _, err := f.writer.Write(formattedRow) + return err +} + +// formatRow formats the given table row to fit the configured width by +// giving each cell an equal width and wrapping the text in cells that exceed it. +func (f *tableFormatter) formatRow(row []string) []byte { + columnWidths := f.columnWidths() + grid := f.fitToColumns(row, columnWidths) + + strRes := strings.Builder{} + for _, row := range grid { + strRes.WriteString(strings.Join(row, " ") + "\n") + } + return []byte(strRes.String()) +} + +// fitToColumns returns a the given row split over a matrix in which all columns have equal length. +// Longer values are split over multiple cells and shorter (or empty) ones are padded with " ". +func (f *tableFormatter) fitToColumns(cells []string, columnWidths []int) [][]string { + maxLinesPerCell := f.lineCount(cells, columnWidths) + + grid := make([][]string, maxLinesPerCell) + for i := 0; i < maxLinesPerCell; i++ { + grid[i] = make([]string, len(cells)) + } + + for i, cell := range cells { + columnWidth := columnWidths[i] + lineCount := len(cell) / columnWidth + for j := 0; j < lineCount; j++ { + begin := j * columnWidth + end := (j + 1) * columnWidth + grid[j][i] = cell[begin:end] + } + + charactersLeft := len(cell) % columnWidth + if charactersLeft != 0 { + grid[lineCount][i] = cell[len(cell)-charactersLeft:] + strings.Repeat(" ", columnWidth-charactersLeft) + } else if lineCount < maxLinesPerCell { + grid[lineCount][i] = strings.Repeat(" ", columnWidth) + } + + for j := lineCount + 1; j < maxLinesPerCell; j++ { + grid[j][i] = strings.Repeat(" ", columnWidth) + } + } + + return grid +} + +// lineCount returns the number of lines the given table row will occupy after splitting the +// cell values that exceed their column width. +func (f *tableFormatter) lineCount(row []string, widths []int) int { + maxLinesPerCell := 1 + for i, value := range row { + lines := len(value) / widths[i] + if len(value)%widths[i] != 0 { + lines++ + } + if lines > maxLinesPerCell { + maxLinesPerCell = lines + } + } + return maxLinesPerCell +} + +// columnWidths returns the width of each column based on their maximum widths +// and the table width. +func (f *tableFormatter) columnWidths() []int { + if f.computedColumnWidths != nil { + return f.computedColumnWidths + } + adjustedWidths := make([]int, len(f.columns)) + + // Distribute the table width equally between all columns and leave a margin of 2 characters between them. + columnsLeft := len(f.columns) + widthLeft := f.tableWidth - 2*(len(f.columns)-1) + widthPerColumn := widthLeft / columnsLeft + adjusted := true + for adjusted { + adjusted = false + for i, col := range f.columns { + // fix columns that have a smaller maximum width than the current width/column and have not been fixed yet. + if adjustedWidths[i] == 0 && col.maxWidth != 0 && col.maxWidth < widthPerColumn { + adjustedWidths[i] = col.maxWidth + widthLeft -= col.maxWidth + columnsLeft-- + adjusted = true + } + } + // If all columns are fixed to their max width, distribute the remaining width equally between all of them. + if columnsLeft == 0 { + for i := range adjustedWidths { + adjustedWidths[i] += widthLeft / len(adjustedWidths) + } + break + } + // Recalculate the width/column for the remaining unadjusted columns. + widthPerColumn = widthLeft / columnsLeft + } + + // distribute the remaining width equally between columns with no maximum width. + for i := range adjustedWidths { + if adjustedWidths[i] == 0 { + adjustedWidths[i] = widthPerColumn + } + } + f.computedColumnWidths = adjustedWidths + return adjustedWidths +} diff --git a/internals/secrethub/list_formatters_test.go b/internals/secrethub/list_formatters_test.go new file mode 100644 index 00000000..c07945ff --- /dev/null +++ b/internals/secrethub/list_formatters_test.go @@ -0,0 +1,130 @@ +package secrethub + +import ( + "strings" + "testing" + + "github.com/secrethub/secrethub-go/internals/assert" +) + +func Test_columnFormatter_columnWidths(t *testing.T) { + cases := map[string]struct { + formatter tableFormatter + expected []int + }{ + "all columns fit": { + formatter: tableFormatter{ + tableWidth: 102, + columns: []tableColumn{ + {maxWidth: 10}, + {maxWidth: 10}, + }, + }, + expected: []int{50, 50}, + }, + "no columns fit": { + formatter: tableFormatter{ + tableWidth: 12, + columns: []tableColumn{ + {maxWidth: 10}, + {maxWidth: 10}, + }, + }, + expected: []int{5, 5}, + }, + "one column fits": { + formatter: tableFormatter{ + tableWidth: 27, + columns: []tableColumn{ + {maxWidth: 10}, + {maxWidth: 20}, + }, + }, + expected: []int{10, 15}, + }, + "multiple adjustments": { + formatter: tableFormatter{ + tableWidth: 106, + columns: []tableColumn{ + {maxWidth: 27}, + {maxWidth: 26}, + {maxWidth: 25}, + {maxWidth: 20}, + }, + }, + expected: []int{27, 26, 25, 20}, + }, + "no max width for some all fit": { + formatter: tableFormatter{ + tableWidth: 64, + columns: []tableColumn{ + {maxWidth: 15}, + {}, + {maxWidth: 15}, + }, + }, + expected: []int{15, 30, 15}, + }, + "no max width for some not all fit": { + formatter: tableFormatter{ + tableWidth: 64, + columns: []tableColumn{ + {maxWidth: 50}, + {}, + {maxWidth: 10}, + }, + }, + expected: []int{25, 25, 10}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + result := tc.formatter.columnWidths() + assert.Equal(t, result, tc.expected) + }) + } +} + +func Test_columnFormatter_formatRow(t *testing.T) { + cases := map[string]struct { + formatter tableFormatter + row []string + expected string + }{ + "all cells fit": { + formatter: tableFormatter{ + tableWidth: 102, + computedColumnWidths: []int{50, 50}, + columns: []tableColumn{{}, {}}, + }, + row: []string{"foo", "bar"}, + expected: "foo" + strings.Repeat(" ", 47) + " " + "bar" + strings.Repeat(" ", 47) + "\n", + }, + "wrapping": { + formatter: tableFormatter{ + tableWidth: 6, + computedColumnWidths: []int{2, 2}, + columns: []tableColumn{{}, {}}, + }, + row: []string{"foo", "bar"}, + expected: "fo ba\no r \n", + }, + "fits exactly": { + formatter: tableFormatter{ + tableWidth: 8, + computedColumnWidths: []int{3, 3}, + columns: []tableColumn{{}, {}}, + }, + row: []string{"foo", "bar"}, + expected: "foo bar\n", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + result := tc.formatter.formatRow(tc.row) + assert.Equal(t, string(result), tc.expected) + }) + } +} diff --git a/internals/secrethub/mkdir.go b/internals/secrethub/mkdir.go index 226c76b0..37e94438 100644 --- a/internals/secrethub/mkdir.go +++ b/internals/secrethub/mkdir.go @@ -2,11 +2,13 @@ package secrethub import ( "fmt" + "os" + + "github.com/secrethub/secrethub-go/internals/api" + "github.com/secrethub/secrethub-go/pkg/secrethub" "github.com/secrethub/secrethub-cli/internals/cli/ui" "github.com/secrethub/secrethub-cli/internals/secrethub/command" - - "github.com/secrethub/secrethub-go/internals/api" ) // Errors @@ -17,7 +19,7 @@ var ( // MkDirCommand creates a new directory inside a repository. type MkDirCommand struct { io ui.IO - path api.DirPath + paths dirPathList parents bool newClient newClientFunc } @@ -33,7 +35,7 @@ func NewMkDirCommand(io ui.IO, newClient newClientFunc) *MkDirCommand { // Register registers the command, arguments and flags on the provided Registerer. func (cmd *MkDirCommand) Register(r command.Registerer) { clause := r.Command("mkdir", "Create a new directory.") - clause.Arg("dir-path", "The path to the directory").Required().PlaceHolder(dirPathPlaceHolder).SetValue(&cmd.path) + clause.Arg("dir-paths", "The paths to the directories").Required().PlaceHolder(dirPathsPlaceHolder).SetValue(&cmd.paths) clause.Flag("parents", "Create parent directories if needed. Does not error when directories already exist.").BoolVar(&cmd.parents) command.BindAction(clause, cmd.Run) @@ -41,28 +43,50 @@ func (cmd *MkDirCommand) Register(r command.Registerer) { // Run executes the command. func (cmd *MkDirCommand) Run() error { - if cmd.path.IsRepoPath() { - return ErrMkDirOnRootDir - } - client, err := cmd.newClient() if err != nil { return err } - if cmd.parents { - err = client.Dirs().CreateAll(cmd.path.Value()) - if err != nil { - return err - } - } else { - _, err = client.Dirs().Create(cmd.path.Value()) + for _, path := range cmd.paths { + err := cmd.createDirectory(client, path) if err != nil { - return err + fmt.Fprintf(os.Stderr, "Could not create a new directory at %s: %s\n", path, err) + } else { + fmt.Fprintf(cmd.io.Output(), "Created a new directory at %s\n", path) } } + return nil +} - fmt.Fprintf(cmd.io.Stdout(), "Created a new directory at %s\n", cmd.path) +// createDirectory validates the given path and creates a directory on it. +func (cmd *MkDirCommand) createDirectory(client secrethub.ClientInterface, path string) error { + dirPath, err := api.NewDirPath(path) + if err != nil { + return err + } + if dirPath.IsRepoPath() { + return ErrMkDirOnRootDir + } + if cmd.parents { + return client.Dirs().CreateAll(dirPath.Value()) + } + _, err = client.Dirs().Create(dirPath.Value()) + return err +} + +// dirPathList represents the value of a repeatable directory path argument. +type dirPathList []string + +func (d *dirPathList) String() string { + return "" +} +func (d *dirPathList) Set(path string) error { + *d = append(*d, path) return nil } + +func (d *dirPathList) IsCumulative() bool { + return true +} diff --git a/internals/secrethub/mkdir_test.go b/internals/secrethub/mkdir_test.go index acfd2b56..bbf27b1e 100644 --- a/internals/secrethub/mkdir_test.go +++ b/internals/secrethub/mkdir_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/api/uuid" @@ -16,13 +16,13 @@ import ( func TestMkDirCommand(t *testing.T) { cases := map[string]struct { - path string + paths []string newClient func() (secrethub.ClientInterface, error) stdout string err error }{ "success": { - path: "namespace/repo/dir", + paths: []string{"namespace/repo/dir"}, newClient: func() (secrethub.ClientInterface, error) { return fakeclient.Client{ DirService: &fakeclient.DirService{ @@ -42,13 +42,29 @@ func TestMkDirCommand(t *testing.T) { stdout: "Created a new directory at namespace/repo/dir\n", err: nil, }, - "on root dir": { - path: "namespace/repo", - stdout: "", - err: ErrMkDirOnRootDir, + "success multiple dirs": { + paths: []string{"namespace/repo/dir1", "namespace/repo/dir2"}, + newClient: func() (secrethub.ClientInterface, error) { + return fakeclient.Client{ + DirService: &fakeclient.DirService{ + CreateFunc: func(path string) (*api.Dir, error) { + return &api.Dir{ + DirID: uuid.New(), + BlindName: "blindname", + Name: "dir", + Status: api.StatusOK, + CreatedAt: time.Now().UTC(), + LastModifiedAt: time.Now().UTC(), + }, nil + }, + }, + }, nil + }, + stdout: "Created a new directory at namespace/repo/dir1\nCreated a new directory at namespace/repo/dir2\n", + err: nil, }, "new client fails": { - path: "namespace/repo/dir", + paths: []string{"namespace/repo/dir"}, newClient: func() (secrethub.ClientInterface, error) { return nil, errio.Namespace("test").Code("foo").Error("bar") }, @@ -56,7 +72,7 @@ func TestMkDirCommand(t *testing.T) { err: errio.Namespace("test").Code("foo").Error("bar"), }, "create dir fails": { - path: "namespace/repo/dir", + paths: []string{"namespace/repo/dir"}, newClient: func() (secrethub.ClientInterface, error) { return fakeclient.Client{ DirService: &fakeclient.DirService{ @@ -67,23 +83,135 @@ func TestMkDirCommand(t *testing.T) { }, nil }, stdout: "", - err: api.ErrDirAlreadyExists, + }, + "create dir fails on second dir": { + paths: []string{"namespace/repo/dir1", "namespace/repo/dir2"}, + newClient: func() (secrethub.ClientInterface, error) { + return fakeclient.Client{ + DirService: &fakeclient.DirService{ + CreateFunc: func(path string) (*api.Dir, error) { + if path == "namespace/repo/dir2" { + return nil, api.ErrDirAlreadyExists + } + return &api.Dir{ + DirID: uuid.New(), + BlindName: "blindname", + Name: "dir", + Status: api.StatusOK, + CreatedAt: time.Now().UTC(), + LastModifiedAt: time.Now().UTC(), + }, nil + }, + }, + }, nil + }, + stdout: "Created a new directory at namespace/repo/dir1\n", + }, + "create dir fails on first dir": { + paths: []string{"namespace/repo/dir1", "namespace/repo/dir2"}, + newClient: func() (secrethub.ClientInterface, error) { + return fakeclient.Client{ + DirService: &fakeclient.DirService{ + CreateFunc: func(path string) (*api.Dir, error) { + if path == "namespace/repo/dir1" { + return nil, api.ErrDirAlreadyExists + } + return &api.Dir{ + DirID: uuid.New(), + BlindName: "blindname", + Name: "dir", + Status: api.StatusOK, + CreatedAt: time.Now().UTC(), + LastModifiedAt: time.Now().UTC(), + }, nil + }, + }, + }, nil + }, + stdout: "Created a new directory at namespace/repo/dir2\n", }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { - io := ui.NewFakeIO() + io := fakeui.NewIO(t) + dirPaths := dirPathList{} + for _, path := range tc.paths { + _ = dirPaths.Set(path) + } cmd := MkDirCommand{ io: io, - path: api.DirPath(tc.path), + paths: dirPaths, newClient: tc.newClient, } err := cmd.Run() assert.Equal(t, err, tc.err) - assert.Equal(t, tc.stdout, io.StdOut.String()) + assert.Equal(t, tc.stdout, io.Out.String()) + }) + } +} + +func TestCreateDirectory(t *testing.T) { + cases := map[string]struct { + client secrethub.ClientInterface + path string + err error + }{ + "success": { + client: fakeclient.Client{ + DirService: &fakeclient.DirService{ + CreateFunc: func(path string) (*api.Dir, error) { + return &api.Dir{ + DirID: uuid.New(), + BlindName: "blindname", + Name: "dir", + Status: api.StatusOK, + CreatedAt: time.Now().UTC(), + LastModifiedAt: time.Now().UTC(), + }, nil + }, + }, + }, + path: "namespace/repo/dir", + }, + "root dir": { + client: fakeclient.Client{ + DirService: &fakeclient.DirService{ + CreateFunc: func(path string) (*api.Dir, error) { + return &api.Dir{ + DirID: uuid.New(), + BlindName: "blindname", + Name: "dir", + Status: api.StatusOK, + CreatedAt: time.Now().UTC(), + LastModifiedAt: time.Now().UTC(), + }, nil + }, + }, + }, + path: "namespace/repo", + err: ErrMkDirOnRootDir, + }, + "create dir fails": { + client: fakeclient.Client{ + DirService: &fakeclient.DirService{ + CreateFunc: func(path string) (*api.Dir, error) { + return nil, api.ErrDirAlreadyExists + }, + }, + }, + path: "namespace/repo/dir", + err: api.ErrDirAlreadyExists, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + cmd := MkDirCommand{} + err := cmd.createDirectory(tc.client, tc.path) + assert.Equal(t, err, tc.err) }) } } diff --git a/internals/secrethub/org_init.go b/internals/secrethub/org_init.go index ce39c8fc..b936b906 100644 --- a/internals/secrethub/org_init.go +++ b/internals/secrethub/org_init.go @@ -47,7 +47,7 @@ func (cmd *OrgInitCommand) Run() error { return ErrMissingFlags } else if !cmd.force && incompleteInput { fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "Before initializing a new organization, we need to know a few things about your organization. "+ "Please answer the questions below, followed by an [ENTER]\n\n", ) @@ -68,7 +68,7 @@ func (cmd *OrgInitCommand) Run() error { } // Print a whitespace line here for readability. - fmt.Fprintln(cmd.io.Stdout(), "") + fmt.Fprintln(cmd.io.Output(), "") } client, err := cmd.newClient() @@ -76,14 +76,14 @@ func (cmd *OrgInitCommand) Run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "Creating organization...\n") + fmt.Fprintf(cmd.io.Output(), "Creating organization...\n") resp, err := client.Orgs().Create(cmd.name.Value(), cmd.description) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Creation complete! The organization %s is now ready to use.\n", resp.Name) + fmt.Fprintf(cmd.io.Output(), "Creation complete! The organization %s is now ready to use.\n", resp.Name) return nil } diff --git a/internals/secrethub/org_init_test.go b/internals/secrethub/org_init_test.go index 8e90c9df..c298d1d3 100644 --- a/internals/secrethub/org_init_test.go +++ b/internals/secrethub/org_init_test.go @@ -3,7 +3,7 @@ package secrethub import ( "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -73,7 +73,7 @@ func TestOrgInitCommand_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -81,7 +81,7 @@ func TestOrgInitCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/org_inspect.go b/internals/secrethub/org_inspect.go index bdd58fad..123d2df6 100644 --- a/internals/secrethub/org_inspect.go +++ b/internals/secrethub/org_inspect.go @@ -62,7 +62,7 @@ func (cmd *OrgInspectCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), output) + fmt.Fprintln(cmd.io.Output(), output) return nil } diff --git a/internals/secrethub/org_inspect_test.go b/internals/secrethub/org_inspect_test.go index 10085d83..063e76f0 100644 --- a/internals/secrethub/org_inspect_test.go +++ b/internals/secrethub/org_inspect_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -156,7 +156,7 @@ func TestOrgInspectCommand_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -164,7 +164,7 @@ func TestOrgInspectCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/org_invite.go b/internals/secrethub/org_invite.go index bf7fd172..459193ce 100644 --- a/internals/secrethub/org_invite.go +++ b/internals/secrethub/org_invite.go @@ -51,7 +51,7 @@ func (cmd *OrgInviteCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -61,14 +61,14 @@ func (cmd *OrgInviteCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Inviting user...") + fmt.Fprintln(cmd.io.Output(), "Inviting user...") resp, err := client.Orgs().Members().Invite(cmd.orgName.Value(), cmd.username, cmd.role) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Invite complete! The user %s is now %s of the %s organization.\n", resp.User.Username, resp.Role, cmd.orgName) + fmt.Fprintf(cmd.io.Output(), "Invite complete! The user %s is now %s of the %s organization.\n", resp.User.Username, resp.Role, cmd.orgName) return nil } diff --git a/internals/secrethub/org_invite_test.go b/internals/secrethub/org_invite_test.go index 52322b74..0aebe43c 100644 --- a/internals/secrethub/org_invite_test.go +++ b/internals/secrethub/org_invite_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -111,7 +111,7 @@ func TestOrgInviteCommand_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.in) tc.cmd.io = io @@ -120,7 +120,7 @@ func TestOrgInviteCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, io.PromptOut.String(), tc.promptOut) }) } diff --git a/internals/secrethub/org_list_users.go b/internals/secrethub/org_list_users.go index bdc7b743..7bfbedc7 100644 --- a/internals/secrethub/org_list_users.go +++ b/internals/secrethub/org_list_users.go @@ -63,7 +63,7 @@ func (cmd *OrgListUsersCommand) run() error { sort.Sort(api.SortOrgMemberByUsername(resp)) - w := tabwriter.NewWriter(cmd.io.Stdout(), 0, 2, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.io.Output(), 0, 2, 2, ' ', 0) fmt.Fprintf(w, "%s\t%s\t%s\n", "USER", "ROLE", "LAST CHANGED") for _, member := range resp { diff --git a/internals/secrethub/org_list_users_test.go b/internals/secrethub/org_list_users_test.go index 687947a0..4ef90049 100644 --- a/internals/secrethub/org_list_users_test.go +++ b/internals/secrethub/org_list_users_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -84,7 +84,7 @@ func TestOrgListUsersCommand_run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -92,7 +92,7 @@ func TestOrgListUsersCommand_run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, argOrg, tc.ArgListOrgMember) }) } diff --git a/internals/secrethub/org_ls.go b/internals/secrethub/org_ls.go index 1fe27927..fccc24b4 100644 --- a/internals/secrethub/org_ls.go +++ b/internals/secrethub/org_ls.go @@ -65,10 +65,10 @@ func (cmd *OrgLsCommand) run() error { if cmd.quiet { for _, org := range resp { - fmt.Fprintf(cmd.io.Stdout(), "%s\n", org.Name) + fmt.Fprintf(cmd.io.Output(), "%s\n", org.Name) } } else { - w := tabwriter.NewWriter(cmd.io.Stdout(), 0, 2, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.io.Output(), 0, 2, 2, ' ', 0) fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "NAME", "REPOS", "USERS", "CREATED") diff --git a/internals/secrethub/org_ls_test.go b/internals/secrethub/org_ls_test.go index 6417566c..7e987b85 100644 --- a/internals/secrethub/org_ls_test.go +++ b/internals/secrethub/org_ls_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -144,7 +144,7 @@ func TestOrgLsCommand_run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -152,7 +152,7 @@ func TestOrgLsCommand_run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/org_purchase.go b/internals/secrethub/org_purchase.go index e40a4ae0..a7da9083 100644 --- a/internals/secrethub/org_purchase.go +++ b/internals/secrethub/org_purchase.go @@ -28,8 +28,8 @@ func (cmd *OrgPurchaseCommand) Register(r command.Registerer) { // Run prints instructions on purchasing a SecretHub subscription. func (cmd OrgPurchaseCommand) Run() error { - fmt.Fprintf(cmd.io.Stdout(), "An organization subscription for SecretHub can be purchased through the billing dashboard.\n\n") - fmt.Fprintf(cmd.io.Stdout(), "For more information, check out:\nhttps://secrethub.io/docs/organizations/upgrade/\n\n") + fmt.Fprintf(cmd.io.Output(), "An organization subscription for SecretHub can be purchased through the billing dashboard.\n\n") + fmt.Fprintf(cmd.io.Output(), "For more information, check out:\nhttps://secrethub.io/docs/organizations/upgrade/\n\n") return nil } diff --git a/internals/secrethub/org_revoke.go b/internals/secrethub/org_revoke.go index 7de7e360..dccfc0bf 100644 --- a/internals/secrethub/org_revoke.go +++ b/internals/secrethub/org_revoke.go @@ -53,7 +53,7 @@ func (cmd *OrgRevokeCommand) Run() error { if len(planned.Repos) > 0 { fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "[WARNING] Revoking %s from the %s organization will revoke the user from %d repositories, "+ "automatically flagging secrets for rotation.\n\n"+ "A revocation plan has been generated and is shown below. "+ @@ -65,7 +65,7 @@ func (cmd *OrgRevokeCommand) Run() error { len(planned.Repos), ) - err = writeOrgRevokeRepoList(cmd.io.Stdout(), planned.Repos...) + err = writeOrgRevokeRepoList(cmd.io.Output(), planned.Repos...) if err != nil { return err } @@ -74,10 +74,10 @@ func (cmd *OrgRevokeCommand) Run() error { failed := planned.StatusCounts[api.StatusFailed] unaffected := planned.StatusCounts[api.StatusOK] - fmt.Fprintf(cmd.io.Stdout(), "Revocation plan: %d to flag, %d to fail, %d OK.\n\n", flagged, failed, unaffected) + fmt.Fprintf(cmd.io.Output(), "Revocation plan: %d to flag, %d to fail, %d OK.\n\n", flagged, failed, unaffected) } else { fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "The user %s has no memberships to any of %s's repos and can be safely removed.\n\n", cmd.username, cmd.orgName, @@ -94,11 +94,11 @@ func (cmd *OrgRevokeCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Name does not match. Aborting.") + fmt.Fprintln(cmd.io.Output(), "Name does not match. Aborting.") return nil } - fmt.Fprintf(cmd.io.Stdout(), "\nRevoking user...\n") + fmt.Fprintf(cmd.io.Output(), "\nRevoking user...\n") revoked, err := client.Orgs().Members().Revoke(cmd.orgName.Value(), cmd.username, nil) if err != nil { @@ -106,8 +106,8 @@ func (cmd *OrgRevokeCommand) Run() error { } if len(revoked.Repos) > 0 { - fmt.Fprintln(cmd.io.Stdout(), "") - err = writeOrgRevokeRepoList(cmd.io.Stdout(), revoked.Repos...) + fmt.Fprintln(cmd.io.Output(), "") + err = writeOrgRevokeRepoList(cmd.io.Output(), revoked.Repos...) if err != nil { return err } @@ -117,14 +117,14 @@ func (cmd *OrgRevokeCommand) Run() error { unaffected := revoked.StatusCounts[api.StatusOK] fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "Revoke complete! Repositories: %d flagged, %d failed, %d OK.\n", flagged, failed, unaffected, ) } else { - fmt.Fprintln(cmd.io.Stdout(), "Revoke complete!") + fmt.Fprintln(cmd.io.Output(), "Revoke complete!") } return nil diff --git a/internals/secrethub/org_revoke_test.go b/internals/secrethub/org_revoke_test.go index 8cb18b89..2b84c7d2 100644 --- a/internals/secrethub/org_revoke_test.go +++ b/internals/secrethub/org_revoke_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -130,7 +130,7 @@ func TestOrgRevokeCommand_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.promptIn) tc.cmd.io = io @@ -139,7 +139,7 @@ func TestOrgRevokeCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/org_rm.go b/internals/secrethub/org_rm.go index 7f855ca5..12a651b0 100644 --- a/internals/secrethub/org_rm.go +++ b/internals/secrethub/org_rm.go @@ -51,7 +51,7 @@ func (cmd *OrgRmCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Name does not match. Aborting.") + fmt.Fprintln(cmd.io.Output(), "Name does not match. Aborting.") return nil } @@ -60,14 +60,14 @@ func (cmd *OrgRmCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Deleting organization...") + fmt.Fprintln(cmd.io.Output(), "Deleting organization...") err = client.Orgs().Delete(cmd.name.Value()) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Delete complete! The organization %s has been permanently deleted.\n", cmd.name) + fmt.Fprintf(cmd.io.Output(), "Delete complete! The organization %s has been permanently deleted.\n", cmd.name) return nil } diff --git a/internals/secrethub/org_rm_test.go b/internals/secrethub/org_rm_test.go index 1091e80a..32caaf03 100644 --- a/internals/secrethub/org_rm_test.go +++ b/internals/secrethub/org_rm_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/assert" "github.com/secrethub/secrethub-go/internals/errio" @@ -95,7 +95,7 @@ func TestOrgRmCommand_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.promptIn) io.PromptErr = tc.promptErr tc.cmd.io = io @@ -107,7 +107,7 @@ func TestOrgRmCommand_Run(t *testing.T) { assert.Equal(t, err, tc.err) assert.Equal(t, io.PromptOut.String(), tc.promptOut) assert.Equal(t, argName, tc.argName) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/org_set_role.go b/internals/secrethub/org_set_role.go index c2444794..65286a31 100644 --- a/internals/secrethub/org_set_role.go +++ b/internals/secrethub/org_set_role.go @@ -43,14 +43,14 @@ func (cmd *OrgSetRoleCommand) Run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "Setting role...\n") + fmt.Fprintf(cmd.io.Output(), "Setting role...\n") resp, err := client.Orgs().Members().Update(cmd.orgName.Value(), cmd.username, cmd.role) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Set complete! The user %s is %s of the %s organization.\n", resp.User.Username, resp.Role, cmd.orgName) + fmt.Fprintf(cmd.io.Output(), "Set complete! The user %s is %s of the %s organization.\n", resp.User.Username, resp.Role, cmd.orgName) return nil } diff --git a/internals/secrethub/org_set_role_test.go b/internals/secrethub/org_set_role_test.go index f0079e8c..a403f40d 100644 --- a/internals/secrethub/org_set_role_test.go +++ b/internals/secrethub/org_set_role_test.go @@ -3,7 +3,7 @@ package secrethub import ( "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -80,7 +80,7 @@ func TestOrgSetRoleCommand_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -88,7 +88,7 @@ func TestOrgSetRoleCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, argOrgName, tc.ArgOrgName) assert.Equal(t, argUsername, tc.ArgUsername) assert.Equal(t, argRole, tc.ArgRole) diff --git a/internals/secrethub/pager/pager.go b/internals/secrethub/pager/pager.go new file mode 100644 index 00000000..f1780e7b --- /dev/null +++ b/internals/secrethub/pager/pager.go @@ -0,0 +1,169 @@ +package pager + +import ( + "bytes" + "errors" + "io" + "os" + "os/exec" + "syscall" +) + +const ( + secrethubPagerEnvvar = "$SECRETHUB_PAGER" + pagerEnvvar = "$PAGER" + fallbackPagerLineCount = 100 +) + +var ErrPagerClosed = errors.New("cannot write to closed terminal pager") +var ErrPagerNotFound = errors.New("no terminal pager available. Please configure a terminal pager by setting the $PAGER environment variable or install \"less\" or \"more\"") + +// pager is a writer that is piped to a terminal pager command. +type pager struct { + writer io.WriteCloser + cmd *exec.Cmd + done <-chan struct{} + closed bool +} + +func NewWithFallback(outputWriter io.Writer) (io.WriteCloser, error) { + pager, err := New(outputWriter) + if err == ErrPagerNotFound { + return newFallbackPager(outputWriter), nil + } else if err != nil { + return nil, err + } + return pager, nil +} + +// New runs the terminal pager configured in the OS environment +// and returns a writer that is piped to the standard input of the pager command. +func New(outputWriter io.Writer) (io.WriteCloser, error) { + pagerCommand, err := pagerCommand() + if err != nil { + return nil, err + } + + cmd := exec.Command(pagerCommand) + + writer, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + + cmd.Stdout = outputWriter + cmd.Stderr = os.Stderr + + err = cmd.Start() + if err != nil { + return nil, err + } + done := make(chan struct{}, 1) + go func() { + _ = cmd.Wait() + done <- struct{}{} + }() + return &pager{writer: writer, cmd: cmd, done: done}, nil +} + +// Write pipes the data to the terminal pager. +// It returns errPagerClosed if the terminal pager has been closed. +func (p *pager) Write(data []byte) (n int, err error) { + if p.isClosed() { + return 0, ErrPagerClosed + } + return p.writer.Write(data) +} + +// Close closes the writer to the terminal pager and waits for the terminal pager to close. +func (p *pager) Close() error { + err := p.writer.Close() + if err != nil { + return err + } + if p.closed { + return nil + } + err = p.cmd.Process.Signal(syscall.SIGINT) + if err != nil { + err = p.cmd.Process.Kill() + if err != nil { + return err + } + } + <-p.done + return nil +} + +// isClosed checks if the terminal pager process has been stopped. +func (p *pager) isClosed() bool { + if p.closed { + return true + } + select { + case <-p.done: + p.closed = true + return true + default: + return false + } +} + +// pagerCommand returns the name of the terminal pager configured in OS environment. +// It first checks the $SECRETHUB_PAGER environment variable and if it is not set to a valid pager it checks $PAGER. +// If no pager is configured it falls back to "less" than "more", returning an error if neither are available. +func pagerCommand() (string, error) { + if pager, err := exec.LookPath(os.ExpandEnv(secrethubPagerEnvvar)); err == nil { + return pager, nil + } + + if pager, err := exec.LookPath(os.ExpandEnv(pagerEnvvar)); err == nil { + return pager, nil + } + + if pager, err := exec.LookPath("less"); err == nil { + return pager, nil + } + + if pager, err := exec.LookPath("more"); err == nil { + return pager, nil + } + + return "", ErrPagerNotFound +} + +// newFallbackPaginatedWriter returns a pager that closes after outputting a fixed number of lines without pagination +// and returns errPagerNotFound on the last (or any subsequent) write. +func newFallbackPager(w io.Writer) io.WriteCloser { + return &fallbackPager{ + linesLeft: fallbackPagerLineCount, + writer: w, + } +} + +type fallbackPager struct { + writer io.Writer + linesLeft int +} + +func (p *fallbackPager) Write(data []byte) (int, error) { + if p.linesLeft == 0 { + return 0, ErrPagerNotFound + } + + lines := bytes.Count(data, []byte{'\n'}) + if lines > p.linesLeft { + data = bytes.Join(bytes.Split(data, []byte{'\n'})[:p.linesLeft], []byte{'\n'}) + data = append(data, '\n') + } + p.linesLeft -= bytes.Count(data, []byte{'\n'}) + n, err := p.writer.Write(data) + if p.linesLeft == 0 { + err = ErrPagerNotFound + } + return n, err +} + +func (p *fallbackPager) Close() error { + return nil +} diff --git a/internals/secrethub/pager/pager_test.go b/internals/secrethub/pager/pager_test.go new file mode 100644 index 00000000..ca14a185 --- /dev/null +++ b/internals/secrethub/pager/pager_test.go @@ -0,0 +1,53 @@ +package pager + +import ( + "bytes" + "testing" + + "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" + "gotest.tools/assert" +) + +func TestFallbackPager_Write(t *testing.T) { + cases := map[string]struct { + pager *fallbackPager + param string + expected string + expectedErr error + }{ + "no lines left": { + pager: &fallbackPager{linesLeft: 0}, + expectedErr: ErrPagerNotFound, + param: "test\n", + expected: "", + }, + "last line": { + pager: &fallbackPager{linesLeft: 1}, + expectedErr: ErrPagerNotFound, + param: "test\n", + expected: "test\n", + }, + "print more": { + pager: &fallbackPager{linesLeft: 2}, + param: "test1\ntest2\ntest3\ntest4", + expected: "test1\ntest2\n", + expectedErr: ErrPagerNotFound, + }, + "more lines left": { + pager: &fallbackPager{linesLeft: 3}, + expectedErr: nil, + param: "test\n", + expected: "test\n", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buffer := bytes.Buffer{} + tc.pager.writer = &fakes.Pager{Buffer: &buffer} + _, err := tc.pager.Write([]byte(tc.param)) + assert.Equal(t, err, tc.expectedErr) + assert.Equal(t, buffer.String(), tc.expected) + }) + } +} diff --git a/internals/secrethub/path_placeholders.go b/internals/secrethub/path_placeholders.go index 42c3e9cd..1aead2d7 100644 --- a/internals/secrethub/path_placeholders.go +++ b/internals/secrethub/path_placeholders.go @@ -3,6 +3,7 @@ package secrethub const ( repoPathPlaceHolder = "/" dirPathPlaceHolder = repoPathPlaceHolder + "/[/ ...]" + dirPathsPlaceHolder = dirPathPlaceHolder + "..." optionalDirPathPlaceHolder = repoPathPlaceHolder + "[/ ...]" secretPathPlaceHolder = optionalDirPathPlaceHolder + "/" secretPathOptionalVersionPlaceHolder = secretPathPlaceHolder + "[:]" diff --git a/internals/secrethub/printenv.go b/internals/secrethub/printenv.go index ece91a11..44f597d9 100644 --- a/internals/secrethub/printenv.go +++ b/internals/secrethub/printenv.go @@ -27,7 +27,7 @@ func NewPrintEnvCommand(app *cli.App, io ui.IO) *PrintEnvCommand { // Run prints out debug statements about all environment variables. func (cmd *PrintEnvCommand) Run() error { - err := cmd.app.PrintEnv(cmd.io.Stdout(), cmd.verbose, cmd.osEnv) + err := cmd.app.PrintEnv(cmd.io.Output(), cmd.verbose, cmd.osEnv) if err != nil { return err } diff --git a/internals/secrethub/read.go b/internals/secrethub/read.go index e74b7d3b..295a4ea1 100644 --- a/internals/secrethub/read.go +++ b/internals/secrethub/read.go @@ -76,7 +76,7 @@ func (cmd *ReadCommand) Run() error { } fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "Copied %s to clipboard. It will be cleared after %s.\n", cmd.path, units.HumanDuration(cmd.clearClipboardAfter), @@ -96,7 +96,7 @@ func (cmd *ReadCommand) Run() error { } if cmd.outFile == "" && !cmd.useClipboard { - fmt.Fprintf(cmd.io.Stdout(), "%s", string(secretData)) + fmt.Fprintf(cmd.io.Output(), "%s", string(secretData)) } return nil diff --git a/internals/secrethub/repo_export.go b/internals/secrethub/repo_export.go index 5a85fdd7..23ffe444 100644 --- a/internals/secrethub/repo_export.go +++ b/internals/secrethub/repo_export.go @@ -71,7 +71,7 @@ func (cmd *RepoExportCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Name does not match. Aborting.") + fmt.Fprintln(cmd.io.Output(), "Name does not match. Aborting.") return nil } diff --git a/internals/secrethub/repo_init.go b/internals/secrethub/repo_init.go index 4e78756b..df801140 100644 --- a/internals/secrethub/repo_init.go +++ b/internals/secrethub/repo_init.go @@ -39,14 +39,14 @@ func (cmd *RepoInitCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), "Creating repository...") + fmt.Fprintln(cmd.io.Output(), "Creating repository...") _, err = client.Repos().Create(cmd.path.Value()) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Create complete! The repository %s is now ready to use.\n", cmd.path.String()) + fmt.Fprintf(cmd.io.Output(), "Create complete! The repository %s is now ready to use.\n", cmd.path.String()) return nil } diff --git a/internals/secrethub/repo_init_test.go b/internals/secrethub/repo_init_test.go index 8ab85e26..ac40fea8 100644 --- a/internals/secrethub/repo_init_test.go +++ b/internals/secrethub/repo_init_test.go @@ -3,7 +3,7 @@ package secrethub import ( "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -72,7 +72,7 @@ func TestRepoInitCommand_Run(t *testing.T) { } } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) cmd.io = io // Run @@ -80,7 +80,7 @@ func TestRepoInitCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, argPath, tc.argPath) }) } diff --git a/internals/secrethub/repo_inspect.go b/internals/secrethub/repo_inspect.go index 9e2e7f80..66165648 100644 --- a/internals/secrethub/repo_inspect.go +++ b/internals/secrethub/repo_inspect.go @@ -62,7 +62,7 @@ func (cmd *RepoInspectCommand) Run() error { return err } - fmt.Fprintln(cmd.io.Stdout(), output) + fmt.Fprintln(cmd.io.Output(), output) return nil } diff --git a/internals/secrethub/repo_inspect_test.go b/internals/secrethub/repo_inspect_test.go index 7002a5c6..f6349630 100644 --- a/internals/secrethub/repo_inspect_test.go +++ b/internals/secrethub/repo_inspect_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -115,7 +115,7 @@ func TestInspectRepo_Run(t *testing.T) { }, tc.newClientErr } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Act @@ -123,7 +123,7 @@ func TestInspectRepo_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/repo_invite.go b/internals/secrethub/repo_invite.go index b39488dd..317a86d3 100644 --- a/internals/secrethub/repo_invite.go +++ b/internals/secrethub/repo_invite.go @@ -59,18 +59,18 @@ func (cmd *RepoInviteCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } - fmt.Fprintln(cmd.io.Stdout(), "Inviting user...") + fmt.Fprintln(cmd.io.Output(), "Inviting user...") _, err = client.Repos().Users().Invite(cmd.path.Value(), cmd.username) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Invite complete! The user %s is now a member of the %s repository.\n", cmd.username, cmd.path) + fmt.Fprintf(cmd.io.Output(), "Invite complete! The user %s is now a member of the %s repository.\n", cmd.username, cmd.path) return nil } diff --git a/internals/secrethub/repo_invite_test.go b/internals/secrethub/repo_invite_test.go index 4b67c143..07d20dbc 100644 --- a/internals/secrethub/repo_invite_test.go +++ b/internals/secrethub/repo_invite_test.go @@ -3,7 +3,7 @@ package secrethub import ( "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -99,7 +99,7 @@ func TestRepoInviteCommand_Run(t *testing.T) { } } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -107,7 +107,7 @@ func TestRepoInviteCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, argGetUsername, tc.getArgUsername) assert.Equal(t, argInviteUsername, tc.inviteArgUsername) assert.Equal(t, argPath, tc.inviteArgPath) diff --git a/internals/secrethub/repo_ls.go b/internals/secrethub/repo_ls.go index f5ffeb95..f13ae0b2 100644 --- a/internals/secrethub/repo_ls.go +++ b/internals/secrethub/repo_ls.go @@ -75,10 +75,10 @@ func (cmd *RepoLSCommand) run() error { if cmd.quiet { for _, repo := range list { - fmt.Fprintf(cmd.io.Stdout(), "%s\n", repo.Path()) + fmt.Fprintf(cmd.io.Output(), "%s\n", repo.Path()) } } else { - w := tabwriter.NewWriter(cmd.io.Stdout(), 0, 2, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.io.Output(), 0, 2, 2, ' ', 0) fmt.Fprintf(w, "%s\t%s\t%s\n", "NAME", "STATUS", "CREATED") for _, repo := range list { fmt.Fprintf(w, "%s\t%s\t%s\n", repo.Path(), repo.Status, cmd.timeFormatter.Format(repo.CreatedAt.Local())) diff --git a/internals/secrethub/repo_ls_test.go b/internals/secrethub/repo_ls_test.go index 040bd3f5..514d1634 100644 --- a/internals/secrethub/repo_ls_test.go +++ b/internals/secrethub/repo_ls_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/fakes" "github.com/secrethub/secrethub-go/internals/api" @@ -120,7 +120,7 @@ func TestRepoLSCommand_run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io if tc.newClientErr != nil { @@ -140,7 +140,7 @@ func TestRepoLSCommand_run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/repo_revoke.go b/internals/secrethub/repo_revoke.go index 8de8c22a..02b2f5cf 100644 --- a/internals/secrethub/repo_revoke.go +++ b/internals/secrethub/repo_revoke.go @@ -70,12 +70,12 @@ func (cmd *RepoRevokeCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } - fmt.Fprint(cmd.io.Stdout(), "Revoking account...\n\n") + fmt.Fprint(cmd.io.Output(), "Revoking account...\n\n") var revoked *api.RevokeRepoResponse if cmd.accountName.IsService() { @@ -88,7 +88,7 @@ func (cmd *RepoRevokeCommand) Run() error { } if revoked.Status == api.StatusFailed { - fmt.Fprintf(cmd.io.Stdout(), + fmt.Fprintf(cmd.io.Output(), "\nRevoke failed! The account %s is the only admin on the repo %s."+ "You need to make sure another account has admin rights on the repository or you can remove the repo.", prettyName, @@ -101,7 +101,7 @@ func (cmd *RepoRevokeCommand) Run() error { return err } - w := tabwriter.NewWriter(cmd.io.Stdout(), 0, 2, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.io.Output(), 0, 2, 2, ' ', 0) countUnaffected, countFlagged := printFlaggedSecrets(w, rootDir.RootDir, cmd.path.GetNamespace()) @@ -111,9 +111,9 @@ func (cmd *RepoRevokeCommand) Run() error { } if countFlagged > 0 { - fmt.Fprintln(cmd.io.Stdout()) + fmt.Fprintln(cmd.io.Output()) } - fmt.Fprintf(cmd.io.Stdout(), + fmt.Fprintf(cmd.io.Output(), "Revoke complete! The account %s can no longer access the %s repository. "+ "Make sure you overwrite or delete all flagged secrets. "+ "Secrets: %d unaffected, %d flagged\n", diff --git a/internals/secrethub/repo_revoke_test.go b/internals/secrethub/repo_revoke_test.go index 14f523c7..6fc4e53a 100644 --- a/internals/secrethub/repo_revoke_test.go +++ b/internals/secrethub/repo_revoke_test.go @@ -3,7 +3,7 @@ package secrethub import ( "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/api/uuid" @@ -341,7 +341,7 @@ func TestRepoRevokeCommand_Run(t *testing.T) { } } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io // Run @@ -349,7 +349,7 @@ func TestRepoRevokeCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/repo_rm.go b/internals/secrethub/repo_rm.go index 28e37a57..9c33eef8 100644 --- a/internals/secrethub/repo_rm.go +++ b/internals/secrethub/repo_rm.go @@ -60,18 +60,18 @@ func (cmd *RepoRmCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Name does not match. Aborting.") + fmt.Fprintln(cmd.io.Output(), "Name does not match. Aborting.") return nil } - fmt.Fprintln(cmd.io.Stdout(), "Removing repository...") + fmt.Fprintln(cmd.io.Output(), "Removing repository...") err = client.Repos().Delete(cmd.path.Value()) if err != nil { return err } - fmt.Fprintf(cmd.io.Stdout(), "Removal complete! The repository %s has been permanently removed.\n", cmd.path) + fmt.Fprintf(cmd.io.Output(), "Removal complete! The repository %s has been permanently removed.\n", cmd.path) return nil } diff --git a/internals/secrethub/repo_rm_test.go b/internals/secrethub/repo_rm_test.go index 16c1a00a..840328f9 100644 --- a/internals/secrethub/repo_rm_test.go +++ b/internals/secrethub/repo_rm_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -133,7 +134,7 @@ func TestRepoRmCommand_Run(t *testing.T) { } } - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Buffer = bytes.NewBufferString(tc.promptIn) io.PromptIn.ReadErr = tc.promptReadErr io.PromptErr = tc.promptErr @@ -144,7 +145,7 @@ func TestRepoRmCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) assert.Equal(t, io.PromptOut.String(), tc.promptOut) }) } diff --git a/internals/secrethub/rm.go b/internals/secrethub/rm.go index 7815d724..21b0c1d6 100644 --- a/internals/secrethub/rm.go +++ b/internals/secrethub/rm.go @@ -121,7 +121,7 @@ func rmSecretVersion(client secrethub.ClientInterface, secretPath api.SecretPath } fmt.Fprintf( - io.Stdout(), + io.Output(), "Removal complete! The secret version %s has been permanently removed.\n", secretPath, ) @@ -151,7 +151,7 @@ func rmSecret(client secrethub.ClientInterface, secretPath api.SecretPath, force } fmt.Fprintf( - io.Stdout(), + io.Output(), "Removal complete! The secret %s has been permanently removed.\n", secretPath, ) @@ -181,7 +181,7 @@ func rmDir(client secrethub.ClientInterface, dirPath api.DirPath, force bool, io } fmt.Fprintf( - io.Stdout(), + io.Output(), "Removal complete! The directory %s has been permanently removed.\n", dirPath, ) @@ -210,7 +210,7 @@ func askRmConfirmation(io ui.IO, confirmationText string, force bool, expected . } if !confirmed { - fmt.Fprintln(io.Stdout(), "Name does not match. Aborting.") + fmt.Fprintln(io.Output(), "Name does not match. Aborting.") return false, nil } return true, nil diff --git a/internals/secrethub/run_test.go b/internals/secrethub/run_test.go index b074a23a..fb6d4295 100644 --- a/internals/secrethub/run_test.go +++ b/internals/secrethub/run_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-cli/internals/secrethub/tpl" "github.com/secrethub/secrethub-cli/internals/secrethub/tpl/fakes" @@ -477,7 +477,7 @@ func TestRunCommand_Run(t *testing.T) { }{ "success, no secrets": { command: RunCommand{ - io: ui.NewFakeIO(), + io: fakeui.NewIO(t), environment: &environment{ osStat: osStatNotExist, }, @@ -517,7 +517,7 @@ func TestRunCommand_Run(t *testing.T) { "missing": "path/to/unexisting/secret", }, }, - io: ui.NewFakeIO(), + io: fakeui.NewIO(t), newClient: func() (secrethub.ClientInterface, error) { return fakeclient.Client{ SecretService: &fakeclient.SecretService{ @@ -542,7 +542,7 @@ func TestRunCommand_Run(t *testing.T) { }, osStat: osStatNotExist, }, - io: ui.NewFakeIO(), + io: fakeui.NewIO(t), newClient: func() (secrethub.ClientInterface, error) { return fakeclient.Client{ SecretService: &fakeclient.SecretService{ @@ -587,7 +587,7 @@ func TestRunCommand_Run(t *testing.T) { "os env secret not found": { command: RunCommand{ command: []string{"echo", "test"}, - io: ui.NewFakeIO(), + io: fakeui.NewIO(t), environment: &environment{ osEnv: []string{"TEST=secrethub://nonexistent/secret/path"}, osStat: osStatNotExist, @@ -610,7 +610,7 @@ func TestRunCommand_Run(t *testing.T) { command: RunCommand{ ignoreMissingSecrets: true, command: []string{"echo", "test"}, - io: ui.NewFakeIO(), + io: fakeui.NewIO(t), environment: &environment{ osEnv: []string{"TEST=secrethub://nonexistent/secret/path"}, osStat: osStatNotExist, @@ -1081,12 +1081,15 @@ func TestRunCommand_RunWithFile(t *testing.T) { defer os.Remove(scriptFile) } - fakeIO := ui.NewFakeIO() + fakeIO := fakeui.NewIO(t) tc.command.io = fakeIO err := tc.command.Run() assert.Equal(t, err, tc.err) - assert.Equal(t, fakeIO.StdOut.String(), tc.expectedStdOut) + + stdout, err := fakeIO.ReadStdout() + assert.OK(t, err) + assert.Equal(t, string(stdout), tc.expectedStdOut) }) } } diff --git a/internals/secrethub/service.go b/internals/secrethub/service.go index fb636660..9a8d187a 100644 --- a/internals/secrethub/service.go +++ b/internals/secrethub/service.go @@ -23,6 +23,7 @@ func NewServiceCommand(io ui.IO, newClient newClientFunc) *ServiceCommand { func (cmd *ServiceCommand) Register(r command.Registerer) { clause := r.Command("service", "Manage service accounts.") NewServiceAWSCommand(cmd.io, cmd.newClient).Register(clause) + NewServiceGCPCommand(cmd.io, cmd.newClient).Register(clause) NewServiceDeployCommand(cmd.io).Register(clause) NewServiceInitCommand(cmd.io, cmd.newClient).Register(clause) NewServiceLsCommand(cmd.io, cmd.newClient).Register(clause) diff --git a/internals/secrethub/service_aws_init.go b/internals/secrethub/service_aws_init.go index 16bf7d91..71bcf4ef 100644 --- a/internals/secrethub/service_aws_init.go +++ b/internals/secrethub/service_aws_init.go @@ -59,7 +59,7 @@ func (cmd *ServiceAWSInitCommand) Run() error { } if cmd.role == "" && cmd.kmsKeyID == "" { - fmt.Fprintln(cmd.io.Stdout(), "This command creates a new service account for use on AWS. For help on this, run `secrethub service aws init --help`.") + fmt.Fprintln(cmd.io.Output(), "This command creates a new service account for use on AWS. For help on this, run `secrethub service aws init --help`.") } cfg := aws.NewConfig() @@ -85,7 +85,7 @@ func (cmd *ServiceAWSInitCommand) Run() error { } accountID := aws.StringValue(identity.Account) - fmt.Fprintf(cmd.io.Stdout(), "Detected access to AWS account %s.", accountID) + fmt.Fprintf(cmd.io.Output(), "Detected access to AWS account %s.", accountID) if cfg.Region == nil && cmd.kmsKeyID != "" { // When the region is not configured in the AWS configuration and not supplied using the flag, use @@ -97,9 +97,9 @@ func (cmd *ServiceAWSInitCommand) Run() error { } if cfg.Region != nil { - fmt.Fprintf(cmd.io.Stdout(), "Using region %s.", *cfg.Region) + fmt.Fprintf(cmd.io.Output(), "Using region %s.", *cfg.Region) } - fmt.Fprintln(cmd.io.Stdout()) + fmt.Fprintln(cmd.io.Output()) if cfg.Region == nil { region, err := ui.ChooseDynamicOptions(cmd.io, "Which region do you want to use for KMS?", getAWSRegionOptions, true, "region") @@ -149,8 +149,8 @@ func (cmd *ServiceAWSInitCommand) Run() error { } } - fmt.Fprintln(cmd.io.Stdout(), "Successfully created a new service account with ID: "+service.ServiceID) - fmt.Fprintf(cmd.io.Stdout(), "Any host that assumes the IAM role %s can now automatically authenticate to SecretHub and fetch the secrets the service has been given access to.\n", roleNameFromRole(cmd.role)) + fmt.Fprintln(cmd.io.Output(), "Successfully created a new service account with ID: "+service.ServiceID) + fmt.Fprintf(cmd.io.Output(), "Any host that assumes the IAM role %s can now automatically authenticate to SecretHub and fetch the secrets the service has been given access to.\n", roleNameFromRole(cmd.role)) return nil } diff --git a/internals/secrethub/service_deploy_winrm.go b/internals/secrethub/service_deploy_winrm.go index 3902db0a..8c6c5d5c 100644 --- a/internals/secrethub/service_deploy_winrm.go +++ b/internals/secrethub/service_deploy_winrm.go @@ -179,23 +179,23 @@ func (cmd *ServiceDeployWinRmCommand) Run() error { deployer := newWindowsDeployer(client, destinationPath) - if !cmd.io.Stdin().IsPiped() { + if !cmd.io.IsInputPiped() { return ErrNoDataOnStdin } - credential, err := ioutil.ReadAll(cmd.io.Stdin()) + credential, err := ioutil.ReadAll(cmd.io.Input()) if err != nil { return err } // Copy the config to the host. - fmt.Fprintln(cmd.io.Stdout(), "Deploying configuration...") + fmt.Fprintln(cmd.io.Output(), "Deploying configuration...") err = deployer.configure(credential) if err != nil { return err } - fmt.Fprintln(cmd.io.Stdout(), "Deploy complete! The service account can now be used to connect to SecretHub from the host.") + fmt.Fprintln(cmd.io.Output(), "Deploy complete! The service account can now be used to connect to SecretHub from the host.") return nil } @@ -203,7 +203,7 @@ func (cmd *ServiceDeployWinRmCommand) Run() error { // checkWinRMTLS checks if the given schema corresponds to the given CLI flags. func (cmd *ServiceDeployWinRmCommand) checkWinRMTLS() (bool, error) { if cmd.resourceURI.Scheme == "http" { - fmt.Fprintln(cmd.io.Stdout(), "WARNING: insecure no tls flag is set! We recommend to always use TLS.") + fmt.Fprintln(cmd.io.Output(), "WARNING: insecure no tls flag is set! We recommend to always use TLS.") return false, nil } @@ -221,7 +221,7 @@ func (cmd *ServiceDeployWinRmCommand) checkWinRMTLS() (bool, error) { // checkWinRMVerifyCert checks if the given schema corresponds to the given CLI flags. func (cmd *ServiceDeployWinRmCommand) checkWinRMVerifyCert() bool { if cmd.noVerify { - fmt.Fprintln(cmd.io.Stdout(), "WARNING: insecure no verify cert flag is set! We recommend to always verify the certificate.") + fmt.Fprintln(cmd.io.Output(), "WARNING: insecure no verify cert flag is set! We recommend to always verify the certificate.") return true } diff --git a/internals/secrethub/service_gcp.go b/internals/secrethub/service_gcp.go new file mode 100644 index 00000000..ab9ee0d8 --- /dev/null +++ b/internals/secrethub/service_gcp.go @@ -0,0 +1,27 @@ +package secrethub + +import ( + "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/secrethub/command" +) + +// ServiceGCPCommand handles GCP services. +type ServiceGCPCommand struct { + io ui.IO + newClient newClientFunc +} + +// NewServiceGCPCommand creates a new ServiceGCPCommand. +func NewServiceGCPCommand(io ui.IO, newClient newClientFunc) *ServiceGCPCommand { + return &ServiceGCPCommand{ + io: io, + newClient: newClient, + } +} + +// Register registers the command and its sub-commands on the provided Registerer. +func (cmd *ServiceGCPCommand) Register(r command.Registerer) { + clause := r.Command("gcp", "Manage GCP service accounts.").Hidden() + NewServiceGCPInitCommand(cmd.io, cmd.newClient).Register(clause) + NewServiceGCPLsCommand(cmd.io, cmd.newClient).Register(clause) +} diff --git a/internals/secrethub/service_gcp_init.go b/internals/secrethub/service_gcp_init.go new file mode 100644 index 00000000..8e41558a --- /dev/null +++ b/internals/secrethub/service_gcp_init.go @@ -0,0 +1,306 @@ +package secrethub + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "google.golang.org/api/cloudkms/v1" + "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/iam/v1" + "google.golang.org/api/option" + "google.golang.org/api/transport" + + "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/secrethub/command" + "github.com/secrethub/secrethub-go/internals/gcp" + + "github.com/secrethub/secrethub-go/internals/api" + "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" +) + +// ServiceGCPInitCommand initializes a service for GCP. +type ServiceGCPInitCommand struct { + description string + repo api.RepoPath + kmsKeyResourceID string + serviceAccountEmail string + permission string + io ui.IO + newClient newClientFunc +} + +// NewServiceGCPInitCommand creates a new ServiceGCPInitCommand. +func NewServiceGCPInitCommand(io ui.IO, newClient newClientFunc) *ServiceGCPInitCommand { + return &ServiceGCPInitCommand{ + io: io, + newClient: newClient, + } +} + +// Run initializes an GCP service. +func (cmd *ServiceGCPInitCommand) Run() error { + client, err := cmd.newClient() + if err != nil { + return err + } + + if cmd.serviceAccountEmail == "" && cmd.kmsKeyResourceID == "" { + fmt.Fprintln(cmd.io.Stdout(), "This command creates a new service account for use on GCP. For help on this, run `secrethub service gcp init --help`.") + + var projectID string + creds, err := transport.Creds(context.Background()) + if err == nil && creds.ProjectID != "" { + projectID = creds.ProjectID + } else { + var projectLister gcpProjectOptionLister + chosenProjectID, err := ui.ChooseDynamicOptions(cmd.io, "What GCP project do you want to use?", projectLister.Options, true, "project") + if err != nil { + return err + } + + projectID = chosenProjectID + } + + serviceAccountLister := gcpServiceAccountOptionLister{ + ProjectID: projectID, + } + serviceAccountEmail, err := ui.ChooseDynamicOptionsValidate(cmd.io, "What is the email of the service account you want to use?", serviceAccountLister.Options, "service account", api.ValidateGCPServiceAccountEmail) + if err != nil { + return err + } + cmd.serviceAccountEmail = serviceAccountEmail + + kmsKeyLister, err := newGCPKeyOptionsLister(projectID) + if err != nil { + return err + } + keyring, err := ui.ChooseDynamicOptions(cmd.io, "In which keyring is the KMS key you want to use for encrypting the service account's key?", kmsKeyLister.KeyringOptions, true, "keyring") + if err != nil { + return err + } + kmsKey, err := ui.ChooseDynamicOptions(cmd.io, "What is the KMS key you want to use for encrypting the service account's key?", kmsKeyLister.KeyOptions(keyring), true, "kms key") + if err != nil { + return err + } + cmd.kmsKeyResourceID = kmsKey + } + + if cmd.serviceAccountEmail == "" { + serviceAccountEmail, err := ui.AskAndValidate(cmd.io, "What is the email of the GCP Service Account that should have access to the service?\n", 3, api.ValidateGCPServiceAccountEmail) + if err != nil { + return err + } + cmd.serviceAccountEmail = strings.TrimSpace(serviceAccountEmail) + } + + if cmd.kmsKeyResourceID == "" { + kmsKey, err := ui.AskAndValidate(cmd.io, "What is the Resource ID of the KMS-key that should be used for encrypting the service's account key?\n", 3, api.ValidateGCPKMSKeyResourceID) + if err != nil { + return err + } + cmd.kmsKeyResourceID = strings.TrimSpace(kmsKey) + } + + if cmd.description == "" { + cmd.description = "GCP Service Account " + roleNameFromRole(cmd.serviceAccountEmail) + } + + service, err := client.Services().Create(cmd.repo.Value(), cmd.description, credentials.CreateGCPServiceAccount(cmd.serviceAccountEmail, cmd.kmsKeyResourceID)) + if err == api.ErrCredentialAlreadyExists { + return ErrRoleAlreadyTaken + } else if err != nil { + return err + } + + if cmd.permission != "" { + err = givePermission(service, cmd.repo, cmd.permission, client) + if err != nil { + return err + } + } + + fmt.Fprintln(cmd.io.Stdout(), "Successfully created a new service account with ID: "+service.ServiceID) + fmt.Fprintf(cmd.io.Stdout(), "Any host using the Service Account %s can now automatically authenticate to SecretHub and fetch the secrets the service has been given access to.\n", cmd.serviceAccountEmail) + + return nil +} + +// Register registers the command, arguments and flags on the provided Registerer. +func (cmd *ServiceGCPInitCommand) Register(r command.Registerer) { + clause := r.Command("init", "Create a new service account that is tied to an GCP Service Account.") + clause.Arg("repo", "The service account is attached to the repository in this path.").Required().PlaceHolder(repoPathPlaceHolder).SetValue(&cmd.repo) + clause.Flag("kms-key", "The Resource ID of the KMS-key to be used for encrypting the service's account key.").StringVar(&cmd.kmsKeyResourceID) + clause.Flag("service-account-email", "The email of the GCP Service Account that should have access to this service account.").StringVar(&cmd.serviceAccountEmail) + clause.Flag("description", "A description for the service so others will recognize it. Defaults to the name of the role that is attached to the service.").StringVar(&cmd.description) + clause.Flag("descr", "").Hidden().StringVar(&cmd.description) + clause.Flag("desc", "").Hidden().StringVar(&cmd.description) + clause.Flag("permission", "Create an access rule giving the service account permission on a directory. Accepted permissions are `read`, `write` and `admin`. Use `--permission ` to give permission on the root of the repo and `--permission [/ ...]:` to give permission on a subdirectory.").StringVar(&cmd.permission) + + clause.HelpLong("The native GCP identity provider uses a combination of GCP IAM and GCP KMS to provide access to SecretHub for any service running on GCP. For this to work, a GCP Service Account and a KMS key are needed.\n" + + "\n" + + " - The GCP Service Account should be the service account that is assumed by the service during execution.\n" + + " - The KMS key is a key that is used for encryption of the account. Decryption permission on this key must be granted to the previously described GCP Service Account.\n" + + "\n" + + "To create a new service that uses the GCP identity provider, the CLI must have encryption access to the KMS key that will be used by the service account. Therefore GCP application default credentials should be configured on this system. To achieve this, first install the Google Cloud SDK (https://cloud.google.com/sdk/docs/quickstarts) and then run `gcloud auth application-default login`.", + ) + + command.BindAction(clause, cmd.Run) +} + +type gcpProjectOptionLister struct { + nextPage string +} + +func (l *gcpProjectOptionLister) Options() ([]ui.Option, bool, error) { + // Explicitly setting the credentials is needed to avoid a permission denied error from cloudresourcemanager. + creds, err := transport.Creds(context.Background()) + if err != nil { + return nil, false, gcp.HandleError(err) + } + + crm, err := cloudresourcemanager.NewService(context.Background(), option.WithTokenSource(creds.TokenSource)) + if err != nil { + return nil, false, gcp.HandleError(err) + } + + resp, err := crm.Projects.List().Filter("lifecycleState:ACTIVE").PageToken(l.nextPage).PageSize(10).Do() + if err != nil { + return nil, false, gcp.HandleError(err) + } + + options := make([]ui.Option, len(resp.Projects)) + for i, project := range resp.Projects { + options[i] = ui.Option{ + Value: project.ProjectId, + Display: fmt.Sprintf("%s (%s)", project.Name, project.ProjectId), + } + } + + l.nextPage = resp.NextPageToken + return options, resp.NextPageToken == "", nil +} + +type gcpServiceAccountOptionLister struct { + ProjectID string + nextPage string +} + +func (l *gcpServiceAccountOptionLister) Options() ([]ui.Option, bool, error) { + iamService, err := iam.NewService(context.Background()) + if err != nil { + return nil, false, gcp.HandleError(err) + } + + resp, err := iamService.Projects.ServiceAccounts.List("projects/" + l.ProjectID).PageToken(l.nextPage).PageSize(10).Do() + if err != nil { + return nil, false, gcp.HandleError(err) + } + + options := make([]ui.Option, len(resp.Accounts)) + for i, account := range resp.Accounts { + display := account.Email + if account.Description != "" { + display += " (" + account.Description + ")" + } + options[i] = ui.Option{ + Value: account.Email, + Display: display, + } + } + + l.nextPage = resp.NextPageToken + return options, resp.NextPageToken == "", nil +} + +func newGCPKeyOptionsLister(projectID string) (*gcpKMSKeyOptionLister, error) { + kmsService, err := cloudkms.NewService(context.Background()) + if err != nil { + return nil, gcp.HandleError(err) + } + + return &gcpKMSKeyOptionLister{ + projectID: projectID, + kmsService: kmsService, + }, nil +} + +type gcpKMSKeyOptionLister struct { + projectID string + nextPage string + kmsService *cloudkms.Service +} + +func (l *gcpKMSKeyOptionLister) KeyringOptions() ([]ui.Option, bool, error) { + options := make([]ui.Option, 0, 16) + + errChan := make(chan error, 1) + resChan := make(chan ui.Option, 16) + var wg sync.WaitGroup + + ctx, cancelTimeout := context.WithTimeout(context.Background(), 15*time.Second) + defer cancelTimeout() + + err := l.kmsService.Projects.Locations.List("projects/"+l.projectID).Pages(ctx, func(resp *cloudkms.ListLocationsResponse) error { + for _, loc := range resp.Locations { + wg.Add(1) + go func(locationName string) { + err := l.kmsService.Projects.Locations.KeyRings.List(locationName).Pages(ctx, func(resp *cloudkms.ListKeyRingsResponse) error { + for _, keyring := range resp.KeyRings { + resChan <- ui.Option{ + Value: keyring.Name, + Display: keyring.Name, + } + } + return nil + }) + wg.Done() + if err != nil { + select { + case errChan <- err: + default: + } + } + }(loc.Name) + } + return nil + }) + if err != nil { + return nil, false, gcp.HandleError(err) + } + go func() { + wg.Wait() + close(resChan) + }() + for res := range resChan { + options = append(options, res) + } + select { + case err := <-errChan: + return nil, false, gcp.HandleError(err) + default: + return options, false, nil + } +} + +func (l *gcpKMSKeyOptionLister) KeyOptions(keyring string) func() ([]ui.Option, bool, error) { + return func() ([]ui.Option, bool, error) { + resp, err := l.kmsService.Projects.Locations.KeyRings.CryptoKeys.List(keyring).PageSize(10).Filter("purpose:ENCRYPT_DECRYPT").PageToken(l.nextPage).Do() + if err != nil { + return nil, false, gcp.HandleError(err) + } + + options := make([]ui.Option, len(resp.CryptoKeys)) + for i, key := range resp.CryptoKeys { + options[i] = ui.Option{ + Value: key.Name, + Display: key.Name, + } + } + + l.nextPage = resp.NextPageToken + return options, resp.NextPageToken == "", nil + } +} diff --git a/internals/secrethub/service_init.go b/internals/secrethub/service_init.go index a23fc0d6..5a7698b7 100644 --- a/internals/secrethub/service_init.go +++ b/internals/secrethub/service_init.go @@ -88,7 +88,7 @@ func (cmd *ServiceInitCommand) Run() error { return err } - fmt.Fprintf(cmd.io.Stdout(), "Copied account configuration for %s to clipboard. It will be cleared after 45 seconds.\n", service.ServiceID) + fmt.Fprintf(cmd.io.Output(), "Copied account configuration for %s to clipboard. It will be cleared after 45 seconds.\n", service.ServiceID) } else if cmd.file != "" { err = ioutil.WriteFile(cmd.file, posix.AddNewLine(out), cmd.fileMode.FileMode()) if err != nil { @@ -96,13 +96,13 @@ func (cmd *ServiceInitCommand) Run() error { } fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "Written account configuration for %s to %s. Be sure to remove it when you're done.\n", service.ServiceID, cmd.file, ) } else { - fmt.Fprintf(cmd.io.Stdout(), "%s", posix.AddNewLine(out)) + fmt.Fprintf(cmd.io.Output(), "%s", posix.AddNewLine(out)) } return nil diff --git a/internals/secrethub/service_ls.go b/internals/secrethub/service_ls.go index aaa6d0fe..59bad3da 100644 --- a/internals/secrethub/service_ls.go +++ b/internals/secrethub/service_ls.go @@ -46,6 +46,18 @@ func NewServiceAWSLsCommand(io ui.IO, newClient newClientFunc) *ServiceLsCommand } } +func NewServiceGCPLsCommand(io ui.IO, newClient newClientFunc) *ServiceLsCommand { + return &ServiceLsCommand{ + io: io, + newClient: newClient, + newServiceTable: newGCPServiceTable, + filters: []func(service *api.Service) bool{ + isGCPService, + }, + help: "List all GCP service accounts in a given repository.", + } +} + // Register registers the command, arguments and flags on the provided Registerer. func (cmd *ServiceLsCommand) Register(r command.Registerer) { clause := r.Command("ls", cmd.help) @@ -83,10 +95,10 @@ outer: if cmd.quiet { for _, service := range included { - fmt.Fprintf(cmd.io.Stdout(), "%s\n", service.ServiceID) + fmt.Fprintf(cmd.io.Output(), "%s\n", service.ServiceID) } } else { - w := tabwriter.NewWriter(cmd.io.Stdout(), 0, 2, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.io.Output(), 0, 2, 2, ' ', 0) serviceTable := cmd.newServiceTable(NewTimeFormatter(cmd.useTimestamps)) fmt.Fprintln(w, strings.Join(serviceTable.header(), "\t")) @@ -162,3 +174,27 @@ func isAWSService(service *api.Service) bool { return service.Credential.Type == api.CredentialTypeAWS } + +type gcpServiceTable struct { + baseServiceTable +} + +func newGCPServiceTable(timeFormatter TimeFormatter) serviceTable { + return gcpServiceTable{baseServiceTable{timeFormatter: timeFormatter}} +} + +func (sw gcpServiceTable) header() []string { + return sw.baseServiceTable.header("SERVICE-ACCOUNT-EMAIL", "KMS-KEY") +} + +func (sw gcpServiceTable) row(service *api.Service) []string { + return sw.baseServiceTable.row(service, service.Credential.Metadata[api.CredentialMetadataGCPServiceAccountEmail], service.Credential.Metadata[api.CredentialMetadataGCPKMSKeyResourceID]) +} + +func isGCPService(service *api.Service) bool { + if service == nil { + return false + } + + return service.Credential.Type == api.CredentialTypeGCPServiceAccount +} diff --git a/internals/secrethub/service_ls_test.go b/internals/secrethub/service_ls_test.go index 795d803b..dffac450 100644 --- a/internals/secrethub/service_ls_test.go +++ b/internals/secrethub/service_ls_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -47,7 +47,9 @@ func TestServiceLsCommand_Run(t *testing.T) { }, nil }, }, - out: "ID DESCRIPTION TYPE CREATED\ntest foobar key About an hour ago\nsecond foobarbaz key 2 hours ago\n", + out: "" + + "ID DESCRIPTION TYPE CREATED\n" + + "test foobar key About an hour ago\nsecond foobarbaz key 2 hours ago\n", }, "success quiet": { cmd: ServiceLsCommand{ @@ -91,7 +93,9 @@ func TestServiceLsCommand_Run(t *testing.T) { }, nil }, }, - out: "ID DESCRIPTION ROLE KMS-KEY CREATED\ntest foobar arn:aws:iam::123456:role/path/to/role 12345678-1234-1234-1234-123456789012 About an hour ago\n", + out: "" + + "ID DESCRIPTION ROLE KMS-KEY CREATED\n" + + "test foobar arn:aws:iam::123456:role/path/to/role 12345678-1234-1234-1234-123456789012 About an hour ago\n", }, "success aws filter": { cmd: ServiceLsCommand{ @@ -126,7 +130,72 @@ func TestServiceLsCommand_Run(t *testing.T) { }, nil }, }, - out: "ID DESCRIPTION ROLE KMS-KEY CREATED\ntest foobar arn:aws:iam::123456:role/path/to/role arn:aws:kms:us-east-1:123456:key/12345678-1234-1234-1234-123456789012 About an hour ago\n", + out: "" + + "ID DESCRIPTION ROLE KMS-KEY CREATED\n" + + "test foobar arn:aws:iam::123456:role/path/to/role arn:aws:kms:us-east-1:123456:key/12345678-1234-1234-1234-123456789012 About an hour ago\n", + }, + "success gcp": { + cmd: ServiceLsCommand{ + newServiceTable: newGCPServiceTable, + }, + serviceService: fakeclient.ServiceService{ + ListFunc: func(path string) ([]*api.Service, error) { + return []*api.Service{ + { + ServiceID: "test", + Description: "foobar", + Credential: &api.Credential{ + Type: api.CredentialTypeGCPServiceAccount, + Metadata: map[string]string{ + api.CredentialMetadataGCPServiceAccountEmail: "service-account@secrethub-test-1234567890.iam.gserviceaccount.com", + api.CredentialMetadataGCPKMSKeyResourceID: "projects/secrethub-test-1234567890.iam/locations/global/keyRings/test/cryptoKeys/test", + }, + }, + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + }, nil + }, + }, + out: "" + + "ID DESCRIPTION SERVICE-ACCOUNT-EMAIL KMS-KEY CREATED\n" + + "test foobar service-account@secrethub-test-1234567890.iam.gserviceaccount.com projects/secrethub-test-1234567890.iam/locations/global/keyRings/test/cryptoKeys/test About an hour ago\n", + }, + "success gcp filter": { + cmd: ServiceLsCommand{ + newServiceTable: newGCPServiceTable, + filters: []func(*api.Service) bool{ + isGCPService, + }, + }, + serviceService: fakeclient.ServiceService{ + ListFunc: func(path string) ([]*api.Service, error) { + return []*api.Service{ + { + ServiceID: "test", + Description: "foobar", + Credential: &api.Credential{ + Type: api.CredentialTypeGCPServiceAccount, + Metadata: map[string]string{ + api.CredentialMetadataGCPServiceAccountEmail: "service-account@secrethub-test-1234567890.iam.gserviceaccount.com", + api.CredentialMetadataGCPKMSKeyResourceID: "projects/secrethub-test-1234567890.iam/locations/global/keyRings/test/cryptoKeys/test", + }, + }, + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + { + ServiceID: "test2", + Description: "foobarbaz", + Credential: &api.Credential{ + Type: api.CredentialTypeKey, + }, + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + }, nil + }, + }, + out: "" + + "ID DESCRIPTION SERVICE-ACCOUNT-EMAIL KMS-KEY CREATED\n" + + "test foobar service-account@secrethub-test-1234567890.iam.gserviceaccount.com projects/secrethub-test-1234567890.iam/locations/global/keyRings/test/cryptoKeys/test About an hour ago\n", }, "new client error": { newClientErr: errors.New("error"), @@ -145,7 +214,7 @@ func TestServiceLsCommand_Run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io if tc.newClientErr != nil { @@ -165,7 +234,7 @@ func TestServiceLsCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/set.go b/internals/secrethub/set.go index 1f420058..73059e41 100644 --- a/internals/secrethub/set.go +++ b/internals/secrethub/set.go @@ -76,7 +76,7 @@ func (cmd *SetCommand) Run() error { } for _, c := range presenter.EmptyConsumables() { - fmt.Fprintf(cmd.io.Stdout(), "Warning: %s contains no secret declarations.\n", c) + fmt.Fprintf(cmd.io.Output(), "Warning: %s contains no secret declarations.\n", c) } secrets := make(map[string]api.SecretVersion) @@ -88,14 +88,14 @@ func (cmd *SetCommand) Run() error { secrets[path] = *secret } - fmt.Fprintln(cmd.io.Stdout(), "Setting secrets...") + fmt.Fprintln(cmd.io.Output(), "Setting secrets...") err = presenter.Set(secrets) if err != nil { return err } - fmt.Fprintln(cmd.io.Stdout(), "Set complete! The secrets are now available on your system.") + fmt.Fprintln(cmd.io.Output(), "Set complete! The secrets are now available on your system.") return nil } diff --git a/internals/secrethub/signup.go b/internals/secrethub/signup.go index 459afd9a..2d8f3eca 100644 --- a/internals/secrethub/signup.go +++ b/internals/secrethub/signup.go @@ -38,7 +38,7 @@ func NewSignUpCommand(io ui.IO, newClient newClientFunc, credentialStore Credent io: io, newClient: newClient, credentialStore: credentialStore, - progressPrinter: progress.NewPrinter(io.Stdout(), 500*time.Millisecond), + progressPrinter: progress.NewPrinter(io.Output(), 500*time.Millisecond), } } @@ -78,7 +78,7 @@ func (cmd *SignUpCommand) Run() error { } if !confirmed { - fmt.Fprintln(cmd.io.Stdout(), "Aborting.") + fmt.Fprintln(cmd.io.Output(), "Aborting.") return nil } } @@ -112,12 +112,12 @@ func (cmd *SignUpCommand) Run() error { return err } } - fmt.Fprintln(cmd.io.Stdout()) + fmt.Fprintln(cmd.io.Output()) } } fmt.Fprintf( - cmd.io.Stdout(), + cmd.io.Output(), "An account credential will be generated and stored at %s. "+ "Losing this credential means you lose the ability to decrypt your secrets. "+ "So keep it safe.\n", @@ -141,7 +141,7 @@ func (cmd *SignUpCommand) Run() error { return err } - fmt.Fprint(cmd.io.Stdout(), "Setting up your account...") + fmt.Fprint(cmd.io.Output(), "Setting up your account...") cmd.progressPrinter.Start() credential := credentials.CreateKey() _, err = client.Users().Create(cmd.username, cmd.email, cmd.fullName, credential) @@ -186,7 +186,7 @@ func (cmd *SignUpCommand) Run() error { } cmd.progressPrinter.Stop() - fmt.Fprint(cmd.io.Stdout(), "Created your account.\n\n") + fmt.Fprint(cmd.io.Output(), "Created your account.\n\n") createWorkspace := cmd.org != "" if !createWorkspace { @@ -194,9 +194,9 @@ func (cmd *SignUpCommand) Run() error { if err != nil { return err } - fmt.Fprintln(cmd.io.Stdout()) + fmt.Fprintln(cmd.io.Output()) if !createWorkspace { - fmt.Fprint(cmd.io.Stdout(), "You can create a shared workspace later using `secrethub org init`.\n\n") + fmt.Fprint(cmd.io.Output(), "You can create a shared workspace later using `secrethub org init`.\n\n") } } if createWorkspace { @@ -212,20 +212,20 @@ func (cmd *SignUpCommand) Run() error { return err } } - fmt.Fprint(cmd.io.Stdout(), "Creating your shared workspace...") + fmt.Fprint(cmd.io.Output(), "Creating your shared workspace...") cmd.progressPrinter.Start() _, err := client.Orgs().Create(cmd.org, cmd.orgDescription) cmd.progressPrinter.Stop() if err == api.ErrOrgAlreadyExists { - fmt.Fprintf(cmd.io.Stdout(), "The workspace %s already exists. If it is your organization, ask a colleague to invite you to the workspace. You can also create a new one using `secrethub org init`.\n", cmd.org) + fmt.Fprintf(cmd.io.Output(), "The workspace %s already exists. If it is your organization, ask a colleague to invite you to the workspace. You can also create a new one using `secrethub org init`.\n", cmd.org) } else if err != nil { return err } else { - fmt.Fprint(cmd.io.Stdout(), "Created your shared workspace.\n\n") + fmt.Fprint(cmd.io.Output(), "Created your shared workspace.\n\n") } } - fmt.Fprintf(cmd.io.Stdout(), "Setup complete. To read your first secret, run:\n\n secrethub read %s\n\n", secretPath) + fmt.Fprintf(cmd.io.Output(), "Setup complete. To read your first secret, run:\n\n secrethub read %s\n\n", secretPath) return nil } diff --git a/internals/secrethub/signup_test.go b/internals/secrethub/signup_test.go index 85aaa954..5a575da7 100644 --- a/internals/secrethub/signup_test.go +++ b/internals/secrethub/signup_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/secrethub/secrethub-cli/internals/cli/progress/fakeprogress" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/assert" "github.com/secrethub/secrethub-go/pkg/secrethub" @@ -26,7 +26,7 @@ func TestSignUpCommand_Run(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Setup - io := ui.NewFakeIO() + io := fakeui.NewIO(t) tc.cmd.io = io io.PromptIn.Buffer = bytes.NewBufferString(tc.promptIn) @@ -44,7 +44,7 @@ func TestSignUpCommand_Run(t *testing.T) { // Assert assert.Equal(t, err, tc.err) assert.Equal(t, io.PromptOut.String(), tc.promptOut) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } } diff --git a/internals/secrethub/tree.go b/internals/secrethub/tree.go index 1934292a..3fd756c3 100644 --- a/internals/secrethub/tree.go +++ b/internals/secrethub/tree.go @@ -38,7 +38,7 @@ func (cmd *TreeCommand) Run() error { return err } - printTree(t, cmd.io.Stdout()) + printTree(t, cmd.io.Output()) return nil } diff --git a/internals/secrethub/variable_reader_test.go b/internals/secrethub/variable_reader_test.go index 875bf672..56123aee 100644 --- a/internals/secrethub/variable_reader_test.go +++ b/internals/secrethub/variable_reader_test.go @@ -3,10 +3,11 @@ package secrethub import ( "testing" - "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" - "github.com/secrethub/secrethub-cli/internals/secrethub/tpl" "github.com/secrethub/secrethub-go/internals/assert" + + "github.com/secrethub/secrethub-cli/internals/secrethub/tpl" ) func TestVariableReader(t *testing.T) { @@ -155,7 +156,7 @@ func TestPromptVariableReader(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - io := ui.NewFakeIO() + io := fakeui.NewIO(t) io.PromptIn.Reads = tc.promptIn reader := newPromptMissingVariableReader(reader, io) diff --git a/internals/secrethub/write.go b/internals/secrethub/write.go index 04abea4b..316e48a6 100644 --- a/internals/secrethub/write.go +++ b/internals/secrethub/write.go @@ -82,8 +82,8 @@ func (cmd *WriteCommand) Run() error { if err != nil { return ErrReadFile(cmd.inFile, err) } - } else if cmd.io.Stdin().IsPiped() { - data, err = ioutil.ReadAll(cmd.io.Stdin()) + } else if cmd.io.IsInputPiped() { + data, err = ioutil.ReadAll(cmd.io.Input()) if err != nil { return ui.ErrReadInput(err) } @@ -110,7 +110,7 @@ func (cmd *WriteCommand) Run() error { return errEmptySecret } - _, err = fmt.Fprint(cmd.io.Stdout(), "Writing secret value...\n") + _, err = fmt.Fprint(cmd.io.Output(), "Writing secret value...\n") if err != nil { return err } @@ -125,7 +125,7 @@ func (cmd *WriteCommand) Run() error { return err } - _, err = fmt.Fprintf(cmd.io.Stdout(), "Write complete! The given value has been written to %s:%d\n", cmd.path, version.Version) + _, err = fmt.Fprintf(cmd.io.Output(), "Write complete! The given value has been written to %s:%d\n", cmd.path, version.Version) if err != nil { return err } diff --git a/internals/secrethub/write_test.go b/internals/secrethub/write_test.go index 909d3c22..af00dc07 100644 --- a/internals/secrethub/write_test.go +++ b/internals/secrethub/write_test.go @@ -7,6 +7,7 @@ import ( "github.com/secrethub/secrethub-cli/internals/cli/clip" "github.com/secrethub/secrethub-cli/internals/cli/clip/fakeclip" "github.com/secrethub/secrethub-cli/internals/cli/ui" + "github.com/secrethub/secrethub-cli/internals/cli/ui/fakeui" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/assert" @@ -19,18 +20,20 @@ func TestWriteCommand_Run(t *testing.T) { testErr := errio.Namespace("test").Code("test").Error("test error") cases := map[string]struct { - cmd WriteCommand - writeFunc func(path string, data []byte) (*api.SecretVersion, error) - in string - piped bool - promptIn string - promptOut string - promptErr error - readErr error - err error - path api.SecretPath - data []byte - out string + cmd WriteCommand + writeFunc func(path string, data []byte) (*api.SecretVersion, error) + in string + piped bool + promptIn string + promptOut string + promptErr error + passwordIn string + passwordErr error + readErr error + err error + path api.SecretPath + data []byte + out string }{ "path with version": { cmd: WriteCommand{ @@ -123,8 +126,8 @@ func TestWriteCommand_Run(t *testing.T) { cmd: WriteCommand{ path: "namespace/repo/secret", }, - promptIn: "asked secret value", - promptOut: "Please type in the value of the secret, followed by an [ENTER]:\n", + passwordIn: "asked secret value", + promptOut: "Please type in the value of the secret, followed by an [ENTER]:\n", writeFunc: func(path string, data []byte) (*api.SecretVersion, error) { return &api.SecretVersion{ Version: 1, @@ -135,13 +138,21 @@ func TestWriteCommand_Run(t *testing.T) { data: []byte("asked secret value"), out: "Writing secret value...\nWrite complete! The given value has been written to namespace/repo/secret:1\n", }, - "ask secret error": { + "ask secret prompt error": { cmd: WriteCommand{ path: "namespace/repo/secret", }, promptErr: testErr, err: testErr, }, + "ask secret read password error": { + cmd: WriteCommand{ + path: "namespace/repo/secret", + }, + promptOut: "Please type in the value of the secret, followed by an [ENTER]:", + passwordErr: testErr, + err: ui.ErrReadInput(testErr), + }, "piped read error": { cmd: WriteCommand{ path: "namespace/repo/secret", @@ -200,12 +211,14 @@ func TestWriteCommand_Run(t *testing.T) { }, nil } - io := ui.NewFakeIO() - io.StdIn.ReadErr = tc.readErr + io := fakeui.NewIO(t) + io.In.ReadErr = tc.readErr io.PromptIn.Buffer = bytes.NewBufferString(tc.promptIn) io.PromptErr = tc.promptErr - io.StdIn.Piped = tc.piped - io.StdIn.Buffer = bytes.NewBufferString(tc.in) + io.PasswordReader.Buffer = bytes.NewBufferString(tc.passwordIn) + io.PasswordReader.ReadErr = tc.passwordErr + io.In.Piped = tc.piped + io.In.Buffer = bytes.NewBufferString(tc.in) tc.cmd.io = io @@ -217,7 +230,7 @@ func TestWriteCommand_Run(t *testing.T) { assert.Equal(t, argPath, tc.path) assert.Equal(t, argData, tc.data) assert.Equal(t, io.PromptOut.String(), tc.promptOut) - assert.Equal(t, io.StdOut.String(), tc.out) + assert.Equal(t, io.Out.String(), tc.out) }) } }