From dcb5d5675a810f6d5f0058d81493130ee6647a0e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 29 May 2025 11:30:17 -0500 Subject: [PATCH 01/44] Add internal models These internal models are introduced to reduce decoupling. The idea is to operate internal model within the project boundaries and convert to appropriate Connect or SDK models in the places where it's necessary. --- go.mod | 28 +++++--- go.sum | 48 ++++++++----- pkg/onepassword/model/file.go | 27 +++++++ pkg/onepassword/model/item.go | 87 ++++++++++++++++++++++ pkg/onepassword/model/item_field.go | 7 ++ pkg/onepassword/model/item_test.go | 108 ++++++++++++++++++++++++++++ pkg/onepassword/model/vault.go | 23 ++++++ pkg/onepassword/model/vault_test.go | 36 ++++++++++ 8 files changed, 338 insertions(+), 26 deletions(-) create mode 100644 pkg/onepassword/model/file.go create mode 100644 pkg/onepassword/model/item.go create mode 100644 pkg/onepassword/model/item_field.go create mode 100644 pkg/onepassword/model/item_test.go create mode 100644 pkg/onepassword/model/vault.go create mode 100644 pkg/onepassword/model/vault_test.go diff --git a/go.mod b/go.mod index 91b5035..565f620 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module github.com/1Password/onepassword-operator -go 1.21 +go 1.22.0 -toolchain go1.21.5 +toolchain go1.24.1 require ( github.com/1Password/connect-sdk-go v1.5.3 + github.com/1password/onepassword-sdk-go v0.3.0 github.com/onsi/ginkgo/v2 v2.14.0 github.com/onsi/gomega v1.30.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 @@ -20,16 +21,19 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/extism/go-sdk v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -38,6 +42,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -53,22 +58,25 @@ require ( github.com/prometheus/common v0.51.1 // indirect github.com/prometheus/procfs v0.13.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.19.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 108a196..1f02584 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk= github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs= +github.com/1password/onepassword-sdk-go v0.3.0 h1:PC3J08hOH7xmt5QjpakhjZzx0XfbBb4SkBVEqgYYG54= +github.com/1password/onepassword-sdk-go v0.3.0/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -12,16 +14,20 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.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/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo= +github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc= 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= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -32,6 +38,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -54,6 +62,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= +github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -102,8 +112,12 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= @@ -111,6 +125,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -134,8 +150,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -150,18 +166,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -169,8 +185,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -181,8 +197,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/onepassword/model/file.go b/pkg/onepassword/model/file.go new file mode 100644 index 0000000..7b97bed --- /dev/null +++ b/pkg/onepassword/model/file.go @@ -0,0 +1,27 @@ +package model + +import ( + "errors" +) + +// File represents a file stored in 1Password. +type File struct { + ID string + Name string + Size int + ContentPath string + content []byte +} + +// Content returns the content of the file if they have been loaded and returns an error if they have not been loaded. +// Use `client.GetFileContent(file *File)` instead to make sure the content is fetched automatically if not present. +func (f *File) Content() ([]byte, error) { + if f.content == nil { + return nil, errors.New("file content not loaded") + } + return f.content, nil +} + +func (f *File) SetContent(content []byte) { + f.content = content +} diff --git a/pkg/onepassword/model/item.go b/pkg/onepassword/model/item.go new file mode 100644 index 0000000..9066204 --- /dev/null +++ b/pkg/onepassword/model/item.go @@ -0,0 +1,87 @@ +package model + +import ( + "time" + + connect "github.com/1Password/connect-sdk-go/onepassword" + sdk "github.com/1password/onepassword-sdk-go" +) + +// Item represents 1Password item. +type Item struct { + ID string + VaultID string + Version int + Tags []string + Fields []ItemField + Files []File + CreatedAt time.Time +} + +// FromConnectItem populates the Item from a Connect item. +func (i *Item) FromConnectItem(item *connect.Item) { + i.ID = item.ID + i.VaultID = item.Vault.ID + i.Version = item.Version + + for _, tag := range item.Tags { + i.Tags = append(i.Tags, tag) + } + + for _, field := range item.Fields { + i.Fields = append(i.Fields, ItemField{ + Label: field.Label, + Value: field.Value, + }) + } + + for _, file := range item.Files { + i.Files = append(i.Files, File{ + ID: file.ID, + Name: file.Name, + Size: file.Size, + }) + } + + i.CreatedAt = item.CreatedAt +} + +// FromSDKItem populates the Item from an SDK item. +func (i *Item) FromSDKItem(item *sdk.Item) { + i.ID = item.ID + i.VaultID = item.VaultID + i.Version = int(item.Version) + + for _, tag := range item.Tags { + i.Tags = append(i.Tags, tag) + } + + for _, field := range item.Fields { + i.Fields = append(i.Fields, ItemField{ + Label: field.Title, + Value: field.Value, + }) + } + + for _, file := range item.Files { + i.Files = append(i.Files, File{ + ID: file.Attributes.ID, + Name: file.Attributes.Name, + Size: int(file.Attributes.Size), + }) + } + + i.CreatedAt = item.CreatedAt +} + +// FromSDKItemOverview populates the Item from an SDK item overview. +func (i *Item) FromSDKItemOverview(item *sdk.ItemOverview) { + i.ID = item.ID + i.VaultID = item.VaultID + + for _, tag := range item.Tags { + i.Tags = append(i.Tags, tag) + } + + i.CreatedAt = item.CreatedAt +} diff --git a/pkg/onepassword/model/item_field.go b/pkg/onepassword/model/item_field.go new file mode 100644 index 0000000..f88a044 --- /dev/null +++ b/pkg/onepassword/model/item_field.go @@ -0,0 +1,7 @@ +package model + +// ItemField Representation of a single field on an Item +type ItemField struct { + Label string + Value string +} diff --git a/pkg/onepassword/model/item_test.go b/pkg/onepassword/model/item_test.go new file mode 100644 index 0000000..8b5b178 --- /dev/null +++ b/pkg/onepassword/model/item_test.go @@ -0,0 +1,108 @@ +package model + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + connect "github.com/1Password/connect-sdk-go/onepassword" + sdk "github.com/1password/onepassword-sdk-go" +) + +func TestItem_FromConnectItem(t *testing.T) { + connectItem := &connect.Item{ + ID: "test-item-id", + Vault: connect.ItemVault{ + ID: "test-vault-id", + }, + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []*connect.ItemField{ + {Label: "field1", Value: "value1"}, + {Label: "field2", Value: "value2"}, + }, + Files: []*connect.File{ + {ID: "file1", Name: "file1.txt", Size: 1234}, + {ID: "file2", Name: "file2.txt", Size: 1234}, + }, + CreatedAt: time.Now(), + } + + item := &Item{} + item.FromConnectItem(connectItem) + + require.Equal(t, connectItem.ID, item.ID) + require.Equal(t, connectItem.Vault.ID, item.VaultID) + require.Equal(t, connectItem.Version, item.Version) + require.ElementsMatch(t, connectItem.Tags, item.Tags) + + for i, field := range connectItem.Fields { + require.Equal(t, field.Label, item.Fields[i].Label) + require.Equal(t, field.Value, item.Fields[i].Value) + } + + for i, file := range connectItem.Files { + require.Equal(t, file.ID, item.Files[i].ID) + require.Equal(t, file.Name, item.Files[i].Name) + require.Equal(t, file.Size, item.Files[i].Size) + } + + require.Equal(t, connectItem.CreatedAt, item.CreatedAt) +} + +func TestItem_FromSDKItem(t *testing.T) { + sdkItem := &sdk.Item{ + ID: "test-item-id", + VaultID: "test-vault-id", + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []sdk.ItemField{ + {ID: "1", Title: "field1", Value: "value1"}, + {ID: "2", Title: "field2", Value: "value2"}, + }, + Files: []sdk.ItemFile{ + {Attributes: sdk.FileAttributes{Name: "file1.txt", Size: 1234}, FieldID: "file1"}, + {Attributes: sdk.FileAttributes{Name: "file2.txt", Size: 1234}, FieldID: "file2"}, + }, + CreatedAt: time.Now(), + } + + item := &Item{} + item.FromSDKItem(sdkItem) + + require.Equal(t, sdkItem.ID, item.ID) + require.Equal(t, sdkItem.VaultID, item.VaultID) + require.Equal(t, int(sdkItem.Version), item.Version) + require.ElementsMatch(t, sdkItem.Tags, item.Tags) + + for i, field := range sdkItem.Fields { + require.Equal(t, field.Title, item.Fields[i].Label) + require.Equal(t, field.Value, item.Fields[i].Value) + } + + for i, file := range sdkItem.Files { + require.Equal(t, file.Attributes.ID, item.Files[i].ID) + require.Equal(t, file.Attributes.Name, item.Files[i].Name) + require.Equal(t, int(file.Attributes.Size), item.Files[i].Size) + } + + require.Equal(t, sdkItem.CreatedAt, item.CreatedAt) +} + +func TestItem_FromSDKItemOverview(t *testing.T) { + sdkItemOverview := &sdk.ItemOverview{ + ID: "test-item-id", + VaultID: "test-vault-id", + Tags: []string{"tag1", "tag2"}, + CreatedAt: time.Now(), + } + + item := &Item{} + item.FromSDKItemOverview(sdkItemOverview) + + require.Equal(t, sdkItemOverview.ID, item.ID) + require.Equal(t, sdkItemOverview.VaultID, item.VaultID) + require.ElementsMatch(t, sdkItemOverview.Tags, item.Tags) + require.Equal(t, sdkItemOverview.CreatedAt, item.CreatedAt) +} diff --git a/pkg/onepassword/model/vault.go b/pkg/onepassword/model/vault.go new file mode 100644 index 0000000..e90e492 --- /dev/null +++ b/pkg/onepassword/model/vault.go @@ -0,0 +1,23 @@ +package model + +import ( + "time" + + connect "github.com/1Password/connect-sdk-go/onepassword" + sdk "github.com/1password/onepassword-sdk-go" +) + +type Vault struct { + ID string + CreatedAt time.Time +} + +func (v *Vault) FromConnectVault(vault *connect.Vault) { + v.ID = vault.ID + v.CreatedAt = vault.CreatedAt +} + +func (v *Vault) FromSDKVault(vault *sdk.VaultOverview) { + v.ID = vault.ID + v.CreatedAt = time.Now() // TODO: add to SDK and use it instead of time.Now() +} diff --git a/pkg/onepassword/model/vault_test.go b/pkg/onepassword/model/vault_test.go new file mode 100644 index 0000000..48c7be2 --- /dev/null +++ b/pkg/onepassword/model/vault_test.go @@ -0,0 +1,36 @@ +package model + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + connect "github.com/1Password/connect-sdk-go/onepassword" + sdk "github.com/1password/onepassword-sdk-go" +) + +func TestVault_FromConnectVault(t *testing.T) { + connectVault := &connect.Vault{ + ID: "test-id", + CreatedAt: time.Now(), + } + + vault := &Vault{} + vault.FromConnectVault(connectVault) + + require.Equal(t, connectVault.ID, vault.ID) + require.Equal(t, connectVault.CreatedAt, vault.CreatedAt) +} + +// TODO: check CreatedAt when available +func TestVault_FromSDKVault(t *testing.T) { + sdkVault := &sdk.VaultOverview{ + ID: "test-id", + } + + vault := &Vault{} + vault.FromSDKVault(sdkVault) + + require.Equal(t, sdkVault.ID, vault.ID) +} From 8881782559f47bb93d29ae4da80c30ed38d6510b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 29 May 2025 13:10:15 -0500 Subject: [PATCH 02/44] Create Connect client wrapper --- go.mod | 1 + pkg/onepassword/client/connect/connect.go | 79 +++++ .../client/connect/connect_test.go | 269 ++++++++++++++++++ pkg/onepassword/client/mock/connect.go | 130 +++++++++ 4 files changed, 479 insertions(+) create mode 100644 pkg/onepassword/client/connect/connect.go create mode 100644 pkg/onepassword/client/connect/connect_test.go create mode 100644 pkg/onepassword/client/mock/connect.go diff --git a/go.mod b/go.mod index 565f620..791c535 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/prometheus/common v0.51.1 // indirect github.com/prometheus/procfs v0.13.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go new file mode 100644 index 0000000..d1da4ef --- /dev/null +++ b/pkg/onepassword/client/connect/connect.go @@ -0,0 +1,79 @@ +package connect + +import ( + "fmt" + + "github.com/1Password/connect-sdk-go/connect" + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +// Config holds the configuration for the Connect client. +type Config struct { + ConnectHost string + ConnectToken string + UserAgent string +} + +// Connect is a client for interacting with 1Password using the Connect API. +type Connect struct { + client connect.Client +} + +func NewClient(config Config) *Connect { + return &Connect{ + client: connect.NewClientWithUserAgent(config.ConnectHost, config.ConnectToken, config.UserAgent), + } +} + +func (c *Connect) GetItemByID(vaultID, itemID string) (*model.Item, error) { + connectItem, err := c.client.GetItemByUUID(itemID, vaultID) + if err != nil { + return nil, err + } + + var item model.Item + item.FromConnectItem(connectItem) + return &item, nil +} + +func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { + // Get all items in the vault with the specified title + connectItems, err := c.client.GetItemsByTitle(itemTitle, vaultID) + if err != nil { + return nil, err + } + + var items []model.Item + for _, connectItem := range connectItems { + var item model.Item + item.FromConnectItem(&connectItem) + items = append(items, item) + } + + return items, nil +} + +func (c *Connect) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { + return c.client.GetFileContent(&onepassword.File{ + ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", vaultID, itemID, fileID), + }) +} + +func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) { + connectVaults, err := c.client.GetVaultsByTitle(vaultQuery) + if err != nil { + return nil, err + } + + var vaults []model.Vault + for _, connectVault := range connectVaults { + if vaultQuery == connectVault.Name { + var vault model.Vault + vault.FromConnectVault(&connectVault) + vaults = append(vaults, vault) + } + } + + return vaults, nil +} diff --git a/pkg/onepassword/client/connect/connect_test.go b/pkg/onepassword/client/connect/connect_test.go new file mode 100644 index 0000000..557e9ff --- /dev/null +++ b/pkg/onepassword/client/connect/connect_test.go @@ -0,0 +1,269 @@ +package connect + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/onepassword/client/mock" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +const VaultTitleEmployee = "Employee" + +func TestConnect_GetItemByID(t *testing.T) { + connectItem := createItem() + + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, item *model.Item, err error) + }{ + "should return an item": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return(connectItem, nil) + return mockConnectClient + }, + check: func(t *testing.T, item *model.Item, err error) { + require.NoError(t, err) + checkItem(t, connectItem, item) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return((*onepassword.Item)(nil), errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, item *model.Item, err error) { + require.Error(t, err) + require.Nil(t, item) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + item, err := client.GetItemByID("vault-id", "item-id") + tc.check(t, item, err) + }) + } +} + +func TestConnect_GetItemsByTitle(t *testing.T) { + connectItem1 := createItem() + connectItem2 := createItem() + + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, items []model.Item, err error) + }{ + "should return a single item": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return( + []onepassword.Item{ + *connectItem1, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, connectItem1.ID, items[0].ID) + }, + }, + "should return two items": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return( + []onepassword.Item{ + *connectItem1, + *connectItem2, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 2) + checkItem(t, connectItem1, &items[0]) + checkItem(t, connectItem2, &items[1]) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return([]onepassword.Item{}, errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, items []model.Item, err error) { + require.Error(t, err) + require.Nil(t, items) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + items, err := client.GetItemsByTitle("vault-id", "item-title") + tc.check(t, items, err) + }) + } +} + +func TestConnect_GetFileContent(t *testing.T) { + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, content []byte, err error) + }{ + "should return file content": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetFileContent", &onepassword.File{ + ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content", + }).Return([]byte("file content"), nil) + return mockConnectClient + }, + check: func(t *testing.T, content []byte, err error) { + require.NoError(t, err) + require.Equal(t, []byte("file content"), content) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetFileContent", &onepassword.File{ + ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content", + }).Return(nil, errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, content []byte, err error) { + require.Error(t, err) + require.Nil(t, content) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + content, err := client.GetFileContent("vault-id", "item-id", "file-id") + tc.check(t, content, err) + }) + } +} + +func TestConnect_GetVaultsByTitle(t *testing.T) { + testCases := map[string]struct { + mockClient func() *mock.ConnectClientMock + check func(t *testing.T, vaults []model.Vault, err error) + }{ + "should return a single vault": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{ + { + ID: "test-id", + Name: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Name: "Some other vault", + }, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 1) + require.Equal(t, "test-id", vaults[0].ID) + }, + }, + "should return a two vaults": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{ + { + ID: "test-id", + Name: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Name: VaultTitleEmployee, + }, + }, nil) + return mockConnectClient + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 2) + // Check the first vault + require.Equal(t, "test-id", vaults[0].ID) + // Check the second vault + require.Equal(t, "test-id-2", vaults[1].ID) + }, + }, + "should return an error": { + mockClient: func() *mock.ConnectClientMock { + mockConnectClient := &mock.ConnectClientMock{} + mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{}, errors.New("error")) + return mockConnectClient + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.Error(t, err) + require.Empty(t, vaults) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &Connect{client: tc.mockClient()} + vault, err := client.GetVaultsByTitle(VaultTitleEmployee) + tc.check(t, vault, err) + }) + } +} + +func createItem() *onepassword.Item { + return &onepassword.Item{ + ID: "test-id", + Vault: onepassword.ItemVault{ID: "test-vault-id"}, + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []*onepassword.ItemField{ + {Label: "label1", Value: "value1"}, + {Label: "label2", Value: "value2"}, + }, + Files: []*onepassword.File{ + {ID: "file-id-1", Name: "file1.txt", Size: 1234}, + {ID: "file-id-2", Name: "file2.txt", Size: 1234}, + }, + } +} + +func checkItem(t *testing.T, expected *onepassword.Item, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.Vault.ID, actual.VaultID) + require.Equal(t, expected.Version, actual.Version) + require.ElementsMatch(t, expected.Tags, actual.Tags) + + for i, field := range expected.Fields { + require.Equal(t, field.Label, actual.Fields[i].Label) + require.Equal(t, field.Value, actual.Fields[i].Value) + } + + for i, file := range expected.Files { + require.Equal(t, file.ID, actual.Files[i].ID) + require.Equal(t, file.Name, actual.Files[i].Name) + require.Equal(t, file.Size, actual.Files[i].Size) + } + + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} diff --git a/pkg/onepassword/client/mock/connect.go b/pkg/onepassword/client/mock/connect.go new file mode 100644 index 0000000..a38c4f6 --- /dev/null +++ b/pkg/onepassword/client/mock/connect.go @@ -0,0 +1,130 @@ +package mock + +import ( + "github.com/stretchr/testify/mock" + + "github.com/1Password/connect-sdk-go/onepassword" +) + +// ConnectClientMock is a mock implementation of the ConnectClient interface +type ConnectClientMock struct { + mock.Mock +} + +func (c *ConnectClientMock) GetVaults() ([]onepassword.Vault, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetVault(uuid string) (*onepassword.Vault, error) { + args := c.Called(uuid) + return args.Get(0).(*onepassword.Vault), args.Error(1) +} + +func (c *ConnectClientMock) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetVaultByTitle(title string) (*onepassword.Vault, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { + args := c.Called(title) + return args.Get(0).([]onepassword.Vault), args.Error(1) +} + +func (c *ConnectClientMock) GetItems(vaultQuery string) ([]onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) { + args := c.Called(uuid, vaultQuery) + return args.Get(0).(*onepassword.Item), args.Error(1) +} + +func (c *ConnectClientMock) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) { + args := c.Called(title, vaultQuery) + return args.Get(0).([]onepassword.Item), args.Error(1) +} + +func (c *ConnectClientMock) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) DeleteItem(item *onepassword.Item, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) DeleteItemByID(itemUUID string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) DeleteItemByTitle(title string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) GetFileContent(file *onepassword.File) ([]byte, error) { + args := c.Called(file) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]byte), args.Error(1) +} + +func (c *ConnectClientMock) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error { + //TODO implement me + panic("implement me") +} + +func (c *ConnectClientMock) LoadStruct(config interface{}) error { + //TODO implement me + panic("implement me") +} From a49c6ee045bdc5fe3ce9746b8ba5c32069429f18 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 29 May 2025 16:06:02 -0500 Subject: [PATCH 03/44] Add SDK client wrapper --- .../client/connect/connect_test.go | 54 +--- pkg/onepassword/client/sdk/sdk.go | 91 ++++++ pkg/onepassword/client/sdk/sdk_test.go | 269 ++++++++++++++++++ pkg/onepassword/client/testing/item.go | 110 +++++++ .../client/{ => testing}/mock/connect.go | 0 pkg/onepassword/client/testing/mock/sdk.go | 89 ++++++ 6 files changed, 567 insertions(+), 46 deletions(-) create mode 100644 pkg/onepassword/client/sdk/sdk.go create mode 100644 pkg/onepassword/client/sdk/sdk_test.go create mode 100644 pkg/onepassword/client/testing/item.go rename pkg/onepassword/client/{ => testing}/mock/connect.go (100%) create mode 100644 pkg/onepassword/client/testing/mock/sdk.go diff --git a/pkg/onepassword/client/connect/connect_test.go b/pkg/onepassword/client/connect/connect_test.go index 557e9ff..ef21b90 100644 --- a/pkg/onepassword/client/connect/connect_test.go +++ b/pkg/onepassword/client/connect/connect_test.go @@ -7,14 +7,15 @@ import ( "github.com/stretchr/testify/require" "github.com/1Password/connect-sdk-go/onepassword" - "github.com/1Password/onepassword-operator/pkg/onepassword/client/mock" + clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing" + "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock" "github.com/1Password/onepassword-operator/pkg/onepassword/model" ) const VaultTitleEmployee = "Employee" func TestConnect_GetItemByID(t *testing.T) { - connectItem := createItem() + connectItem := clienttesting.CreateConnectItem() testCases := map[string]struct { mockClient func() *mock.ConnectClientMock @@ -28,7 +29,7 @@ func TestConnect_GetItemByID(t *testing.T) { }, check: func(t *testing.T, item *model.Item, err error) { require.NoError(t, err) - checkItem(t, connectItem, item) + clienttesting.CheckConnectItemMapping(t, connectItem, item) }, }, "should return an error": { @@ -54,8 +55,8 @@ func TestConnect_GetItemByID(t *testing.T) { } func TestConnect_GetItemsByTitle(t *testing.T) { - connectItem1 := createItem() - connectItem2 := createItem() + connectItem1 := clienttesting.CreateConnectItem() + connectItem2 := clienttesting.CreateConnectItem() testCases := map[string]struct { mockClient func() *mock.ConnectClientMock @@ -89,8 +90,8 @@ func TestConnect_GetItemsByTitle(t *testing.T) { check: func(t *testing.T, items []model.Item, err error) { require.NoError(t, err) require.Len(t, items, 2) - checkItem(t, connectItem1, &items[0]) - checkItem(t, connectItem2, &items[1]) + clienttesting.CheckConnectItemMapping(t, connectItem1, &items[0]) + clienttesting.CheckConnectItemMapping(t, connectItem2, &items[1]) }, }, "should return an error": { @@ -228,42 +229,3 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { }) } } - -func createItem() *onepassword.Item { - return &onepassword.Item{ - ID: "test-id", - Vault: onepassword.ItemVault{ID: "test-vault-id"}, - Version: 1, - Tags: []string{"tag1", "tag2"}, - Fields: []*onepassword.ItemField{ - {Label: "label1", Value: "value1"}, - {Label: "label2", Value: "value2"}, - }, - Files: []*onepassword.File{ - {ID: "file-id-1", Name: "file1.txt", Size: 1234}, - {ID: "file-id-2", Name: "file2.txt", Size: 1234}, - }, - } -} - -func checkItem(t *testing.T, expected *onepassword.Item, actual *model.Item) { - t.Helper() - - require.Equal(t, expected.ID, actual.ID) - require.Equal(t, expected.Vault.ID, actual.VaultID) - require.Equal(t, expected.Version, actual.Version) - require.ElementsMatch(t, expected.Tags, actual.Tags) - - for i, field := range expected.Fields { - require.Equal(t, field.Label, actual.Fields[i].Label) - require.Equal(t, field.Value, actual.Fields[i].Value) - } - - for i, file := range expected.Files { - require.Equal(t, file.ID, actual.Files[i].ID) - require.Equal(t, file.Name, actual.Files[i].Name) - require.Equal(t, file.Size, actual.Files[i].Size) - } - - require.Equal(t, expected.CreatedAt, actual.CreatedAt) -} diff --git a/pkg/onepassword/client/sdk/sdk.go b/pkg/onepassword/client/sdk/sdk.go new file mode 100644 index 0000000..eafd687 --- /dev/null +++ b/pkg/onepassword/client/sdk/sdk.go @@ -0,0 +1,91 @@ +package sdk + +import ( + "context" + + "github.com/1Password/onepassword-operator/pkg/onepassword/model" + sdk "github.com/1password/onepassword-sdk-go" +) + +// Config holds the configuration for the 1Password SDK client. +type Config struct { + ServiceAccountToken string + IntegrationName string + IntegrationVersion string +} + +// SDK is a client for interacting with 1Password using the SDK. +type SDK struct { + client *sdk.Client +} + +func NewClient(config Config) (*SDK, error) { + client, err := sdk.NewClient(context.Background(), + sdk.WithServiceAccountToken(config.ServiceAccountToken), + sdk.WithIntegrationInfo(config.IntegrationName, config.IntegrationVersion), + ) + if err != nil { + return nil, err + } + + return &SDK{ + client: client, + }, nil +} + +func (s *SDK) GetItemByID(vaultID, itemID string) (*model.Item, error) { + sdkItem, err := s.client.Items().Get(context.Background(), vaultID, itemID) + if err != nil { + return nil, err + } + + var item model.Item + item.FromSDKItem(&sdkItem) + return &item, nil +} + +func (s *SDK) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { + // Get all items in the vault + sdkItems, err := s.client.Items().List(context.Background(), vaultID) + if err != nil { + return nil, err + } + + // Filter items by title + var items []model.Item + for _, sdkItem := range sdkItems { + if sdkItem.Title == itemTitle { + var item model.Item + item.FromSDKItemOverview(&sdkItem) + items = append(items, item) + } + } + + return items, nil +} + +func (s *SDK) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { + return s.client.Items().Files().Read(context.Background(), vaultID, itemID, sdk.FileAttributes{ + ID: fileID, + }) +} + +func (s *SDK) GetVaultsByTitle(title string) ([]model.Vault, error) { + // List all vaults + sdkVaults, err := s.client.Vaults().List(context.Background()) + if err != nil { + return nil, err + } + + // Filter vaults by title + var vaults []model.Vault + for _, sdkVault := range sdkVaults { + if sdkVault.Title == title { + var vault model.Vault + vault.FromSDKVault(&sdkVault) + vaults = append(vaults, vault) + } + } + + return vaults, nil +} diff --git a/pkg/onepassword/client/sdk/sdk_test.go b/pkg/onepassword/client/sdk/sdk_test.go new file mode 100644 index 0000000..5455c5b --- /dev/null +++ b/pkg/onepassword/client/sdk/sdk_test.go @@ -0,0 +1,269 @@ +package sdk + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing" + clientmock "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" + sdk "github.com/1password/onepassword-sdk-go" +) + +const VaultTitleEmployee = "Employee" + +func TestSDK_GetItemByID(t *testing.T) { + sdkItem := clienttesting.CreateSDKItem() + + testCases := map[string]struct { + mockItemAPI func() *clientmock.ItemAPIMock + check func(t *testing.T, item *model.Item, err error) + }{ + "should return a single vault": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("Get", context.Background(), "vault-id", "item-id").Return(*sdkItem, nil) + return m + }, + check: func(t *testing.T, item *model.Item, err error) { + require.NoError(t, err) + clienttesting.CheckSDKItemMapping(t, sdkItem, item) + }, + }, + "should return an error": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("Get", context.Background(), "vault-id", "item-id").Return(sdk.Item{}, errors.New("error")) + return m + }, + check: func(t *testing.T, item *model.Item, err error) { + require.Error(t, err) + require.Empty(t, item) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + ItemsAPI: tc.mockItemAPI(), + }, + } + item, err := client.GetItemByID("vault-id", "item-id") + tc.check(t, item, err) + }) + } +} + +func TestSDK_GetItemsByTitle(t *testing.T) { + sdkItem1 := clienttesting.CreateSDKItemOverview() + sdkItem2 := clienttesting.CreateSDKItemOverview() + + testCases := map[string]struct { + mockItemAPI func() *clientmock.ItemAPIMock + check func(t *testing.T, items []model.Item, err error) + }{ + "should return a single item": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + + copySDKItem2 := *sdkItem2 + copySDKItem2.Title = "Some other item" + + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{ + *sdkItem1, + copySDKItem2, + }, nil) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 1) + clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0]) + }, + }, + "should return a two items": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{ + *sdkItem1, + *sdkItem2, + }, nil) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 2) + clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0]) + clienttesting.CheckSDKItemOverviewMapping(t, sdkItem2, &items[1]) + }, + }, + "should return an error": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, errors.New("error")) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.Error(t, err) + require.Empty(t, items) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + ItemsAPI: tc.mockItemAPI(), + }, + } + items, err := client.GetItemsByTitle("vault-id", "item-title") + tc.check(t, items, err) + }) + } +} + +func TestSDK_GetFileContent(t *testing.T) { + testCases := map[string]struct { + mockItemAPI func() *clientmock.ItemAPIMock + check func(t *testing.T, content []byte, err error) + }{ + "should return file content": { + mockItemAPI: func() *clientmock.ItemAPIMock { + fileMock := &clientmock.FileAPIMock{} + fileMock.On("Read", mock.Anything, "vault-id", "item-id", + mock.MatchedBy(func(attr sdk.FileAttributes) bool { + return attr.ID == "file-id" + }), + ).Return([]byte("file content"), nil) + + itemMock := &clientmock.ItemAPIMock{ + FilesAPI: fileMock, + } + itemMock.On("Files").Return(fileMock) + + return itemMock + }, + check: func(t *testing.T, content []byte, err error) { + require.NoError(t, err) + require.Equal(t, []byte("file content"), content) + }, + }, + "should return an error": { + mockItemAPI: func() *clientmock.ItemAPIMock { + fileMock := &clientmock.FileAPIMock{} + fileMock.On("Read", mock.Anything, "vault-id", "item-id", + mock.MatchedBy(func(attr sdk.FileAttributes) bool { + return attr.ID == "file-id" + }), + ).Return(nil, errors.New("error")) + + itemMock := &clientmock.ItemAPIMock{ + FilesAPI: fileMock, + } + itemMock.On("Files").Return(fileMock) + + return itemMock + }, + check: func(t *testing.T, content []byte, err error) { + require.Error(t, err) + require.Nil(t, content) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + ItemsAPI: tc.mockItemAPI(), + }, + } + content, err := client.GetFileContent("vault-id", "item-id", "file-id") + tc.check(t, content, err) + }) + } +} + +// TODO: check CreatedAt as soon as a new SDK version returns it +func TestSDK_GetVaultsByTitle(t *testing.T) { + testCases := map[string]struct { + mockVaultAPI func() *clientmock.VaultAPIMock + check func(t *testing.T, vaults []model.Vault, err error) + }{ + "should return a single vault": { + mockVaultAPI: func() *clientmock.VaultAPIMock { + m := &clientmock.VaultAPIMock{} + m.On("List", context.Background()).Return([]sdk.VaultOverview{ + { + ID: "test-id", + Title: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Title: "Some other vault", + }, + }, nil) + return m + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 1) + require.Equal(t, "test-id", vaults[0].ID) + }, + }, + "should return a two vaults": { + mockVaultAPI: func() *clientmock.VaultAPIMock { + m := &clientmock.VaultAPIMock{} + m.On("List", context.Background()).Return([]sdk.VaultOverview{ + { + ID: "test-id", + Title: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Title: VaultTitleEmployee, + }, + }, nil) + return m + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 2) + // Check the first vault + require.Equal(t, "test-id", vaults[0].ID) + // Check the second vault + require.Equal(t, "test-id-2", vaults[1].ID) + }, + }, + "should return an error": { + mockVaultAPI: func() *clientmock.VaultAPIMock { + m := &clientmock.VaultAPIMock{} + m.On("List", context.Background()).Return([]sdk.VaultOverview{}, errors.New("error")) + return m + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.Error(t, err) + require.Empty(t, vaults) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + VaultsAPI: tc.mockVaultAPI(), + }, + } + vault, err := client.GetVaultsByTitle(VaultTitleEmployee) + tc.check(t, vault, err) + }) + } +} diff --git a/pkg/onepassword/client/testing/item.go b/pkg/onepassword/client/testing/item.go new file mode 100644 index 0000000..a44dd77 --- /dev/null +++ b/pkg/onepassword/client/testing/item.go @@ -0,0 +1,110 @@ +package testing + +import ( + sdk "github.com/1password/onepassword-sdk-go" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +func CreateConnectItem() *onepassword.Item { + return &onepassword.Item{ + ID: "test-id", + Vault: onepassword.ItemVault{ID: "test-vault-id"}, + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []*onepassword.ItemField{ + {Label: "label1", Value: "value1"}, + {Label: "label2", Value: "value2"}, + }, + Files: []*onepassword.File{ + {ID: "file-id-1", Name: "file1.txt", Size: 1234}, + {ID: "file-id-2", Name: "file2.txt", Size: 1234}, + }, + } +} + +func CreateSDKItem() *sdk.Item { + return &sdk.Item{ + ID: "test-id", + VaultID: "test-vault-id", + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []sdk.ItemField{ + {Title: "label1", Value: "value1"}, + {Title: "label2", Value: "value2"}, + }, + Files: []sdk.ItemFile{ + {Attributes: sdk.FileAttributes{ID: "file-id-1", Name: "file1.txt", Size: 1234}}, + {Attributes: sdk.FileAttributes{ID: "file-id-2", Name: "file2.txt", Size: 1234}}, + }, + CreatedAt: time.Now(), + } +} + +func CreateSDKItemOverview() *sdk.ItemOverview { + return &sdk.ItemOverview{ + ID: "test-id", + Title: "item-title", + VaultID: "test-vault-id", + Tags: []string{"tag1", "tag2"}, + CreatedAt: time.Now(), + } +} + +func CheckConnectItemMapping(t *testing.T, expected *onepassword.Item, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.Vault.ID, actual.VaultID) + require.Equal(t, expected.Version, actual.Version) + require.ElementsMatch(t, expected.Tags, actual.Tags) + + for i, field := range expected.Fields { + require.Equal(t, field.Label, actual.Fields[i].Label) + require.Equal(t, field.Value, actual.Fields[i].Value) + } + + for i, file := range expected.Files { + require.Equal(t, file.ID, actual.Files[i].ID) + require.Equal(t, file.Name, actual.Files[i].Name) + require.Equal(t, file.Size, actual.Files[i].Size) + } + + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} + +func CheckSDKItemMapping(t *testing.T, expected *sdk.Item, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.VaultID, actual.VaultID) + require.Equal(t, int(expected.Version), actual.Version) + require.ElementsMatch(t, expected.Tags, actual.Tags) + + for i, field := range expected.Fields { + require.Equal(t, field.Title, actual.Fields[i].Label) + require.Equal(t, field.Value, actual.Fields[i].Value) + } + + for i, file := range expected.Files { + require.Equal(t, file.Attributes.ID, actual.Files[i].ID) + require.Equal(t, file.Attributes.Name, actual.Files[i].Name) + require.Equal(t, int(file.Attributes.Size), actual.Files[i].Size) + } + + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} + +func CheckSDKItemOverviewMapping(t *testing.T, expected *sdk.ItemOverview, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.VaultID, actual.VaultID) + require.ElementsMatch(t, expected.Tags, actual.Tags) + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} diff --git a/pkg/onepassword/client/mock/connect.go b/pkg/onepassword/client/testing/mock/connect.go similarity index 100% rename from pkg/onepassword/client/mock/connect.go rename to pkg/onepassword/client/testing/mock/connect.go diff --git a/pkg/onepassword/client/testing/mock/sdk.go b/pkg/onepassword/client/testing/mock/sdk.go new file mode 100644 index 0000000..5940dfd --- /dev/null +++ b/pkg/onepassword/client/testing/mock/sdk.go @@ -0,0 +1,89 @@ +package mock + +import ( + "context" + + "github.com/stretchr/testify/mock" + + sdk "github.com/1password/onepassword-sdk-go" +) + +type VaultAPIMock struct { + mock.Mock +} + +func (v *VaultAPIMock) List(ctx context.Context) ([]sdk.VaultOverview, error) { + args := v.Called(ctx) + return args.Get(0).([]sdk.VaultOverview), args.Error(1) +} + +type ItemAPIMock struct { + mock.Mock + FilesAPI sdk.ItemsFilesAPI +} + +func (i *ItemAPIMock) Create(ctx context.Context, params sdk.ItemCreateParams) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Get(ctx context.Context, vaultID string, itemID string) (sdk.Item, error) { + args := i.Called(ctx, vaultID, itemID) + return args.Get(0).(sdk.Item), args.Error(1) +} + +func (i *ItemAPIMock) Put(ctx context.Context, item sdk.Item) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Delete(ctx context.Context, vaultID string, itemID string) error { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Archive(ctx context.Context, vaultID string, itemID string) error { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) List(ctx context.Context, vaultID string, filters ...sdk.ItemListFilter) ([]sdk.ItemOverview, error) { + args := i.Called(ctx, vaultID, filters) + return args.Get(0).([]sdk.ItemOverview), args.Error(1) +} + +func (i *ItemAPIMock) Shares() sdk.ItemsSharesAPI { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Files() sdk.ItemsFilesAPI { + return i.FilesAPI +} + +type FileAPIMock struct { + mock.Mock +} + +func (f *FileAPIMock) Attach(ctx context.Context, item sdk.Item, fileParams sdk.FileCreateParams) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (f *FileAPIMock) Delete(ctx context.Context, item sdk.Item, sectionID string, fieldID string) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (f *FileAPIMock) ReplaceDocument(ctx context.Context, item sdk.Item, docParams sdk.DocumentCreateParams) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (f *FileAPIMock) Read(ctx context.Context, vaultID, itemID string, attributes sdk.FileAttributes) ([]byte, error) { + args := f.Called(ctx, vaultID, itemID, attributes) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]byte), args.Error(1) +} From 432f2c6cf69423c42e441ba4c7449f606d977f36 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 29 May 2025 16:06:55 -0500 Subject: [PATCH 04/44] Add Client instance that utilizes either Connect or SDK --- pkg/onepassword/client/client.go | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pkg/onepassword/client/client.go diff --git a/pkg/onepassword/client/client.go b/pkg/onepassword/client/client.go new file mode 100644 index 0000000..f368f94 --- /dev/null +++ b/pkg/onepassword/client/client.go @@ -0,0 +1,44 @@ +package client + +import ( + "errors" + + "github.com/1Password/onepassword-operator/pkg/onepassword/client/connect" + "github.com/1Password/onepassword-operator/pkg/onepassword/client/sdk" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +// Client is an interface for interacting with 1Password items and vaults. +type Client interface { + GetItemByID(vaultID, itemID string) (*model.Item, error) + GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) + GetFileContent(vaultID, itemID, fileID string) ([]byte, error) + GetVaultsByTitle(title string) ([]model.Vault, error) +} + +// Config holds the configuration for creating a new 1Password client. +type Config struct { + ConnectHost string + ConnectToken string + UserAgent string + ServiceAccountToken string + IntegrationName string + IntegrationVersion string +} + +// NewClient creates a new 1Password client based on the provided configuration. +func NewClient(config Config) (Client, error) { + if config.ServiceAccountToken != "" { + return sdk.NewClient(sdk.Config{ + ServiceAccountToken: config.ServiceAccountToken, + IntegrationName: config.IntegrationName, + IntegrationVersion: config.IntegrationVersion, + }) + } else if config.ConnectHost != "" && config.ConnectToken != "" { + return connect.NewClient(connect.Config{ + ConnectHost: config.ConnectHost, + ConnectToken: config.ConnectToken, + }), nil + } + return nil, errors.New("invalid configuration. Either Connect or Service Account credentials should be set") +} From 1498c223a5b215c43b025041d3c88e0f550b75d9 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 29 May 2025 17:23:49 -0500 Subject: [PATCH 05/44] Use 1Password Client to initialize operator either with Connect or Service Accounts --- cmd/main.go | 17 +++++----- internal/controller/deployment_controller.go | 7 ++-- .../controller/onepassworditem_controller.go | 9 +++-- .../kubernetes_secrets_builder.go | 11 +++--- pkg/onepassword/client/client.go | 34 ++++++++----------- pkg/onepassword/client/connect/connect.go | 3 +- pkg/onepassword/items.go | 22 ++++++------ pkg/onepassword/secret_update_handler.go | 16 ++++----- 8 files changed, 56 insertions(+), 63 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e92006e..3ee68f7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,8 +35,6 @@ import ( "strings" "time" - "github.com/1Password/connect-sdk-go/connect" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -54,6 +52,7 @@ import ( onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1" "github.com/1Password/onepassword-operator/internal/controller" op "github.com/1Password/onepassword-operator/pkg/onepassword" + opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client" "github.com/1Password/onepassword-operator/pkg/utils" "github.com/1Password/onepassword-operator/version" //+kubebuilder:scaffold:imports @@ -153,16 +152,16 @@ func main() { } // Setup One Password Client - opConnectClient, err := connect.NewClientFromEnvironment() + opClient, err := opclient.NewClient(version.OperatorVersion) if err != nil { - setupLog.Error(err, "unable to create Connect client") + setupLog.Error(err, "unable to create 1Password client") os.Exit(1) } if err = (&controller.OnePasswordItemReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - OpConnectClient: opConnectClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + OpClient: opClient, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem") os.Exit(1) @@ -172,7 +171,7 @@ func main() { if err = (&controller.DeploymentReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - OpConnectClient: opConnectClient, + OpClient: opClient, OpAnnotationRegExp: r, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Deployment") @@ -202,7 +201,7 @@ func main() { } // Setup update secrets task - updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments()) + updatedSecretsPoller := op.NewManager(mgr.GetClient(), opClient, shouldAutoRestartDeployments()) done := make(chan bool) ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets()) go func() { diff --git a/internal/controller/deployment_controller.go b/internal/controller/deployment_controller.go index 67553e8..db3b2f4 100644 --- a/internal/controller/deployment_controller.go +++ b/internal/controller/deployment_controller.go @@ -31,11 +31,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/1Password/connect-sdk-go/connect" - kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/logs" op "github.com/1Password/onepassword-operator/pkg/onepassword" + opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client" "github.com/1Password/onepassword-operator/pkg/utils" appsv1 "k8s.io/api/apps/v1" @@ -55,7 +54,7 @@ var logDeployment = logf.Log.WithName("controller_deployment") type DeploymentReconciler struct { client.Client Scheme *runtime.Scheme - OpConnectClient connect.Client + OpClient opclient.Client OpAnnotationRegExp *regexp.Regexp } @@ -196,7 +195,7 @@ func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deplo return nil } - item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, annotations[op.ItemPathAnnotation]) + item, err := op.GetOnePasswordItemByPath(r.OpClient, annotations[op.ItemPathAnnotation]) if err != nil { return fmt.Errorf("Failed to retrieve item: %v", err) } diff --git a/internal/controller/onepassworditem_controller.go b/internal/controller/onepassworditem_controller.go index 57610b9..d38e55b 100644 --- a/internal/controller/onepassworditem_controller.go +++ b/internal/controller/onepassworditem_controller.go @@ -28,12 +28,11 @@ import ( "context" "fmt" - "github.com/1Password/connect-sdk-go/connect" - onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/logs" op "github.com/1Password/onepassword-operator/pkg/onepassword" + opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client" "github.com/1Password/onepassword-operator/pkg/utils" corev1 "k8s.io/api/core/v1" @@ -52,8 +51,8 @@ var finalizer = "onepassword.com/finalizer.secret" // OnePasswordItemReconciler reconciles a OnePasswordItem object type OnePasswordItemReconciler struct { client.Client - Scheme *runtime.Scheme - OpConnectClient connect.Client + Scheme *runtime.Scheme + OpClient opclient.Client } //+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete @@ -164,7 +163,7 @@ func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv secretType := resource.Type autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation] - item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath) + item, err := op.GetOnePasswordItemByPath(r.OpClient, resource.Spec.ItemPath) if err != nil { return fmt.Errorf("Failed to retrieve item: %v", err) } diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder.go b/pkg/kubernetessecrets/kubernetes_secrets_builder.go index ccc3a81..d7982fd 100644 --- a/pkg/kubernetessecrets/kubernetes_secrets_builder.go +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder.go @@ -3,6 +3,7 @@ package kubernetessecrets import ( "context" "fmt" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" "regexp" "strings" @@ -11,8 +12,6 @@ import ( errs "errors" - "github.com/1Password/connect-sdk-go/onepassword" - "github.com/1Password/onepassword-operator/pkg/utils" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -34,11 +33,11 @@ var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type var log = logf.Log -func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error { +func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *model.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error { itemVersion := fmt.Sprint(item.Version) secretAnnotations := map[string]string{ VersionAnnotation: itemVersion, - ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID), + ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID), } if autoRestart != "" { @@ -92,7 +91,7 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa return nil } -func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret { +func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item model.Item, ownerRef *metav1.OwnerReference) *corev1.Secret { var ownerRefs []metav1.OwnerReference if ownerRef != nil { ownerRefs = []metav1.OwnerReference{*ownerRef} @@ -111,7 +110,7 @@ func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotation } } -func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { +func BuildKubernetesSecretData(fields []model.ItemField, files []model.File) map[string][]byte { secretData := map[string][]byte{} for i := 0; i < len(fields); i++ { if fields[i].Value != "" { diff --git a/pkg/onepassword/client/client.go b/pkg/onepassword/client/client.go index f368f94..89be108 100644 --- a/pkg/onepassword/client/client.go +++ b/pkg/onepassword/client/client.go @@ -2,6 +2,7 @@ package client import ( "errors" + "os" "github.com/1Password/onepassword-operator/pkg/onepassword/client/connect" "github.com/1Password/onepassword-operator/pkg/onepassword/client/sdk" @@ -16,29 +17,24 @@ type Client interface { GetVaultsByTitle(title string) ([]model.Vault, error) } -// Config holds the configuration for creating a new 1Password client. -type Config struct { - ConnectHost string - ConnectToken string - UserAgent string - ServiceAccountToken string - IntegrationName string - IntegrationVersion string -} - // NewClient creates a new 1Password client based on the provided configuration. -func NewClient(config Config) (Client, error) { - if config.ServiceAccountToken != "" { +func NewClient(integrationVersion string) (Client, error) { + connectHost, _ := os.LookupEnv("OP_CONNECT_HOST") + connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN") + serviceAccountToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN") + + if serviceAccountToken != "" { return sdk.NewClient(sdk.Config{ - ServiceAccountToken: config.ServiceAccountToken, - IntegrationName: config.IntegrationName, - IntegrationVersion: config.IntegrationVersion, + ServiceAccountToken: serviceAccountToken, + IntegrationName: "1password-operator", + IntegrationVersion: integrationVersion, }) - } else if config.ConnectHost != "" && config.ConnectToken != "" { + } else if connectHost != "" && connectToken != "" { return connect.NewClient(connect.Config{ - ConnectHost: config.ConnectHost, - ConnectToken: config.ConnectToken, + ConnectHost: connectHost, + ConnectToken: connectToken, }), nil } - return nil, errors.New("invalid configuration. Either Connect or Service Account credentials should be set") + + return nil, errors.New("invalid configuration. Connect or Service Account credentials should be set") } diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go index d1da4ef..92c5619 100644 --- a/pkg/onepassword/client/connect/connect.go +++ b/pkg/onepassword/client/connect/connect.go @@ -20,9 +20,10 @@ type Connect struct { client connect.Client } +// NewClient creates a new Connect client using provided configuration. func NewClient(config Config) *Connect { return &Connect{ - client: connect.NewClientWithUserAgent(config.ConnectHost, config.ConnectToken, config.UserAgent), + client: connect.NewClient(config.ConnectHost, config.ConnectToken), } } diff --git a/pkg/onepassword/items.go b/pkg/onepassword/items.go index f608013..1ed0934 100644 --- a/pkg/onepassword/items.go +++ b/pkg/onepassword/items.go @@ -4,36 +4,36 @@ import ( "fmt" "strings" - "github.com/1Password/connect-sdk-go/connect" - "github.com/1Password/connect-sdk-go/onepassword" - logf "sigs.k8s.io/controller-runtime/pkg/log" + + opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" ) var logger = logf.Log.WithName("retrieve_item") -func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) { - vaultValue, itemValue, err := ParseVaultAndItemFromPath(path) +func GetOnePasswordItemByPath(opClient opclient.Client, path string) (*model.Item, error) { + vaultIdentifier, itemIdentifier, err := ParseVaultAndItemFromPath(path) if err != nil { return nil, err } - vaultId, err := getVaultId(opConnectClient, vaultValue) + vaultID, err := getVaultID(opClient, vaultIdentifier) if err != nil { return nil, err } - itemId, err := getItemId(opConnectClient, itemValue, vaultId) + itemID, err := getItemID(opClient, vaultID, itemIdentifier) if err != nil { return nil, err } - item, err := opConnectClient.GetItem(itemId, vaultId) + item, err := opClient.GetItemByID(itemID, vaultID) if err != nil { return nil, err } for _, file := range item.Files { - _, err := opConnectClient.GetFileContent(file) + _, err := opClient.GetFileContent(vaultID, itemID, file.ID) if err != nil { return nil, err } @@ -50,7 +50,7 @@ func ParseVaultAndItemFromPath(path string) (string, string, error) { return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) } -func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { +func getVaultID(client opclient.Client, vaultIdentifier string) (string, error) { if !IsValidClientUUID(vaultIdentifier) { vaults, err := client.GetVaultsByTitle(vaultIdentifier) if err != nil { @@ -75,7 +75,7 @@ func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { return vaultIdentifier, nil } -func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) { +func getItemID(client opclient.Client, vaultId, itemIdentifier string) (string, error) { if !IsValidClientUUID(itemIdentifier) { items, err := client.GetItemsByTitle(itemIdentifier, vaultId) if err != nil { diff --git a/pkg/onepassword/secret_update_handler.go b/pkg/onepassword/secret_update_handler.go index a24e7e1..d7f0156 100644 --- a/pkg/onepassword/secret_update_handler.go +++ b/pkg/onepassword/secret_update_handler.go @@ -8,10 +8,10 @@ import ( onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/logs" + opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" "github.com/1Password/onepassword-operator/pkg/utils" - "github.com/1Password/connect-sdk-go/connect" - "github.com/1Password/connect-sdk-go/onepassword" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,17 +23,17 @@ const lockTag = "operator.1password.io:ignore-secret" var log = logf.Log.WithName("update_op_kubernetes_secrets_task") -func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler { +func NewManager(kubernetesClient client.Client, opClient opclient.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler { return &SecretUpdateHandler{ client: kubernetesClient, - opConnectClient: opConnectClient, + opClient: opClient, shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal, } } type SecretUpdateHandler struct { client client.Client - opConnectClient connect.Client + opClient opclient.Client shouldAutoRestartDeploymentsGlobal bool } @@ -121,14 +121,14 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* OnePasswordItemPath := h.getPathFromOnePasswordItem(secret) - item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) + item, err := GetOnePasswordItemByPath(h.opClient, OnePasswordItemPath) if err != nil { log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name) continue } itemVersion := fmt.Sprint(item.Version) - itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) + itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID) if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString { if isItemLockedForForcedRestarts(item) { @@ -159,7 +159,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* return updatedSecrets, nil } -func isItemLockedForForcedRestarts(item *onepassword.Item) bool { +func isItemLockedForForcedRestarts(item *model.Item) bool { tags := item.Tags for i := 0; i < len(tags); i++ { if tags[i] == lockTag { From f88ea6696b9f40569a20d25f698cb15a01d8b3d4 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 30 May 2025 14:30:06 -0500 Subject: [PATCH 06/44] Update tests to use testify mock --- .../onepassword.com_onepassworditems.yaml | 19 ++- .../controller/deployment_controller_test.go | 32 +--- .../onepassworditem_controller_test.go | 49 ++---- internal/controller/suite_test.go | 37 +++- .../kubernetes_secrets_builder_test.go | 46 +++-- pkg/mocks/mocksecretserver.go | 158 +++--------------- pkg/onepassword/secret_update_handler_test.go | 57 ++++--- 7 files changed, 140 insertions(+), 258 deletions(-) diff --git a/config/crd/bases/onepassword.com_onepassworditems.yaml b/config/crd/bases/onepassword.com_onepassworditems.yaml index 49de008..c6d9599 100644 --- a/config/crd/bases/onepassword.com_onepassworditems.yaml +++ b/config/crd/bases/onepassword.com_onepassworditems.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.13.0 + controller-gen.kubebuilder.io/version: v0.14.0 name: onepassworditems.onepassword.com spec: group: onepassword.com @@ -20,14 +20,19 @@ spec: description: OnePasswordItem is the Schema for the onepassworditems API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object diff --git a/internal/controller/deployment_controller_test.go b/internal/controller/deployment_controller_test.go index 8ffdb8b..85364d0 100644 --- a/internal/controller/deployment_controller_test.go +++ b/internal/controller/deployment_controller_test.go @@ -2,9 +2,6 @@ package controller import ( "context" - "github.com/1Password/connect-sdk-go/onepassword" - "github.com/1Password/onepassword-operator/pkg/mocks" - op "github.com/1Password/onepassword-operator/pkg/onepassword" "time" . "github.com/onsi/ginkgo/v2" @@ -17,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" + op "github.com/1Password/onepassword-operator/pkg/onepassword" ) const ( @@ -106,17 +104,8 @@ var _ = Describe("Deployment controller", func() { } mockGetItemFunc := func() { - mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - item := onepassword.Item{} - item.Fields = []*onepassword.ItemField{} - for k, v := range item1.Data { - item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) - } - item.Version = item1.Version - item.Vault.ID = vaultUUID - item.ID = uuid - return &item, nil - } + // mock GetItemByID to return test item 'item1' + mockGetItemByIDFunc.Return(item1.ToModel(), nil) } BeforeEach(func() { @@ -151,17 +140,10 @@ var _ = Describe("Deployment controller", func() { It("Should update existing K8s Secret using deployment", func() { By("Updating secret") - mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - item := onepassword.Item{} - item.Fields = []*onepassword.ItemField{} - for k, v := range item2.Data { - item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) - } - item.Version = item2.Version - item.Vault.ID = vaultUUID - item.ID = uuid - return &item, nil - } + + // mock GetItemByID to return test item 'item2' + mockGetItemByIDFunc.Return(item2.ToModel(), nil) + Eventually(func() error { updatedDeployment := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ diff --git a/internal/controller/onepassworditem_controller_test.go b/internal/controller/onepassworditem_controller_test.go index 43b736d..58cf9f2 100644 --- a/internal/controller/onepassworditem_controller_test.go +++ b/internal/controller/onepassworditem_controller_test.go @@ -2,10 +2,6 @@ package controller import ( "context" - - "github.com/1Password/connect-sdk-go/onepassword" - "github.com/1Password/onepassword-operator/pkg/mocks" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -16,6 +12,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" ) const ( @@ -32,17 +29,8 @@ var _ = Describe("OnePasswordItem controller", func() { err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace)) Expect(err).ToNot(HaveOccurred()) - mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - item := onepassword.Item{} - item.Fields = []*onepassword.ItemField{} - for k, v := range item1.Data { - item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) - } - item.Version = item1.Version - item.Vault.ID = vaultUUID - item.ID = uuid - return &item, nil - } + item := item1.ToModel() + mockGetItemByIDFunc.Return(item, nil) }) Context("Happy path", func() { @@ -99,17 +87,13 @@ var _ = Describe("OnePasswordItem controller", func() { "password": []byte("##newPassword##"), "extraField": []byte("dev"), } - mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - item := onepassword.Item{} - item.Fields = []*onepassword.ItemField{} - for k, v := range newData { - item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) - } - item.Version = item1.Version + 1 - item.Vault.ID = vaultUUID - item.ID = uuid - return &item, nil + + item := item2.ToModel() + for k, v := range newData { + item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v}) } + mockGetItemByIDFunc.Return(item, nil) + _, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) Expect(err).ToNot(HaveOccurred()) @@ -178,18 +162,11 @@ var _ = Describe("OnePasswordItem controller", func() { "ice-cream-type": []byte(iceCream), } - mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - item := onepassword.Item{} - item.Title = "!my sECReT it3m%" - item.Fields = []*onepassword.ItemField{} - for k, v := range testData { - item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) - } - item.Version = item1.Version + 1 - item.Vault.ID = vaultUUID - item.ID = uuid - return &item, nil + item := item2.ToModel() + for k, v := range testData { + item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v}) } + mockGetItemByIDFunc.Return(item, nil) By("Creating a new OnePasswordItem successfully") Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 137b583..8791e6a 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -26,13 +26,12 @@ package controller import ( "context" + "github.com/stretchr/testify/mock" "path/filepath" "regexp" "testing" "time" - "github.com/1Password/onepassword-operator/pkg/mocks" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -45,6 +44,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1" + "github.com/1Password/onepassword-operator/pkg/mocks" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" //+kubebuilder:scaffold:imports ) @@ -78,8 +79,11 @@ var ( cancel context.CancelFunc onePasswordItemReconciler *OnePasswordItemReconciler deploymentReconciler *DeploymentReconciler + mockGetItemByIDFunc *mock.Call item1 = &TestItem{ + ItemID: "nwrhuano7bcwddcviubpp4mhfq", + VaultID: "hfnjvi6aymbsnfc2xeeoheizda", Name: "test-item", Version: 123, Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq", @@ -94,6 +98,8 @@ var ( } item2 = &TestItem{ + ItemID: "nwrhuano7bcwddcviubpp4mhf2", + VaultID: "hfnjvi6aymbsnfc2xeeoheizd2", Name: "test-item2", Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2", Version: 456, @@ -109,6 +115,8 @@ var ( ) type TestItem struct { + ItemID string + VaultID string Name string Version int Path string @@ -116,6 +124,20 @@ type TestItem struct { SecretData map[string][]byte } +func (ti *TestItem) ToModel() *model.Item { + item := &model.Item{} + item.Version = ti.Version + item.VaultID = ti.VaultID + item.ID = ti.ItemID + + item.Fields = []model.ItemField{} + for k, v := range ti.Data { + item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v}) + } + + return item +} + func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -153,12 +175,13 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - opConnectClient := &mocks.TestClient{} + mockOpClient := &mocks.TestClient{} + mockGetItemByIDFunc = mockOpClient.On("GetItemByID", mock.Anything, mock.Anything) onePasswordItemReconciler = &OnePasswordItemReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - OpConnectClient: opConnectClient, + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + OpClient: mockOpClient, } err = (onePasswordItemReconciler).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) @@ -167,7 +190,7 @@ var _ = BeforeSuite(func() { deploymentReconciler = &DeploymentReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), - OpConnectClient: opConnectClient, + OpClient: mockOpClient, OpAnnotationRegExp: r, } err = (deploymentReconciler).SetupWithManager(k8sManager) diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go b/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go index 7ffc3f0..c7128b1 100644 --- a/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go @@ -3,11 +3,10 @@ package kubernetessecrets import ( "context" "fmt" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" "strings" "testing" - "github.com/1Password/connect-sdk-go/onepassword" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -21,10 +20,10 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { secretName := "test-secret-name" namespace := "test" - item := onepassword.Item{} + item := model.Item{} item.Fields = generateFields(5) item.Version = 123 - item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda" item.ID = "h46bb3jddvay7nxopfhvlwg35q" kubeClient := fake.NewClientBuilder().Build() @@ -49,10 +48,10 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) { secretName := "test-secret-name" namespace := "test" - item := onepassword.Item{} + item := model.Item{} item.Fields = generateFields(5) item.Version = 123 - item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda" item.ID = "h46bb3jddvay7nxopfhvlwg35q" kubeClient := fake.NewClientBuilder().Build() @@ -94,10 +93,10 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { secretName := "test-secret-update" namespace := "test" - item := onepassword.Item{} + item := model.Item{} item.Fields = generateFields(5) item.Version = 123 - item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda" item.ID = "h46bb3jddvay7nxopfhvlwg35q" kubeClient := fake.NewClientBuilder().Build() @@ -111,10 +110,10 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { } // Updating kubernetes secret with new item - newItem := onepassword.Item{} + newItem := model.Item{} newItem.Fields = generateFields(6) newItem.Version = 456 - newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + newItem.VaultID = "hfnjvi6aymbsnfc2xeeoheizda" newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil) if err != nil { @@ -147,7 +146,7 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { annotations := map[string]string{ annotationKey: annotationValue, } - item := onepassword.Item{} + item := model.Item{} item.Fields = generateFields(5) labels := map[string]string{} secretType := "" @@ -173,10 +172,10 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) { "annotationKey": "annotationValue", } labels := map[string]string{} - item := onepassword.Item{} + item := model.Item{} secretType := "" - item.Fields = []*onepassword.ItemField{ + item.Fields = []model.ItemField{ { Label: "label w%th invalid ch!rs-", Value: "value1", @@ -209,10 +208,10 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { secretName := "tls-test-secret-name" namespace := "test" - item := onepassword.Item{} + item := model.Item{} item.Fields = generateFields(5) item.Version = 123 - item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda" item.ID = "h46bb3jddvay7nxopfhvlwg35q" kubeClient := fake.NewClientBuilder().Build() @@ -235,13 +234,13 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { } } -func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { +func compareAnnotationsToItem(annotations map[string]string, item model.Item, t *testing.T) { actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) if err != nil { t.Errorf("Was unable to parse Item Path") } - if actualVaultId != item.Vault.ID { - t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId) + if actualVaultId != item.VaultID { + t.Errorf("Expected annotation vault id to be %v but was %v", item.VaultID, actualVaultId) } if actualItemId != item.ID { t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId) @@ -255,7 +254,7 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It } } -func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) { +func compareFields(actualFields []model.ItemField, secretData map[string][]byte, t *testing.T) { for i := 0; i < len(actualFields); i++ { value, found := secretData[actualFields[i].Label] if !found { @@ -267,14 +266,13 @@ func compareFields(actualFields []*onepassword.ItemField, secretData map[string] } } -func generateFields(numToGenerate int) []*onepassword.ItemField { - fields := []*onepassword.ItemField{} +func generateFields(numToGenerate int) []model.ItemField { + fields := []model.ItemField{} for i := 0; i < numToGenerate; i++ { - field := onepassword.ItemField{ + fields = append(fields, model.ItemField{ Label: "key" + fmt.Sprint(i), Value: "value" + fmt.Sprint(i), - } - fields = append(fields, &field) + }) } return fields } diff --git a/pkg/mocks/mocksecretserver.go b/pkg/mocks/mocksecretserver.go index cb59d3c..c6c3463 100644 --- a/pkg/mocks/mocksecretserver.go +++ b/pkg/mocks/mocksecretserver.go @@ -1,151 +1,37 @@ package mocks import ( - "github.com/1Password/connect-sdk-go/onepassword" + "github.com/stretchr/testify/mock" + + "github.com/1Password/onepassword-operator/pkg/onepassword/model" ) type TestClient struct { - GetVaultsFunc func() ([]onepassword.Vault, error) - GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) - GetVaultFunc func(uuid string) (*onepassword.Vault, error) - GetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error) - GetVaultByTitleFunc func(title string) (*onepassword.Vault, error) - GetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error) - GetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error) - GetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error) - GetItemsFunc func(vaultQuery string) ([]onepassword.Item, error) - GetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error) - CreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) - UpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) - DeleteItemFunc func(item *onepassword.Item, vaultQuery string) error - DeleteItemByIDFunc func(itemUUID string, vaultQuery string) error - DeleteItemByTitleFunc func(title string, vaultQuery string) error - GetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error) - GetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) - GetFileContentFunc func(file *onepassword.File) ([]byte, error) - DownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) - LoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error - LoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error - LoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error - LoadStructFunc func(config interface{}) error + mock.Mock } -var ( - DoGetVaultsFunc func() ([]onepassword.Vault, error) - DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) - DoGetVaultFunc func(uuid string) (*onepassword.Vault, error) - DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error) - DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error) - DoGetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error) - DoGetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error) - DoGetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error) - DoGetItemsFunc func(vaultQuery string) ([]onepassword.Item, error) - DoGetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error) - DoCreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) - DoUpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) - DoDeleteItemFunc func(item *onepassword.Item, vaultQuery string) error - DoDeleteItemByIDFunc func(itemUUID string, vaultQuery string) error - DoDeleteItemByTitleFunc func(title string, vaultQuery string) error - DoGetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error) - DoGetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) - DoGetFileContentFunc func(file *onepassword.File) ([]byte, error) - DoDownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) - DoLoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error - DoLoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error - DoLoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error - DoLoadStructFunc func(config interface{}) error -) - -// Do is the mock client's `Do` func - -func (m *TestClient) GetVaults() ([]onepassword.Vault, error) { - return DoGetVaultsFunc() +func (tc *TestClient) GetItemByID(vaultID, itemID string) (*model.Item, error) { + args := tc.Called(vaultID, itemID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*model.Item), args.Error(1) } -func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { - return DoGetVaultsByTitleFunc(title) +func (tc *TestClient) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { + args := tc.Called(vaultID, itemTitle) + return args.Get(0).([]model.Item), args.Error(1) } -func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) { - return DoGetVaultFunc(vaultQuery) +func (tc *TestClient) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { + args := tc.Called(vaultID, itemID, fileID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]byte), args.Error(1) } -func (m *TestClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { - return DoGetVaultByUUIDFunc(uuid) -} - -func (m *TestClient) GetVaultByTitle(title string) (*onepassword.Vault, error) { - return DoGetVaultByTitleFunc(title) -} - -func (m *TestClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) { - return DoGetItemFunc(itemQuery, vaultQuery) -} - -func (m *TestClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) { - return DoGetItemByUUIDFunc(uuid, vaultQuery) -} - -func (m *TestClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { - return DoGetItemByTitleFunc(title, vaultQuery) -} - -func (m *TestClient) GetItems(vaultQuery string) ([]onepassword.Item, error) { - return DoGetItemsFunc(vaultQuery) -} - -func (m *TestClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) { - return DoGetItemsByTitleFunc(title, vaultQuery) -} - -func (m *TestClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { - return DoCreateItemFunc(item, vaultQuery) -} - -func (m *TestClient) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { - return DoUpdateItemFunc(item, vaultQuery) -} - -func (m *TestClient) DeleteItem(item *onepassword.Item, vaultQuery string) error { - return DoDeleteItemFunc(item, vaultQuery) -} - -func (m *TestClient) DeleteItemByID(itemUUID string, vaultQuery string) error { - return DoDeleteItemByIDFunc(itemUUID, vaultQuery) -} - -func (m *TestClient) DeleteItemByTitle(title string, vaultQuery string) error { - return DoDeleteItemByTitleFunc(title, vaultQuery) -} - -func (m *TestClient) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { - return DoGetFilesFunc(itemQuery, vaultQuery) -} - -func (m *TestClient) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { - return DoGetFileFunc(uuid, itemQuery, vaultQuery) -} - -func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) { - return DoGetFileContentFunc(file) -} - -func (m *TestClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { - return DoDownloadFileFunc(file, targetDirectory, overwrite) -} - -func (m *TestClient) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error { - return DoLoadStructFromItemByUUIDFunc(config, itemUUID, vaultQuery) -} - -func (m *TestClient) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error { - return DoLoadStructFromItemByTitleFunc(config, itemTitle, vaultQuery) -} - -func (m *TestClient) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error { - return DoLoadStructFromItemFunc(config, itemQuery, vaultQuery) -} - -func (m *TestClient) LoadStruct(config interface{}) error { - return DoLoadStructFunc(config) +func (tc *TestClient) GetVaultsByTitle(title string) ([]model.Vault, error) { + args := tc.Called(title) + return args.Get(0).([]model.Vault), args.Error(1) } diff --git a/pkg/onepassword/secret_update_handler_test.go b/pkg/onepassword/secret_update_handler_test.go index 68a06a3..f6dad7f 100644 --- a/pkg/onepassword/secret_update_handler_test.go +++ b/pkg/onepassword/secret_update_handler_test.go @@ -4,11 +4,14 @@ import ( "context" "fmt" "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/1Password/onepassword-operator/pkg/mocks" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" - "github.com/1Password/connect-sdk-go/onepassword" - "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" errors2 "k8s.io/apimachinery/pkg/api/errors" @@ -802,19 +805,20 @@ func TestUpdateSecretHandler(t *testing.T) { // Create a fake client to mock API calls. cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() - opConnectClient := &mocks.TestClient{} - mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - - item := onepassword.Item{} - item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) - item.Version = itemVersion - item.Vault.ID = vaultUUID - item.ID = uuid - return &item, nil - } + mockOpClient := &mocks.TestClient{} + mockOpClient.On("GetItemByID", mock.Anything, mock.Anything).Return(createItem(), nil) + //mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { + // + // item := onepassword.Item{} + // item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) + // item.Version = itemVersion + // item.Vault.ID = vaultUUID + // item.ID = uuid + // return &item, nil + //} h := &SecretUpdateHandler{ client: cl, - opConnectClient: opConnectClient, + opClient: mockOpClient, shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled, } @@ -879,16 +883,23 @@ func TestIsUpdatedSecret(t *testing.T) { assert.True(t, isUpdatedSecret(secretName, updatedSecrets)) } -func generateFields(username, password string) []*onepassword.ItemField { - fields := []*onepassword.ItemField{ - { - Label: "username", - Value: username, - }, - { - Label: "password", - Value: password, +func createItem() *model.Item { + return &model.Item{ + ID: itemId, + VaultID: vaultId, + Version: itemVersion, + Tags: []string{"tag1", "tag2"}, + Fields: []model.ItemField{ + { + Label: "username", + Value: username, + }, + { + Label: "password", + Value: password, + }, }, + Files: []model.File{}, + CreatedAt: time.Now(), } - return fields } From a432b42814ce181430ccb880cc74e076de101f33 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 30 May 2025 14:52:41 -0500 Subject: [PATCH 07/44] Update Docker file to install dependencies and build --- Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7841744..207ceb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.21 as builder +FROM golang:1.22 as builder ARG TARGETOS ARG TARGETARCH @@ -8,13 +8,15 @@ WORKDIR /workspace COPY go.mod go.mod COPY go.sum go.sum +# Download dependencies +RUN go mod download + # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/controller/ internal/controller/ COPY pkg/ pkg/ COPY version/ version/ -COPY vendor/ vendor/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command @@ -22,10 +24,9 @@ COPY vendor/ vendor/ # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 \ - GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ + GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on \ go build \ -ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \ - -mod vendor \ -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary From 97e06e5c4db20137f53b91f2bb82ce3bf930ae6b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 30 May 2025 16:10:25 -0500 Subject: [PATCH 08/44] Bump version to 1.9.0 --- .VERSION | 2 +- CHANGELOG.md | 8 ++++++++ version/version.go | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.VERSION b/.VERSION index b9268da..abb1658 100644 --- a/.VERSION +++ b/.VERSION @@ -1 +1 @@ -1.8.1 \ No newline at end of file +1.9.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 090d1f6..7aa83ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ --- +[//]: # (START/v1.9.0) +# v1.9.0 + +## Features + * Support Service Accounts. {#160} + +--- + [//]: # (START/v1.8.1) # v1.8.1 diff --git a/version/version.go b/version/version.go index 3557c0f..c46e83f 100644 --- a/version/version.go +++ b/version/version.go @@ -1,6 +1,6 @@ package version var ( - OperatorVersion = "1.8.1" + OperatorVersion = "1.9.0" OperatorSDKVersion = "1.34.1" ) From 4757263c66b658a57070e36b53ccf88653ab3417 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 6 Jun 2025 12:53:56 -0500 Subject: [PATCH 09/44] Wrap errors so it's clear either error is coming from SDK or Connect --- pkg/onepassword/client/connect/connect.go | 13 +++++++++---- pkg/onepassword/client/sdk/sdk.go | 16 +++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go index 92c5619..e45df48 100644 --- a/pkg/onepassword/client/connect/connect.go +++ b/pkg/onepassword/client/connect/connect.go @@ -30,7 +30,7 @@ func NewClient(config Config) *Connect { func (c *Connect) GetItemByID(vaultID, itemID string) (*model.Item, error) { connectItem, err := c.client.GetItemByUUID(itemID, vaultID) if err != nil { - return nil, err + return nil, fmt.Errorf("1password Connect error: %w", err) } var item model.Item @@ -42,7 +42,7 @@ func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, erro // Get all items in the vault with the specified title connectItems, err := c.client.GetItemsByTitle(itemTitle, vaultID) if err != nil { - return nil, err + return nil, fmt.Errorf("1password Connect error: %w", err) } var items []model.Item @@ -56,15 +56,20 @@ func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, erro } func (c *Connect) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { - return c.client.GetFileContent(&onepassword.File{ + bytes, err := c.client.GetFileContent(&onepassword.File{ ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", vaultID, itemID, fileID), }) + if err != nil { + return nil, fmt.Errorf("1password Connect error: %w", err) + } + + return bytes, nil } func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) { connectVaults, err := c.client.GetVaultsByTitle(vaultQuery) if err != nil { - return nil, err + return nil, fmt.Errorf("1password Connect error: %w", err) } var vaults []model.Vault diff --git a/pkg/onepassword/client/sdk/sdk.go b/pkg/onepassword/client/sdk/sdk.go index eafd687..b6a89ee 100644 --- a/pkg/onepassword/client/sdk/sdk.go +++ b/pkg/onepassword/client/sdk/sdk.go @@ -2,6 +2,7 @@ package sdk import ( "context" + "fmt" "github.com/1Password/onepassword-operator/pkg/onepassword/model" sdk "github.com/1password/onepassword-sdk-go" @@ -25,7 +26,7 @@ func NewClient(config Config) (*SDK, error) { sdk.WithIntegrationInfo(config.IntegrationName, config.IntegrationVersion), ) if err != nil { - return nil, err + return nil, fmt.Errorf("1password sdk error: %w", err) } return &SDK{ @@ -36,7 +37,7 @@ func NewClient(config Config) (*SDK, error) { func (s *SDK) GetItemByID(vaultID, itemID string) (*model.Item, error) { sdkItem, err := s.client.Items().Get(context.Background(), vaultID, itemID) if err != nil { - return nil, err + return nil, fmt.Errorf("1password sdk error: %w", err) } var item model.Item @@ -48,7 +49,7 @@ func (s *SDK) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { // Get all items in the vault sdkItems, err := s.client.Items().List(context.Background(), vaultID) if err != nil { - return nil, err + return nil, fmt.Errorf("1password sdk error: %w", err) } // Filter items by title @@ -65,16 +66,21 @@ func (s *SDK) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { } func (s *SDK) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { - return s.client.Items().Files().Read(context.Background(), vaultID, itemID, sdk.FileAttributes{ + bytes, err := s.client.Items().Files().Read(context.Background(), vaultID, itemID, sdk.FileAttributes{ ID: fileID, }) + if err != nil { + return nil, fmt.Errorf("1password sdk error: %w", err) + } + + return bytes, nil } func (s *SDK) GetVaultsByTitle(title string) ([]model.Vault, error) { // List all vaults sdkVaults, err := s.client.Vaults().List(context.Background()) if err != nil { - return nil, err + return nil, fmt.Errorf("1password sdk error: %w", err) } // Filter vaults by title From 72511ed68796cda7c756bf396973de700e1d9ee6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 6 Jun 2025 12:56:17 -0500 Subject: [PATCH 10/44] Return error if both Connect and Service Account credentials are provided --- pkg/onepassword/client/client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/onepassword/client/client.go b/pkg/onepassword/client/client.go index 89be108..1b0cd74 100644 --- a/pkg/onepassword/client/client.go +++ b/pkg/onepassword/client/client.go @@ -23,13 +23,19 @@ func NewClient(integrationVersion string) (Client, error) { connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN") serviceAccountToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN") + if connectHost != "" && connectToken != "" && serviceAccountToken != "" { + return nil, errors.New("invalid configuration. Either Connect or Service Account credentials should be set, not both") + } + if serviceAccountToken != "" { return sdk.NewClient(sdk.Config{ ServiceAccountToken: serviceAccountToken, IntegrationName: "1password-operator", IntegrationVersion: integrationVersion, }) - } else if connectHost != "" && connectToken != "" { + } + + if connectHost != "" && connectToken != "" { return connect.NewClient(connect.Config{ ConnectHost: connectHost, ConnectToken: connectToken, From ac06f8db137f547249c8bacfda4cf83aa987a533 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 6 Jun 2025 16:12:25 -0500 Subject: [PATCH 11/44] Add more logs and fix params order --- pkg/onepassword/client/client.go | 3 +++ pkg/onepassword/items.go | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/onepassword/client/client.go b/pkg/onepassword/client/client.go index 1b0cd74..7147381 100644 --- a/pkg/onepassword/client/client.go +++ b/pkg/onepassword/client/client.go @@ -2,6 +2,7 @@ package client import ( "errors" + "fmt" "os" "github.com/1Password/onepassword-operator/pkg/onepassword/client/connect" @@ -28,6 +29,7 @@ func NewClient(integrationVersion string) (Client, error) { } if serviceAccountToken != "" { + fmt.Printf("Using Service Account Token") return sdk.NewClient(sdk.Config{ ServiceAccountToken: serviceAccountToken, IntegrationName: "1password-operator", @@ -36,6 +38,7 @@ func NewClient(integrationVersion string) (Client, error) { } if connectHost != "" && connectToken != "" { + fmt.Printf("Using Connect") return connect.NewClient(connect.Config{ ConnectHost: connectHost, ConnectToken: connectToken, diff --git a/pkg/onepassword/items.go b/pkg/onepassword/items.go index 1ed0934..eb121a3 100644 --- a/pkg/onepassword/items.go +++ b/pkg/onepassword/items.go @@ -19,17 +19,17 @@ func GetOnePasswordItemByPath(opClient opclient.Client, path string) (*model.Ite } vaultID, err := getVaultID(opClient, vaultIdentifier) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to 'getVaultID' for vaultIdentifier='%s': %w", vaultIdentifier, err) } itemID, err := getItemID(opClient, vaultID, itemIdentifier) if err != nil { - return nil, err + return nil, fmt.Errorf("faild to 'getItemID' for vaultID='%s' and itemIdentifier='%s': %w", vaultID, itemIdentifier, err) } - item, err := opClient.GetItemByID(itemID, vaultID) + item, err := opClient.GetItemByID(vaultID, itemID) if err != nil { - return nil, err + return nil, fmt.Errorf("faield to 'GetItemByID' for vaultID='%s' and itemID='%s': %w", vaultID, itemID, err) } for _, file := range item.Files { @@ -77,7 +77,7 @@ func getVaultID(client opclient.Client, vaultIdentifier string) (string, error) func getItemID(client opclient.Client, vaultId, itemIdentifier string) (string, error) { if !IsValidClientUUID(itemIdentifier) { - items, err := client.GetItemsByTitle(itemIdentifier, vaultId) + items, err := client.GetItemsByTitle(vaultId, itemIdentifier) if err != nil { return "", err } From 4baad12e10e33de52d71f17494b18a7cce6c85b3 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Sun, 8 Jun 2025 11:23:10 -0500 Subject: [PATCH 12/44] Add instructions how to use Operator with Service Accounts --- USAGEGUIDE.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/USAGEGUIDE.md b/USAGEGUIDE.md index 05b51f8..956c09c 100644 --- a/USAGEGUIDE.md +++ b/USAGEGUIDE.md @@ -5,9 +5,11 @@ ## Table of Contents +- [Configuration Options](#configuration-options) - [Prerequisites](#prerequisites) - [Deploying 1Password Connect to Kubernetes](#deploying-1password-connect-to-kubernetes) -- [Kubernetes Operator Deployment](#kubernetes-operator-deployment) +- [Kubernetes Operator Deployment With Connect](#kubernetes-operator-deployment-with-connect) +- [Kubernetes Operator Deployment With Service Account](#kubernetes-operator-deployment-with-service-account) - [Usage](#usage) - [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) - [Development](#development) @@ -19,6 +21,11 @@ - [`docker` installed](https://docs.docker.com/get-docker/) - [A `1password-credentials.json` file generated and a 1Password Connect API Token issued for the K8s Operator integration](https://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow) +## Configuration options +There are 2 ways 1Password Operator can talk to 1Password servers: +- **Connect**: It uses the 1Password Connect API to access items in 1Password. +- **Service Account**: It uses [1Password SDK](https://developer.1password.com/docs/sdks/) and [Service Account](https://developer.1password.com/docs/service-accounts) to access items in 1Password. + ## Deploying 1Password Connect to Kubernetes If 1Password Connect is already running, you can skip this step. @@ -60,7 +67,7 @@ Add the following environment variable to the onepassword-connect-operator conta Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the current namespace. -### Kubernetes Operator Deployment +## Kubernetes Operator Deployment with Connect #### Create Kubernetes Secret for OP_CONNECT_TOKEN #### @@ -118,6 +125,64 @@ make deploy make undeploy ``` +## Kubernetes Operator Deployment with Service Account + +#### Create Kubernetes Secret for OP_SERVICE_ACCOUNT_TOKEN #### + +Create a Service Account token for the operator and save it as a Kubernetes Secret: + +```bash +kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN" +``` + +If you do not have a token for the operator, you can generate a token and save it to Kubernetes with the following command: + +```bash +kubectl create secret generic onepassword-service-account-token --from-literal=token=$(op service-account create my-service-account --vault Dev:read_items --vault Test:read_items,write_items) +``` + +**Deploying the Operator** + +An sample Deployment yaml can be found at `/config/manager/manager.yaml`. +To use Operator with Service Account, you need to set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable in the `/config/manager/manager.yaml`. And remove `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` environment variables. + +To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml: + +- **OP_SERVICE_ACCOUNT_TOKEN** *(required)*: Specifies Service Account token within Kubernetes to access the 1Password items. +- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes. +- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password. +- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. + +You can also set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. (Note: the default value is `debug`.) + +Example: +```yaml +. +. +. +containers: + - command: + - /manager + args: + - --leader-elect + - --zap-log-level=info + image: 1password/onepassword-operator:latest +. +. +. +``` +To deploy the operator, simply run the following command: + +```shell +make deploy +``` + +**Undeploy Operator** + +``` +make undeploy +``` + ## Usage To create a Kubernetes Secret from a 1Password item, create a yaml file with the following From 0b6b07b86715943d73a07f1e0d1e24f9fbae527e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Sun, 8 Jun 2025 12:48:55 -0500 Subject: [PATCH 13/44] Requeue after 1 munute if faced reate limit error from 1password --- internal/controller/deployment_controller.go | 12 +++++++++--- internal/controller/onepassworditem_controller.go | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/controller/deployment_controller.go b/internal/controller/deployment_controller.go index db3b2f4..032e8f8 100644 --- a/internal/controller/deployment_controller.go +++ b/internal/controller/deployment_controller.go @@ -28,8 +28,8 @@ import ( "context" "fmt" "regexp" - - "sigs.k8s.io/controller-runtime/pkg/reconcile" + "strings" + "time" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/logs" @@ -46,6 +46,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) var logDeployment = logf.Log.WithName("controller_deployment") @@ -102,7 +103,12 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Handles creation or updating secrets for deployment if needed if err = r.handleApplyingDeployment(deployment, deployment.Namespace, annotations, req); err != nil { - return ctrl.Result{}, err + if strings.Contains(err.Error(), "rate limit") { + reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 1 minute.") + return ctrl.Result{RequeueAfter: time.Minute}, nil + } else { + return ctrl.Result{}, err + } } return ctrl.Result{}, nil } diff --git a/internal/controller/onepassworditem_controller.go b/internal/controller/onepassworditem_controller.go index d38e55b..edc502a 100644 --- a/internal/controller/onepassworditem_controller.go +++ b/internal/controller/onepassworditem_controller.go @@ -27,6 +27,8 @@ package controller import ( "context" "fmt" + "strings" + "time" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" @@ -103,6 +105,12 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ // Handles creation or updating secrets for deployment if needed err = r.handleOnePasswordItem(onepassworditem, req) + if err != nil { + if strings.Contains(err.Error(), "rate limit") { + reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 1 minute.") + return ctrl.Result{RequeueAfter: time.Minute}, nil + } + } if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil { return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr) } From 4527336c37b1ece7d5b9bdaf113a7e5ebfcca649 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 9 Jun 2025 16:01:35 -0500 Subject: [PATCH 14/44] Increase default container resources To avoid operator crashing in ~1 min after re-trying unrecoverable error, for example service account rate limit --- config/manager/manager.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 2506cdb..70e65ee 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -116,9 +116,9 @@ spec: resources: limits: cpu: 500m - memory: 128Mi + memory: 512Mi requests: - cpu: 10m - memory: 64Mi + cpu: 100m + memory: 128Mi serviceAccountName: onepassword-connect-operator terminationGracePeriodSeconds: 10 From c95078c34c30041ae2a5d97bc8605a95a6866e4a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 9 Jun 2025 16:04:23 -0500 Subject: [PATCH 15/44] Retry after 15 minutes, if get rate-limit error As currently service account are last for 1 hour https://developer.1password.com/docs/service-accounts/rate-limits/#hourly-limits --- internal/controller/deployment_controller.go | 4 ++-- internal/controller/onepassworditem_controller.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/deployment_controller.go b/internal/controller/deployment_controller.go index 032e8f8..af5c113 100644 --- a/internal/controller/deployment_controller.go +++ b/internal/controller/deployment_controller.go @@ -104,8 +104,8 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Handles creation or updating secrets for deployment if needed if err = r.handleApplyingDeployment(deployment, deployment.Namespace, annotations, req); err != nil { if strings.Contains(err.Error(), "rate limit") { - reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 1 minute.") - return ctrl.Result{RequeueAfter: time.Minute}, nil + reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.") + return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil } else { return ctrl.Result{}, err } diff --git a/internal/controller/onepassworditem_controller.go b/internal/controller/onepassworditem_controller.go index edc502a..111f94a 100644 --- a/internal/controller/onepassworditem_controller.go +++ b/internal/controller/onepassworditem_controller.go @@ -107,8 +107,8 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ err = r.handleOnePasswordItem(onepassworditem, req) if err != nil { if strings.Contains(err.Error(), "rate limit") { - reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 1 minute.") - return ctrl.Result{RequeueAfter: time.Minute}, nil + reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.") + return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil } } if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil { From 4d2120061d51eddd24f37760e7888effce70df06 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Thu, 12 Jun 2025 10:08:55 -0500 Subject: [PATCH 16/44] Bump onepassword-sdk-version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 791c535..d383404 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.1 require ( github.com/1Password/connect-sdk-go v1.5.3 - github.com/1password/onepassword-sdk-go v0.3.0 + github.com/1password/onepassword-sdk-go v0.3.1 github.com/onsi/ginkgo/v2 v2.14.0 github.com/onsi/gomega v1.30.0 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index 1f02584..72f559c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk= github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs= -github.com/1password/onepassword-sdk-go v0.3.0 h1:PC3J08hOH7xmt5QjpakhjZzx0XfbBb4SkBVEqgYYG54= -github.com/1password/onepassword-sdk-go v0.3.0/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk= +github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc= +github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= From d1be03edd03697a4f726315c20089365cb528b3d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 13 Jun 2025 16:15:41 -0500 Subject: [PATCH 17/44] Update USAGEGUIDE.md --- USAGEGUIDE.md | 232 +++++++++++++------------------------------------- 1 file changed, 60 insertions(+), 172 deletions(-) diff --git a/USAGEGUIDE.md b/USAGEGUIDE.md index 956c09c..b94a9e1 100644 --- a/USAGEGUIDE.md +++ b/USAGEGUIDE.md @@ -5,143 +5,50 @@ ## Table of Contents -- [Configuration Options](#configuration-options) -- [Prerequisites](#prerequisites) -- [Deploying 1Password Connect to Kubernetes](#deploying-1password-connect-to-kubernetes) -- [Kubernetes Operator Deployment With Connect](#kubernetes-operator-deployment-with-connect) -- [Kubernetes Operator Deployment With Service Account](#kubernetes-operator-deployment-with-service-account) -- [Usage](#usage) -- [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) -- [Development](#development) +1. [Prerequisites](#prerequisites) +2. [Configuration Options](#configuration-options) +3. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account) + - [Create a Service Account](#1-create-a-service-account) + - [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account) + - [Deploy the Operator](#3-deploy-the-operator) +4. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect) + - [Deploy with Helm](#1-deploy-with-helm) + - [Deploy manually](#2-deploy-manually) +5. [Logging level](#logging-level) +6. [Usage examples](#usage-examples) +7. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets) +8. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) +9. [Development](#development) + +--- ## Prerequisites - [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/) - [`kubectl` installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - [`docker` installed](https://docs.docker.com/get-docker/) -- [A `1password-credentials.json` file generated and a 1Password Connect API Token issued for the K8s Operator integration](https://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow) + +--- ## Configuration options There are 2 ways 1Password Operator can talk to 1Password servers: - **Connect**: It uses the 1Password Connect API to access items in 1Password. - **Service Account**: It uses [1Password SDK](https://developer.1password.com/docs/sdks/) and [Service Account](https://developer.1password.com/docs/service-accounts) to access items in 1Password. -## Deploying 1Password Connect to Kubernetes +--- -If 1Password Connect is already running, you can skip this step. +## Use Kubernetes Operator with Service Account -There are options to deploy 1Password Connect: - -- [Deploy with Helm](#deploy-with-helm) -- [Deploy using the Connect Operator](#deploy-using-the-connect-operator) - -### Deploy with Helm - -The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes. - -[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts) - -### Deploy using the Connect Operator - -This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however, it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired. - -Encode the `1password-credentials.json` file you generated in the prerequisite steps and save it to a file named `op-session`: - -```bash -cat 1password-credentials.json | base64 | \ - tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session -``` - -Create a Kubernetes secret from the op-session file: - -```bash -kubectl create secret generic op-credentials --from-file=op-session -``` - -Add the following environment variable to the onepassword-connect-operator container in `/config/manager/manager.yaml`: - -```yaml -- name: MANAGE_CONNECT - value: "true" -``` - -Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the current namespace. - -## Kubernetes Operator Deployment with Connect - -#### Create Kubernetes Secret for OP_CONNECT_TOKEN #### - -Create a Connect token for the operator and save it as a Kubernetes Secret: - -```bash -kubectl create secret generic onepassword-token --from-literal=token="" -``` - -If you do not have a token for the operator, you can generate a token and save it to Kubernetes with the following command: - -```bash -kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token op-k8s-operator --vault ) -``` - -**Deploying the Operator** - -An sample Deployment yaml can be found at `/config/manager/manager.yaml`. - -To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml: - -- **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 1Password Connect. -- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes. -- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. -- **MANAGE_CONNECT** *(default: false)*: If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the current namespace. -- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. - -You can also set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. (Note: the default value is `debug`.) - -Example: -```yaml -. -. -. -containers: - - command: - - /manager - args: - - --leader-elect - - --zap-log-level=info - image: 1password/onepassword-operator:latest -. -. -. -``` -To deploy the operator, simply run the following command: - -```shell -make deploy -``` - -**Undeploy Operator** - -``` -make undeploy -``` - -## Kubernetes Operator Deployment with Service Account - -#### Create Kubernetes Secret for OP_SERVICE_ACCOUNT_TOKEN #### - -Create a Service Account token for the operator and save it as a Kubernetes Secret: +### 1. [Create a service account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account) +### 2. Create a Kubernetes secret for the Service Account +- Set `OP_SERVICE_ACCOUNT_TOKEN` environment variable to the service account token you created in the previous step. This token will be used by the operator to access 1Password items. +- Create Kubernetes secret: ```bash kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN" ``` -If you do not have a token for the operator, you can generate a token and save it to Kubernetes with the following command: - -```bash -kubectl create secret generic onepassword-service-account-token --from-literal=token=$(op service-account create my-service-account --vault Dev:read_items --vault Test:read_items,write_items) -``` - -**Deploying the Operator** +### 3. Deploy the Operator An sample Deployment yaml can be found at `/config/manager/manager.yaml`. To use Operator with Service Account, you need to set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable in the `/config/manager/manager.yaml`. And remove `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` environment variables. @@ -153,24 +60,6 @@ To further configure the 1Password Kubernetes Operator the following Environment - **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password. - **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. -You can also set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. (Note: the default value is `debug`.) - -Example: -```yaml -. -. -. -containers: - - command: - - /manager - args: - - --leader-elect - - --zap-log-level=info - image: 1password/onepassword-operator:latest -. -. -. -``` To deploy the operator, simply run the following command: ```shell @@ -183,59 +72,56 @@ make deploy make undeploy ``` -## Usage +--- -To create a Kubernetes Secret from a 1Password item, create a yaml file with the following +## Use Kubernetes Operator with Connect +### 1. [Deploy with Helm](https://developer.1password.com/docs/k8s/operator/?deployment-type=helm#helm-step-1) +### 2. [Deploy manually](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#manual-step-1) + +To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml: + +- **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 1Password Connect. +- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes. +- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. +- **MANAGE_CONNECT** *(default: false)*: If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the current namespace. +- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. + +--- + +## Logging level +You can set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. The default value is `debug`. + +Example: ```yaml -apiVersion: onepassword.com/v1 -kind: OnePasswordItem -metadata: - name: #this name will also be used for naming the generated kubernetes secret -spec: - itemPath: "vaults//items/" +.... +containers: + - command: + - /manager + args: + - --leader-elect + - --zap-log-level=info + image: 1password/onepassword-operator:latest +.... ``` -Deploy the OnePasswordItem to Kubernetes: +--- -```bash -kubectl apply -f .yaml -``` +## [Usage examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) -To test that the Kubernetes Secret check that the following command returns a secret: +--- -```bash -kubectl get secret -``` - -**Note:** Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. - -To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: deployment-example - annotations: - operator.1password.io/item-path: "vaults//items/" - operator.1password.io/item-name: "" -``` - -Applying this yaml file will create a Kubernetes Secret with the name `` and contents from the location specified at the specified Item Path. +## How 1Password Items Map to Kubernetes Secrets The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password. In case of fields that store files, the file's contents will be used as the value. Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence. -**Note:** Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret. +Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret. If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes. ---- - -**NOTE** If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item. @@ -302,6 +188,8 @@ metadata: If the value is not set, the auto restart settings on the deployment will be used. +--- + ## Development ### How it works From 0582c2d6e1b3100674dd5b2b7e7c5b1fc00b27d2 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 13 Jun 2025 16:22:18 -0500 Subject: [PATCH 18/44] Bump go version to 1.24 --- Dockerfile | 2 +- go.mod | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 207ceb3..385d016 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.22 as builder +FROM golang:1.24 as builder ARG TARGETOS ARG TARGETARCH diff --git a/go.mod b/go.mod index d383404..bf42587 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/1Password/onepassword-operator -go 1.22.0 +go 1.24 -toolchain go1.24.1 +toolchain go1.24.4 require ( github.com/1Password/connect-sdk-go v1.5.3 From 64aad3d573681e9212f5b7bb9369003b07ffebc6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 13 Jun 2025 16:23:45 -0500 Subject: [PATCH 19/44] Revert version change, as should be done in the release MR --- version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version/version.go b/version/version.go index c46e83f..3557c0f 100644 --- a/version/version.go +++ b/version/version.go @@ -1,6 +1,6 @@ package version var ( - OperatorVersion = "1.9.0" + OperatorVersion = "1.8.1" OperatorSDKVersion = "1.34.1" ) From 475860a364db9d7021bf847353239b5ad75c030b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 19:43:45 -0500 Subject: [PATCH 20/44] Make changelog description more detailed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa83ba..d43eb48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ # v1.9.0 ## Features - * Support Service Accounts. {#160} + * Support fetching secrets using Service Accounts to authenticate with 1Password. {#160} --- From cff4d194ba72f920ad1738e152ac8350c310c023 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 19:45:23 -0500 Subject: [PATCH 21/44] Update constructor function name --- cmd/main.go | 2 +- pkg/onepassword/client/client.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 3ee68f7..e096a5f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -152,7 +152,7 @@ func main() { } // Setup One Password Client - opClient, err := opclient.NewClient(version.OperatorVersion) + opClient, err := opclient.NewFromEnvironment(version.OperatorVersion) if err != nil { setupLog.Error(err, "unable to create 1Password client") os.Exit(1) diff --git a/pkg/onepassword/client/client.go b/pkg/onepassword/client/client.go index 7147381..52db568 100644 --- a/pkg/onepassword/client/client.go +++ b/pkg/onepassword/client/client.go @@ -18,8 +18,8 @@ type Client interface { GetVaultsByTitle(title string) ([]model.Vault, error) } -// NewClient creates a new 1Password client based on the provided configuration. -func NewClient(integrationVersion string) (Client, error) { +// NewFromEnvironment creates a new 1Password client based on the provided configuration. +func NewFromEnvironment(integrationVersion string) (Client, error) { connectHost, _ := os.LookupEnv("OP_CONNECT_HOST") connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN") serviceAccountToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN") From 1fa5bccec2c97b73e5d2095afde70aa102f06340 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:03:36 -0500 Subject: [PATCH 22/44] Upse `copy` to copy tags --- pkg/onepassword/model/item.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/onepassword/model/item.go b/pkg/onepassword/model/item.go index 9066204..ebf13e4 100644 --- a/pkg/onepassword/model/item.go +++ b/pkg/onepassword/model/item.go @@ -52,9 +52,8 @@ func (i *Item) FromSDKItem(item *sdk.Item) { i.VaultID = item.VaultID i.Version = int(item.Version) - for _, tag := range item.Tags { - i.Tags = append(i.Tags, tag) - } + i.Tags = make([]string, len(item.Tags)) + copy(i.Tags, item.Tags) for _, field := range item.Fields { i.Fields = append(i.Fields, ItemField{ @@ -79,9 +78,8 @@ func (i *Item) FromSDKItemOverview(item *sdk.ItemOverview) { i.ID = item.ID i.VaultID = item.VaultID - for _, tag := range item.Tags { - i.Tags = append(i.Tags, tag) - } + i.Tags = make([]string, len(item.Tags)) + copy(i.Tags, item.Tags) i.CreatedAt = item.CreatedAt } From 922f3c8929763ac3f4d1a16f830c43a3e4e04d66 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:07:29 -0500 Subject: [PATCH 23/44] Map `CreatedAt` --- pkg/onepassword/model/vault.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/onepassword/model/vault.go b/pkg/onepassword/model/vault.go index e90e492..5cf26e1 100644 --- a/pkg/onepassword/model/vault.go +++ b/pkg/onepassword/model/vault.go @@ -19,5 +19,5 @@ func (v *Vault) FromConnectVault(vault *connect.Vault) { func (v *Vault) FromSDKVault(vault *sdk.VaultOverview) { v.ID = vault.ID - v.CreatedAt = time.Now() // TODO: add to SDK and use it instead of time.Now() + v.CreatedAt = vault.CreatedAt } From a0475d71705d14a705d7bf64a8d030f145ad65ba Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:09:35 -0500 Subject: [PATCH 24/44] Check `created_at` in the SDK mapper test --- pkg/onepassword/model/vault_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/onepassword/model/vault_test.go b/pkg/onepassword/model/vault_test.go index 48c7be2..d6ba34b 100644 --- a/pkg/onepassword/model/vault_test.go +++ b/pkg/onepassword/model/vault_test.go @@ -23,14 +23,15 @@ func TestVault_FromConnectVault(t *testing.T) { require.Equal(t, connectVault.CreatedAt, vault.CreatedAt) } -// TODO: check CreatedAt when available func TestVault_FromSDKVault(t *testing.T) { sdkVault := &sdk.VaultOverview{ - ID: "test-id", + ID: "test-id", + CreatedAt: time.Now(), } vault := &Vault{} vault.FromSDKVault(sdkVault) require.Equal(t, sdkVault.ID, vault.ID) + require.Equal(t, sdkVault.CreatedAt, vault.CreatedAt) } From ae9b673f962b0e6e0673d0b1783d62c3578c67b2 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:11:13 -0500 Subject: [PATCH 25/44] Remove commented code --- pkg/onepassword/secret_update_handler_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/onepassword/secret_update_handler_test.go b/pkg/onepassword/secret_update_handler_test.go index f6dad7f..7c93bd6 100644 --- a/pkg/onepassword/secret_update_handler_test.go +++ b/pkg/onepassword/secret_update_handler_test.go @@ -807,15 +807,6 @@ func TestUpdateSecretHandler(t *testing.T) { mockOpClient := &mocks.TestClient{} mockOpClient.On("GetItemByID", mock.Anything, mock.Anything).Return(createItem(), nil) - //mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { - // - // item := onepassword.Item{} - // item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) - // item.Version = itemVersion - // item.Vault.ID = vaultUUID - // item.ID = uuid - // return &item, nil - //} h := &SecretUpdateHandler{ client: cl, opClient: mockOpClient, From aa1b4ba85788e42cc08b71100c682009c11c2a0c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:12:38 -0500 Subject: [PATCH 26/44] Capitalize 1Password in error --- pkg/onepassword/client/connect/connect.go | 8 ++++---- pkg/onepassword/client/sdk/sdk.go | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go index e45df48..8dda08b 100644 --- a/pkg/onepassword/client/connect/connect.go +++ b/pkg/onepassword/client/connect/connect.go @@ -30,7 +30,7 @@ func NewClient(config Config) *Connect { func (c *Connect) GetItemByID(vaultID, itemID string) (*model.Item, error) { connectItem, err := c.client.GetItemByUUID(itemID, vaultID) if err != nil { - return nil, fmt.Errorf("1password Connect error: %w", err) + return nil, fmt.Errorf("1Password Connect error: %w", err) } var item model.Item @@ -42,7 +42,7 @@ func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, erro // Get all items in the vault with the specified title connectItems, err := c.client.GetItemsByTitle(itemTitle, vaultID) if err != nil { - return nil, fmt.Errorf("1password Connect error: %w", err) + return nil, fmt.Errorf("1Password Connect error: %w", err) } var items []model.Item @@ -60,7 +60,7 @@ func (c *Connect) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", vaultID, itemID, fileID), }) if err != nil { - return nil, fmt.Errorf("1password Connect error: %w", err) + return nil, fmt.Errorf("1Password Connect error: %w", err) } return bytes, nil @@ -69,7 +69,7 @@ func (c *Connect) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) { connectVaults, err := c.client.GetVaultsByTitle(vaultQuery) if err != nil { - return nil, fmt.Errorf("1password Connect error: %w", err) + return nil, fmt.Errorf("1Password Connect error: %w", err) } var vaults []model.Vault diff --git a/pkg/onepassword/client/sdk/sdk.go b/pkg/onepassword/client/sdk/sdk.go index b6a89ee..dcddda9 100644 --- a/pkg/onepassword/client/sdk/sdk.go +++ b/pkg/onepassword/client/sdk/sdk.go @@ -26,7 +26,7 @@ func NewClient(config Config) (*SDK, error) { sdk.WithIntegrationInfo(config.IntegrationName, config.IntegrationVersion), ) if err != nil { - return nil, fmt.Errorf("1password sdk error: %w", err) + return nil, fmt.Errorf("1Password sdk error: %w", err) } return &SDK{ @@ -37,7 +37,7 @@ func NewClient(config Config) (*SDK, error) { func (s *SDK) GetItemByID(vaultID, itemID string) (*model.Item, error) { sdkItem, err := s.client.Items().Get(context.Background(), vaultID, itemID) if err != nil { - return nil, fmt.Errorf("1password sdk error: %w", err) + return nil, fmt.Errorf("1Password sdk error: %w", err) } var item model.Item @@ -49,7 +49,7 @@ func (s *SDK) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { // Get all items in the vault sdkItems, err := s.client.Items().List(context.Background(), vaultID) if err != nil { - return nil, fmt.Errorf("1password sdk error: %w", err) + return nil, fmt.Errorf("1Password sdk error: %w", err) } // Filter items by title @@ -70,7 +70,7 @@ func (s *SDK) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { ID: fileID, }) if err != nil { - return nil, fmt.Errorf("1password sdk error: %w", err) + return nil, fmt.Errorf("1Password sdk error: %w", err) } return bytes, nil @@ -80,7 +80,7 @@ func (s *SDK) GetVaultsByTitle(title string) ([]model.Vault, error) { // List all vaults sdkVaults, err := s.client.Vaults().List(context.Background()) if err != nil { - return nil, fmt.Errorf("1password sdk error: %w", err) + return nil, fmt.Errorf("1Password sdk error: %w", err) } // Filter vaults by title From 9d0736285f39ce546413036bbda7235f775bb27c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:18:57 -0500 Subject: [PATCH 27/44] Fix type --- pkg/onepassword/client/sdk/sdk_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/onepassword/client/sdk/sdk_test.go b/pkg/onepassword/client/sdk/sdk_test.go index 5455c5b..501fa3c 100644 --- a/pkg/onepassword/client/sdk/sdk_test.go +++ b/pkg/onepassword/client/sdk/sdk_test.go @@ -23,7 +23,7 @@ func TestSDK_GetItemByID(t *testing.T) { mockItemAPI func() *clientmock.ItemAPIMock check func(t *testing.T, item *model.Item, err error) }{ - "should return a single vault": { + "should return a single item": { mockItemAPI: func() *clientmock.ItemAPIMock { m := &clientmock.ItemAPIMock{} m.On("Get", context.Background(), "vault-id", "item-id").Return(*sdkItem, nil) From f164a93b7230ba7b1ebe3124b01700ab0585a5d3 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:51:16 -0500 Subject: [PATCH 28/44] Re-arrange env vars --- config/manager/manager.yaml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 70e65ee..582577b 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -75,25 +75,33 @@ spec: image: 1password/onepassword-operator:latest name: manager env: - - name: WATCH_NAMESPACE - value: "default" + - name: OPERATOR_NAME + value: "onepassword-connect-operator" - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - - name: OPERATOR_NAME - value: "onepassword-connect-operator" - - name: OP_CONNECT_HOST - value: "http://onepassword-connect:8080" + - name: WATCH_NAMESPACE + value: "default" - name: POLLING_INTERVAL value: "10" + - name: AUTO_RESTART + value: "false" + - name: OP_CONNECT_HOST + value: "http://onepassword-connect:8080" - name: OP_CONNECT_TOKEN valueFrom: secretKeyRef: name: onepassword-token key: token - - name: AUTO_RESTART + - name: MANAGE_CONNECT value: "false" +# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN and OP_CONNECT_HOST env vars. +# - name: OP_SERVICE_ACCOUNT_TOKEN +# valueFrom: +# secretKeyRef: +# name: onepassword-service-account-token +# key: token securityContext: allowPrivilegeEscalation: false capabilities: From 1aa27fdba0ba4c7ed15aefcbc9ff69a9bddab7cd Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 20:54:45 -0500 Subject: [PATCH 29/44] Sort imports --- internal/controller/suite_test.go | 2 +- pkg/kubernetessecrets/kubernetes_secrets_builder.go | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 8791e6a..f8d1232 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -26,7 +26,6 @@ package controller import ( "context" - "github.com/stretchr/testify/mock" "path/filepath" "regexp" "testing" @@ -34,6 +33,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder.go b/pkg/kubernetessecrets/kubernetes_secrets_builder.go index d7982fd..5d5b52b 100644 --- a/pkg/kubernetessecrets/kubernetes_secrets_builder.go +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder.go @@ -2,16 +2,13 @@ package kubernetessecrets import ( "context" + errs "errors" "fmt" - "github.com/1Password/onepassword-operator/pkg/onepassword/model" - + "reflect" "regexp" "strings" - "reflect" - - errs "errors" - + "github.com/1Password/onepassword-operator/pkg/onepassword/model" "github.com/1Password/onepassword-operator/pkg/utils" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" From 55b5781d7a9b7e2175f164850db3f5429a84b041 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 21:04:22 -0500 Subject: [PATCH 30/44] Pass logger to print what what type of client is used Connect or Service Account --- cmd/main.go | 5 ++++- pkg/onepassword/client/client.go | 16 +++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e096a5f..01304f4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -152,7 +152,10 @@ func main() { } // Setup One Password Client - opClient, err := opclient.NewFromEnvironment(version.OperatorVersion) + opClient, err := opclient.NewFromEnvironment(opclient.Config{ + Logger: setupLog, + Version: version.OperatorVersion, + }) if err != nil { setupLog.Error(err, "unable to create 1Password client") os.Exit(1) diff --git a/pkg/onepassword/client/client.go b/pkg/onepassword/client/client.go index 52db568..a77867e 100644 --- a/pkg/onepassword/client/client.go +++ b/pkg/onepassword/client/client.go @@ -2,9 +2,10 @@ package client import ( "errors" - "fmt" "os" + "github.com/go-logr/logr" + "github.com/1Password/onepassword-operator/pkg/onepassword/client/connect" "github.com/1Password/onepassword-operator/pkg/onepassword/client/sdk" "github.com/1Password/onepassword-operator/pkg/onepassword/model" @@ -18,8 +19,13 @@ type Client interface { GetVaultsByTitle(title string) ([]model.Vault, error) } +type Config struct { + Logger logr.Logger + Version string +} + // NewFromEnvironment creates a new 1Password client based on the provided configuration. -func NewFromEnvironment(integrationVersion string) (Client, error) { +func NewFromEnvironment(cfg Config) (Client, error) { connectHost, _ := os.LookupEnv("OP_CONNECT_HOST") connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN") serviceAccountToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN") @@ -29,16 +35,16 @@ func NewFromEnvironment(integrationVersion string) (Client, error) { } if serviceAccountToken != "" { - fmt.Printf("Using Service Account Token") + cfg.Logger.Info("Using Service Account Token") return sdk.NewClient(sdk.Config{ ServiceAccountToken: serviceAccountToken, IntegrationName: "1password-operator", - IntegrationVersion: integrationVersion, + IntegrationVersion: cfg.Version, }) } if connectHost != "" && connectToken != "" { - fmt.Printf("Using Connect") + cfg.Logger.Info("Using 1Password Connect") return connect.NewClient(connect.Config{ ConnectHost: connectHost, ConnectToken: connectToken, From 704116b855d59f6af0540d2cc4811d2351a68660 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 21:10:33 -0500 Subject: [PATCH 31/44] Remove user agent from Connect config as it's set automatically when initializing Connect client --- pkg/onepassword/client/connect/connect.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go index 8dda08b..8cbe7f4 100644 --- a/pkg/onepassword/client/connect/connect.go +++ b/pkg/onepassword/client/connect/connect.go @@ -12,7 +12,6 @@ import ( type Config struct { ConnectHost string ConnectToken string - UserAgent string } // Connect is a client for interacting with 1Password using the Connect API. From 2373fbc87fa44fe12f28fe2ba863c87abe95c377 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 21:32:00 -0500 Subject: [PATCH 32/44] Updated mapping to be faster --- pkg/onepassword/client/connect/connect.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go index 8cbe7f4..992fa49 100644 --- a/pkg/onepassword/client/connect/connect.go +++ b/pkg/onepassword/client/connect/connect.go @@ -44,11 +44,11 @@ func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, erro return nil, fmt.Errorf("1Password Connect error: %w", err) } - var items []model.Item - for _, connectItem := range connectItems { + items := make([]model.Item, len(connectItems)) + for i, connectItem := range connectItems { var item model.Item item.FromConnectItem(&connectItem) - items = append(items, item) + items[i] = item } return items, nil @@ -71,12 +71,12 @@ func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) { return nil, fmt.Errorf("1Password Connect error: %w", err) } - var vaults []model.Vault - for _, connectVault := range connectVaults { + vaults := make([]model.Vault, len(connectVaults)) + for i, connectVault := range connectVaults { if vaultQuery == connectVault.Name { var vault model.Vault vault.FromConnectVault(&connectVault) - vaults = append(vaults, vault) + vaults[i] = vault } } From 32360d8a837194f1de317872c77bf6af3c4696df Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 21:53:46 -0500 Subject: [PATCH 33/44] Remove todos from mocked methods --- .../client/testing/mock/connect.go | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/onepassword/client/testing/mock/connect.go b/pkg/onepassword/client/testing/mock/connect.go index a38c4f6..cd11eae 100644 --- a/pkg/onepassword/client/testing/mock/connect.go +++ b/pkg/onepassword/client/testing/mock/connect.go @@ -12,7 +12,7 @@ type ConnectClientMock struct { } func (c *ConnectClientMock) GetVaults() ([]onepassword.Vault, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } @@ -22,12 +22,12 @@ func (c *ConnectClientMock) GetVault(uuid string) (*onepassword.Vault, error) { } func (c *ConnectClientMock) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) GetVaultByTitle(title string) (*onepassword.Vault, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } @@ -37,12 +37,12 @@ func (c *ConnectClientMock) GetVaultsByTitle(title string) ([]onepassword.Vault, } func (c *ConnectClientMock) GetItems(vaultQuery string) ([]onepassword.Item, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } @@ -52,7 +52,7 @@ func (c *ConnectClientMock) GetItemByUUID(uuid string, vaultQuery string) (*onep } func (c *ConnectClientMock) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } @@ -62,37 +62,37 @@ func (c *ConnectClientMock) GetItemsByTitle(title string, vaultQuery string) ([] } func (c *ConnectClientMock) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) DeleteItem(item *onepassword.Item, vaultQuery string) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) DeleteItemByID(itemUUID string, vaultQuery string) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) DeleteItemByTitle(title string, vaultQuery string) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } @@ -105,26 +105,26 @@ func (c *ConnectClientMock) GetFileContent(file *onepassword.File) ([]byte, erro } func (c *ConnectClientMock) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } func (c *ConnectClientMock) LoadStruct(config interface{}) error { - //TODO implement me + // implement when need to mock this method panic("implement me") } From 0903bacfbd3946f7854b20cbe2b48af3eaaed090 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 21:55:32 -0500 Subject: [PATCH 34/44] Check Create_At in the test --- pkg/onepassword/client/sdk/sdk_test.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pkg/onepassword/client/sdk/sdk_test.go b/pkg/onepassword/client/sdk/sdk_test.go index 501fa3c..bab4755 100644 --- a/pkg/onepassword/client/sdk/sdk_test.go +++ b/pkg/onepassword/client/sdk/sdk_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -191,8 +192,8 @@ func TestSDK_GetFileContent(t *testing.T) { } } -// TODO: check CreatedAt as soon as a new SDK version returns it func TestSDK_GetVaultsByTitle(t *testing.T) { + now := time.Now() testCases := map[string]struct { mockVaultAPI func() *clientmock.VaultAPIMock check func(t *testing.T, vaults []model.Vault, err error) @@ -202,12 +203,14 @@ func TestSDK_GetVaultsByTitle(t *testing.T) { m := &clientmock.VaultAPIMock{} m.On("List", context.Background()).Return([]sdk.VaultOverview{ { - ID: "test-id", - Title: VaultTitleEmployee, + ID: "test-id", + Title: VaultTitleEmployee, + CreatedAt: now, }, { - ID: "test-id-2", - Title: "Some other vault", + ID: "test-id-2", + Title: "Some other vault", + CreatedAt: now, }, }, nil) return m @@ -216,6 +219,7 @@ func TestSDK_GetVaultsByTitle(t *testing.T) { require.NoError(t, err) require.Len(t, vaults, 1) require.Equal(t, "test-id", vaults[0].ID) + require.Equal(t, now, vaults[0].CreatedAt) }, }, "should return a two vaults": { @@ -223,12 +227,14 @@ func TestSDK_GetVaultsByTitle(t *testing.T) { m := &clientmock.VaultAPIMock{} m.On("List", context.Background()).Return([]sdk.VaultOverview{ { - ID: "test-id", - Title: VaultTitleEmployee, + ID: "test-id", + Title: VaultTitleEmployee, + CreatedAt: now, }, { - ID: "test-id-2", - Title: VaultTitleEmployee, + ID: "test-id-2", + Title: VaultTitleEmployee, + CreatedAt: now, }, }, nil) return m @@ -238,8 +244,10 @@ func TestSDK_GetVaultsByTitle(t *testing.T) { require.Len(t, vaults, 2) // Check the first vault require.Equal(t, "test-id", vaults[0].ID) + require.Equal(t, now, vaults[0].CreatedAt) // Check the second vault require.Equal(t, "test-id-2", vaults[1].ID) + require.Equal(t, now, vaults[1].CreatedAt) }, }, "should return an error": { From 17d44d90bddcf91a648ba88d2b5fbfc5883103a9 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 21:56:51 -0500 Subject: [PATCH 35/44] Check for empty list --- pkg/onepassword/client/sdk/sdk_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/onepassword/client/sdk/sdk_test.go b/pkg/onepassword/client/sdk/sdk_test.go index bab4755..c9f5d68 100644 --- a/pkg/onepassword/client/sdk/sdk_test.go +++ b/pkg/onepassword/client/sdk/sdk_test.go @@ -104,6 +104,17 @@ func TestSDK_GetItemsByTitle(t *testing.T) { clienttesting.CheckSDKItemOverviewMapping(t, sdkItem2, &items[1]) }, }, + "should return empty list": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, nil) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 0) + }, + }, "should return an error": { mockItemAPI: func() *clientmock.ItemAPIMock { m := &clientmock.ItemAPIMock{} From bb7b565457adfe4fcd942852d7f6ce96c1e10d4b Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 22:04:23 -0500 Subject: [PATCH 36/44] As we check for vault name, we can't initialize the array of exact size as don't know how many items well have ther --- pkg/onepassword/client/connect/connect.go | 7 +++-- .../client/connect/connect_test.go | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pkg/onepassword/client/connect/connect.go b/pkg/onepassword/client/connect/connect.go index 992fa49..64bd344 100644 --- a/pkg/onepassword/client/connect/connect.go +++ b/pkg/onepassword/client/connect/connect.go @@ -71,14 +71,13 @@ func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) { return nil, fmt.Errorf("1Password Connect error: %w", err) } - vaults := make([]model.Vault, len(connectVaults)) - for i, connectVault := range connectVaults { + var vaults []model.Vault + for _, connectVault := range connectVaults { if vaultQuery == connectVault.Name { var vault model.Vault vault.FromConnectVault(&connectVault) - vaults[i] = vault + vaults = append(vaults, vault) } } - return vaults, nil } diff --git a/pkg/onepassword/client/connect/connect_test.go b/pkg/onepassword/client/connect/connect_test.go index ef21b90..0eef3be 100644 --- a/pkg/onepassword/client/connect/connect_test.go +++ b/pkg/onepassword/client/connect/connect_test.go @@ -3,6 +3,7 @@ package connect import ( "errors" "testing" + "time" "github.com/stretchr/testify/require" @@ -159,6 +160,7 @@ func TestConnect_GetFileContent(t *testing.T) { } func TestConnect_GetVaultsByTitle(t *testing.T) { + now := time.Now() testCases := map[string]struct { mockClient func() *mock.ConnectClientMock check func(t *testing.T, vaults []model.Vault, err error) @@ -168,12 +170,14 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { mockConnectClient := &mock.ConnectClientMock{} mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{ { - ID: "test-id", - Name: VaultTitleEmployee, + ID: "test-id", + Name: VaultTitleEmployee, + CreatedAt: now, }, { - ID: "test-id-2", - Name: "Some other vault", + ID: "test-id-2", + Name: "Some other vault", + CreatedAt: now, }, }, nil) return mockConnectClient @@ -182,6 +186,7 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { require.NoError(t, err) require.Len(t, vaults, 1) require.Equal(t, "test-id", vaults[0].ID) + require.Equal(t, now, vaults[0].CreatedAt) }, }, "should return a two vaults": { @@ -189,12 +194,14 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { mockConnectClient := &mock.ConnectClientMock{} mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{ { - ID: "test-id", - Name: VaultTitleEmployee, + ID: "test-id", + Name: VaultTitleEmployee, + CreatedAt: now, }, { - ID: "test-id-2", - Name: VaultTitleEmployee, + ID: "test-id-2", + Name: VaultTitleEmployee, + CreatedAt: now, }, }, nil) return mockConnectClient @@ -204,8 +211,9 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { require.Len(t, vaults, 2) // Check the first vault require.Equal(t, "test-id", vaults[0].ID) + require.Equal(t, now, vaults[0].CreatedAt) // Check the second vault - require.Equal(t, "test-id-2", vaults[1].ID) + require.Equal(t, now, vaults[1].CreatedAt) }, }, "should return an error": { From 458ed24ca3bb8a4dda9e66e93a0fe44fc1b3e5fc Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Mon, 16 Jun 2025 22:05:06 -0500 Subject: [PATCH 37/44] Check second vault ID --- pkg/onepassword/client/connect/connect_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/onepassword/client/connect/connect_test.go b/pkg/onepassword/client/connect/connect_test.go index 0eef3be..be86d0e 100644 --- a/pkg/onepassword/client/connect/connect_test.go +++ b/pkg/onepassword/client/connect/connect_test.go @@ -213,6 +213,7 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { require.Equal(t, "test-id", vaults[0].ID) require.Equal(t, now, vaults[0].CreatedAt) // Check the second vault + require.Equal(t, "test-id-2", vaults[1].ID) require.Equal(t, now, vaults[1].CreatedAt) }, }, From ac646ec56cd509c4cee864d34a87255693f82bb6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 17 Jun 2025 11:16:57 -0500 Subject: [PATCH 38/44] Rename `vaultIdentifier` and `itemIdentifier` for more clarity --- pkg/onepassword/items.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/onepassword/items.go b/pkg/onepassword/items.go index eb121a3..ad42276 100644 --- a/pkg/onepassword/items.go +++ b/pkg/onepassword/items.go @@ -13,18 +13,18 @@ import ( var logger = logf.Log.WithName("retrieve_item") func GetOnePasswordItemByPath(opClient opclient.Client, path string) (*model.Item, error) { - vaultIdentifier, itemIdentifier, err := ParseVaultAndItemFromPath(path) + vaultNameOrID, itemNameOrID, err := ParseVaultAndItemFromPath(path) if err != nil { return nil, err } - vaultID, err := getVaultID(opClient, vaultIdentifier) + vaultID, err := getVaultID(opClient, vaultNameOrID) if err != nil { - return nil, fmt.Errorf("failed to 'getVaultID' for vaultIdentifier='%s': %w", vaultIdentifier, err) + return nil, fmt.Errorf("failed to 'getVaultID' for vaultNameOrID='%s': %w", vaultNameOrID, err) } - itemID, err := getItemID(opClient, vaultID, itemIdentifier) + itemID, err := getItemID(opClient, vaultID, itemNameOrID) if err != nil { - return nil, fmt.Errorf("faild to 'getItemID' for vaultID='%s' and itemIdentifier='%s': %w", vaultID, itemIdentifier, err) + return nil, fmt.Errorf("faild to 'getItemID' for vaultID='%s' and itemNameOrID='%s': %w", vaultID, itemNameOrID, err) } item, err := opClient.GetItemByID(vaultID, itemID) @@ -50,15 +50,15 @@ func ParseVaultAndItemFromPath(path string) (string, string, error) { return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) } -func getVaultID(client opclient.Client, vaultIdentifier string) (string, error) { - if !IsValidClientUUID(vaultIdentifier) { - vaults, err := client.GetVaultsByTitle(vaultIdentifier) +func getVaultID(client opclient.Client, vaultNameOrID string) (string, error) { + if !IsValidClientUUID(vaultNameOrID) { + vaults, err := client.GetVaultsByTitle(vaultNameOrID) if err != nil { return "", err } if len(vaults) == 0 { - return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier) + return "", fmt.Errorf("No vaults found with identifier %q", vaultNameOrID) } oldestVault := vaults[0] @@ -68,22 +68,22 @@ func getVaultID(client opclient.Client, vaultIdentifier string) (string, error) oldestVault = returnedVault } } - logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID)) + logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultNameOrID, oldestVault.ID)) } - vaultIdentifier = oldestVault.ID + vaultNameOrID = oldestVault.ID } - return vaultIdentifier, nil + return vaultNameOrID, nil } -func getItemID(client opclient.Client, vaultId, itemIdentifier string) (string, error) { - if !IsValidClientUUID(itemIdentifier) { - items, err := client.GetItemsByTitle(vaultId, itemIdentifier) +func getItemID(client opclient.Client, vaultId, itemNameOrID string) (string, error) { + if !IsValidClientUUID(itemNameOrID) { + items, err := client.GetItemsByTitle(vaultId, itemNameOrID) if err != nil { return "", err } if len(items) == 0 { - return "", fmt.Errorf("No items found with identifier %q", itemIdentifier) + return "", fmt.Errorf("No items found with identifier %q", itemNameOrID) } oldestItem := items[0] @@ -93,9 +93,9 @@ func getItemID(client opclient.Client, vaultId, itemIdentifier string) (string, oldestItem = returnedItem } } - logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID)) + logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemNameOrID, oldestItem.ID)) } - itemIdentifier = oldestItem.ID + itemNameOrID = oldestItem.ID } - return itemIdentifier, nil + return itemNameOrID, nil } From 49a5e93c44776425f730f84eb7e05021362c0bf9 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 17 Jun 2025 11:19:21 -0500 Subject: [PATCH 39/44] Remove GO111MODULE=on as it set to 'on' by default from Go 1.16 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 385d016..86e2234 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ COPY version/ version/ # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 \ - GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on \ + GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ go build \ -ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \ -a -o manager cmd/main.go From 842c8febdc89eae510f5efcf6ac488d686592eff Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 17 Jun 2025 11:20:01 -0500 Subject: [PATCH 40/44] Update comment --- config/manager/manager.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 582577b..07a6442 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -96,7 +96,7 @@ spec: key: token - name: MANAGE_CONNECT value: "false" -# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN and OP_CONNECT_HOST env vars. +# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars. # - name: OP_SERVICE_ACCOUNT_TOKEN # valueFrom: # secretKeyRef: From fd92ef86aba1ec73827a7f9b17373be67af40db4 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 17 Jun 2025 11:24:43 -0500 Subject: [PATCH 41/44] Revert changelog and version change as will be done in release MR Revert version change as will be dne in release MR --- .VERSION | 2 +- CHANGELOG.md | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.VERSION b/.VERSION index abb1658..b9268da 100644 --- a/.VERSION +++ b/.VERSION @@ -1 +1 @@ -1.9.0 \ No newline at end of file +1.8.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d43eb48..090d1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,6 @@ --- -[//]: # (START/v1.9.0) -# v1.9.0 - -## Features - * Support fetching secrets using Service Accounts to authenticate with 1Password. {#160} - ---- - [//]: # (START/v1.8.1) # v1.8.1 From c9b969def1fe53635f4dd6da49b10aa91390d45d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 17 Jun 2025 11:27:55 -0500 Subject: [PATCH 42/44] Update comments --- .../client/testing/mock/connect.go | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/onepassword/client/testing/mock/connect.go b/pkg/onepassword/client/testing/mock/connect.go index cd11eae..e511c64 100644 --- a/pkg/onepassword/client/testing/mock/connect.go +++ b/pkg/onepassword/client/testing/mock/connect.go @@ -12,7 +12,7 @@ type ConnectClientMock struct { } func (c *ConnectClientMock) GetVaults() ([]onepassword.Vault, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } @@ -22,12 +22,12 @@ func (c *ConnectClientMock) GetVault(uuid string) (*onepassword.Vault, error) { } func (c *ConnectClientMock) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) GetVaultByTitle(title string) (*onepassword.Vault, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } @@ -37,12 +37,12 @@ func (c *ConnectClientMock) GetVaultsByTitle(title string) ([]onepassword.Vault, } func (c *ConnectClientMock) GetItems(vaultQuery string) ([]onepassword.Item, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } @@ -52,7 +52,7 @@ func (c *ConnectClientMock) GetItemByUUID(uuid string, vaultQuery string) (*onep } func (c *ConnectClientMock) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } @@ -62,37 +62,37 @@ func (c *ConnectClientMock) GetItemsByTitle(title string, vaultQuery string) ([] } func (c *ConnectClientMock) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) DeleteItem(item *onepassword.Item, vaultQuery string) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) DeleteItemByID(itemUUID string, vaultQuery string) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) DeleteItemByTitle(title string, vaultQuery string) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } @@ -105,26 +105,26 @@ func (c *ConnectClientMock) GetFileContent(file *onepassword.File) ([]byte, erro } func (c *ConnectClientMock) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } func (c *ConnectClientMock) LoadStruct(config interface{}) error { - // implement when need to mock this method + // Only implement this if mocking is needed panic("implement me") } From b717823fd07691e2108fd2ec08370d47d0d06c36 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 17 Jun 2025 13:12:58 -0500 Subject: [PATCH 43/44] Update examples section --- USAGEGUIDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/USAGEGUIDE.md b/USAGEGUIDE.md index b94a9e1..8b155c8 100644 --- a/USAGEGUIDE.md +++ b/USAGEGUIDE.md @@ -107,7 +107,8 @@ containers: --- -## [Usage examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) +## Usage examples +Find usage [examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) on 1Password developer documentation. --- From 7c84f9d3a4ae1f2ee2864b4b74c4f6ffb35b840d Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 24 Jun 2025 14:18:52 -0500 Subject: [PATCH 44/44] Remove prerequisites section from USAGEGUIDE.md --- USAGEGUIDE.md | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/USAGEGUIDE.md b/USAGEGUIDE.md index 8b155c8..7cc205b 100644 --- a/USAGEGUIDE.md +++ b/USAGEGUIDE.md @@ -5,35 +5,27 @@ ## Table of Contents -1. [Prerequisites](#prerequisites) -2. [Configuration Options](#configuration-options) -3. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account) +1. [Configuration Options](#configuration-options) +2. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account) - [Create a Service Account](#1-create-a-service-account) - [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account) - [Deploy the Operator](#3-deploy-the-operator) -4. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect) +3. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect) - [Deploy with Helm](#1-deploy-with-helm) - [Deploy manually](#2-deploy-manually) -5. [Logging level](#logging-level) -6. [Usage examples](#usage-examples) -7. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets) -8. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) -9. [Development](#development) +4. [Logging level](#logging-level) +5. [Usage examples](#usage-examples) +6. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets) +7. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) +8. [Development](#development) ---- - -## Prerequisites - -- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/) -- [`kubectl` installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -- [`docker` installed](https://docs.docker.com/get-docker/) --- ## Configuration options There are 2 ways 1Password Operator can talk to 1Password servers: -- **Connect**: It uses the 1Password Connect API to access items in 1Password. -- **Service Account**: It uses [1Password SDK](https://developer.1password.com/docs/sdks/) and [Service Account](https://developer.1password.com/docs/service-accounts) to access items in 1Password. +- [1Password Service Accounts](https://developer.1password.com/docs/service-accounts) +- [1Password Connect](https://developer.1password.com/docs/connect/) ---