diff --git a/.mockery.yaml b/.mockery.yaml index ad2d245..75378d2 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -10,4 +10,6 @@ packages: github.com/argoproj-labs/ephemeral-access/internal/controller/config: interfaces: Configurer: - LogConfigurer: + github.com/argoproj-labs/ephemeral-access/pkg/plugin: + interfaces: + AccessRequester: diff --git a/cmd/main.go b/cmd/main.go index 17a8c98..639d807 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,7 @@ import ( ephemeralaccessv1alpha1 "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" "github.com/argoproj-labs/ephemeral-access/internal/controller" "github.com/argoproj-labs/ephemeral-access/internal/controller/config" - "github.com/argoproj-labs/ephemeral-access/internal/controller/log" + "github.com/argoproj-labs/ephemeral-access/pkg/log" // +kubebuilder:scaffold:imports ) @@ -60,7 +60,9 @@ func main() { os.Exit(1) } - logger, err := log.NewLogger(config) + level := log.LogLevel(config.LogLevel()) + format := log.LogFormat(config.LogFormat()) + logger, err := log.NewLogger(log.WithLevel(level), log.WithFormat(format)) if err != nil { fmt.Fprintf(os.Stderr, "error creating logger: %s\n", err) os.Exit(1) diff --git a/go.mod b/go.mod index 9a1aa7a..18aa3d3 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 github.com/go-logr/logr v1.4.1 github.com/go-logr/zapr v1.3.0 + github.com/hashicorp/go-hclog v1.5.0 + github.com/hashicorp/go-plugin v1.6.1 github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.32.0 github.com/sethvargo/go-envconfig v1.1.0 @@ -25,6 +27,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -38,14 +41,19 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.16.0 // indirect @@ -65,6 +73,8 @@ require ( golang.org/x/tools v0.18.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 190f124..65a3e70 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -17,6 +19,9 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -51,9 +56,17 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -69,8 +82,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -78,6 +102,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= @@ -107,6 +133,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -142,7 +169,14 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= @@ -168,6 +202,10 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/controller/accessrequest_controller.go b/internal/controller/accessrequest_controller.go index 9a41009..950500f 100644 --- a/internal/controller/accessrequest_controller.go +++ b/internal/controller/accessrequest_controller.go @@ -39,7 +39,7 @@ import ( argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1" api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" "github.com/argoproj-labs/ephemeral-access/internal/controller/config" - "github.com/argoproj-labs/ephemeral-access/internal/controller/log" + "github.com/argoproj-labs/ephemeral-access/pkg/log" ) // AccessRequestReconciler reconciles a AccessRequest object diff --git a/internal/controller/log/log.go b/internal/controller/log/log.go deleted file mode 100644 index 63a873b..0000000 --- a/internal/controller/log/log.go +++ /dev/null @@ -1,107 +0,0 @@ -package log - -import ( - "context" - "fmt" - "strings" - - "github.com/argoproj-labs/ephemeral-access/internal/controller/config" - "github.com/go-logr/logr" - "github.com/go-logr/zapr" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - k8slog "sigs.k8s.io/controller-runtime/pkg/log" -) - -const ( - // we can only have info, debug and error log levels when using - // logr/zapr: https://github.com/go-logr/logr/issues/258 - INFO = 0 - DEBUG = 1 -) - -// logWrapper provides more expressive methods than the ones provided -// by the logr.Logger interface abstracting away the usage of numeric -// log levels. -type logWrapper struct { - Logger *logr.Logger -} - -// New will initialize a new log wrapper with the provided logger. -func New(l *logr.Logger) *logWrapper { - return &logWrapper{ - Logger: l, - } -} - -// FromContext will return a new log wrapper with the extracted logger -// from the given context. -func FromContext(ctx context.Context, keysAndValues ...interface{}) *logWrapper { - l := k8slog.FromContext(ctx, keysAndValues...) - return &logWrapper{ - Logger: &l, - } -} - -// ZapLevel will parse a log level string and return the correspondent zapcore.Level. -func ZapLevel(level string) (zapcore.Level, error) { - var l zapcore.Level - if err := l.UnmarshalText([]byte(level)); err != nil { - return zapcore.InfoLevel, fmt.Errorf("unable to determine log level: %w", err) - } - return l, nil -} - -// Info logs a non-error message with info level. If provided, the given -// key/value pairs are added in the log entry context. -func (l *logWrapper) Info(msg string, keysAndValues ...any) { - l.Logger.V(INFO).Info(msg, keysAndValues...) -} - -// Debug logs a non-error message with debug level. If provided, the given -// key/value pairs are added in the log entry context. -func (l *logWrapper) Debug(msg string, keysAndValues ...any) { - l.Logger.V(DEBUG).Info(msg, keysAndValues...) -} - -// Error logs an error message. If provided, the given key/value pairs are added -// in the log entry context. -func (l *logWrapper) Error(err error, msg string, keysAndValues ...any) { - l.Logger.Error(err, msg, keysAndValues...) -} - -// NewLogger will use the given LogConfigurer to build a new logr.Logger instance. -// It will use zap and the underlying Logger implementation. -// This function should be called only during the controller initialization. -func NewLogger(cfg config.LogConfigurer) (logr.Logger, error) { - logLevel, err := zapcore.ParseLevel(cfg.LogLevel()) - if err != nil { - return logr.Logger{}, fmt.Errorf("error parsing log level from configuration: %s", err) - } - - zapConfig := zap.Config{ - Level: zap.NewAtomicLevelAt(logLevel), - Development: false, - OutputPaths: []string{"stderr"}, - ErrorOutputPaths: []string{"stderr"}, - } - switch strings.ToLower(cfg.LogFormat()) { - case "json": - encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - zapConfig.Encoding = "json" - zapConfig.EncoderConfig = encoderConfig - case "text": - encoderConfig := zap.NewDevelopmentEncoderConfig() - encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - zapConfig.Encoding = "console" - zapConfig.EncoderConfig = encoderConfig - default: - return logr.Logger{}, fmt.Errorf("unsupported log format: %s", cfg.LogFormat()) - } - logger, err := zapConfig.Build() - if err != nil { - return logr.Logger{}, fmt.Errorf("error building logger: %s", err) - } - return zapr.NewLogger(logger), nil -} diff --git a/internal/controller/log/log_test.go b/internal/controller/log/log_test.go deleted file mode 100644 index b76b33e..0000000 --- a/internal/controller/log/log_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package log_test - -import ( - "testing" - - "github.com/argoproj-labs/ephemeral-access/internal/controller/log" - "github.com/argoproj-labs/ephemeral-access/test/mocks" - "github.com/stretchr/testify/assert" -) - -func TestConfiguration(t *testing.T) { - t.Run("will validate if configs are applied without error", func(t *testing.T) { - // Given - logConfigMock := mocks.NewMockConfigurer(t) - logConfigMock.EXPECT().LogLevel().Return("debug") - logConfigMock.EXPECT().LogFormat().Return("json") - - // When - logger, err := log.NewLogger(logConfigMock) - - // Then - assert.NoError(t, err) - assert.NotNil(t, logger) - logConfigMock.AssertNumberOfCalls(t, "LogLevel", 1) - logConfigMock.AssertNumberOfCalls(t, "LogFormat", 1) - }) - t.Run("will return error if provided invalid log level", func(t *testing.T) { - // Given - logConfigMock := mocks.NewMockConfigurer(t) - logConfigMock.EXPECT().LogLevel().Return("invalid_log_level") - - // When - _, err := log.NewLogger(logConfigMock) - - // Then - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid_log_level") - logConfigMock.AssertNumberOfCalls(t, "LogLevel", 1) - }) - t.Run("will return error if provided invalid log format", func(t *testing.T) { - // Given - logConfigMock := mocks.NewMockConfigurer(t) - logConfigMock.EXPECT().LogLevel().Return("debug") - logConfigMock.EXPECT().LogFormat().Return("invalid_log_format") - - // When - _, err := log.NewLogger(logConfigMock) - - // Then - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid_log_format") - logConfigMock.AssertNumberOfCalls(t, "LogLevel", 1) - logConfigMock.AssertNumberOfCalls(t, "LogFormat", 2) - }) -} diff --git a/internal/controller/service.go b/internal/controller/service.go index a860fe2..8f82cf9 100644 --- a/internal/controller/service.go +++ b/internal/controller/service.go @@ -8,7 +8,7 @@ import ( argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1" api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" "github.com/argoproj-labs/ephemeral-access/internal/controller/config" - "github.com/argoproj-labs/ephemeral-access/internal/controller/log" + "github.com/argoproj-labs/ephemeral-access/pkg/log" "github.com/cnf/structhash" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/retry" diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..1bb6f4a --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,174 @@ +package log + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + hclog "github.com/hashicorp/go-hclog" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + k8slog "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + // we can only have info, debug and error log levels when using + // logr/zapr: https://github.com/go-logr/logr/issues/258 + INFO = 0 + DEBUG = 1 + DebugLevel LogLevel = "debug" + InfoLevel LogLevel = "info" + TextFormat LogFormat = "text" + JsonFormat LogFormat = "json" +) + +type LogLevel string +type LogFormat string + +// String will return the string representation for this LogLevel +func (l LogLevel) String() string { + return string(l) +} + +// String will return the string representation for this LogFormat +func (l LogFormat) String() string { + return string(l) +} + +// LogConfig is a LogConfigurer implementation +type LogConfig struct { + logLevel LogLevel + logFormat LogFormat +} + +type Opts func(*LogConfig) + +func WithLevel(level LogLevel) Opts { + return func(c *LogConfig) { + c.logLevel = level + } +} + +func WithFormat(format LogFormat) Opts { + return func(c *LogConfig) { + c.logFormat = format + } +} + +// LogWrapper provides more expressive methods than the ones provided +// by the logr.Logger interface abstracting away the usage of numeric +// log levels. +type LogWrapper struct { + Logger *logr.Logger +} + +// New will initialize a new log wrapper with the provided logger. +func New(l *logr.Logger) *LogWrapper { + return &LogWrapper{ + Logger: l, + } +} + +// FromContext will return a new log wrapper with the extracted logger +// from the given context. +func FromContext(ctx context.Context, keysAndValues ...interface{}) *LogWrapper { + l := k8slog.FromContext(ctx, keysAndValues...) + return &LogWrapper{ + Logger: &l, + } +} + +// Info logs a non-error message with info level. If provided, the given +// key/value pairs are added in the log entry context. +func (l *LogWrapper) Info(msg string, keysAndValues ...any) { + l.Logger.V(INFO).Info(msg, keysAndValues...) +} + +// Debug logs a non-error message with debug level. If provided, the given +// key/value pairs are added in the log entry context. +func (l *LogWrapper) Debug(msg string, keysAndValues ...any) { + l.Logger.V(DEBUG).Info(msg, keysAndValues...) +} + +// Error logs an error message. If provided, the given key/value pairs are added +// in the log entry context. +func (l *LogWrapper) Error(err error, msg string, keysAndValues ...any) { + l.Logger.Error(err, msg, keysAndValues...) +} + +// NewZapLogger will initialize and return a new zap.Logger +func NewZapLogger(opts ...Opts) (*zap.Logger, error) { + cfg := logConfig(opts...) + logLevel, err := zapcore.ParseLevel(cfg.logLevel.String()) + if err != nil { + return nil, fmt.Errorf("error parsing log level from configuration: %s", err) + } + + zapConfig := zap.Config{ + Level: zap.NewAtomicLevelAt(logLevel), + Development: false, + DisableCaller: true, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + switch cfg.logFormat { + case JsonFormat: + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + zapConfig.Encoding = "json" + zapConfig.EncoderConfig = encoderConfig + case TextFormat: + encoderConfig := zap.NewDevelopmentEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + zapConfig.Encoding = "console" + zapConfig.EncoderConfig = encoderConfig + default: + return nil, fmt.Errorf("unsupported log format: %s", string(cfg.logFormat)) + } + logger, err := zapConfig.Build() + if err != nil { + return nil, fmt.Errorf("error building logger: %s", err) + } + return logger, nil +} + +// logConfig will build a new LogConfig based on the given opts. +func logConfig(opts ...Opts) *LogConfig { + // set the default values + cfg := &LogConfig{ + logLevel: InfoLevel, + logFormat: TextFormat, + } + for _, opt := range opts { + opt(cfg) + } + return cfg +} + +// NewPluginLogger will initialize a logger to be used in ephemeral-access plugins. +func NewPluginLogger(opts ...Opts) (hclog.Logger, error) { + cfg := logConfig(opts...) + jsonFormat := false + if cfg.logFormat == JsonFormat { + jsonFormat = true + } + return hclog.New(&hclog.LoggerOptions{ + Name: "plugin", + Level: hclog.LevelFromString(string(cfg.logLevel)), + JSONFormat: jsonFormat, + IncludeLocation: false, + }), nil +} + +// NewLogger will use the given LogConfigurer to build a new logr.Logger instance. +// It will use zap and the underlying Logger implementation. +// This function should be called only during the controller initialization. +func NewLogger(opts ...Opts) (logr.Logger, error) { + zapLogger, err := NewZapLogger(opts...) + if err != nil { + return logr.Logger{}, fmt.Errorf("error creating zap logger: %s", err) + } + return zapr.NewLogger(zapLogger), nil +} diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go new file mode 100644 index 0000000..9cb1e8c --- /dev/null +++ b/pkg/log/log_test.go @@ -0,0 +1,74 @@ +package log_test + +import ( + "testing" + + "github.com/argoproj-labs/ephemeral-access/pkg/log" + "github.com/stretchr/testify/assert" +) + +func TestLoggerConfiguration(t *testing.T) { + t.Run("will validate if default configurations are applied", func(t *testing.T) { + // When + logger, err := log.NewLogger() + + // Then + assert.NoError(t, err) + assert.NotNil(t, logger) + }) + t.Run("will validate if configs are applied without error", func(t *testing.T) { + // When + logger, err := log.NewLogger( + log.WithLevel(log.DebugLevel), + log.WithFormat(log.JsonFormat), + ) + + // Then + assert.NoError(t, err) + assert.NotNil(t, logger) + }) + t.Run("will return error if provided invalid log level", func(t *testing.T) { + + // When + _, err := log.NewLogger(log.WithLevel(log.LogLevel("invalid_log_level"))) + + // Then + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid_log_level") + }) + t.Run("will return error if provided invalid log format", func(t *testing.T) { + // When + _, err := log.NewLogger(log.WithFormat(log.LogFormat("invalid_log_format"))) + + // Then + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid_log_format") + }) +} + +func TestPluginLogger(t *testing.T) { + t.Run("will validate if configs are applied without error", func(t *testing.T) { + // When + logger, err := log.NewPluginLogger( + log.WithLevel(log.DebugLevel), + log.WithFormat(log.JsonFormat), + ) + + // Then + assert.NoError(t, err) + assert.NotNil(t, logger) + assert.True(t, logger.IsDebug()) + assert.Equal(t, "plugin", logger.Name()) + }) + + t.Run("will validate if default configurations are applied", func(t *testing.T) { + // When + logger, err := log.NewPluginLogger() + + // Then + assert.NoError(t, err) + assert.NotNil(t, logger) + assert.True(t, logger.IsInfo()) + assert.Equal(t, "plugin", logger.Name()) + }) +} diff --git a/pkg/plugin/rpc.go b/pkg/plugin/rpc.go new file mode 100644 index 0000000..941b6e6 --- /dev/null +++ b/pkg/plugin/rpc.go @@ -0,0 +1,259 @@ +package plugin + +import ( + "encoding/gob" + "fmt" + "net/rpc" + "os/exec" + + argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1" + api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" + + "github.com/hashicorp/go-hclog" + goPlugin "github.com/hashicorp/go-plugin" +) + +type GrantStatus string +type RevokeStatus string + +const ( + Granted GrantStatus = "granted" + GrantPending GrantStatus = "grant-pending" + Denied GrantStatus = "denied" + Revoked RevokeStatus = "revoked" + RevokePending RevokeStatus = "revoke-pending" + Key string = "ephemeralaccess" +) + +func init() { + gob.Register(&PluginError{}) +} + +// PluginError the error type returned by the rpc server over the wire +type PluginError struct { + Err string +} + +// Error the error implementation +func (pe *PluginError) Error() string { + return pe.Err +} + +// AccessRequester defines the main interface that should be implemented by +// ephemeral access plugins. +type AccessRequester interface { + Init() error + GrantAccess(ar *api.AccessRequest, app *argocd.Application) (*GrantResponse, error) + RevokeAccess(ar *api.AccessRequest, app *argocd.Application) (*RevokeResponse, error) +} + +// GrantResponse defines the response that will be returned by access +// request plugins. +type GrantResponse struct { + Status GrantStatus + Message string +} + +// RevokeResponse defines the response that will be returned by access +// request plugins. +type RevokeResponse struct { + Status RevokeStatus + Message string +} + +// GrantAccessArgsRPC wraps the args that are sent to the GrantAccess function +// over RPC. +type GrantAccessArgsRPC struct { + AccReq *api.AccessRequest + App *argocd.Application +} + +// RevokeAccessArgsRPC wraps the args that are sent to the RevokeAccess function +// over RPC. +type RevokeAccessArgsRPC struct { + AccReq *api.AccessRequest + App *argocd.Application +} + +// InitResponseRPC wraps the response that are received by the Init function over +// RPC. +type InitResponseRPC struct { + Err error +} + +// GrantAccessResponseRPC wraps the response that are received by the GrantAccess +// function over RPC. +type GrantAccessResponseRPC struct { + Response *GrantResponse + Err error +} + +// RevokeAccessResponseRPC wraps the response that are received by the GrantAccess +// function over RPC. +type RevokeAccessResponseRPC struct { + Response *RevokeResponse + Err error +} + +// AccessRequesterRPCServer is the server side stub used by AccessRequester +// plugins. +type AccessRequesterRPCServer struct { + Impl AccessRequester +} + +// Init is the server side stub implementation of the Init function. +func (s *AccessRequesterRPCServer) Init(args any, resp *InitResponseRPC) error { + err := s.Impl.Init() + if err != nil { + resp.Err = &PluginError{ + Err: err.Error(), + } + } + return nil +} + +// GrantAccess is the server side stub implementation of the GrantAccess function. +func (s *AccessRequesterRPCServer) GrantAccess(args GrantAccessArgsRPC, resp *GrantAccessResponseRPC) error { + gr, err := s.Impl.GrantAccess(args.AccReq, args.App) + resp.Response = gr + if err != nil { + resp.Err = &PluginError{ + Err: err.Error(), + } + } + return nil +} + +// RevokeAccess is the server side stub implementation of the RevokeAccess function. +func (s *AccessRequesterRPCServer) RevokeAccess(args RevokeAccessArgsRPC, resp *RevokeAccessResponseRPC) error { + rr, err := s.Impl.RevokeAccess(args.AccReq, args.App) + resp.Response = rr + if err != nil { + resp.Err = &PluginError{ + Err: err.Error(), + } + } + return nil +} + +// AccessRequesterRPCClient is the client side stub used by AccessRequester +// plugins. +type AccessRequesterRPCClient struct { + client *rpc.Client +} + +// Init is the client side stub implementation of the Init function. +func (c *AccessRequesterRPCClient) Init() error { + resp := InitResponseRPC{} + err := c.client.Call("Plugin.Init", new(interface{}), &resp) + if err != nil { + return fmt.Errorf("Init RPC call error: %s", err) + } + return resp.Err +} + +// GrantAccess is the client side stub implementation of the GrantAccess function. +func (c *AccessRequesterRPCClient) GrantAccess(ar *api.AccessRequest, app *argocd.Application) (*GrantResponse, error) { + resp := GrantAccessResponseRPC{} + args := GrantAccessArgsRPC{ + AccReq: ar, + App: app, + } + err := c.client.Call("Plugin.GrantAccess", &args, &resp) + if err != nil { + return nil, fmt.Errorf("GrantAccess RPC call error: %s", err) + } + return resp.Response, resp.Err +} + +// RevokeAccess is the client side stub implementation of the RevokeAccess function. +func (c *AccessRequesterRPCClient) RevokeAccess(ar *api.AccessRequest, app *argocd.Application) (*RevokeResponse, error) { + resp := RevokeAccessResponseRPC{} + args := RevokeAccessArgsRPC{ + AccReq: ar, + App: app, + } + err := c.client.Call("Plugin.RevokeAccess", &args, &resp) + if err != nil { + return nil, fmt.Errorf("RevokeAccess RPC call error: %s", err) + } + return resp.Response, resp.Err +} + +// AccessRequestPlugin is the implementation of plugin.Plugin so we can serve/consume +// +// This has two methods: +// - Server(): must return an RPC server for this plugin type. +// - Client(): must return an implementation of the interface that communicates +// over an RPC client. +type AccessRequestPlugin struct { + Impl AccessRequester +} + +// Server will build and return the server side stub for AccessRequester plugings. +func (p *AccessRequestPlugin) Server(*goPlugin.MuxBroker) (interface{}, error) { + return &AccessRequesterRPCServer{Impl: p.Impl}, nil +} + +// Client will build and return the client side stub for AccessRequester plugings. +func (AccessRequestPlugin) Client(b *goPlugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &AccessRequesterRPCClient{client: c}, nil +} + +// handshake returns the handshake config used by AccessRequester plugins. +func handshake() goPlugin.HandshakeConfig { + return goPlugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "EPHEMERAL_ACCESS_PLUGIN", + MagicCookieValue: "ephemeralaccess", + } +} + +// NewServerConfig will build and return a new instance of server stub configs. +func NewServerConfig(impl AccessRequester, log hclog.Logger) *goPlugin.ServeConfig { + pluginMap := map[string]goPlugin.Plugin{ + Key: &AccessRequestPlugin{ + Impl: impl, + }, + } + return &goPlugin.ServeConfig{ + HandshakeConfig: handshake(), + Plugins: pluginMap, + Logger: log, + } +} + +// NewClientConfig will build and return a new instance of client stub configs. +func NewClientConfig(pluginPath string, log hclog.Logger) *goPlugin.ClientConfig { + pluginMap := map[string]goPlugin.Plugin{ + Key: &AccessRequestPlugin{}, + } + return &goPlugin.ClientConfig{ + HandshakeConfig: handshake(), + Plugins: pluginMap, + Cmd: exec.Command(pluginPath), + Logger: log, + } +} + +// GetAccessRequester will attempt to instantiate a new AccessRequester from the +// provided client. The returned AccessRequester will invoke RPC calls targeting +// the plugin implementation on method calls. +func GetAccessRequester(client *goPlugin.Client) (AccessRequester, error) { + rpcClient, err := client.Client() + if err != nil { + return nil, fmt.Errorf("error retrieving rpc client: %w", err) + } + + raw, err := rpcClient.Dispense(Key) + if err != nil { + return nil, fmt.Errorf("error getting a new plugin instance: %w", err) + } + + plugin, ok := raw.(AccessRequester) + if !ok { + return nil, fmt.Errorf("returned plugin instance is not AccessRequester") + } + + return plugin, nil +} diff --git a/pkg/plugin/rpc_test.go b/pkg/plugin/rpc_test.go new file mode 100644 index 0000000..d9e59fa --- /dev/null +++ b/pkg/plugin/rpc_test.go @@ -0,0 +1,226 @@ +package plugin_test + +import ( + "context" + "fmt" + "testing" + "time" + + argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1" + api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" + "github.com/argoproj-labs/ephemeral-access/pkg/plugin" + "github.com/argoproj-labs/ephemeral-access/test/mocks" + goPlugin "github.com/hashicorp/go-plugin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type fixture struct { + accessRequesterMock *mocks.MockAccessRequester + cancel func() + client plugin.AccessRequester +} + +func newFixture(t *testing.T) *fixture { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan *goPlugin.ReattachConfig, 1) + + mock := mocks.NewMockAccessRequester(t) + srvConfig := plugin.NewServerConfig(mock, nil) + srvConfig.Test = &goPlugin.ServeTestConfig{ + Context: ctx, + ReattachConfigCh: ch, + } + + go goPlugin.Serve(srvConfig) + + // We should get a config + var config *goPlugin.ReattachConfig + select { + case config = <-ch: + case <-time.After(2000 * time.Millisecond): + t.Fatal("ReattachConfig not received: timed out!") + } + if config == nil { + t.Fatal("config should not be nil") + } + + cliConfig := plugin.NewClientConfig("", nil) + cliConfig.Cmd = nil + cliConfig.Reattach = config + client := goPlugin.NewClient(cliConfig) + + plugin, err := plugin.GetAccessRequester(client) + if err != nil { + t.Fatalf("error getting AccessRequester: %s", err) + } + return &fixture{ + accessRequesterMock: mock, + cancel: cancel, + client: plugin, + } +} + +func TestAccessRequesterRPC(t *testing.T) { + newAccessRequest := func(name, namespace, roletemplate, username string) *api.AccessRequest { + return &api.AccessRequest{ + Spec: api.AccessRequestSpec{ + RoleTemplateName: roletemplate, + Application: api.TargetApplication{ + Name: name, + Namespace: namespace, + }, + Subjects: []api.Subject{ + { + Username: username, + }, + }, + }, + } + } + newApplication := func(project string) *argocd.Application { + return &argocd.Application{ + Spec: argocd.ApplicationSpec{ + Project: project, + }, + } + } + t.Run("will validate Init is invoked without errors", func(t *testing.T) { + // Given + f := newFixture(t) + defer f.cancel() + f.accessRequesterMock.EXPECT().Init().Return(nil) + + // When + err := f.client.Init() + + // Then + assert.NoError(t, err) + f.accessRequesterMock.AssertNumberOfCalls(t, "Init", 1) + }) + t.Run("will validate Init is invoked returning error", func(t *testing.T) { + // Given + f := newFixture(t) + defer f.cancel() + expectedError := fmt.Errorf("Init error") + f.accessRequesterMock.EXPECT().Init().Return(expectedError) + + // When + err := f.client.Init() + + // Then + assert.Error(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + f.accessRequesterMock.AssertNumberOfCalls(t, "Init", 1) + f.accessRequesterMock.AssertNumberOfCalls(t, "GrantAccess", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "RevokeAccess", 0) + }) + t.Run("will validate GrantAccess is invoked without errors", func(t *testing.T) { + // Given + f := newFixture(t) + defer f.cancel() + ar := newAccessRequest("some-ar", "some-ns", "some-roletmpl", "some-user") + app := newApplication("some-project") + var receivedAr *api.AccessRequest + var receivedApp *argocd.Application + expectedMessage := "some grant message" + runFn := func(ar *api.AccessRequest, app *argocd.Application) (*plugin.GrantResponse, error) { + receivedAr = ar + receivedApp = app + return &plugin.GrantResponse{ + Status: plugin.Granted, + Message: expectedMessage, + }, nil + + } + f.accessRequesterMock.EXPECT().GrantAccess(ar, app). + RunAndReturn(runFn) + + // When + resp, err := f.client.GrantAccess(ar, app) + + // Then + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, ar, receivedAr) + assert.Equal(t, app, receivedApp) + assert.Equal(t, plugin.Granted, resp.Status) + assert.Equal(t, expectedMessage, resp.Message) + f.accessRequesterMock.AssertNumberOfCalls(t, "Init", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "GrantAccess", 1) + f.accessRequesterMock.AssertNumberOfCalls(t, "RevokeAccess", 0) + }) + t.Run("will validate GrantAccess properly returns error", func(t *testing.T) { + // Given + f := newFixture(t) + defer f.cancel() + expectedErr := "grant access error" + f.accessRequesterMock.EXPECT().GrantAccess(mock.Anything, mock.Anything). + Return(nil, fmt.Errorf(expectedErr)) + + // When + resp, err := f.client.GrantAccess(nil, nil) + + // Then + assert.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, expectedErr, err.Error()) + f.accessRequesterMock.AssertNumberOfCalls(t, "Init", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "GrantAccess", 1) + f.accessRequesterMock.AssertNumberOfCalls(t, "RevokeAccess", 0) + }) + t.Run("will validate RevokeAccess is invoked without errors", func(t *testing.T) { + // Given + f := newFixture(t) + defer f.cancel() + ar := newAccessRequest("some-ar", "some-ns", "some-roletmpl", "some-user") + app := newApplication("some-project") + var receivedAr *api.AccessRequest + var receivedApp *argocd.Application + expectedMessage := "some revoke message" + runFn := func(ar *api.AccessRequest, app *argocd.Application) (*plugin.RevokeResponse, error) { + receivedAr = ar + receivedApp = app + return &plugin.RevokeResponse{ + Status: plugin.Revoked, + Message: expectedMessage, + }, nil + + } + f.accessRequesterMock.EXPECT().RevokeAccess(ar, app). + RunAndReturn(runFn) + + // When + resp, err := f.client.RevokeAccess(ar, app) + + // Then + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, ar, receivedAr) + assert.Equal(t, app, receivedApp) + assert.Equal(t, plugin.Revoked, resp.Status) + assert.Equal(t, expectedMessage, resp.Message) + f.accessRequesterMock.AssertNumberOfCalls(t, "Init", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "GrantAccess", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "RevokeAccess", 1) + }) + t.Run("will validate RevokeAccess properly returns error", func(t *testing.T) { + // Given + f := newFixture(t) + defer f.cancel() + expectedErr := "revoke access error" + f.accessRequesterMock.EXPECT().RevokeAccess(mock.Anything, mock.Anything). + Return(nil, fmt.Errorf(expectedErr)) + + // When + resp, err := f.client.RevokeAccess(nil, nil) + + // Then + assert.Error(t, err) + assert.Nil(t, resp) + assert.Equal(t, expectedErr, err.Error()) + f.accessRequesterMock.AssertNumberOfCalls(t, "Init", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "GrantAccess", 0) + f.accessRequesterMock.AssertNumberOfCalls(t, "RevokeAccess", 1) + }) +} diff --git a/test/mocks/mock_AccessRequester.go b/test/mocks/mock_AccessRequester.go new file mode 100644 index 0000000..a6d9a53 --- /dev/null +++ b/test/mocks/mock_AccessRequester.go @@ -0,0 +1,202 @@ +// Code generated by mockery v2.45.0. DO NOT EDIT. + +package mocks + +import ( + argoprojv1alpha1 "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1" + mock "github.com/stretchr/testify/mock" + + plugin "github.com/argoproj-labs/ephemeral-access/pkg/plugin" + + v1alpha1 "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1" +) + +// MockAccessRequester is an autogenerated mock type for the AccessRequester type +type MockAccessRequester struct { + mock.Mock +} + +type MockAccessRequester_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAccessRequester) EXPECT() *MockAccessRequester_Expecter { + return &MockAccessRequester_Expecter{mock: &_m.Mock} +} + +// GrantAccess provides a mock function with given fields: ar, app +func (_m *MockAccessRequester) GrantAccess(ar *v1alpha1.AccessRequest, app *argoprojv1alpha1.Application) (*plugin.GrantResponse, error) { + ret := _m.Called(ar, app) + + if len(ret) == 0 { + panic("no return value specified for GrantAccess") + } + + var r0 *plugin.GrantResponse + var r1 error + if rf, ok := ret.Get(0).(func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) (*plugin.GrantResponse, error)); ok { + return rf(ar, app) + } + if rf, ok := ret.Get(0).(func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) *plugin.GrantResponse); ok { + r0 = rf(ar, app) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.GrantResponse) + } + } + + if rf, ok := ret.Get(1).(func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) error); ok { + r1 = rf(ar, app) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccessRequester_GrantAccess_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GrantAccess' +type MockAccessRequester_GrantAccess_Call struct { + *mock.Call +} + +// GrantAccess is a helper method to define mock.On call +// - ar *v1alpha1.AccessRequest +// - app *argoprojv1alpha1.Application +func (_e *MockAccessRequester_Expecter) GrantAccess(ar interface{}, app interface{}) *MockAccessRequester_GrantAccess_Call { + return &MockAccessRequester_GrantAccess_Call{Call: _e.mock.On("GrantAccess", ar, app)} +} + +func (_c *MockAccessRequester_GrantAccess_Call) Run(run func(ar *v1alpha1.AccessRequest, app *argoprojv1alpha1.Application)) *MockAccessRequester_GrantAccess_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*v1alpha1.AccessRequest), args[1].(*argoprojv1alpha1.Application)) + }) + return _c +} + +func (_c *MockAccessRequester_GrantAccess_Call) Return(_a0 *plugin.GrantResponse, _a1 error) *MockAccessRequester_GrantAccess_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccessRequester_GrantAccess_Call) RunAndReturn(run func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) (*plugin.GrantResponse, error)) *MockAccessRequester_GrantAccess_Call { + _c.Call.Return(run) + return _c +} + +// Init provides a mock function with given fields: +func (_m *MockAccessRequester) Init() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Init") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockAccessRequester_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init' +type MockAccessRequester_Init_Call struct { + *mock.Call +} + +// Init is a helper method to define mock.On call +func (_e *MockAccessRequester_Expecter) Init() *MockAccessRequester_Init_Call { + return &MockAccessRequester_Init_Call{Call: _e.mock.On("Init")} +} + +func (_c *MockAccessRequester_Init_Call) Run(run func()) *MockAccessRequester_Init_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockAccessRequester_Init_Call) Return(_a0 error) *MockAccessRequester_Init_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockAccessRequester_Init_Call) RunAndReturn(run func() error) *MockAccessRequester_Init_Call { + _c.Call.Return(run) + return _c +} + +// RevokeAccess provides a mock function with given fields: ar, app +func (_m *MockAccessRequester) RevokeAccess(ar *v1alpha1.AccessRequest, app *argoprojv1alpha1.Application) (*plugin.RevokeResponse, error) { + ret := _m.Called(ar, app) + + if len(ret) == 0 { + panic("no return value specified for RevokeAccess") + } + + var r0 *plugin.RevokeResponse + var r1 error + if rf, ok := ret.Get(0).(func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) (*plugin.RevokeResponse, error)); ok { + return rf(ar, app) + } + if rf, ok := ret.Get(0).(func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) *plugin.RevokeResponse); ok { + r0 = rf(ar, app) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.RevokeResponse) + } + } + + if rf, ok := ret.Get(1).(func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) error); ok { + r1 = rf(ar, app) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccessRequester_RevokeAccess_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevokeAccess' +type MockAccessRequester_RevokeAccess_Call struct { + *mock.Call +} + +// RevokeAccess is a helper method to define mock.On call +// - ar *v1alpha1.AccessRequest +// - app *argoprojv1alpha1.Application +func (_e *MockAccessRequester_Expecter) RevokeAccess(ar interface{}, app interface{}) *MockAccessRequester_RevokeAccess_Call { + return &MockAccessRequester_RevokeAccess_Call{Call: _e.mock.On("RevokeAccess", ar, app)} +} + +func (_c *MockAccessRequester_RevokeAccess_Call) Run(run func(ar *v1alpha1.AccessRequest, app *argoprojv1alpha1.Application)) *MockAccessRequester_RevokeAccess_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*v1alpha1.AccessRequest), args[1].(*argoprojv1alpha1.Application)) + }) + return _c +} + +func (_c *MockAccessRequester_RevokeAccess_Call) Return(_a0 *plugin.RevokeResponse, _a1 error) *MockAccessRequester_RevokeAccess_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccessRequester_RevokeAccess_Call) RunAndReturn(run func(*v1alpha1.AccessRequest, *argoprojv1alpha1.Application) (*plugin.RevokeResponse, error)) *MockAccessRequester_RevokeAccess_Call { + _c.Call.Return(run) + return _c +} + +// NewMockAccessRequester creates a new instance of MockAccessRequester. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockAccessRequester(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAccessRequester { + mock := &MockAccessRequester{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}