diff --git a/go.mod b/go.mod index 8fbc7dc..5b412f7 100644 --- a/go.mod +++ b/go.mod @@ -4,28 +4,82 @@ go 1.23.0 require ( github.com/blang/semver v3.5.1+incompatible + github.com/cloudnative-pg/cloudnative-pg v1.23.3 github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20240903080817-f919c9abfe74 github.com/onsi/ginkgo/v2 v2.20.0 github.com/onsi/gomega v1.34.1 + k8s.io/api v0.30.3 k8s.io/apimachinery v0.30.3 + sigs.k8s.io/controller-runtime v0.18.4 + ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudnative-pg/cnpg-i v0.0.0-20240806095732-9ea12e76a6ee // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // 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.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // 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 + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kubernetes-csi/external-snapshotter/client/v8 v8.0.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.2 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sys v0.23.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.24.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.65.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 + k8s.io/apiextensions-apiserver v0.30.3 // indirect + k8s.io/client-go v0.30.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 42e2662..fb43dd2 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,73 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudnative-pg/cloudnative-pg v1.23.3 h1:0wh1wIq33XfxuUIwDVOP79rVvPsLaxSnb1TIl3SRHno= +github.com/cloudnative-pg/cloudnative-pg v1.23.3/go.mod h1:ZqOJI7oelGJF6MZ+Oe4Wx9wxzXFtMqUl8M5ErFI6//Q= +github.com/cloudnative-pg/cnpg-i v0.0.0-20240806095732-9ea12e76a6ee h1:ThaqhdK95kDSpLZi1+Qr66NjdBmCVkBakccZfIyVl7Q= +github.com/cloudnative-pg/cnpg-i v0.0.0-20240806095732-9ea12e76a6ee/go.mod h1:UILpBDaWvXcYC5kY5DMaVEEQY5483CBApMuHIn0GJdg= github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20240903080817-f919c9abfe74 h1:IdlMUGIKhMUmUzNTV231s5btwNiamEe95k7U+I3CIkg= github.com/cloudnative-pg/cnpg-i-machinery v0.0.0-20240903080817-f919c9abfe74/go.mod h1:yjdup5+jTddOAYnD9EwLkRigJsn/KgCzg+GYoFsoeFk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.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/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.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.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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/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= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +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/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -24,50 +75,145 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= -github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.0.0 h1:mjQG0Vakr2h246kEDR85U8y8ZhPgT3bguTCajRa/jaw= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.0.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y= +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/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.2 h1:6UsAv+jAevuGO2yZFU/BukV4o9NKnFMOuoouSA4G0ns= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.2/go.mod h1:XYrdZw5dW12Cjkt4ndbeNZZTBp4UCHtW0ccR9+sTtPU= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +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= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 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= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= +k8s.io/apiextensions-apiserver v0.30.3 h1:oChu5li2vsZHx2IvnGP3ah8Nj3KyqG3kRSaKmijhB9U= +k8s.io/apiextensions-apiserver v0.30.3/go.mod h1:uhXxYDkMAvl6CJw4lrDN4CPbONkF3+XL9cacCT44kV4= k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= +k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= +sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/catalog/catalog.go b/pkg/catalog/catalog.go new file mode 100644 index 0000000..8b9d3ca --- /dev/null +++ b/pkg/catalog/catalog.go @@ -0,0 +1,332 @@ +/* +Copyright The CloudNativePG Contributors + +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 catalog is the implementation of a backup catalog +package catalog + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" + "strconv" + "time" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" +) + +// Catalog is a list of backup infos belonging to the same server +type Catalog struct { + // The list of backups + List []BarmanBackup `json:"backups_list"` +} + +// NewCatalogFromBarmanCloudBackupList parses the output of barman-cloud-backup-list +func NewCatalogFromBarmanCloudBackupList(rawJSON string) (*Catalog, error) { + result := &Catalog{} + err := json.Unmarshal([]byte(rawJSON), result) + if err != nil { + return nil, err + } + + for idx := range result.List { + if err := result.List[idx].deserializeBackupTimeStrings(); err != nil { + return nil, err + } + } + + // Sort the list of backups in order of time + sort.Sort(result) + + return result, nil +} + +var currentTLIRegex = regexp.MustCompile("^(|latest)$") + +// LatestBackupInfo gets the information about the latest successful backup +func (catalog *Catalog) LatestBackupInfo() *BarmanBackup { + if catalog.Len() == 0 { + return nil + } + + // the code below assumes the catalog to be sorted, therefore, we enforce it first + sort.Sort(catalog) + + // Skip errored backups and return the latest valid one + for i := len(catalog.List) - 1; i >= 0; i-- { + if catalog.List[i].isBackupDone() { + return &catalog.List[i] + } + } + + return nil +} + +// FirstRecoverabilityPoint gets the start time of the first backup in +// the catalog +func (catalog *Catalog) FirstRecoverabilityPoint() *time.Time { + if catalog.Len() == 0 { + return nil + } + + // the code below assumes the catalog to be sorted, therefore, we enforce it first + sort.Sort(catalog) + + // Skip errored backups and return the first valid one + for i := 0; i < len(catalog.List); i++ { + if !catalog.List[i].isBackupDone() { + continue + } + + return &catalog.List[i].EndTime + } + + return nil +} + +type recoveryTargetAdapter interface { + GetBackupID() string + GetTargetTime() string + GetTargetLSN() string + GetTargetTLI() string +} + +// FindBackupInfo finds the backup info that should be used to file +// a PITR request via target parameters specified within `RecoveryTarget` +func (catalog *Catalog) FindBackupInfo( + lsnFactory lsnFactory, + recoveryTarget recoveryTargetAdapter, +) (*BarmanBackup, error) { + // Check that BackupID is not empty. In such case, always use the + // backup ID provided by the user. + if recoveryTarget.GetBackupID() != "" { + return catalog.findBackupFromID(recoveryTarget.GetBackupID()) + } + + // The user has not specified any backup ID. As a result we need + // to automatically detect the backup from which to start the + // recovery process. + + // Set the timeline + targetTLI := recoveryTarget.GetTargetTLI() + + // Sort the catalog, as that's what the code below expects + sort.Sort(catalog) + + // The first step is to check any time based research + if t := recoveryTarget.GetTargetTime(); t != "" { + return catalog.findClosestBackupFromTargetTime(t, targetTLI) + } + + // The second step is to check any LSN based research + if t := recoveryTarget.GetTargetLSN(); t != "" { + return catalog.findClosestBackupFromTargetLSN(lsnFactory, t, targetTLI) + } + + // The fallback is to use the latest available backup in chronological order + return catalog.findLatestBackupFromTimeline(targetTLI), nil +} + +type LsnAdapter interface { + Parse() (int64, error) + LessAdapter(other LsnAdapter) bool +} + +type lsnFactory func(string) LsnAdapter + +func (catalog *Catalog) findClosestBackupFromTargetLSN( + lsnFactory lsnFactory, + targetLSNString string, + targetTLI string, +) (*BarmanBackup, error) { + targetLSN := lsnFactory(targetLSNString) + if _, err := targetLSN.Parse(); err != nil { + return nil, fmt.Errorf("while parsing recovery target targetLSN: %s", err.Error()) + } + for i := len(catalog.List) - 1; i >= 0; i-- { + barmanBackup := catalog.List[i] + if !barmanBackup.isBackupDone() { + continue + } + if (strconv.Itoa(barmanBackup.TimeLine) == targetTLI || + // if targetTLI is not an integer, it will be ignored actually + currentTLIRegex.MatchString(targetTLI)) && + lsnFactory(barmanBackup.BeginLSN).LessAdapter(targetLSN) { + return &catalog.List[i], nil + } + } + return nil, nil +} + +func (catalog *Catalog) findClosestBackupFromTargetTime( + targetTimeString string, + targetTLI string, +) (*BarmanBackup, error) { + targetTime, err := utils.ParseTargetTime(nil, targetTimeString) + if err != nil { + return nil, fmt.Errorf("while parsing recovery target targetTime: %s", err.Error()) + } + for i := len(catalog.List) - 1; i >= 0; i-- { + barmanBackup := catalog.List[i] + if !barmanBackup.isBackupDone() { + continue + } + if (strconv.Itoa(barmanBackup.TimeLine) == targetTLI || + // if targetTLI is not an integer, it will be ignored actually + currentTLIRegex.MatchString(targetTLI)) && + !barmanBackup.EndTime.After(targetTime) { + return &catalog.List[i], nil + } + } + return nil, nil +} + +func (catalog *Catalog) findLatestBackupFromTimeline(targetTLI string) *BarmanBackup { + for i := len(catalog.List) - 1; i >= 0; i-- { + barmanBackup := catalog.List[i] + if !barmanBackup.isBackupDone() { + continue + } + if strconv.Itoa(barmanBackup.TimeLine) == targetTLI || + // if targetTLI is not an integer, it will be ignored actually + currentTLIRegex.MatchString(targetTLI) { + return &catalog.List[i] + } + } + + return nil +} + +func (catalog *Catalog) findBackupFromID(backupID string) (*BarmanBackup, error) { + if backupID == "" { + return nil, fmt.Errorf("no backupID provided") + } + for _, barmanBackup := range catalog.List { + if !barmanBackup.isBackupDone() { + continue + } + if barmanBackup.ID == backupID { + return &barmanBackup, nil + } + } + return nil, fmt.Errorf("no backup found with ID %s", backupID) +} + +// BarmanBackup represent a backup as created +// by Barman +type BarmanBackup struct { + // The backup name, can be used as a way to identify a backup. + // Populated only if the backup was executed with barman 3.3.0+. + BackupName string `json:"backup_name,omitempty"` + + // The backup label + Label string `json:"backup_label"` + + // The moment where the backup started + BeginTimeString string `json:"begin_time"` + + // The moment where the backup ended + EndTimeString string `json:"end_time"` + + // The moment where the backup ended + BeginTime time.Time + + // The moment where the backup ended + EndTime time.Time + + // The WAL where the backup started + BeginWal string `json:"begin_wal"` + + // The WAL where the backup ended + EndWal string `json:"end_wal"` + + // The LSN where the backup started + BeginLSN string `json:"begin_xlog"` + + // The LSN where the backup ended + EndLSN string `json:"end_xlog"` + + // The systemID of the cluster + SystemID string `json:"systemid"` + + // The ID of the backup + ID string `json:"backup_id"` + + // The error output if present + Error string `json:"error"` + + // The TimeLine + TimeLine int `json:"timeline"` +} + +type barmanBackupShow struct { + Cloud BarmanBackup `json:"cloud,omitempty"` +} + +// NewBackupFromBarmanCloudBackupShow parses the output of barman-cloud-backup-show +func NewBackupFromBarmanCloudBackupShow(rawJSON string) (*BarmanBackup, error) { + result := &barmanBackupShow{} + err := json.Unmarshal([]byte(rawJSON), result) + if err != nil { + return nil, err + } + + if err := result.Cloud.deserializeBackupTimeStrings(); err != nil { + return nil, err + } + + return &result.Cloud, nil +} + +func (b *BarmanBackup) deserializeBackupTimeStrings() error { + // barmanTimeLayout is the format that is being used to parse + // the backupInfo from barman-cloud-backup-list + const ( + barmanTimeLayout = "Mon Jan 2 15:04:05 2006" + ) + + var err error + if b.BeginTimeString != "" { + b.BeginTime, err = time.Parse(barmanTimeLayout, b.BeginTimeString) + if err != nil { + return err + } + } + + if b.EndTimeString != "" { + b.EndTime, err = time.Parse(barmanTimeLayout, b.EndTimeString) + if err != nil { + return err + } + } + + return nil +} + +func (b *BarmanBackup) isBackupDone() bool { + return !b.BeginTime.IsZero() && !b.EndTime.IsZero() +} + +// NewCatalog creates a new sorted backup catalog, given a list of backup infos +// belonging to the same server. +func NewCatalog(list []BarmanBackup) *Catalog { + result := &Catalog{ + List: list, + } + sort.Sort(result) + + return result +} diff --git a/pkg/catalog/catalog_test.go b/pkg/catalog/catalog_test.go new file mode 100644 index 0000000..77f4bfc --- /dev/null +++ b/pkg/catalog/catalog_test.go @@ -0,0 +1,249 @@ +/* +Copyright The CloudNativePG Contributors + +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 catalog + +import ( + "time" + + v1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Backup catalog", func() { + catalog := NewCatalog([]BarmanBackup{ + { + ID: "202101021200", + BeginTime: time.Date(2021, 1, 2, 12, 0, 0, 0, time.UTC), + EndTime: time.Date(2021, 1, 2, 12, 30, 0, 0, time.UTC), + TimeLine: 1, + }, + { + ID: "202101011200", + BeginTime: time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC), + EndTime: time.Date(2021, 1, 1, 12, 30, 0, 0, time.UTC), + TimeLine: 1, + }, + { + ID: "202101031200", + BeginTime: time.Date(2021, 1, 3, 12, 0, 0, 0, time.UTC), + EndTime: time.Date(2021, 1, 3, 12, 30, 0, 0, time.UTC), + TimeLine: 1, + }, + }) + + It("contains sorted data", func() { + Expect(catalog.List).To(HaveLen(3)) + Expect(catalog.List[0].ID).To(Equal("202101011200")) + Expect(catalog.List[1].ID).To(Equal("202101021200")) + Expect(catalog.List[2].ID).To(Equal("202101031200")) + }) + + It("can detect the first recoverability point", func() { + Expect(*catalog.FirstRecoverabilityPoint()).To( + Equal(time.Date(2021, 1, 1, 12, 30, 0, 0, time.UTC))) + }) + + It("can get the latest backupinfo", func() { + Expect(catalog.LatestBackupInfo().ID).To(Equal("202101031200")) + }) + + It("can find the closest backup info when there is one", func() { + recoveryTarget := &v1.RecoveryTarget{TargetTime: time.Now().Format("2006-01-02 15:04:04")} + closestBackupInfo, err := catalog.FindBackupInfo(recoveryTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(closestBackupInfo.ID).To(Equal("202101031200")) + + recoveryTarget = &v1.RecoveryTarget{TargetTime: time.Date(2021, 1, 2, 12, 30, 0, + 0, time.UTC).Format("2006-01-02 15:04:04")} + closestBackupInfo, err = catalog.FindBackupInfo(recoveryTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(closestBackupInfo.ID).To(Equal("202101021200")) + }) + + It("will return an empty result when the closest backup cannot be found", func() { + recoveryTarget := &v1.RecoveryTarget{TargetTime: time.Date(2019, 1, 2, 12, 30, + 0, 0, time.UTC).Format("2006-01-02 15:04:04")} + closestBackupInfo, err := catalog.FindBackupInfo(recoveryTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(closestBackupInfo).To(BeNil()) + }) + + It("can find the backup info when BackupID is provided", func() { + recoveryTarget := &v1.RecoveryTarget{TargetName: "recovery_point_1", BackupID: "202101021200"} + BackupInfo, err := catalog.FindBackupInfo(recoveryTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(BackupInfo.ID).To(Equal("202101021200")) + + trueVal := true + recoveryTarget = &v1.RecoveryTarget{TargetImmediate: &trueVal, BackupID: "202101011200"} + BackupInfo, err = catalog.FindBackupInfo(recoveryTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(BackupInfo.ID).To(Equal("202101011200")) + }) +}) + +var _ = Describe("barman-cloud-backup-list parsing", func() { + const barmanCloudListOutput = `{ + "backups_list": [ + { + "backup_label": "'START WAL LOCATION:[...]", + "begin_offset": 40, + "begin_time": "Tue Oct 20 11:52:31 2020", + "begin_wal": "000000010000000000000006", + "begin_xlog": "0/6000028", + "config_file": "/var/lib/postgresql/data/pgdata/postgresql.conf", + "copy_stats": { + "total_time": 4.285494, + "number_of_workers": 2, + "analysis_time": 0, + "analysis_time_per_item": { + "data": 0 + }, + "copy_time_per_item": { + "data": 1.368199 + }, + "serialized_copy_time_per_item": { + "data": 0.433392 + }, + "copy_time": 1.368199, + "serialized_copy_time": 0.433392 + }, + "deduplicated_size": null, + "end_offset": 312, + "end_time": "Tue Oct 20 11:52:34 2020", + "end_wal": "000000010000000000000006", + "end_xlog": "0/6000138", + "error": null, + "hba_file": "/var/lib/postgresql/data/pgdata/pg_hba.conf", + "ident_file": "/var/lib/postgresql/data/pgdata/pg_ident.conf", + "included_files": [ + "/var/lib/postgresql/data/pgdata/custom.conf" + ], + "mode": null, + "pgdata": "/var/lib/postgresql/data/pgdata", + "server_name": "cloud", + "size": null, + "status": "DONE", + "systemid": "6885668674852188181", + "tablespaces": null, + "timeline": 1, + "version": 120004, + "xlog_segment_size": 16777216, + "backup_id": "20201020T115231" + }, + { + "backup_id": "20191020T115231" + } + ] +}` + + It("must parse a correct output", func() { + result, err := NewCatalogFromBarmanCloudBackupList(barmanCloudListOutput) + Expect(err).ToNot(HaveOccurred()) + Expect(result.List).To(HaveLen(2)) + Expect(result.List[0].ID).To(Equal("20201020T115231")) + Expect(result.List[0].SystemID).To(Equal("6885668674852188181")) + Expect(result.List[0].BeginTimeString).To(Equal("Tue Oct 20 11:52:31 2020")) + Expect(result.List[0].EndTimeString).To(Equal("Tue Oct 20 11:52:34 2020")) + }) + + It("must extract the latest backup id", func() { + result, err := NewCatalogFromBarmanCloudBackupList(barmanCloudListOutput) + Expect(err).ToNot(HaveOccurred()) + Expect(result.LatestBackupInfo().ID).To(Equal("20201020T115231")) + }) +}) + +var _ = Describe("barman-cloud-backup-show parsing", func() { + const barmanCloudShowOutput = `{ + "cloud":{ + "backup_label": null, + "begin_offset": 40, + "begin_time": "Tue Jan 19 03:14:08 2038", + "begin_wal": "000000010000000000000002", + "begin_xlog": "0/2000028", + "compression": null, + "config_file": "/pgdata/location/postgresql.conf", + "copy_stats": null, + "deduplicated_size": null, + "end_offset": 184, + "end_time": "Tue Jan 19 04:14:08 2038", + "end_wal": "000000010000000000000004", + "end_xlog": "0/20000B8", + "error": null, + "hba_file": "/pgdata/location/pg_hba.conf", + "ident_file": "/pgdata/location/pg_ident.conf", + "included_files": null, + "mode": "concurrent", + "pgdata": "/pgdata/location", + "server_name": "main", + "size": null, + "snapshots_info": { + "provider": "gcp", + "provider_info": { + "project": "test_project" + }, + "snapshots": [ + { + "mount": { + "mount_options": "rw,noatime", + "mount_point": "/opt/disk0" + }, + "provider": { + "device_name": "dev0", + "snapshot_name": "snapshot0", + "snapshot_project": "test_project" + } + }, + { + "mount": { + "mount_options": "rw", + "mount_point": "/opt/disk1" + }, + "provider": { + "device_name": "dev1", + "snapshot_name": "snapshot1", + "snapshot_project": "test_project" + } + } + ] + }, + "status": "DONE", + "systemid": "6885668674852188181", + "tablespaces": [ + ["tbs1", 16387, "/fake/location"], + ["tbs2", 16405, "/another/location"] + ], + "timeline": 1, + "version": 150000, + "xlog_segment_size": 16777216, + "backup_id": "20201020T115231" + } +}` + + It("must parse a correct output", func() { + result, err := NewBackupFromBarmanCloudBackupShow(barmanCloudShowOutput) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + Expect(result.ID).To(Equal("20201020T115231")) + Expect(result.SystemID).To(Equal("6885668674852188181")) + Expect(result.BeginTimeString).To(Equal("Tue Jan 19 03:14:08 2038")) + Expect(result.EndTimeString).To(Equal("Tue Jan 19 04:14:08 2038")) + }) +}) diff --git a/pkg/catalog/sorting.go b/pkg/catalog/sorting.go new file mode 100644 index 0000000..bd362ab --- /dev/null +++ b/pkg/catalog/sorting.go @@ -0,0 +1,42 @@ +/* +Copyright The CloudNativePG Contributors + +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 catalog + +// Len implements sort.Interface +func (catalog *Catalog) Len() int { + return len(catalog.List) +} + +// Less implements sort.Interface +func (catalog *Catalog) Less(i, j int) bool { + if catalog.List[i].BeginTime.IsZero() { + // backups which are not completed go to the bottom + return false + } + + if catalog.List[i].EndTime.IsZero() { + // backups which are not completed go to the bottom + return true + } + + return catalog.List[i].BeginTime.Before(catalog.List[j].EndTime) +} + +// Swap implements sort.Interface +func (catalog *Catalog) Swap(i, j int) { + catalog.List[j], catalog.List[i] = catalog.List[i], catalog.List[j] +} diff --git a/pkg/catalog/suite_test.go b/pkg/catalog/suite_test.go new file mode 100644 index 0000000..f58e328 --- /dev/null +++ b/pkg/catalog/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright The CloudNativePG Contributors + +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 catalog + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCatalog(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Barman catalog test suite") +} diff --git a/pkg/command/backuplist.go b/pkg/command/backuplist.go new file mode 100644 index 0000000..e106a39 --- /dev/null +++ b/pkg/command/backuplist.go @@ -0,0 +1,181 @@ +/* +Copyright The CloudNativePG Contributors + +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 command contains the utilities to interact with barman-cloud. +// +// This package is able to download the backup catalog, given an object store, +// and to find the required backup to recreate a cluster, given a certain point +// in time. It can also delete backups according to barman object store configuration and retention policies, +// and find the latest successful backup. This is useful to recovery from the last consistent state. +// We detect the possible commands to be executed, fulfilling the barman capabilities, +// and define an interface for building commands. +// +// A backup catalog is represented by the Catalog structure, and can be +// created using the NewCatalog function or by downloading it from an +// object store via GetBackupList. A backup catalog is just a sorted +// list of BackupInfo objects. +// +// We also have features to gather all the environment variables required +// for the barman-cloud utilities to work correctly. +// +// The functions which call the barman-cloud utilities (such as GetBackupList) +// require the environment variables to be passed, and the calling code is +// supposed gather them (i.e. via the EnvSetCloudCredentials) before calling +// them. +// A Kubernetes client is required to get the environment variables, as we +// need to download the content from the required secrets, but is not required +// to call barman-cloud. +package command + +import ( + "bytes" + "context" + "fmt" + "github.com/cloudnative-pg/plugin-barman-cloud/pkg/catalog" + barmanTypes "github.com/cloudnative-pg/plugin-barman-cloud/pkg/types" + "os/exec" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" + barmanCapabilities "github.com/cloudnative-pg/plugin-barman-cloud/pkg/capabilities" +) + +func executeQueryCommand( + ctx context.Context, + barmanCommand string, + barmanConfiguration *barmanTypes.BarmanObjectStoreConfiguration, + serverName string, + additionalOptions []string, + env []string, +) (string, error) { + contextLogger := log.FromContext(ctx).WithName("barman") + + options := []string{"--format", "json"} + + if barmanConfiguration.EndpointURL != "" { + options = append(options, "--endpoint-url", barmanConfiguration.EndpointURL) + } + + options, err := AppendCloudProviderOptionsFromConfiguration(options, barmanConfiguration) + if err != nil { + return "", err + } + + options = append(options, barmanConfiguration.DestinationPath, serverName) + options = append(options, additionalOptions...) + + var stdoutBuffer bytes.Buffer + var stderrBuffer bytes.Buffer + cmd := exec.Command(barmanCommand, options...) // #nosec G204 + cmd.Env = env + cmd.Stdout = &stdoutBuffer + cmd.Stderr = &stderrBuffer + err = cmd.Run() + if err != nil { + contextLogger.Error(err, + "Can't extract backup id", + "command", barmanCommand, + "options", options, + "stdout", stdoutBuffer.String(), + "stderr", stderrBuffer.String()) + return "", err + } + + return stdoutBuffer.String(), nil +} + +// GetBackupList returns the catalog reading it from the object store +func GetBackupList( + ctx context.Context, + barmanConfiguration *barmanTypes.BarmanObjectStoreConfiguration, + serverName string, + env []string, +) (*catalog.Catalog, error) { + contextLogger := log.FromContext(ctx).WithName("barman") + + rawJSON, err := executeQueryCommand( + ctx, + barmanCapabilities.BarmanCloudBackupList, + barmanConfiguration, + serverName, + []string{}, + env, + ) + if err != nil { + return nil, err + } + backupList, err := catalog.NewCatalogFromBarmanCloudBackupList(rawJSON) + if err != nil { + contextLogger.Error(err, "Can't parse barman output", + "command", barmanCapabilities.BarmanCloudBackupList, + "output", rawJSON) + return nil, err + } + + return backupList, nil +} + +// GetBackupByName returns the backup data found for a given backup +func GetBackupByName( + ctx context.Context, + backupName string, + serverName string, + barmanConfiguration *barmanTypes.BarmanObjectStoreConfiguration, + env []string, +) (*catalog.BarmanBackup, error) { + contextLogger := log.FromContext(ctx) + + rawJSON, err := executeQueryCommand( + ctx, + barmanCapabilities.BarmanCloudBackupShow, + barmanConfiguration, + serverName, + []string{backupName}, + env, + ) + if err != nil { + return nil, err + } + + contextLogger.Debug("raw backup barman object", "rawBarmanObject", rawJSON) + + return catalog.NewBackupFromBarmanCloudBackupShow(rawJSON) +} + +// GetLatestBackup returns the latest executed backup +func GetLatestBackup( + ctx context.Context, + serverName string, + barmanConfiguration *barmanTypes.BarmanObjectStoreConfiguration, + env []string, +) (*catalog.BarmanBackup, error) { + contextLogger := log.FromContext(ctx) + // Extracting the latest backup using barman-cloud-backup-list + backupList, err := GetBackupList(ctx, barmanConfiguration, serverName, env) + if err != nil { + // Proper logging already happened inside GetBackupList + return nil, err + } + + contextLogger.Debug("raw backup list object", "backupList", backupList) + + // We have just made a new backup, if the backup list is empty + // something is going wrong in the cloud storage + if backupList.Len() == 0 { + return nil, fmt.Errorf("no backup found on the remote object storage") + } + + return backupList.LatestBackupInfo(), nil +} diff --git a/pkg/command/commandbuilder.go b/pkg/command/commandbuilder.go new file mode 100644 index 0000000..97bd926 --- /dev/null +++ b/pkg/command/commandbuilder.go @@ -0,0 +1,134 @@ +/* +Copyright The CloudNativePG Contributors + +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 command + +import ( + "fmt" + barmanTypes "github.com/cloudnative-pg/plugin-barman-cloud/pkg/types" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" + barmanCapabilities "github.com/cloudnative-pg/plugin-barman-cloud/pkg/capabilities" +) + +// CloudWalRestoreOptions returns the options needed to execute the barman command successfully +func CloudWalRestoreOptions( + configuration *barmanTypes.BarmanObjectStoreConfiguration, + clusterName string, +) ([]string, error) { + var options []string + if len(configuration.EndpointURL) > 0 { + options = append( + options, + "--endpoint-url", + configuration.EndpointURL) + } + + options, err := AppendCloudProviderOptionsFromConfiguration(options, configuration) + if err != nil { + return nil, err + } + + serverName := clusterName + if len(configuration.ServerName) != 0 { + serverName = configuration.ServerName + } + + options = append(options, configuration.DestinationPath, serverName) + options = configuration.Wal.AppendRestoreAdditionalCommandArgs(options) + + return options, nil +} + +// AppendCloudProviderOptionsFromConfiguration takes an options array and adds the cloud provider specified +// in the Barman configuration object +func AppendCloudProviderOptionsFromConfiguration( + options []string, + barmanConfiguration *barmanTypes.BarmanObjectStoreConfiguration, +) ([]string, error) { + return appendCloudProviderOptions(options, barmanConfiguration.BarmanCredentials) +} + +// AppendCloudProviderOptionsFromBackup takes an options array and adds the cloud provider specified +// in the Backup object +func AppendCloudProviderOptionsFromBackup( + options []string, + credentials barmanTypes.BarmanCredentials, +) ([]string, error) { + return appendCloudProviderOptions(options, credentials) +} + +// appendCloudProviderOptions takes an options array and adds the cloud provider specified as arguments +func appendCloudProviderOptions(options []string, credentials barmanTypes.BarmanCredentials) ([]string, error) { + capabilities, err := barmanCapabilities.CurrentCapabilities() + if err != nil { + return nil, err + } + + switch { + case credentials.AWS != nil: + if capabilities.HasS3 { + options = append( + options, + "--cloud-provider", + "aws-s3") + } + case credentials.Azure != nil: + if !capabilities.HasAzure { + err := fmt.Errorf( + "barman >= 2.13 is required to use Azure object storage, current: %v", + capabilities.Version) + log.Error(err, "Barman version not supported") + return nil, err + } + + options = append( + options, + "--cloud-provider", + "azure-blob-storage") + + if !credentials.Azure.InheritFromAzureAD { + break + } + + if !capabilities.HasAzureManagedIdentity { + err := fmt.Errorf( + "barman >= 2.18 is required to use azureInheritFromAzureAD, current: %v", + capabilities.Version) + log.Error(err, "Barman version not supported") + return nil, err + } + + options = append( + options, + "--credential", + "managed-identity") + case credentials.Google != nil: + if !capabilities.HasGoogle { + err := fmt.Errorf( + "barman >= 2.19 is required to use Google Cloud Storage, current: %v", + capabilities.Version) + log.Error(err, "Barman version not supported") + return nil, err + } + options = append( + options, + "--cloud-provider", + "google-cloud-storage") + } + + return options, nil +} diff --git a/pkg/command/commandbuilder_test.go b/pkg/command/commandbuilder_test.go new file mode 100644 index 0000000..59e7476 --- /dev/null +++ b/pkg/command/commandbuilder_test.go @@ -0,0 +1,60 @@ +/* +Copyright The CloudNativePG Contributors + +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 command + +import ( + barmanTypes "github.com/cloudnative-pg/plugin-barman-cloud/pkg/types" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("barmanCloudWalRestoreOptions", func() { + const namespace = "test" + var storageConf *barmanTypes.BarmanObjectStoreConfiguration + BeforeEach(func() { + storageConf = &barmanTypes.BarmanObjectStoreConfiguration{ + DestinationPath: "s3://bucket-name/", + } + + }) + + It("should generate correct arguments without the wal stanza", func() { + options, err := CloudWalRestoreOptions(storageConf, "test-cluster") + Expect(err).ToNot(HaveOccurred()) + Expect(strings.Join(options, " ")). + To( + Equal( + "s3://bucket-name/ test-cluster", + )) + }) + + It("should generate correct arguments", func() { + extraOptions := []string{"--read-timeout=60", "-vv"} + storageConf.Wal = &barmanTypes.WalBackupConfiguration{ + RestoreAdditionalCommandArgs: extraOptions, + } + options, err := CloudWalRestoreOptions(storageConf, "test-cluster") + Expect(err).ToNot(HaveOccurred()) + Expect(strings.Join(options, " ")). + To( + Equal( + "s3://bucket-name/ test-cluster --read-timeout=60 -vv", + )) + }) +}) diff --git a/pkg/command/errors.go b/pkg/command/errors.go new file mode 100644 index 0000000..f2ff731 --- /dev/null +++ b/pkg/command/errors.go @@ -0,0 +1,101 @@ +/* +Copyright The CloudNativePG Contributors + +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 command + +import ( + "fmt" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" + barmanCapabilities "github.com/cloudnative-pg/plugin-barman-cloud/pkg/capabilities" +) + +const ( + // Connectivity to csp was ok but operation still failed error code + // https://docs.pgbarman.org/release/3.10.0/barman-cloud-restore.1.html + operationErrorCode = 1 + + // Network related error + // https://docs.pgbarman.org/release/3.10.0/barman-cloud-restore.1.html + networkErrorCode = 2 + + // CLI related error + // https://docs.pgbarman.org/release/3.10.0/barman-cloud-restore.1.html + cliErrorCode = 3 + + // General barman cloud errors + // https://docs.pgbarman.org/release/3.10.0/barman-cloud-restore.1.html + generalErrorCode = 4 +) + +// errorDescriptions are the human descriptions of the error codes +var errorDescriptions = map[int]string{ + operationErrorCode: "Operation error", + networkErrorCode: "Network error", + cliErrorCode: "CLI argument parsing error", + generalErrorCode: "General error", +} + +// CloudRestoreError is raised when barman-cloud-restore fails +type CloudRestoreError struct { + // The exit code returned by Barman + ExitCode int + + // This is true when Barman can return significant error codes + HasRestoreErrorCodes bool +} + +// Error implements the error interface +func (err *CloudRestoreError) Error() string { + msg, ok := errorDescriptions[err.ExitCode] + if !ok { + msg = "Generic failure" + } + + return fmt.Sprintf("%s (exit code %v)", msg, err.ExitCode) +} + +// IsRetriable returns true whether the error is temporary, and +// it could be a good idea to retry the restore later +func (err *CloudRestoreError) IsRetriable() bool { + return (err.ExitCode == networkErrorCode || err.ExitCode == generalErrorCode) && err.HasRestoreErrorCodes +} + +// UnmarshalBarmanCloudRestoreExitCode returns the correct error +// for a certain barman-cloud-restore exit code +func UnmarshalBarmanCloudRestoreExitCode(exitCode int) error { + if exitCode == 0 { + return nil + } + + var currentCapabilities *barmanCapabilities.Capabilities + currentCapabilities, err := barmanCapabilities.CurrentCapabilities() + if err != nil { + log.Error(err, "error while detecting Barman capabilities") + + // We default to old exit codes when we could not detect + // the Barman capabilities + return &CloudRestoreError{ + ExitCode: exitCode, + HasRestoreErrorCodes: false, + } + } + + return &CloudRestoreError{ + ExitCode: exitCode, + HasRestoreErrorCodes: currentCapabilities.HasErrorCodesForRestore, + } +} diff --git a/pkg/command/suite_test.go b/pkg/command/suite_test.go new file mode 100644 index 0000000..217e00c --- /dev/null +++ b/pkg/command/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright The CloudNativePG Contributors + +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 command + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCommand(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Barman test suite") +} diff --git a/pkg/credentials/env.go b/pkg/credentials/env.go index e1f08d0..dc429d9 100644 --- a/pkg/credentials/env.go +++ b/pkg/credentials/env.go @@ -21,53 +21,80 @@ import ( "context" "fmt" barmanTypes "github.com/cloudnative-pg/plugin-barman-cloud/pkg/types" - corev1 "k8s.io/api/core/v1" + "os" "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // ScratchDataDirectory is the directory to be used for scratch data + ScratchDataDirectory = "/controller" + + // CertificatesDir location to store the certificates + CertificatesDir = ScratchDataDirectory + "/certificates/" + // BarmanBackupEndpointCACertificateLocation is the location where the barman endpoint + // CA certificate is stored + BarmanBackupEndpointCACertificateLocation = CertificatesDir + BarmanBackupEndpointCACertificateFileName + + // BarmanBackupEndpointCACertificateFileName is the name of the file in which the barman endpoint + // CA certificate for backups is stored + BarmanBackupEndpointCACertificateFileName = "backup-" + BarmanEndpointCACertificateFileName - "github.com/cloudnative-pg/cloudnative-pg/pkg/fileutils" - "github.com/cloudnative-pg/cloudnative-pg/pkg/postgres" + // BarmanRestoreEndpointCACertificateLocation is the location where the barman endpoint + // CA certificate is stored + BarmanRestoreEndpointCACertificateLocation = CertificatesDir + BarmanRestoreEndpointCACertificateFileName + + // BarmanRestoreEndpointCACertificateFileName is the name of the file in which the barman endpoint + // CA certificate for restores is stored + BarmanRestoreEndpointCACertificateFileName = "restore-" + BarmanEndpointCACertificateFileName + + // BarmanEndpointCACertificateFileName is the name of the file in which the barman endpoint + // CA certificate is stored + BarmanEndpointCACertificateFileName = "barman-ca.crt" ) // EnvSetBackupCloudCredentials sets the AWS environment variables needed for backups // given the configuration inside the cluster func EnvSetBackupCloudCredentials( ctx context.Context, + fileUtils FileUtils, c client.Client, namespace string, configuration *barmanTypes.BarmanObjectStoreConfiguration, env []string, ) ([]string, error) { if configuration.EndpointCA != nil && configuration.BarmanCredentials.AWS != nil { - env = append(env, fmt.Sprintf("AWS_CA_BUNDLE=%s", postgres.BarmanBackupEndpointCACertificateLocation)) + env = append(env, fmt.Sprintf("AWS_CA_BUNDLE=%s", BarmanBackupEndpointCACertificateLocation)) } else if configuration.EndpointCA != nil && configuration.BarmanCredentials.Azure != nil { - env = append(env, fmt.Sprintf("REQUESTS_CA_BUNDLE=%s", postgres.BarmanBackupEndpointCACertificateLocation)) + env = append(env, fmt.Sprintf("REQUESTS_CA_BUNDLE=%s", BarmanBackupEndpointCACertificateLocation)) } - return envSetCloudCredentials(ctx, c, namespace, configuration, env) + return envSetCloudCredentials(ctx, fileUtils, c, namespace, configuration, env) } // EnvSetRestoreCloudCredentials sets the AWS environment variables needed for restores // given the configuration inside the cluster func EnvSetRestoreCloudCredentials( ctx context.Context, + fileUtils FileUtils, c client.Client, namespace string, configuration *barmanTypes.BarmanObjectStoreConfiguration, env []string, ) ([]string, error) { if configuration.EndpointCA != nil && configuration.BarmanCredentials.AWS != nil { - env = append(env, fmt.Sprintf("AWS_CA_BUNDLE=%s", postgres.BarmanRestoreEndpointCACertificateLocation)) + env = append(env, fmt.Sprintf("AWS_CA_BUNDLE=%s", BarmanRestoreEndpointCACertificateLocation)) } else if configuration.EndpointCA != nil && configuration.BarmanCredentials.Azure != nil { - env = append(env, fmt.Sprintf("REQUESTS_CA_BUNDLE=%s", postgres.BarmanRestoreEndpointCACertificateLocation)) + env = append(env, fmt.Sprintf("REQUESTS_CA_BUNDLE=%s", BarmanRestoreEndpointCACertificateLocation)) } - return envSetCloudCredentials(ctx, c, namespace, configuration, env) + return envSetCloudCredentials(ctx, fileUtils, c, namespace, configuration, env) } // envSetCloudCredentials sets the AWS environment variables given the configuration // inside the cluster func envSetCloudCredentials( ctx context.Context, + fileUtils FileUtils, c client.Client, namespace string, configuration *barmanTypes.BarmanObjectStoreConfiguration, @@ -78,7 +105,7 @@ func envSetCloudCredentials( } if configuration.BarmanCredentials.Google != nil { - return envSetGoogleCredentials(ctx, c, namespace, configuration.BarmanCredentials.Google, env) + return envSetGoogleCredentials(ctx, fileUtils, c, namespace, configuration.BarmanCredentials.Google, env) } return envSetAzureCredentials(ctx, c, namespace, configuration, env) @@ -241,6 +268,7 @@ func envSetAzureCredentials( func envSetGoogleCredentials( ctx context.Context, + fileUtils FileUtils, c client.Client, namespace string, googleCredentials *barmanTypes.GoogleCredentials, @@ -250,7 +278,7 @@ func envSetGoogleCredentials( if googleCredentials.GKEEnvironment && googleCredentials.ApplicationCredentials == nil { - return env, reconcileGoogleCredentials(googleCredentials, applicationCredentialsContent) + return env, reconcileGoogleCredentials(googleCredentials, applicationCredentialsContent, fileUtils) } applicationCredentialsContent, err := extractValueFromSecret( @@ -263,7 +291,7 @@ func envSetGoogleCredentials( return nil, err } - if err := reconcileGoogleCredentials(googleCredentials, applicationCredentialsContent); err != nil { + if err := reconcileGoogleCredentials(googleCredentials, applicationCredentialsContent, fileUtils); err != nil { return nil, err } @@ -272,17 +300,23 @@ func envSetGoogleCredentials( return env, nil } +type FileUtils struct { + RemoveFile func(string) error + WriteFileAtomic func(fileName string, contents []byte, perm os.FileMode) (bool, error) +} + func reconcileGoogleCredentials( googleCredentials *barmanTypes.GoogleCredentials, applicationCredentialsContent []byte, + fileUtils FileUtils, ) error { credentialsPath := "/controller/.application_credentials.json" if googleCredentials == nil { - return fileutils.RemoveFile(credentialsPath) + return fileUtils.RemoveFile(credentialsPath) } - _, err := fileutils.WriteFileAtomic(credentialsPath, applicationCredentialsContent, 0o600) + _, err := fileUtils.WriteFileAtomic(credentialsPath, applicationCredentialsContent, 0o600) return err } diff --git a/pkg/restorer/restorer.go b/pkg/restorer/restorer.go index 5727da5..ce7dd25 100644 --- a/pkg/restorer/restorer.go +++ b/pkg/restorer/restorer.go @@ -21,14 +21,12 @@ import ( "context" "errors" "fmt" + "github.com/cloudnative-pg/plugin-barman-cloud/pkg/spool" "math" "os/exec" "sync" "time" - apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" - "github.com/cloudnative-pg/cloudnative-pg/pkg/management/barman/spool" - "github.com/cloudnative-pg/cloudnative-pg/pkg/management/execlog" "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" barmanCapabilities "github.com/cloudnative-pg/plugin-barman-cloud/pkg/capabilities" ) @@ -43,9 +41,6 @@ var ErrWALNotFound = errors.New("WAL not found") // WALRestorer is a structure containing every info needed to restore // some WALs from the object storage type WALRestorer struct { - // The cluster for which we are archiving - cluster *apiv1.Cluster - // The spool of WAL files to be archived in parallel spool *spool.WALSpool @@ -72,22 +67,23 @@ type Result struct { } // New creates a new WAL restorer -func New(ctx context.Context, cluster *apiv1.Cluster, env []string, spoolDirectory string) ( - restorer *WALRestorer, - err error, -) { +func New( + ctx context.Context, + utils spool.FileUtils, + env []string, + spoolDirectory string, +) (restorer *WALRestorer, err error) { contextLog := log.FromContext(ctx) var walRecoverSpool *spool.WALSpool - if walRecoverSpool, err = spool.New(spoolDirectory); err != nil { + if walRecoverSpool, err = spool.New(utils, spoolDirectory); err != nil { contextLog.Info("Cannot initialize the WAL spool", "spoolDirectory", spoolDirectory) return nil, fmt.Errorf("while creating spool directory: %w", err) } restorer = &WALRestorer{ - cluster: cluster, - spool: walRecoverSpool, - env: env, + spool: walRecoverSpool, + env: env, } return restorer, nil } @@ -155,6 +151,7 @@ func (restorer *WALRestorer) RestoreList( fetchList []string, destinationPath string, options []string, + runStream func(cmd *exec.Cmd, cmdName string) error, ) (resultList []Result) { resultList = make([]Result, len(fetchList)) contextLog := log.FromContext(ctx) @@ -174,7 +171,7 @@ func (restorer *WALRestorer) RestoreList( } result.StartTime = time.Now() - result.Err = restorer.Restore(fetchList[walIndex], result.DestinationPath, options) + result.Err = restorer.Restore(runStream, fetchList[walIndex], result.DestinationPath, options) result.EndTime = time.Now() elapsedWalTime := result.EndTime.Sub(result.StartTime) @@ -219,7 +216,11 @@ func (restorer *WALRestorer) RestoreList( } // Restore restores a WAL file from the object store -func (restorer *WALRestorer) Restore(walName, destinationPath string, baseOptions []string) error { +func (restorer *WALRestorer) Restore( + runStream func(cmd *exec.Cmd, cmdName string) error, + walName, destinationPath string, + baseOptions []string, +) error { const ( exitCodeBucketOrWalNotFound = 1 exitCodeConnectivityError = 2 @@ -245,7 +246,7 @@ func (restorer *WALRestorer) Restore(walName, destinationPath string, baseOption options...) // #nosec G204 barmanCloudWalRestoreCmd.Env = restorer.env - err := execlog.RunStreaming(barmanCloudWalRestoreCmd, barmanCapabilities.BarmanCloudWalRestore) + err := runStream(barmanCloudWalRestoreCmd, barmanCapabilities.BarmanCloudWalRestore) if err == nil { return nil } diff --git a/pkg/spool/spool.go b/pkg/spool/spool.go new file mode 100644 index 0000000..2df67ed --- /dev/null +++ b/pkg/spool/spool.go @@ -0,0 +1,110 @@ +/* +Copyright The CloudNativePG Contributors + +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 spool implements a WAL pooler keeping track of which WALs we have archived +package spool + +import ( + "fmt" + "github.com/cloudnative-pg/cloudnative-pg/pkg/management/log" + "io/fs" + "os" + "path" + "path/filepath" +) + +// ErrorNonExistentFile is returned when the spool tried to work +// on a file which doesn't exist +var ErrorNonExistentFile = fs.ErrNotExist + +// WALSpool is a way to keep track of which WAL files were processes from the parallel +// feature and not by PostgreSQL request. +// It works using a directory, under which we create an empty file carrying the name +// of the WAL we archived +type WALSpool struct { + spoolDirectory string + utils FileUtils +} + +type FileUtils struct { + EnsureDirectoryExists func(string) error + FileExists func(string) (bool, error) + MoveFile func(string, string) error +} + +// New create new WAL spool +func New(fileUtils FileUtils, spoolDirectory string) (*WALSpool, error) { + if err := fileUtils.EnsureDirectoryExists(spoolDirectory); err != nil { + log.Warning("Cannot create the spool directory", "spoolDirectory", spoolDirectory) + return nil, fmt.Errorf("while creating spool directory: %w", err) + } + + return &WALSpool{ + spoolDirectory: spoolDirectory, + utils: fileUtils, + }, nil +} + +// Contains checks if a certain file is in the spool or not +func (spool *WALSpool) Contains(walFile string) (bool, error) { + walFile = path.Base(walFile) + return spool.utils.FileExists(path.Join(spool.spoolDirectory, walFile)) +} + +// Remove removes a WAL file from the spool. If the WAL file doesn't +// exist an error is returned +func (spool *WALSpool) Remove(walFile string) error { + walFile = path.Base(walFile) + + err := os.Remove(path.Join(spool.spoolDirectory, walFile)) + if err != nil && os.IsNotExist(err) { + return ErrorNonExistentFile + } + return err +} + +// Touch ensure that a certain WAL file is included into the spool as an empty file +func (spool *WALSpool) Touch(walFile string) (err error) { + var f *os.File + + walFile = path.Base(walFile) + fileName := path.Join(spool.spoolDirectory, walFile) + if f, err = os.Create(filepath.Clean(fileName)); err != nil { + return err + } + if err = f.Close(); err != nil { + log.Warning("Cannot close empty file, error skipped", "fileName", fileName, "err", err) + } + return nil +} + +// MoveOut moves out a file from the spool to the destination file +func (spool *WALSpool) MoveOut(walName, destination string) (err error) { + // We cannot use os.Rename here, as it will not work between different + // volumes, such as moving files from an EmptyDir volume to the data + // directory. + // Given that, we rely on the old strategy to copy stuff around. + err = spool.utils.MoveFile(path.Join(spool.spoolDirectory, walName), destination) + if err != nil && os.IsNotExist(err) { + return ErrorNonExistentFile + } + return err +} + +// FileName gets the name of the file for the given WAL inside the spool +func (spool *WALSpool) FileName(walName string) string { + return path.Join(spool.spoolDirectory, walName) +} diff --git a/pkg/spool/spool_test.go b/pkg/spool/spool_test.go new file mode 100644 index 0000000..31ad21b --- /dev/null +++ b/pkg/spool/spool_test.go @@ -0,0 +1,96 @@ +/* +Copyright The CloudNativePG Contributors + +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 spool + +import ( + "os" + "path" + + "github.com/cloudnative-pg/cloudnative-pg/pkg/fileutils" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Spool", func() { + var tmpDir string + var tmpDir2 string + var spool *WALSpool + + _ = BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "spool-test-") + Expect(err).NotTo(HaveOccurred()) + + tmpDir2, err = os.MkdirTemp("", "spool-test-tmp-") + Expect(err).NotTo(HaveOccurred()) + + spool, err = New(tmpDir) + Expect(err).NotTo(HaveOccurred()) + }) + + _ = AfterEach(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + Expect(os.RemoveAll(tmpDir2)).To(Succeed()) + }) + + It("create and removes files from/into the spool", func() { + var err error + const walFile = "000000020000068A00000002" + + // This WAL file doesn't exist + Expect(spool.Contains(walFile)).To(BeFalse()) + + // If I try to remove a WAL file that doesn't exist, I obtain an error + err = spool.Remove(walFile) + Expect(err).To(Equal(ErrorNonExistentFile)) + + // I add it into the spool + err = spool.Touch(walFile) + Expect(err).NotTo(HaveOccurred()) + + // Now the file exists + Expect(spool.Contains(walFile)).To(BeTrue()) + + // I can now remove it + err = spool.Remove(walFile) + Expect(err).NotTo(HaveOccurred()) + + // And now it doesn't exist again + Expect(spool.Contains(walFile)).To(BeFalse()) + }) + + It("can move out files from the spool", func() { + var err error + const walFile = "000000020000068A00000003" + + err = spool.Touch(walFile) + Expect(err).ToNot(HaveOccurred()) + + // Move out this file + destinationPath := path.Join(tmpDir2, "testFile") + err = spool.MoveOut(walFile, destinationPath) + Expect(err).ToNot(HaveOccurred()) + Expect(spool.Contains(walFile)).To(BeFalse()) + Expect(fileutils.FileExists(destinationPath)).To(BeTrue()) + }) + + It("can determine names for each WAL files", func() { + const walFile = "000000020000068A00000004" + Expect(spool.FileName(walFile)).To(Equal(path.Join(tmpDir, walFile))) + }) +}) diff --git a/pkg/spool/suite_test.go b/pkg/spool/suite_test.go new file mode 100644 index 0000000..5dd70ee --- /dev/null +++ b/pkg/spool/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright The CloudNativePG Contributors + +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 spool + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCatalog(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Spool test suite") +}