From 48ed260273417e0881ab0b77f0f6b8b488200194 Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:20:46 +0100 Subject: [PATCH 1/9] build(deps): add sqlx, squirrel, and sqlite dependencies Signed-off-by: Fabrizio Sestito --- go.mod | 16 ++++++++++++++++ go.sum | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/go.mod b/go.mod index cd00b12..4c915d2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.23.0 godebug default=go1.23 require ( + github.com/Masterminds/squirrel v1.5.4 github.com/google/uuid v1.6.0 + github.com/jmoiron/sqlx v1.4.0 github.com/nats-io/nats-server/v2 v2.10.21 github.com/nats-io/nats.go v1.37.0 github.com/onsi/ginkgo/v2 v2.20.2 @@ -19,6 +21,7 @@ require ( k8s.io/component-base v0.31.1 k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 + modernc.org/sqlite v1.33.1 sigs.k8s.io/controller-runtime v0.19.0 sigs.k8s.io/structured-merge-diff/v4 v4.4.1 ) @@ -38,6 +41,7 @@ require ( github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -60,13 +64,17 @@ require ( github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -74,12 +82,14 @@ require ( github.com/nats-io/jwt/v2 v2.5.8 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -125,6 +135,12 @@ require ( k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kms v0.31.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 45859ff..b9af4b9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -48,6 +52,8 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -85,10 +91,14 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -105,8 +115,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -126,6 +146,8 @@ github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= @@ -143,6 +165,8 @@ github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJN github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -161,6 +185,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -242,6 +267,7 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -314,6 +340,32 @@ k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUx k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= From 26b3709b652e4bc71a5055ebeba0a13f3889570b Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:42:05 +0100 Subject: [PATCH 2/9] feat(storage): implement SBOM store Signed-off-by: Fabrizio Sestito --- internal/registry/registry.go | 39 -- .../registry/sbombastic/scanresult/storage.go | 52 -- .../sbombastic/scanresult/strategy.go | 108 ---- internal/registry/storage.go | 109 ---- internal/storage/sbom_schema.go | 20 + internal/storage/sbom_store.go | 527 ++++++++++++++++++ internal/storage/sbom_store_test.go | 392 +++++++++++++ internal/storage/sbom_strategy.go | 89 +++ 8 files changed, 1028 insertions(+), 308 deletions(-) delete mode 100644 internal/registry/registry.go delete mode 100644 internal/registry/sbombastic/scanresult/storage.go delete mode 100644 internal/registry/sbombastic/scanresult/strategy.go delete mode 100644 internal/registry/storage.go create mode 100644 internal/storage/sbom_schema.go create mode 100644 internal/storage/sbom_store.go create mode 100644 internal/storage/sbom_store_test.go create mode 100644 internal/storage/sbom_strategy.go diff --git a/internal/registry/registry.go b/internal/registry/registry.go deleted file mode 100644 index f5107ae..0000000 --- a/internal/registry/registry.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package registry - -import ( - "fmt" - - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" -) - -// REST implements a RESTStorage for API services against etcd -type REST struct { - *genericregistry.Store -} - -// RESTInPeace is just a simple function that panics on error. -// Otherwise returns the given storage object. It is meant to be -// a wrapper for wardle registries. -func RESTInPeace(storage *REST, err error) *REST { - if err != nil { - err = fmt.Errorf("unable to create REST storage for a resource due to %v, will die", err) - panic(err) - } - return storage -} diff --git a/internal/registry/sbombastic/scanresult/storage.go b/internal/registry/sbombastic/scanresult/storage.go deleted file mode 100644 index 1cb04ab..0000000 --- a/internal/registry/sbombastic/scanresult/storage.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package scanresult - -import ( - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/generic" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - - "github.com/rancher/sbombastic/api/storage/v1alpha1" - "github.com/rancher/sbombastic/internal/registry" -) - -// NewREST returns a RESTStorage object that will work against API services. -func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*registry.REST, error) { - strategy := NewStrategy(scheme) - - store := &genericregistry.Store{ - NewFunc: func() runtime.Object { return &v1alpha1.ScanResult{} }, - NewListFunc: func() runtime.Object { return &v1alpha1.ScanResultList{} }, - PredicateFunc: MatchFlunder, - DefaultQualifiedResource: v1alpha1.Resource("scanresults"), - SingularQualifiedResource: v1alpha1.Resource("scanresult"), - Storage: genericregistry.DryRunnableStorage{Codec: nil, Storage: ®istry.MyStorage{}}, - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - - // TODO: define table converter that exposes more than name/creation timestamp - TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("scanresults")), - } - options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs} - if err := store.CompleteWithOptions(options); err != nil { - return nil, err - } - return ®istry.REST{Store: store}, nil -} diff --git a/internal/registry/sbombastic/scanresult/strategy.go b/internal/registry/sbombastic/scanresult/strategy.go deleted file mode 100644 index 2449091..0000000 --- a/internal/registry/sbombastic/scanresult/strategy.go +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package scanresult - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/storage" - "k8s.io/apiserver/pkg/storage/names" - - "github.com/rancher/sbombastic/api/storage/v1alpha1" -) - -// NewStrategy creates and returns a flunderStrategy instance -func NewStrategy(typer runtime.ObjectTyper) scanresultStrategy { - return scanresultStrategy{typer, names.SimpleNameGenerator} -} - -// GetAttrs returns labels.Set, fields.Set, and error in case the given runtime.Object is not a Flunder -func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { - apiserver, ok := obj.(*v1alpha1.ScanResult) - if !ok { - return nil, nil, fmt.Errorf("given object is not a Flunder") - } - return labels.Set(apiserver.ObjectMeta.Labels), SelectableFields(apiserver), nil -} - -// MatchFlunder is the filter used by the generic etcd backend to watch events -// from etcd to clients of the apiserver only interested in specific labels/fields. -func MatchFlunder(label labels.Selector, field fields.Selector) storage.SelectionPredicate { - return storage.SelectionPredicate{ - Label: label, - Field: field, - GetAttrs: GetAttrs, - } -} - -// SelectableFields returns a field set that represents the object. -func SelectableFields(obj *v1alpha1.ScanResult) fields.Set { - return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true) -} - -type scanresultStrategy struct { - runtime.ObjectTyper - names.NameGenerator -} - -func (scanresultStrategy) NamespaceScoped() bool { - return true -} - -func (scanresultStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { -} - -func (scanresultStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { -} - -func (scanresultStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - // flunder := obj.(*wardle.Flunder) - // return validation.ValidateFlunder(flunder) - - return field.ErrorList{} -} - -// WarningsOnCreate returns warnings for the creation of the given object. -func (scanresultStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { - return nil -} - -func (scanresultStrategy) AllowCreateOnUpdate() bool { - return false -} - -func (scanresultStrategy) AllowUnconditionalUpdate() bool { - return false -} - -func (scanresultStrategy) Canonicalize(obj runtime.Object) { -} - -func (scanresultStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return field.ErrorList{} -} - -// WarningsOnUpdate returns warnings for the given update. -func (scanresultStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { - return nil -} diff --git a/internal/registry/storage.go b/internal/registry/storage.go deleted file mode 100644 index d04c5ca..0000000 --- a/internal/registry/storage.go +++ /dev/null @@ -1,109 +0,0 @@ -package registry - -import ( - "context" - "errors" - "fmt" - "log" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/storage" - - "github.com/rancher/sbombastic/api/storage/v1alpha1" -) - -var _ storage.Interface = &MyStorage{} - -var store *v1alpha1.ScanResult - -// MyStorage implements the Interface and logs calls to each method. -type MyStorage struct{} - -// Versioner returns the versioner associated with the interface -func (s *MyStorage) Versioner() storage.Versioner { - log.Println("Versioner() called") - return storage.APIObjectVersioner{} -} - -// Create logs the creation of an object -func (s *MyStorage) Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error { - log.Printf("Create() called: key=%s, ttl=%d\n %v %v", key, ttl, obj, out) - // resourceVersion should not be set on create - if version, err := s.Versioner().ObjectResourceVersion(obj); err == nil && version != 0 { - msg := "resourceVersion should not be set on objects to be created" - log.Println(msg) - return errors.New(msg) - } - store = obj.(*v1alpha1.ScanResult) - store.DeepCopyInto(out.(*v1alpha1.ScanResult)) - return nil -} - -func preprocess(obj *v1alpha1.ScanResult) *v1alpha1.ScanResult { - return obj -} - -// Delete logs the deletion of an object by key -func (s *MyStorage) Delete( - ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, - validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object, -) error { - log.Printf("Delete() called: key=%s\n", key) - return nil -} - -// Watch logs the start of a watch on a key -func (s *MyStorage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { - log.Printf("Watch() called: key=%s\n", key) - return watch.NewEmptyWatch(), nil -} - -// Get logs the retrieval of an object by key -func (s *MyStorage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { - log.Printf("Get() called: key=%s opts=%v\n", key, opts) - if store == nil { - return storage.NewKeyNotFoundError(key, 0) - } - // Type assert objPtr to the specific type (e.g., *v1alpha1.ScanResult) - if result, ok := objPtr.(*v1alpha1.ScanResult); ok { - store.DeepCopyInto(result) - } else { - return fmt.Errorf("expected objPtr to be *v1alpha1.ScanResult, got %T", objPtr) - } - - return nil -} - -// GetList logs the retrieval of a list of objects -func (s *MyStorage) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { - log.Printf("GetList() called: key=%s\n", key) - return nil -} - -// GuaranteedUpdate logs the update of an object with retry logic -func (s *MyStorage) GuaranteedUpdate( - ctx context.Context, key string, destination runtime.Object, ignoreNotFound bool, - preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object, -) error { - log.Printf("GuaranteedUpdate() called: key=%s, ignoreNotFound=%v\n", key, ignoreNotFound) - return nil -} - -// Count logs the counting of entries under a key -func (s *MyStorage) Count(key string) (int64, error) { - log.Printf("Count() called: key=%s\n", key) - return 1, nil -} - -// ReadinessCheck logs the readiness check -func (s *MyStorage) ReadinessCheck() error { - log.Println("ReadinessCheck() called") - return nil -} - -// RequestWatchProgress logs a request to watch progress -func (s *MyStorage) RequestWatchProgress(ctx context.Context) error { - log.Println("RequestWatchProgress() called") - return nil -} diff --git a/internal/storage/sbom_schema.go b/internal/storage/sbom_schema.go new file mode 100644 index 0000000..8762c89 --- /dev/null +++ b/internal/storage/sbom_schema.go @@ -0,0 +1,20 @@ +package storage + +const CreateSBOMTableSQL = ` +CREATE TABLE IF NOT EXISTS sboms ( + id INTEGER PRIMARY KEY, + name VARCHAR(253) NOT NULL, + namespace VARCHAR(253) NOT NULL, + object TEXT NOT NULL, + UNIQUE(name, namespace) +); +` + +// sbomSchema is the schema for the sbom table +// Note: the struct fields must be exported in order to work. +type sbomSchema struct { + ID int `db:"id"` + Name string `db:"name"` + Namespace string `db:"namespace"` + Object []byte `db:"object"` +} diff --git a/internal/storage/sbom_store.go b/internal/storage/sbom_store.go new file mode 100644 index 0000000..283e750 --- /dev/null +++ b/internal/storage/sbom_store.go @@ -0,0 +1,527 @@ +//nolint:wrapcheck // We want to return the errors from k8s.io/apiserver/pkg/storage as they are. +package storage + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/storage" + + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + + "github.com/rancher/sbombastic/api/storage/v1alpha1" +) + +// NewSBOMStore returns a store registry that will work against API services. +func NewSBOMStore(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, db *sqlx.DB) (*genericregistry.Store, error) { + strategy := NewStrategy(scheme) + + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &v1alpha1.SBOM{} }, + NewListFunc: func() runtime.Object { return &v1alpha1.SBOMList{} }, + PredicateFunc: MatchSBOM, + DefaultQualifiedResource: v1alpha1.Resource("sboms"), + SingularQualifiedResource: v1alpha1.Resource("sbom"), + Storage: genericregistry.DryRunnableStorage{ + Storage: &sbomStore{ + broadcaster: watch.NewBroadcaster(1000, watch.WaitIfChannelFull), + db: db, + }, + }, + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + + // TODO: define table converter that exposes more than name/creation timestamp + TableConvertor: rest.NewDefaultTableConvertor(v1alpha1.Resource("sboms")), + } + + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs} + if err := store.CompleteWithOptions(options); err != nil { + return nil, fmt.Errorf("unable to complete store with options: %w", err) + } + + return store, nil +} + +var _ storage.Interface = &sbomStore{} + +type sbomStore struct { + broadcaster *watch.Broadcaster + db *sqlx.DB +} + +// Returns Versioner associated with this interface. +func (s *sbomStore) Versioner() storage.Versioner { + return storage.APIObjectVersioner{} +} + +// Create adds a new object at a key unless it already exists. 'ttl' is time-to-live +// in seconds (0 means forever). If no error is returned and out is not nil, out will be +// set to the read value from database. +func (s *sbomStore) Create(ctx context.Context, key string, obj, out runtime.Object, _ uint64) error { + name, namespace := extractNameAndNamespace(key) + if name == "" || namespace == "" { + return storage.NewInternalErrorf("invalid key: %s", key) + } + + sbom, ok := obj.(*v1alpha1.SBOM) + if !ok { + return storage.NewInvalidObjError(key, fmt.Sprintf("unexpected object type: %T", obj)) + } + + if err := s.Versioner().UpdateObject(obj, 1); err != nil { + return storage.NewInternalError(err.Error()) + } + + bytes, err := json.Marshal(sbom) + if err != nil { + return storage.NewInternalError(err.Error()) + } + + query, args, err := sq.Insert("sboms"). + Columns("name", "namespace", "object"). + Values(name, namespace, bytes). + ToSql() + if err != nil { + return storage.NewInternalError(err.Error()) + } + + _, err = s.db.ExecContext(ctx, query, args...) + if err != nil { + return storage.NewInternalError(err.Error()) + } + + outSBOM, ok := out.(*v1alpha1.SBOM) + if !ok { + return storage.NewInvalidObjError(key, fmt.Sprintf("unexpected out object type: %T", out)) + } + + *outSBOM = *sbom + + if err := s.broadcaster.Action(watch.Added, sbom); err != nil { + return storage.NewInternalError(err.Error()) + } + + return nil +} + +// Delete removes the specified key and returns the value that existed at that spot. +// If key didn't exist, it will return NotFound storage error. +// If 'cachedExistingObject' is non-nil, it can be used as a suggestion about the +// current version of the object to avoid read operation from storage to get it. +// However, the implementations have to retry in case suggestion is stale. +func (s *sbomStore) Delete( + ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, + validateDeletion storage.ValidateObjectFunc, _ runtime.Object, +) error { + name, namespace := extractNameAndNamespace(key) + if name == "" || namespace == "" { + return storage.NewInternalErrorf("invalid key: %s", key) + } + + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return storage.NewInternalError(err.Error()) + } + defer func() { + if err := tx.Rollback(); !errors.Is(err, sql.ErrTxDone) { + log.Printf("failed to rollback transaction: %v", err) + } + }() + + query, args, err := sq.Delete("sboms"). + Where(sq.Eq{"name": name, "namespace": namespace}). + Suffix("RETURNING *"). + ToSql() + if err != nil { + return storage.NewInternalError(err.Error()) + } + + sbomRecord := &sbomSchema{} + if err := tx.GetContext(ctx, sbomRecord, query, args...); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return storage.NewKeyNotFoundError(key, 0) + } + return storage.NewInternalError(err.Error()) + } + + if err := json.Unmarshal(sbomRecord.Object, out); err != nil { + return storage.NewInternalError(err.Error()) + } + + if err := preconditions.Check(key, out); err != nil { + return err + } + + if err := validateDeletion(ctx, out); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return storage.NewInternalError(err.Error()) + } + + if err := s.broadcaster.Action(watch.Deleted, out); err != nil { + return storage.NewInternalError(err.Error()) + } + + return nil +} + +// Watch begins watching the specified key. Events are decoded into API objects, +// and any items selected by the options in 'opts' are sent down to returned watch.Interface. +// resourceVersion may be used to specify what version to begin watching, +// which should be the current resourceVersion, and no longer rv+1 +// (e.g. reconnecting without missing any updates). +// If resource version is "0", this interface will get current object at given key +// and send it in an "ADDED" event, before watch starts. +func (s *sbomStore) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { + if opts.ResourceVersion == "" { + return s.broadcaster.Watch() + } + + if opts.ResourceVersion == "0" { + sbom := &v1alpha1.SBOM{} + if err := s.Get(ctx, key, storage.GetOptions{}, sbom); err != nil { + return nil, err + } + + return s.broadcaster.WatchWithPrefix([]watch.Event{{Type: watch.Added, Object: sbom}}) + } + + sbomList := &v1alpha1.SBOMList{} + if err := s.GetList(ctx, key, opts, sbomList); err != nil { + return nil, err + } + var events []watch.Event + for _, item := range sbomList.Items { + events = append(events, watch.Event{Type: watch.Added, Object: &item}) + } + + return s.broadcaster.WatchWithPrefix(events) +} + +// Get unmarshals object found at key into objPtr. On a not found error, will either +// return a zero object of the requested type, or an error, depending on 'opts.ignoreNotFound'. +// Treats empty responses and nil response nodes exactly like a not found error. +// The returned contents may be delayed, but it is guaranteed that they will +// match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. +func (s *sbomStore) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { + name, namespace := extractNameAndNamespace(key) + if name == "" || namespace == "" { + return storage.NewInternalErrorf("invalid key: %s", key) + } + + query, args, err := sq.Select("*"). + From("sboms"). + Where(sq.Eq{"name": name, "namespace": namespace}). + ToSql() + if err != nil { + return storage.NewInternalError(err.Error()) + } + + out, ok := objPtr.(*v1alpha1.SBOM) + if !ok { + return storage.NewInvalidObjError(key, fmt.Sprintf("unexpected out object type: %T", out)) + } + + sbomRecord := &sbomSchema{} + if err := s.db.GetContext(ctx, sbomRecord, query, args...); err != nil { + if errors.Is(err, sql.ErrNoRows) { + if opts.IgnoreNotFound { + *out = v1alpha1.SBOM{} + + return nil + } + + return storage.NewKeyNotFoundError(key, 0) + } + return storage.NewInternalError(err.Error()) + } + + err = json.Unmarshal(sbomRecord.Object, out) + if err != nil { + return storage.NewInternalError(err.Error()) + } + + return nil +} + +// GetList unmarshalls objects found at key into a *List api object (an object +// that satisfies runtime.IsList definition). +// The returned contents may be delayed, but it is guaranteed that they will +// match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. +func (s *sbomStore) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + sbomList, ok := listObj.(*v1alpha1.SBOMList) + if !ok { + return storage.NewInvalidObjError(key, fmt.Sprintf("unexpected out object type: %T", listObj)) + } + + queryBuilder := sq.Select("*").From("sboms") + namespace := extractNamespace(key) + if namespace != "" { + queryBuilder = queryBuilder.Where(sq.Eq{"namespace": namespace}) + } + query, args, err := queryBuilder.ToSql() + if err != nil { + return storage.NewInternalError(err.Error()) + } + + var sbomRecords []sbomSchema + if err := s.db.SelectContext(ctx, &sbomRecords, query, args...); err != nil { + return storage.NewInternalError(err.Error()) + } + + for _, sbomRecord := range sbomRecords { + sbom := &v1alpha1.SBOM{} + if err := json.Unmarshal(sbomRecord.Object, sbom); err != nil { + return storage.NewInternalError(err.Error()) + } + + if opts.Predicate.Label != nil { + if !opts.Predicate.Label.Matches(labels.Set(sbom.GetLabels())) { + continue + } + } + sbomList.Items = append(sbomList.Items, *sbom) + } + + return nil +} + +// GuaranteedUpdate keeps calling 'tryUpdate()' to update key 'key' (of type 'destination') +// retrying the update until success if there is index conflict. +// Note that object passed to tryUpdate may change across invocations of tryUpdate() if +// other writers are simultaneously updating it, so tryUpdate() needs to take into account +// the current contents of the object when deciding how the update object should look. +// If the key doesn't exist, it will return NotFound storage error if ignoreNotFound=false +// else `destination` will be set to the zero value of it's type. +// If the eventual successful invocation of `tryUpdate` returns an output with the same serialized +// contents as the input, it won't perform any update, but instead set `destination` to an object with those +// contents. +// If 'cachedExistingObject' is non-nil, it can be used as a suggestion about the +// current version of the object to avoid read operation from storage to get it. +// However, the implementations have to retry in case suggestion is stale. +// +// Example: +// +// s := /* implementation of Interface */ +// err := s.GuaranteedUpdate( +// +// "myKey", &MyType{}, true, preconditions, +// func(input runtime.Object, res ResponseMeta) (runtime.Object, *uint64, error) { +// // Before each invocation of the user defined function, "input" is reset to +// // current contents for "myKey" in database. +// curr := input.(*MyType) // Guaranteed to succeed. +// +// // Make the modification +// curr.Counter++ +// +// // Return the modified object - return an error to stop iterating. Return +// // a uint64 to alter the TTL on the object, or nil to keep it the same value. +// return cur, nil, nil +// }, cachedExistingObject +// +// ) +// +//nolint:gocognit,funlen // This functions can't be easily split into smaller parts. +func (s *sbomStore) GuaranteedUpdate( + ctx context.Context, + key string, + destination runtime.Object, + ignoreNotFound bool, + preconditions *storage.Preconditions, + tryUpdate storage.UpdateFunc, + _ runtime.Object, +) error { + out, ok := destination.(*v1alpha1.SBOM) + if !ok { + return storage.NewInvalidObjError(key, fmt.Sprintf("unexpected out object type: %T", destination)) + } + + name, namespace := extractNameAndNamespace(key) + if name == "" || namespace == "" { + return storage.NewInternalErrorf("invalid key: %s", key) + } + + for { + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return err + } + + defer func() { + if err := tx.Rollback(); !errors.Is(err, sql.ErrTxDone) { + log.Printf("failed to rollback transaction: %v", err) + } + }() + + query, args, err := sq.Select("*"). + From("sboms"). + Where(sq.Eq{"name": name, "namespace": namespace}). + ToSql() + if err != nil { + return storage.NewInternalError(err.Error()) + } + + sbomRecord := &sbomSchema{} + err = tx.GetContext(ctx, sbomRecord, query, args...) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + if !ignoreNotFound { + return storage.NewKeyNotFoundError(key, 0) + } + + *out = v1alpha1.SBOM{} + + return nil + } + return err + } + + currentSBOM := &v1alpha1.SBOM{} + err = json.Unmarshal(sbomRecord.Object, currentSBOM) + if err != nil { + return storage.NewInternalError(err.Error()) + } + + err = preconditions.Check(key, currentSBOM) + if err != nil { + return err + } + + updatedSBOM, _, err := tryUpdate(currentSBOM, storage.ResponseMeta{}) + if err != nil { + continue + } + + version, err := s.Versioner().ObjectResourceVersion(currentSBOM) + if err != nil { + return storage.NewInternalError(err.Error()) + } + if err := s.Versioner().UpdateObject(updatedSBOM, version+1); err != nil { + return storage.NewInternalError(err.Error()) + } + + bytes, err := json.Marshal(updatedSBOM) + if err != nil { + return storage.NewInternalError(err.Error()) + } + + query, args, err = sq.Update("sboms"). + Set("object", bytes). + Where(sq.Eq{"name": name, "namespace": namespace}). + ToSql() + if err != nil { + return storage.NewInternalError(err.Error()) + } + + _, err = tx.ExecContext(ctx, query, args...) + if err != nil { + return storage.NewInternalError(err.Error()) + } + + sbom, ok := updatedSBOM.(*v1alpha1.SBOM) + if !ok { + return storage.NewInvalidObjError(key, fmt.Sprintf("unexpected updated object type: %T", updatedSBOM)) + } + *out = *sbom + + if err := tx.Commit(); err != nil { + return storage.NewInternalError(err.Error()) + } + + if err := s.broadcaster.Action(watch.Modified, updatedSBOM); err != nil { + return storage.NewInternalError(err.Error()) + } + + break + } + + return nil +} + +// Count returns number of different entries under the key (generally being path prefix). +func (s *sbomStore) Count(key string) (int64, error) { + namespace := extractNamespace(key) + + queryBuilder := sq.Select("COUNT(*)").From("sboms") + if namespace != "" { + queryBuilder = queryBuilder.Where(sq.Eq{"namespace": namespace}) + } + + query, args, err := queryBuilder.ToSql() + if err != nil { + return 0, storage.NewInternalError(err.Error()) + } + + var count int64 + if err := s.db.Get(&count, query, args...); err != nil { + return 0, storage.NewInternalError(err.Error()) + } + + return count, nil +} + +// ReadinessCheck checks if the storage is ready for accepting requests. +func (s *sbomStore) ReadinessCheck() error { + return nil +} + +// RequestWatchProgress requests the a watch stream progress status be sent in the +// watch response stream as soon as possible. +// Used for monitor watch progress even if watching resources with no changes. +// +// If watch is lagging, progress status might: +// * be pointing to stale resource version. Use etcd KV request to get linearizable resource version. +// * not be delivered at all. It's recommended to poll request progress periodically. +// +// Note: Only watches with matching context grpc metadata will be notified. +// https://github.com/kubernetes/kubernetes/blob/9325a57125e8502941d1b0c7379c4bb80a678d5c/vendor/go.etcd.io/etcd/client/v3/watch.go#L1037-L1042 +// +// TODO: Remove when storage.Interface will be separate from etc3.store. +// Deprecated: Added temporarily to simplify exposing RequestProgress for watch cache. +func (s *sbomStore) RequestWatchProgress(_ context.Context) error { + // As this is a deprecated method, we are not implementing it. + return nil +} + +// extractNameAndNamespace extracts the name and namespace from the key. +// Used for single object operations. +// Key format: /storage.sbombastic.rancher.io/// +func extractNameAndNamespace(key string) (string, string) { + key = strings.TrimPrefix(key, "/") + parts := strings.Split(key, "/") + if len(parts) == 4 { + return parts[3], parts[2] + } + + return "", "" +} + +// extractNamespace extracts the namespace from the key. +// Used for list operations. +// Key format: /storage.sbombastic.rancher.io// +func extractNamespace(key string) string { + key = strings.TrimPrefix(key, "/") + parts := strings.Split(key, "/") + + if len(parts) == 3 { + return parts[2] + } + + return "" +} diff --git a/internal/storage/sbom_store_test.go b/internal/storage/sbom_store_test.go new file mode 100644 index 0000000..d84f36d --- /dev/null +++ b/internal/storage/sbom_store_test.go @@ -0,0 +1,392 @@ +package storage + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/storage" + "k8s.io/utils/ptr" + + "github.com/jmoiron/sqlx" + _ "modernc.org/sqlite" + + "github.com/rancher/sbombastic/api/storage/v1alpha1" +) + +const keyPrefix = "/storage.sbombastic.rancher.io/sboms" + +type sbomStoreTestSuite struct { + suite.Suite + store *sbomStore + db *sqlx.DB + broadcaster *watch.Broadcaster +} + +func (suite *sbomStoreTestSuite) SetupTest() { + suite.db = sqlx.MustConnect("sqlite", ":memory:") + + suite.db.MustExec(CreateSBOMTableSQL) + + suite.broadcaster = watch.NewBroadcaster(1000, watch.WaitIfChannelFull) + suite.store = &sbomStore{ + broadcaster: suite.broadcaster, + db: suite.db, + } +} + +func (suite *sbomStoreTestSuite) TearDownTest() { + suite.db.Close() + suite.broadcaster.Shutdown() +} + +func TestSBOMStoreTestSuite(t *testing.T) { + suite.Run(t, &sbomStoreTestSuite{}) +} + +func (suite *sbomStoreTestSuite) TestCreate() { + sbom := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + key := keyPrefix + "/default/test" + out := &v1alpha1.SBOM{} + err := suite.store.Create(context.Background(), key, sbom, out, 0) + suite.Require().NoError(err) + + suite.EqualValues(sbom, out) + suite.Equal("1", out.ResourceVersion) +} + +func (suite *sbomStoreTestSuite) TestDelete() { + sbom := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + key := keyPrefix + "/default/test" + + tests := []struct { + name string + preconditions *storage.Preconditions + validateDeletion storage.ValidateObjectFunc + expectedError error + }{ + { + name: "happy path", + preconditions: &storage.Preconditions{}, + validateDeletion: func(_ context.Context, _ runtime.Object) error { + return nil + }, + expectedError: nil, + }, + { + name: "deletion fails with incorrect UID precondition", + preconditions: &storage.Preconditions{UID: ptr.To(types.UID("incorrect-uid"))}, + validateDeletion: func(_ context.Context, _ runtime.Object) error { + return nil + }, + expectedError: storage.NewInvalidObjError(key, "Precondition failed: UID in precondition: incorrect-uid, UID in object meta: "), + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + err := suite.store.Create(context.Background(), key, sbom, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + out := &v1alpha1.SBOM{} + err = suite.store.Delete(context.Background(), key, out, test.preconditions, test.validateDeletion, nil) + + if test.expectedError != nil { + suite.Require().Error(err) + suite.Equal(test.expectedError.Error(), err.Error()) + } else { + suite.Require().NoError(err) + suite.Equal(sbom, out) + + err = suite.store.Get(context.Background(), key, storage.GetOptions{}, &v1alpha1.SBOM{}) + suite.True(storage.IsNotFound(err)) + } + }) + } +} + +func (suite *sbomStoreTestSuite) TestWatchEmptyResourceVersion() { + key := keyPrefix + "/default/test" + opts := storage.ListOptions{ResourceVersion: ""} + + watcher, err := suite.store.Watch(context.Background(), key, opts) + suite.Require().NoError(err) + + suite.broadcaster.Shutdown() + + events := collectEvents(watcher) + suite.Require().Empty(events) +} + +func (suite *sbomStoreTestSuite) TestWatchResourceVersionZero() { + key := keyPrefix + "/default/test" + sbom := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + err := suite.store.Create(context.Background(), key, sbom, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + opts := storage.ListOptions{ResourceVersion: "0"} + + watcher, err := suite.store.Watch(context.Background(), key, opts) + suite.Require().NoError(err) + + validateDeletion := func(_ context.Context, _ runtime.Object) error { + return nil + } + err = suite.store.Delete(context.Background(), key, &v1alpha1.SBOM{}, &storage.Preconditions{}, validateDeletion, nil) + suite.Require().NoError(err) + + suite.broadcaster.Shutdown() + + events := collectEvents(watcher) + suite.Require().Len(events, 2) + suite.Equal(watch.Added, events[0].Type) + suite.Equal(sbom, events[0].Object) + suite.Equal(watch.Deleted, events[1].Type) + suite.Equal(sbom, events[1].Object) +} + +func (suite *sbomStoreTestSuite) TestWatchSpecificResourceVersion() { + key := keyPrefix + "/default" + sbom := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + suite.Require().NoError(suite.store.Create(context.Background(), key+"/test", sbom, &v1alpha1.SBOM{}, 0)) + + opts := storage.ListOptions{ResourceVersion: "1"} + + watcher, err := suite.store.Watch(context.Background(), key, opts) + suite.Require().NoError(err) + + tryUpdate := func(input runtime.Object, _ storage.ResponseMeta) (runtime.Object, *uint64, error) { + return input, ptr.To(uint64(0)), nil + } + updatedSBOM := &v1alpha1.SBOM{} + err = suite.store.GuaranteedUpdate(context.Background(), key+"/test", updatedSBOM, false, &storage.Preconditions{}, tryUpdate, nil) + suite.Require().NoError(err) + + suite.broadcaster.Shutdown() + + events := collectEvents(watcher) + suite.Require().Len(events, 2) + suite.Equal(watch.Added, events[0].Type) + suite.Equal(sbom, events[0].Object) + suite.Equal(watch.Modified, events[1].Type) + suite.Equal(updatedSBOM, events[1].Object) +} + +func (suite *sbomStoreTestSuite) TestWatchWithLabelSelector() { + key := keyPrefix + "/default" + sbom1 := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: "default", + Labels: map[string]string{ + "sbombastic.rancher.io/test": "true", + }, + }, + } + err := suite.store.Create(context.Background(), key+"/test1", sbom1, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + sbom2 := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2", + Namespace: "default", + Labels: map[string]string{}, + }, + } + err = suite.store.Create(context.Background(), key+"/test2", sbom2, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + opts := storage.ListOptions{ + ResourceVersion: "1", + Predicate: storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{ + "sbombastic.rancher.io/test": "true", + }), + }, + } + watcher, err := suite.store.Watch(context.Background(), key, opts) + suite.Require().NoError(err) + + suite.broadcaster.Shutdown() + + events := collectEvents(watcher) + suite.Require().Len(events, 1) + suite.Equal(watch.Added, events[0].Type) + suite.Equal(sbom1, events[0].Object) +} + +// collectEvents reads events from the watcher and returns them in a slice. +func collectEvents(watcher watch.Interface) []watch.Event { + var events []watch.Event + for event := range watcher.ResultChan() { + events = append(events, event) + } + return events +} + +func (suite *sbomStoreTestSuite) TestGuaranteedUpdate() { + sbom := &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: "test-uid", + }, + Spec: v1alpha1.SBOMSpec{ + Data: runtime.RawExtension{ + Raw: []byte("{}"), + }, + }, + } + err := suite.store.Create(context.Background(), keyPrefix+"/default/test", sbom, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + tests := []struct { + name string + key string + ignoreNotFound bool + preconditions *storage.Preconditions + expectedUpdatedSBOM *v1alpha1.SBOM + expectedError error + }{ + { + name: "happy path", + key: keyPrefix + "/default/test", + preconditions: &storage.Preconditions{}, + expectedUpdatedSBOM: &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: "test-uid", + ResourceVersion: "2", + }, + Spec: v1alpha1.SBOMSpec{ + Data: runtime.RawExtension{ + Raw: []byte(`{"foo": "bar"}`), + }, + }, + }, + }, + { + name: "preconditions failed", + key: keyPrefix + "/default/test", + preconditions: &storage.Preconditions{ + UID: ptr.To(types.UID("incorrect-uid")), + }, + + expectedError: storage.NewInvalidObjError(keyPrefix+"/default/test", + "Precondition failed: UID in precondition: incorrect-uid, UID in object meta: test-uid"), + }, + { + name: "not found", + key: keyPrefix + "/default/notfound", + preconditions: &storage.Preconditions{}, + expectedError: storage.NewKeyNotFoundError(keyPrefix+"/default/notfound", 0), + }, + { + name: "not found with ignore not found", + key: keyPrefix + "/default/notfound", + preconditions: &storage.Preconditions{}, + ignoreNotFound: true, + expectedUpdatedSBOM: &v1alpha1.SBOM{}, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + tryUpdate := func(input runtime.Object, _ storage.ResponseMeta) (runtime.Object, *uint64, error) { + input.(*v1alpha1.SBOM).Spec.Data.Raw = []byte(`{"foo": "bar"}`) + + return input, ptr.To(uint64(0)), nil + } + updatedSBOM := &v1alpha1.SBOM{} + err := suite.store.GuaranteedUpdate(context.Background(), test.key, updatedSBOM, test.ignoreNotFound, test.preconditions, tryUpdate, nil) + + if test.expectedError != nil { + suite.Require().Error(err) + suite.Require().Equal(test.expectedError.Error(), err.Error()) + } else { + suite.Require().NoError(err) + suite.Require().Equal(test.expectedUpdatedSBOM, updatedSBOM) + } + }) + } +} + +func (suite *sbomStoreTestSuite) TestCount() { + err := suite.store.Create(context.Background(), keyPrefix+"/default/test1", &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: "default", + }, + }, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + err = suite.store.Create(context.Background(), keyPrefix+"/default/test2", &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2", + Namespace: "default", + }, + }, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + err = suite.store.Create(context.Background(), keyPrefix+"/other/test4", &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test4", + Namespace: "other", + }, + }, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + + tests := []struct { + name string + key string + expectedCount int64 + }{ + { + name: "count entries in default namespace", + key: keyPrefix + "/default", + expectedCount: 2, + }, + { + name: "count all entries", + key: keyPrefix, + expectedCount: 3, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + count, err := suite.store.Count(test.key) + suite.Require().NoError(err) + suite.Require().Equal(test.expectedCount, count) + }) + } +} diff --git a/internal/storage/sbom_strategy.go b/internal/storage/sbom_strategy.go new file mode 100644 index 0000000..b2de6e6 --- /dev/null +++ b/internal/storage/sbom_strategy.go @@ -0,0 +1,89 @@ +package storage + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/storage" + "k8s.io/apiserver/pkg/storage/names" + + "github.com/rancher/sbombastic/api/storage/v1alpha1" +) + +// NewStrategy creates and returns a sbomStrategy instance +func NewStrategy(typer runtime.ObjectTyper) sbomStrategy { //nolint:revive // Unexported return is fine here + return sbomStrategy{typer, names.SimpleNameGenerator} +} + +// GetAttrs returns labels.Set, fields.Set, and error in case the given runtime.Object is not a SBOM +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + apiserver, ok := obj.(*v1alpha1.SBOM) + if !ok { + return nil, nil, fmt.Errorf("given object is not a SBOM, got: %T", obj) + } + return labels.Set(apiserver.ObjectMeta.Labels), SelectableFields(apiserver), nil +} + +// MatchSBOM is the filter used by the generic etcd backend to watch events +// from etcd to clients of the apiserver only interested in specific labels/fields. +func MatchSBOM(label labels.Selector, field fields.Selector) storage.SelectionPredicate { + return storage.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: GetAttrs, + } +} + +// SelectableFields returns a field set that represents the object. +func SelectableFields(obj *v1alpha1.SBOM) fields.Set { + return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true) +} + +type sbomStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +func (sbomStrategy) NamespaceScoped() bool { + return true +} + +func (sbomStrategy) PrepareForCreate(_ context.Context, _ runtime.Object) { +} + +func (sbomStrategy) PrepareForUpdate(_ context.Context, _, _ runtime.Object) { +} + +func (sbomStrategy) Validate(_ context.Context, _ runtime.Object) field.ErrorList { + return field.ErrorList{} +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (sbomStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string { + return nil +} + +func (sbomStrategy) AllowCreateOnUpdate() bool { + return false +} + +func (sbomStrategy) AllowUnconditionalUpdate() bool { + return false +} + +func (sbomStrategy) Canonicalize(_ runtime.Object) { +} + +func (sbomStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList { + return field.ErrorList{} +} + +// WarningsOnUpdate returns warnings for the given update. +func (sbomStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string { + return nil +} From e708ea62e5394a80c02fe68ee1738a34b3a8ca1e Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:42:47 +0100 Subject: [PATCH 3/9] fix(storage): fix wrong SBOM list type Signed-off-by: Fabrizio Sestito --- api/storage/install/install.go | 2 + api/storage/register.go | 39 +++++++++++++++++++ api/storage/v1alpha1/sbom_types.go | 2 +- api/storage/v1alpha1/zz_generated.deepcopy.go | 2 +- pkg/generated/openapi/zz_generated.openapi.go | 4 +- 5 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 api/storage/register.go diff --git a/api/storage/install/install.go b/api/storage/install/install.go index faef96f..602bb3e 100644 --- a/api/storage/install/install.go +++ b/api/storage/install/install.go @@ -17,6 +17,7 @@ limitations under the License. package install import ( + "github.com/rancher/sbombastic/api/storage" "github.com/rancher/sbombastic/api/storage/v1alpha1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -24,6 +25,7 @@ import ( // Install registers the API group and adds types to a scheme func Install(scheme *runtime.Scheme) { + utilruntime.Must(storage.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) utilruntime.Must(scheme.SetVersionPriority(v1alpha1.SchemeGroupVersion)) } diff --git a/api/storage/register.go b/api/storage/register.go new file mode 100644 index 0000000..87bbcda --- /dev/null +++ b/api/storage/register.go @@ -0,0 +1,39 @@ +package storage + +import ( + "github.com/rancher/sbombastic/api/storage/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name used in this package +const GroupName = "storage.sbombastic.rancher.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns back a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder is the scheme builder with scheme init functions to run for this API package + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a common registration function for mapping packaged scoped group & version keys to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &v1alpha1.SBOM{}, + &v1alpha1.SBOMList{}, + ) + return nil +} diff --git a/api/storage/v1alpha1/sbom_types.go b/api/storage/v1alpha1/sbom_types.go index fd4054b..b63ea5b 100644 --- a/api/storage/v1alpha1/sbom_types.go +++ b/api/storage/v1alpha1/sbom_types.go @@ -27,7 +27,7 @@ import ( type SBOMList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []ScanResult `json:"items"` + Items []SBOM `json:"items"` } // +genclient diff --git a/api/storage/v1alpha1/zz_generated.deepcopy.go b/api/storage/v1alpha1/zz_generated.deepcopy.go index 076431c..58d622c 100644 --- a/api/storage/v1alpha1/zz_generated.deepcopy.go +++ b/api/storage/v1alpha1/zz_generated.deepcopy.go @@ -59,7 +59,7 @@ func (in *SBOMList) DeepCopyInto(out *SBOMList) { in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]ScanResult, len(*in)) + *out = make([]SBOM, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index dd3bcdd..4ffd55c 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -173,7 +173,7 @@ func schema_sbombastic_api_storage_v1alpha1_SBOMList(ref common.ReferenceCallbac Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/rancher/sbombastic/api/storage/v1alpha1.ScanResult"), + Ref: ref("github.com/rancher/sbombastic/api/storage/v1alpha1.SBOM"), }, }, }, @@ -184,7 +184,7 @@ func schema_sbombastic_api_storage_v1alpha1_SBOMList(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/rancher/sbombastic/api/storage/v1alpha1.ScanResult", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/rancher/sbombastic/api/storage/v1alpha1.SBOM", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } From 8ff2145be222995d3ed0b94ba9712069b83bb4fc Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:43:34 +0100 Subject: [PATCH 4/9] feat(storage): database setup Signed-off-by: Fabrizio Sestito --- cmd/storage/main.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/storage/main.go b/cmd/storage/main.go index 980acc5..e254db1 100644 --- a/cmd/storage/main.go +++ b/cmd/storage/main.go @@ -17,16 +17,28 @@ limitations under the License. package main import ( + "log" "os" + "github.com/jmoiron/sqlx" "github.com/rancher/sbombastic/cmd/storage/server" + "github.com/rancher/sbombastic/internal/storage" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/component-base/cli" + + _ "modernc.org/sqlite" ) func main() { + db, err := sqlx.Connect("sqlite", "storage.db") + if err != nil { + log.Fatalln(err) + } + + db.MustExec(storage.CreateSBOMTableSQL) + ctx := genericapiserver.SetupSignalContext() - options := server.NewWardleServerOptions(os.Stdout, os.Stderr) + options := server.NewWardleServerOptions(os.Stdout, os.Stderr, db) cmd := server.NewCommandStartWardleServer(ctx, options) code := cli.Run(cmd) os.Exit(code) From d514be61a6046531f189c99fe591d109c4c608ac Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:43:57 +0100 Subject: [PATCH 5/9] feat(storage): use SBOM storage in apiserver Signed-off-by: Fabrizio Sestito --- cmd/storage/server/start.go | 9 +++++++-- internal/apiserver/apiserver.go | 14 ++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/cmd/storage/server/start.go b/cmd/storage/server/start.go index b199f5b..7a5c1e8 100644 --- a/cmd/storage/server/start.go +++ b/cmd/storage/server/start.go @@ -22,6 +22,7 @@ import ( "io" "net" + "github.com/jmoiron/sqlx" "github.com/spf13/cobra" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -54,6 +55,8 @@ type WardleServerOptions struct { StdOut io.Writer StdErr io.Writer + DB *sqlx.DB + AlternateDNS []string } @@ -72,13 +75,15 @@ func WardleVersionToKubeVersion(ver *version.Version) *version.Version { } // NewWardleServerOptions returns a new WardleServerOptions -func NewWardleServerOptions(out, errOut io.Writer) *WardleServerOptions { +func NewWardleServerOptions(out, errOut io.Writer, db *sqlx.DB) *WardleServerOptions { o := &WardleServerOptions{ RecommendedOptions: genericoptions.NewRecommendedOptions( "/registry/sbombastic.rancher.io", apiserver.Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion), ), + DB: db, + StdOut: out, StdErr: errOut, } @@ -229,7 +234,7 @@ func (o WardleServerOptions) RunWardleServer(ctx context.Context) error { return err } - server, err := config.Complete().New() + server, err := config.Complete().New(o.DB) if err != nil { return err } diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index 77fcf27..a077972 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -24,10 +24,10 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" + "github.com/jmoiron/sqlx" "github.com/rancher/sbombastic/api/storage/install" "github.com/rancher/sbombastic/api/storage/v1alpha1" - "github.com/rancher/sbombastic/internal/registry" - "github.com/rancher/sbombastic/internal/registry/sbombastic/scanresult" + "github.com/rancher/sbombastic/internal/storage" ) var ( @@ -95,7 +95,7 @@ func (cfg *Config) Complete() CompletedConfig { } // New returns a new instance of WardleServer from the given config. -func (c completedConfig) New() (*WardleServer, error) { +func (c completedConfig) New(db *sqlx.DB) (*WardleServer, error) { genericServer, err := c.GenericConfig.New("sample-apiserver", genericapiserver.NewEmptyDelegate()) if err != nil { return nil, err @@ -108,7 +108,13 @@ func (c completedConfig) New() (*WardleServer, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v1alpha1.GroupName, Scheme, metav1.ParameterCodec, Codecs) v1alpha1storage := map[string]rest.Storage{} - v1alpha1storage["scanresults"] = registry.RESTInPeace(scanresult.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)) + + sbomStore, err := storage.NewSBOMStore(Scheme, c.GenericConfig.RESTOptionsGetter, db) + if err != nil { + return nil, err + } + + v1alpha1storage["sboms"] = sbomStore apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil { From 4673b512884e21f7284e28a859ec3a8910de7989 Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:44:50 +0100 Subject: [PATCH 6/9] fix(controller): fix Image and SBOM rbac Signed-off-by: Fabrizio Sestito --- helm/templates/controller/role.yaml | 35 +++++-------------------- internal/controller/image_controller.go | 6 ++--- internal/controller/sbom_controller.go | 6 ++--- 3 files changed, 12 insertions(+), 35 deletions(-) diff --git a/helm/templates/controller/role.yaml b/helm/templates/controller/role.yaml index da02386..99104a6 100644 --- a/helm/templates/controller/role.yaml +++ b/helm/templates/controller/role.yaml @@ -7,6 +7,7 @@ rules: - apiGroups: - sbombastic.rancher.io resources: + - images - registries verbs: - create @@ -19,45 +20,21 @@ rules: - apiGroups: - sbombastic.rancher.io resources: + - images/finalizers - registries/finalizers verbs: - update - apiGroups: - sbombastic.rancher.io resources: - - registries/status - verbs: - - get - - patch - - update -- apiGroups: - - sbombastic.sbombastic.rancher.io - resources: - - images - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - sbombastic.sbombastic.rancher.io - resources: - - images/finalizers - verbs: - - update -- apiGroups: - - sbombastic.sbombastic.rancher.io - resources: - images/status + - registries/status verbs: - get - patch - update - apiGroups: - - storage.sbombastic.rancher.io.sbombastic.rancher.io + - storage.sbombastic.rancher.io resources: - sboms verbs: @@ -69,13 +46,13 @@ rules: - update - watch - apiGroups: - - storage.sbombastic.rancher.io.sbombastic.rancher.io + - storage.sbombastic.rancher.io resources: - sboms/finalizers verbs: - update - apiGroups: - - storage.sbombastic.rancher.io.sbombastic.rancher.io + - storage.sbombastic.rancher.io resources: - sboms/status verbs: diff --git a/internal/controller/image_controller.go b/internal/controller/image_controller.go index fa4775a..5148f0b 100644 --- a/internal/controller/image_controller.go +++ b/internal/controller/image_controller.go @@ -38,9 +38,9 @@ type ImageReconciler struct { Publisher messaging.Publisher } -// +kubebuilder:rbac:groups=sbombastic.sbombastic.rancher.io,resources=images,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=sbombastic.sbombastic.rancher.io,resources=images/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=sbombastic.sbombastic.rancher.io,resources=images/finalizers,verbs=update +// +kubebuilder:rbac:groups=sbombastic.rancher.io,resources=images,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=sbombastic.rancher.io,resources=images/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=sbombastic.rancher.io,resources=images/finalizers,verbs=update // Reconcile reconciles an Image. // If the Image doesn't have the SBOM, it sends a create SBOM request to the workers. diff --git a/internal/controller/sbom_controller.go b/internal/controller/sbom_controller.go index 2a56894..5e68f31 100644 --- a/internal/controller/sbom_controller.go +++ b/internal/controller/sbom_controller.go @@ -39,9 +39,9 @@ type SBOMReconciler struct { Scheme *runtime.Scheme } -// +kubebuilder:rbac:groups=storage.sbombastic.rancher.io.sbombastic.rancher.io,resources=sboms,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=storage.sbombastic.rancher.io.sbombastic.rancher.io,resources=sboms/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=storage.sbombastic.rancher.io.sbombastic.rancher.io,resources=sboms/finalizers,verbs=update +// +kubebuilder:rbac:groups=storage.sbombastic.rancher.io,resources=sboms,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=storage.sbombastic.rancher.io,resources=sboms/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=storage.sbombastic.rancher.io,resources=sboms/finalizers,verbs=update // Reconcile reconciles a SBOM. // If all images have SBOMs, it updates the last discovered timestamp on the registry, since the Registry discovery is completed. From 965fd63d984ea5355f52f00cb11e525cfaf83137 Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:45:02 +0100 Subject: [PATCH 7/9] chore: rename storage in registry Signed-off-by: Fabrizio Sestito --- Tiltfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tiltfile b/Tiltfile index 24606d2..f6a34ee 100644 --- a/Tiltfile +++ b/Tiltfile @@ -68,7 +68,7 @@ local_resource( "api", "internal/admission", "internal/apiserver", - "internal/registry", + "internal/storage", "pkg" ], ) From 88ecaf91d6f30cef04a42a807dfc436d2b264f5f Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Wed, 30 Oct 2024 15:51:05 +0100 Subject: [PATCH 8/9] chore: add SBOM example Signed-off-by: Fabrizio Sestito --- examples/sbom.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 examples/sbom.yaml diff --git a/examples/sbom.yaml b/examples/sbom.yaml new file mode 100644 index 0000000..8decf6b --- /dev/null +++ b/examples/sbom.yaml @@ -0,0 +1,11 @@ +apiVersion: storage.sbombastic.rancher.io/v1alpha1 +kind: SBOM +metadata: + name: sbom-example + namespace: default + labels: + sbombastic.rancher.io/registry: registry-example + sbombastic.rancher.io/repository: test +spec: + data: + foo: bar From ab0225650f8423d7793583ee65f70f86752205d7 Mon Sep 17 00:00:00 2001 From: Fabrizio Sestito Date: Thu, 31 Oct 2024 14:04:27 +0100 Subject: [PATCH 9/9] test(storage): check that stored sbom is not modified on error (GuaranteedUpdate) Signed-off-by: Fabrizio Sestito --- internal/storage/sbom_store_test.go | 58 ++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/internal/storage/sbom_store_test.go b/internal/storage/sbom_store_test.go index d84f36d..cfc699d 100644 --- a/internal/storage/sbom_store_test.go +++ b/internal/storage/sbom_store_test.go @@ -253,36 +253,34 @@ func collectEvents(watcher watch.Interface) []watch.Event { } func (suite *sbomStoreTestSuite) TestGuaranteedUpdate() { - sbom := &v1alpha1.SBOM{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - UID: "test-uid", - }, - Spec: v1alpha1.SBOMSpec{ - Data: runtime.RawExtension{ - Raw: []byte("{}"), - }, - }, - } - err := suite.store.Create(context.Background(), keyPrefix+"/default/test", sbom, &v1alpha1.SBOM{}, 0) - suite.Require().NoError(err) - tests := []struct { name string key string ignoreNotFound bool preconditions *storage.Preconditions + sbom *v1alpha1.SBOM expectedUpdatedSBOM *v1alpha1.SBOM expectedError error }{ { name: "happy path", - key: keyPrefix + "/default/test", + key: keyPrefix + "/default/test1", preconditions: &storage.Preconditions{}, + sbom: &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: "default", + UID: "test-uid", + }, + Spec: v1alpha1.SBOMSpec{ + Data: runtime.RawExtension{ + Raw: []byte("{}"), + }, + }, + }, expectedUpdatedSBOM: &v1alpha1.SBOM{ ObjectMeta: metav1.ObjectMeta{ - Name: "test", + Name: "test1", Namespace: "default", UID: "test-uid", ResourceVersion: "2", @@ -300,7 +298,18 @@ func (suite *sbomStoreTestSuite) TestGuaranteedUpdate() { preconditions: &storage.Preconditions{ UID: ptr.To(types.UID("incorrect-uid")), }, - + sbom: &v1alpha1.SBOM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2", + Namespace: "default", + UID: "test-uid", + }, + Spec: v1alpha1.SBOMSpec{ + Data: runtime.RawExtension{ + Raw: []byte("{}"), + }, + }, + }, expectedError: storage.NewInvalidObjError(keyPrefix+"/default/test", "Precondition failed: UID in precondition: incorrect-uid, UID in object meta: test-uid"), }, @@ -321,6 +330,11 @@ func (suite *sbomStoreTestSuite) TestGuaranteedUpdate() { for _, test := range tests { suite.Run(test.name, func() { + if test.sbom != nil { + err := suite.store.Create(context.Background(), test.key, test.sbom, &v1alpha1.SBOM{}, 0) + suite.Require().NoError(err) + } + tryUpdate := func(input runtime.Object, _ storage.ResponseMeta) (runtime.Object, *uint64, error) { input.(*v1alpha1.SBOM).Spec.Data.Raw = []byte(`{"foo": "bar"}`) @@ -332,6 +346,14 @@ func (suite *sbomStoreTestSuite) TestGuaranteedUpdate() { if test.expectedError != nil { suite.Require().Error(err) suite.Require().Equal(test.expectedError.Error(), err.Error()) + + if test.sbom != nil { + // If there is an error, the original object should not be updated. + currentSBOM := &v1alpha1.SBOM{} + err := suite.store.Get(context.Background(), keyPrefix+"/default/test", storage.GetOptions{}, currentSBOM) + suite.Require().NoError(err) + suite.Equal(test.sbom, currentSBOM) + } } else { suite.Require().NoError(err) suite.Require().Equal(test.expectedUpdatedSBOM, updatedSBOM)