diff --git a/.github/workflows/publish-website.yaml b/.github/workflows/publish-website.yaml index f6beb74..416c94c 100644 --- a/.github/workflows/publish-website.yaml +++ b/.github/workflows/publish-website.yaml @@ -40,7 +40,7 @@ jobs: - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: - hugo-version: "0.133.0" + hugo-version: "0.134.0" extended: true - name: Setup Node uses: actions/setup-node@v4 diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 7dc1d4c..3db3948 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -26,6 +26,9 @@ import ( "github.com/sap/cap-operator/pkg/client/clientset/versioned" istio "istio.io/client-go/pkg/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + promop "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" + apiext "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" ) const ( @@ -52,6 +55,16 @@ func main() { klog.Fatal("could not create client for custom resources: ", err.Error()) } + apiExtClient, err := apiext.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for api-extensions: ", err.Error()) + } + + promClient, err := promop.NewForConfig(config) + if err != nil { + klog.Fatal("could not create client for prometheus-operator resources: ", err.Error()) + } + istioClient, err := istio.NewForConfig(config) if err != nil { klog.Fatal("could not create client for istio resources: ", err.Error()) @@ -107,7 +120,7 @@ func main() { Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { klog.InfoS("Started leading: ", LeaseLockName, leaseLockId) - c := controller.NewController(coreClient, crdClient, istioClient, certClient, certManagerClient, dnsClient) + c := controller.NewController(coreClient, crdClient, istioClient, certClient, certManagerClient, dnsClient, apiExtClient, promClient) go c.Start(ctx) }, OnStoppedLeading: func() { diff --git a/crds/sme.sap.com_capapplicationversions.yaml b/crds/sme.sap.com_capapplicationversions.yaml index 93bb710..2f8c0e9 100644 --- a/crds/sme.sap.com_capapplicationversions.yaml +++ b/crds/sme.sap.com_capapplicationversions.yaml @@ -1375,6 +1375,57 @@ spec: format: int32 type: integer type: object + monitoring: + properties: + deletionRules: + oneOf: + - required: + - metrics + - required: + - expression + properties: + expression: + type: string + metrics: + items: + properties: + calculationPeriod: + pattern: ^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$ + type: string + name: + type: string + thresholdValue: + format: double + type: string + type: + enum: + - Gauge + - Counter + type: string + required: + - calculationPeriod + - name + - thresholdValue + - type + type: object + type: array + type: object + scrapeConfig: + properties: + interval: + pattern: ^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$ + type: string + path: + type: string + port: + type: string + scrapeTimeout: + pattern: ^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$ + type: string + required: + - port + type: object + type: object nodeName: type: string nodeSelector: diff --git a/go.mod b/go.mod index e6383b3..84b9a2d 100644 --- a/go.mod +++ b/go.mod @@ -13,16 +13,23 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/lestrrat-go/jwx/v2 v2.1.1 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.77.0 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.77.0 + github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/common v0.59.1 go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/mod v0.21.0 google.golang.org/protobuf v1.34.2 - istio.io/api v1.23.1 - istio.io/client-go v1.23.1 + istio.io/api v1.23.2 + istio.io/client-go v1.23.2 k8s.io/api v0.31.1 + k8s.io/apiextensions-apiserver v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/client-go v0.31.1 k8s.io/code-generator v0.31.1 k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 ) require ( @@ -52,12 +59,12 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect @@ -71,12 +78,11 @@ require ( 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.31.1 // indirect k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7 // indirect k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect + sigs.k8s.io/controller-runtime v0.19.0 // indirect sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 408ff83..7474ee5 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= +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/cert-manager/cert-manager v1.15.3 h1:/u9T0griwd5MegPfWbB7v0KcVcT9OJrEvPNhc9tl7xQ= github.com/cert-manager/cert-manager v1.15.3/go.mod h1:stBge/DTvrhfQMB/93+Y62s+gQgZBsfL1o0C/4AL/mI= +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/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= @@ -54,6 +58,8 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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= @@ -83,6 +89,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os= @@ -92,6 +100,18 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.77.0 h1:qJ0oVdazkVKcmVJNZncFylrbyf8c48sym/2b8P/0Ahg= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.77.0/go.mod h1:D0KY8md81DQKdaR/cXwnhoWB3MYYyc/UjvqE8GFkIvA= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.77.0 h1:5vDKBsY2m3fIpLCviMDa58eVSpjboBJiKn/1yVnzGr8= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.77.0/go.mod h1:cM7BC5TLUST9O98Nrpl2WJDgTIqvY+ArGbjH8bb2LfU= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +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/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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -178,10 +198,10 @@ 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= -istio.io/api v1.23.1 h1:bm2XF0j058FfzWVHUfpmMj4sFDkcD1X609qs5AU97Pc= -istio.io/api v1.23.1/go.mod h1:QPSTGXuIQdnZFEm3myf9NZ5uBMwCdJWUvfj9ZZ+2oBM= -istio.io/client-go v1.23.1 h1:IX2cgUUXnVYo+9H6bFGSp/vuKVLPUkmiN8qk1/mvsYs= -istio.io/client-go v1.23.1/go.mod h1:+fxu+O2GkITM3HEREUWdobvRXqI/UhAAI7hfxqqpRh0= +istio.io/api v1.23.2 h1:FvWi7GC+rWD60/ZFPuulX/h3k+f2Q9qot3dP8CIL8Ss= +istio.io/api v1.23.2/go.mod h1:QPSTGXuIQdnZFEm3myf9NZ5uBMwCdJWUvfj9ZZ+2oBM= +istio.io/client-go v1.23.2 h1:BIt6A+KaUOFin3SzXiDq2Fr/TMBev1+c836R0BfUfhU= +istio.io/client-go v1.23.2/go.mod h1:E08wpMtUulJk2tlWOCUVakjy1bKFxUNm22tM1R1QY0Y= k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= @@ -200,6 +220,8 @@ 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-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/controller/common_test.go b/internal/controller/common_test.go index 5b946c0..5760c1d 100644 --- a/internal/controller/common_test.go +++ b/internal/controller/common_test.go @@ -30,6 +30,9 @@ import ( gardenerdnsscheme "github.com/gardener/external-dns-management/pkg/client/dns/clientset/versioned/scheme" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + monv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + promopFake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" + promopScheme "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/scheme" "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" copfake "github.com/sap/cap-operator/pkg/client/clientset/versioned/fake" smeScheme "github.com/sap/cap-operator/pkg/client/clientset/versioned/scheme" @@ -42,6 +45,9 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextFake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + apiExtScheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -210,6 +216,8 @@ func initializeControllerForReconciliationTests(t *testing.T, items []ResourceAc gardenerdnsscheme.AddToScheme(scheme.Scheme) istioscheme.AddToScheme(scheme.Scheme) certManagerScheme.AddToScheme(scheme.Scheme) + apiExtScheme.AddToScheme(scheme.Scheme) + promopScheme.AddToScheme(scheme.Scheme) coreClient := k8sfake.NewSimpleClientset() copClient := copfake.NewSimpleClientset() @@ -217,6 +225,8 @@ func initializeControllerForReconciliationTests(t *testing.T, items []ResourceAc gardenerCertClient := gardenercertfake.NewSimpleClientset() gardenerDNSClient := gardenerdnsfake.NewSimpleClientset() certManagerClient := certManagerFake.NewSimpleClientset() + apiExtClient := apiextFake.NewSimpleClientset() + promopClient := promopFake.NewSimpleClientset() copClient.PrependReactor("create", "*", generateNameCreateHandler) copClient.PrependReactor("update", "*", removeStatusTimestampHandler) @@ -239,7 +249,7 @@ func initializeControllerForReconciliationTests(t *testing.T, items []ResourceAc gardenerCertClient.PrependReactor("*", "*", getErrorReactorWithResources(t, items)) gardenerCertClient.PrependReactor("delete-collection", "*", getDeleteCollectionHandler(t, gardenerDNSClient)) - c := NewController(coreClient, copClient, istioClient, gardenerCertClient, certManagerClient, gardenerDNSClient) + c := NewController(coreClient, copClient, istioClient, gardenerCertClient, certManagerClient, gardenerDNSClient, apiExtClient, promopClient) c.eventRecorder = events.NewFakeRecorder(10) return c } @@ -408,22 +418,16 @@ func processTestData(t *testing.T, c *Controller, data TestData, dataType TestDa var processFile = func(file string) { defer wg.Done() - i, err := os.ReadFile(file) + + resources, err := readYAMLResourcesFromFile(file) if err != nil { t.Error(err.Error()) } - - fileContents := string(i) - splits := strings.Split(fileContents, "---") - for _, part := range splits { - if part == "\n" || part == "" { - continue - } - + for i := range resources { if dataType == TestDataTypeInitial { - err = addInitialObjectToStore(t, []byte(part), c) + err = addInitialObjectToStore(resources[i], c) } else { - err = compareExpectedWithStore(t, []byte(part), c) + err = compareExpectedWithStore(t, resources[i], c) } if err != nil { t.Error(err.Error()) @@ -438,7 +442,25 @@ func processTestData(t *testing.T, c *Controller, data TestData, dataType TestDa wg.Wait() } -func addInitialObjectToStore(t *testing.T, resource []byte, c *Controller) error { +func readYAMLResourcesFromFile(file string) ([][]byte, error) { + i, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + resources := [][]byte{} + fileContents := string(i) + splits := strings.Split(fileContents, "---") + for _, part := range splits { + if part == "\n" || part == "" { + continue + } + resources = append(resources, []byte(part)) + } + return resources, nil +} + +func addInitialObjectToStore(resource []byte, c *Controller) error { decoder := scheme.Codecs.UniversalDeserializer().Decode obj, _, err := decoder(resource, nil, nil) if err != nil { @@ -533,6 +555,18 @@ func addInitialObjectToStore(t *testing.T, resource []byte, c *Controller) error case *v1alpha1.CAPTenantOperation: err = c.crdInformerFactory.Sme().V1alpha1().CAPTenantOperations().Informer().GetIndexer().Add(obj) } + case *apiextv1.CustomResourceDefinition: + fakeClient, ok := c.apiExtClient.(*apiextFake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) + case *monv1.ServiceMonitor: + fakeClient, ok := c.promClient.(*promopFake.Clientset) + if !ok { + return fmt.Errorf("controller is not using a fake clientset") + } + fakeClient.Tracker().Add(obj) default: return fmt.Errorf("unknown object type") } @@ -592,6 +626,9 @@ func compareExpectedWithStore(t *testing.T, resource []byte, c *Controller) erro case *v1alpha1.CAPTenantOperation: actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("captenantoperations"), mo.GetNamespace(), mo.GetName()) } + case *monv1.ServiceMonitor: + fakeClient := c.promClient.(*promopFake.Clientset) + actual, err = fakeClient.Tracker().Get(gvk.GroupVersion().WithResource("servicemonitors"), mo.GetNamespace(), mo.GetName()) default: return fmt.Errorf("unknown expected object type") } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index d98598e..e7c259f 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -33,6 +33,9 @@ import ( "k8s.io/client-go/tools/events" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" + + promop "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" + apiext "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" ) type Controller struct { @@ -42,6 +45,8 @@ type Controller struct { gardenerCertificateClient gardenerCert.Interface certManagerCertificateClient certManager.Interface gardenerDNSClient gardenerDNS.Interface + apiExtClient apiext.Interface + promClient promop.Interface kubeInformerFactory informers.SharedInformerFactory crdInformerFactory crdInformers.SharedInformerFactory istioInformerFactory istioInformers.SharedInformerFactory @@ -53,7 +58,7 @@ type Controller struct { eventRecorder events.EventRecorder } -func NewController(client kubernetes.Interface, crdClient versioned.Interface, istioClient istio.Interface, gardenerCertificateClient gardenerCert.Interface, certManagerCertificateClient certManager.Interface, gardenerDNSClient gardenerDNS.Interface) *Controller { +func NewController(client kubernetes.Interface, crdClient versioned.Interface, istioClient istio.Interface, gardenerCertificateClient gardenerCert.Interface, certManagerCertificateClient certManager.Interface, gardenerDNSClient gardenerDNS.Interface, apiExtClient apiext.Interface, promClient promop.Interface) *Controller { queues := map[int]workqueue.RateLimitingInterface{ ResourceCAPApplication: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), ResourceCAPApplicationVersion: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), @@ -101,6 +106,8 @@ func NewController(client kubernetes.Interface, crdClient versioned.Interface, i gardenerCertificateClient: gardenerCertificateClient, certManagerCertificateClient: certManagerCertificateClient, gardenerDNSClient: gardenerDNSClient, + apiExtClient: apiExtClient, + promClient: promClient, kubeInformerFactory: kubeInformerFactory, crdInformerFactory: crdInformerFactory, istioInformerFactory: istioInformerFactory, @@ -180,6 +187,13 @@ func (c *Controller) Start(ctx context.Context) { }(k) } + // start version cleanup routines + wg.Add(1) + go func() { + defer wg.Done() + c.startVersionCleanup(qCxt) + }() + // wait for workers wg.Wait() } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 5b42e38..093cef7 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -88,6 +88,8 @@ func TestController_processQueue(t *testing.T) { istioClient: c.istioClient, gardenerCertificateClient: c.gardenerCertificateClient, gardenerDNSClient: c.gardenerDNSClient, + apiExtClient: c.apiExtClient, + promClient: c.promClient, kubeInformerFactory: dummyKubeInformerFactory, crdInformerFactory: c.crdInformerFactory, istioInformerFactory: c.istioInformerFactory, diff --git a/internal/controller/reconcile-capapplicationversion.go b/internal/controller/reconcile-capapplicationversion.go index e3458a5..d740a45 100644 --- a/internal/controller/reconcile-capapplicationversion.go +++ b/internal/controller/reconcile-capapplicationversion.go @@ -13,6 +13,7 @@ import ( "strings" "time" + monv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/sap/cap-operator/internal/util" "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" "golang.org/x/exp/slices" @@ -20,6 +21,8 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apiextensions-apiserver/pkg/apihelpers" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -31,8 +34,9 @@ const ( ) const ( - CategoryWorkload = "Workload" - CategoryService = "Service" + CategoryWorkload = "Workload" + CategoryService = "Service" + CategoryServiceMonitor = "ServiceMonitor" ) const ( @@ -397,7 +401,9 @@ func (c *Controller) updateServices(ca *v1alpha1.CAPApplication, cav *v1alpha1.C return err } } - return nil + + // attempt to reconcile service monitors + return c.updateServiceMonitors(context.TODO(), ca, cav, workloadServicePortInfos) } // newService creates a new Service for a CAV resource. It also sets the appropriate OwnerReferences. @@ -437,6 +443,104 @@ func newService(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion // #endregion Service +// #region ServiceMonitor +func (c *Controller) checkServiceMonitorCapability(ctx context.Context) error { + crdName := "servicemonitors.monitoring.coreos.com" + crd, err := c.apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crdName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("could not get custom resource definition %s: %v", crdName, err) + } + requiredVersion := "v1" + if !apihelpers.HasVersionServed(crd, requiredVersion) { + return fmt.Errorf("version %s of custom resource %s is not served", requiredVersion, crdName) + } + if !apihelpers.IsCRDConditionTrue(crd, apiextv1.Established) { + return fmt.Errorf("custom resource %s condition %s not true", crdName, apiextv1.Established) + } + return nil +} + +func (c *Controller) updateServiceMonitors(ctx context.Context, ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, workloadServicePortInfos []servicePortInfo) error { + if err := c.checkServiceMonitorCapability(ctx); err != nil { + util.LogWarning(err, "could not confirm availability of service monitor resource; service monitors will not be created") + return nil + } + + for i := range cav.Spec.Workloads { + wl := cav.Spec.Workloads[i] + if wl.DeploymentDefinition == nil || wl.DeploymentDefinition.Monitoring == nil || wl.DeploymentDefinition.Monitoring.ScrapeConfig == nil { + continue // do not reconcile service monitors + } + + var wlPortInfos *servicePortInfo + for j := range workloadServicePortInfos { + item := workloadServicePortInfos[j] + if item.WorkloadName == getWorkloadName(cav.Name, wl.Name) { + wlPortInfos = &item + break + } + } + if wlPortInfos == nil { + return fmt.Errorf("could not identify workload port information for workload %s in version %s", wl.Name, cav.Name) + } + + portVerified := false + for j := range wlPortInfos.Ports { + if wlPortInfos.Ports[j].Name == wl.DeploymentDefinition.Monitoring.ScrapeConfig.WorkloadPort { + portVerified = true + break + } + } + if !portVerified { + return fmt.Errorf("invalid port reference in workload %s monitoring config of version %s", wl.Name, cav.Name) + } + + sm, err := c.promClient.MonitoringV1().ServiceMonitors(cav.Namespace).Get(ctx, wlPortInfos.WorkloadName+ServiceSuffix, metav1.GetOptions{}) + if err != nil { + if k8sErrors.IsNotFound(err) { + sm, err = c.promClient.MonitoringV1().ServiceMonitors(cav.Namespace).Create(ctx, newServiceMonitor(ca, cav, &wl, wlPortInfos), metav1.CreateOptions{}) + if err == nil { + util.LogInfo("Service monitor created successfully", string(Processing), cav, sm, "version", cav.Spec.Version) + } + } + } + err = doChecks(err, sm, cav, wlPortInfos.WorkloadName+ServiceSuffix) + if err != nil { + return err + } + } + + return nil +} + +func newServiceMonitor(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion, wl *v1alpha1.WorkloadDetails, wlPortInfos *servicePortInfo) *monv1.ServiceMonitor { + config := wl.DeploymentDefinition.Monitoring.ScrapeConfig + return &monv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: wlPortInfos.WorkloadName + ServiceSuffix, + Namespace: cav.Namespace, + Labels: copyMaps(wl.Labels, getLabels(ca, cav, CategoryServiceMonitor, string(wl.DeploymentDefinition.Type), wlPortInfos.WorkloadName+ServiceSuffix, true)), + Annotations: copyMaps(wl.Annotations, getAnnotations(ca, cav, true)), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cav, v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.CAPApplicationVersionKind)), + }, + }, + Spec: monv1.ServiceMonitorSpec{ + Endpoints: []monv1.Endpoint{{ + Port: config.WorkloadPort, + Interval: monv1.Duration(config.ScrapeInterval), + ScrapeTimeout: monv1.Duration(config.Timeout), + Path: config.Path, + }}, + Selector: metav1.LabelSelector{ + MatchLabels: copyMaps(wl.Labels, getLabels(ca, cav, CategoryService, wlPortInfos.DeploymentType, wlPortInfos.WorkloadName+ServiceSuffix, false)), + }, + }, + } +} + +// #endregion ServiceMonitor + // #region NetworkPolicy func (c *Controller) updateNetworkPolicies(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVersion) error { var ( @@ -601,7 +705,7 @@ func newDeployment(ca *v1alpha1.CAPApplication, cav *v1alpha1.CAPApplicationVers } func createDeployment(params *DeploymentParameters) *appsv1.Deployment { - workloadName := params.CAV.Name + "-" + strings.ToLower(params.WorkloadDetails.Name) + workloadName := getWorkloadName(params.CAV.Name, params.WorkloadDetails.Name) annotations := copyMaps(params.WorkloadDetails.Annotations, getAnnotations(params.CA, params.CAV, true)) labels := copyMaps(params.WorkloadDetails.Labels, getLabels(params.CA, params.CAV, CategoryWorkload, string(params.WorkloadDetails.DeploymentDefinition.Type), workloadName, true)) diff --git a/internal/controller/reconcile-capapplicationversion_test.go b/internal/controller/reconcile-capapplicationversion_test.go index 183b51f..4f8c56d 100644 --- a/internal/controller/reconcile-capapplicationversion_test.go +++ b/internal/controller/reconcile-capapplicationversion_test.go @@ -7,6 +7,7 @@ package controller import ( "context" + "fmt" "testing" ) @@ -802,3 +803,46 @@ func TestCAV_DeploymentFailure(t *testing.T) { }, ) } + +func TestCAV_ServiceMonitorCreation(t *testing.T) { + reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version - service monitor creation", + initialResources: []string{ + "testdata/common/crd-servicemonitors.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-processing.yaml", + "testdata/capapplicationversion/deployments-ready.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + }, + expectedResources: "testdata/version-monitoring/servicemonitors-cav-v1.yaml", + backlogItems: []string{}, + }, + ) +} + +func TestCAV_InvalidMonitoringConfig(t *testing.T) { + err := reconcileTestItem( + context.TODO(), t, + QueueItem{Key: ResourceCAPApplicationVersion, ResourceKey: NamespacedResourceKey{Namespace: "default", Name: "test-cap-01-cav-v1"}}, + TestData{ + description: "capapplication version - service monitor creation", + initialResources: []string{ + "testdata/common/crd-servicemonitors.yaml", + "testdata/common/capapplication.yaml", + "testdata/common/credential-secrets.yaml", + "testdata/version-monitoring/cav-v1-monitoring-port-missing.yaml", + "testdata/capapplicationversion/deployments-ready.yaml", + "testdata/capapplicationversion/content-job-completed.yaml", + }, + expectError: true, + backlogItems: []string{}, + }, + ) + if err == nil || err.Error() != fmt.Sprintf("invalid port reference in workload %s monitoring config of version %s", "app-router", "test-cap-01-cav-v1") { + + } +} diff --git a/internal/controller/reconcile.go b/internal/controller/reconcile.go index b182576..2c941c7 100644 --- a/internal/controller/reconcile.go +++ b/internal/controller/reconcile.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/sap/cap-operator/internal/util" "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" @@ -46,6 +47,7 @@ const ( AnnotationKubernetesDNSTarget = "external-dns.alpha.kubernetes.io/hostname" AnnotationSubscriptionContextSecret = "sme.sap.com/subscription-context-secret" AnnotationProviderSubAccountId = "sme.sap.com/provider-sub-account-id" + AnnotationEnableCleanupMonitoring = "sme.sap.com/enable-cleanup-monitoring" FinalizerCAPApplication = "sme.sap.com/capapplication" FinalizerCAPApplicationVersion = "sme.sap.com/capapplicationversion" FinalizerCAPTenant = "sme.sap.com/captenant" @@ -547,7 +549,7 @@ func updateWorkloadPortInfo(cavName string, workloadName string, deploymentType if len(servicePorts) > 0 { workloadPortInfo = &servicePortInfo{ - WorkloadName: cavName + "-" + workloadName, + WorkloadName: getWorkloadName(cavName, workloadName), DeploymentType: string(deploymentType), Ports: servicePorts, Destinations: destinationDetails, @@ -593,3 +595,7 @@ func updateInitContainers(initContainers []corev1.Container, additionalEnv []cor } return &updatedInitContainers } + +func getWorkloadName(cavName, workloadName string) string { + return fmt.Sprintf("%s-%s", cavName, strings.ToLower(workloadName)) +} diff --git a/internal/controller/reconcile_test.go b/internal/controller/reconcile_test.go index d6ad544..d2995ba 100644 --- a/internal/controller/reconcile_test.go +++ b/internal/controller/reconcile_test.go @@ -27,10 +27,12 @@ import ( certfake "github.com/gardener/cert-management/pkg/client/cert/clientset/versioned/fake" dnsv1alpha1 "github.com/gardener/external-dns-management/pkg/apis/dns/v1alpha1" dnsfake "github.com/gardener/external-dns-management/pkg/client/dns/clientset/versioned/fake" + promopFake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" "github.com/sap/cap-operator/pkg/client/clientset/versioned/fake" istionwv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" + apiextFake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" ) const ( @@ -331,6 +333,10 @@ func getTestController(resources testResources) *Controller { crdClient := fake.NewSimpleClientset(crdObjects...) + apiExtClient := apiextFake.NewSimpleClientset() + + promopClient := promopFake.NewSimpleClientset() + istioClient := istiofake.NewSimpleClientset(istioObjects...) certClient := certfake.NewSimpleClientset(gardenerCertObjects...) @@ -339,7 +345,7 @@ func getTestController(resources testResources) *Controller { dnsClient := dnsfake.NewSimpleClientset(dnsObjects...) - c := NewController(coreClient, crdClient, istioClient, certClient, certManagerCertClient, dnsClient) + c := NewController(coreClient, crdClient, istioClient, certClient, certManagerCertClient, dnsClient, apiExtClient, promopClient) for _, ca := range resources.cas { if ca != nil { diff --git a/internal/controller/testdata/common/crd-servicemonitors.yaml b/internal/controller/testdata/common/crd-servicemonitors.yaml new file mode 100644 index 0000000..f829ea7 --- /dev/null +++ b/internal/controller/testdata/common/crd-servicemonitors.yaml @@ -0,0 +1,66 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + operator.prometheus.io/version: 0.76.0 + creationTimestamp: "2024-08-22T20:48:51Z" + generation: 1 + name: servicemonitors.monitoring.coreos.com + resourceVersion: "35356816" + uid: e9a5eb91-fa13-407b-86f4-58641b190d24 +spec: + conversion: + strategy: None + group: monitoring.coreos.com + names: + categories: + - prometheus-operator + kind: ServiceMonitor + listKind: ServiceMonitorList + plural: servicemonitors + shortNames: + - smon + singular: servicemonitor + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + required: + - spec + type: object + served: true + storage: true +status: + acceptedNames: + categories: + - prometheus-operator + kind: ServiceMonitor + listKind: ServiceMonitorList + plural: servicemonitors + shortNames: + - smon + singular: servicemonitor + conditions: + - lastTransitionTime: "2024-08-22T20:48:52Z" + message: no conflicts found + reason: NoConflicts + status: "True" + type: NamesAccepted + - lastTransitionTime: "2024-08-22T20:48:52Z" + message: the initial names have been accepted + reason: InitialNamesAccepted + status: "True" + type: Established + storedVersions: + - v1 diff --git a/internal/controller/testdata/version-monitoring/ca-cleanup-dry-run-enabled.yaml b/internal/controller/testdata/version-monitoring/ca-cleanup-dry-run-enabled.yaml new file mode 100644 index 0000000..88dc073 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/ca-cleanup-dry-run-enabled.yaml @@ -0,0 +1,41 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + annotations: + sme.sap.com/enable-cleanup-monitoring: "dry-run" + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: xsuaa + name: cap-uaa2 + secret: cap-cap-01-uaa2-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider diff --git a/internal/controller/testdata/version-monitoring/ca-cleanup-enabled.yaml b/internal/controller/testdata/version-monitoring/ca-cleanup-enabled.yaml new file mode 100644 index 0000000..6101a39 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/ca-cleanup-enabled.yaml @@ -0,0 +1,41 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplication +metadata: + finalizers: + - sme.sap.com/capapplication + generation: 2 + name: test-cap-01 + namespace: default + annotations: + sme.sap.com/enable-cleanup-monitoring: "true" + resourceVersion: "11373799" + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + btp: + services: + - class: xsuaa + name: cap-uaa + secret: cap-cap-01-uaa-bind-cf + - class: xsuaa + name: cap-uaa2 + secret: cap-cap-01-uaa2-bind-cf + - class: saas-registry + name: cap-saas-registry + secret: cap-cap-01-saas-bind-cf + - class: service-manager + name: cap-service-manager + secret: cap-cap-01-svc-man-bind-cf + btpAppName: test-cap-01 + domains: + istioIngressGatewayLabels: + - name: app + value: istio-ingressgateway + - name: istio + value: ingressgateway + primary: app-domain.test.local + secondary: + - foo.bar.local + globalAccountId: btp-glo-acc-id + provider: + subDomain: my-provider + tenantId: tenant-id-for-provider diff --git a/internal/controller/testdata/version-monitoring/cat-consumer-v2-ready-never.yaml b/internal/controller/testdata/version-monitoring/cat-consumer-v2-ready-never.yaml new file mode 100644 index 0000000..4cd0bf7 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cat-consumer-v2-ready-never.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 8.9.10 + versionUpgradeStrategy: never +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 diff --git a/internal/controller/testdata/version-monitoring/cat-consumer-v2-upgrading.yaml b/internal/controller/testdata/version-monitoring/cat-consumer-v2-upgrading.yaml new file mode 100644 index 0000000..4cef901 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cat-consumer-v2-upgrading.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-consumer + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: consumer + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + name: test-cap-01-consumer + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-consumer + tenantId: tenant-id-for-consumer + version: 11.12.13 + versionUpgradeStrategy: always +status: + state: Upgrading + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 + conditions: + - message: "waiting for CAPTenantOperation default.test-cap-01-consumer-ctop-gen of type upgrade to complete" + reason: UpgradeOperationCreated + status: "False" + type: Ready diff --git a/internal/controller/testdata/version-monitoring/cat-provider-v2-ready.yaml b/internal/controller/testdata/version-monitoring/cat-provider-v2-ready.yaml new file mode 100644 index 0000000..2088a5c --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cat-provider-v2-ready.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/subscription-context-secret: test-cap-01-gen + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 8.9.10 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v2 diff --git a/internal/controller/testdata/version-monitoring/cat-provider-v3-ready.yaml b/internal/controller/testdata/version-monitoring/cat-provider-v3-ready.yaml new file mode 100644 index 0000000..8461399 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cat-provider-v3-ready.yaml @@ -0,0 +1,38 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPTenant +metadata: + finalizers: + - sme.sap.com/captenant + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + sme.sap.com/subscription-context-secret: test-cap-01-gen + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/btp-tenant-id: tenant-id-for-provider + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + sme.sap.com/tenant-type: provider + name: test-cap-01-provider + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 +spec: + capApplicationInstance: test-cap-01 + subDomain: my-provider + tenantId: tenant-id-for-provider + version: 11.12.13 + versionUpgradeStrategy: always +status: + conditions: + - message: "CAPTenantOperation default.test-cap-01-provider-s6f4l successfully completed" + reason: ProvisioningCompleted + status: "True" + type: Ready + state: Ready + currentCAPApplicationVersionInstance: test-cap-01-cav-v3 diff --git a/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules-error.yaml b/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules-error.yaml new file mode 100644 index 0000000..776db1e --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules-error.yaml @@ -0,0 +1,91 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + thresholdValue: "0" + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest + monitoring: + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" + - name: no-rules + consumedBTPServices: [] + deploymentDefinition: + type: Additional + image: docker.image.repo/some/image:latest + monitoring: {} +status: + conditions: + - reason: ErrorInWorkloadStatus + observedGeneration: 1 + status: "False" + type: Ready + message: "content deployer error in job 'test-cap-01-cav-v1-content'" + observedGeneration: 1 + finishedJobs: + - "test-cap-01-cav-v1-content" + state: Error diff --git a/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules-processing.yaml b/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules-processing.yaml new file mode 100644 index 0000000..385decf --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules-processing.yaml @@ -0,0 +1,93 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + thresholdValue: "0" + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest + ports: + - name: metrics-port + port: 9000 + appProtocol: http + networkPolicy: Cluster + monitoring: + scrapeConfig: + port: metrics-port + interval: 10s + path: /metrics + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" +status: + conditions: + - reason: ReadyForProcessing + status: "False" + observedGeneration: 1 + type: Ready + finishedJobs: + - test-cap-01-cav-v1-content + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules.yaml b/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules.yaml new file mode 100644 index 0000000..fa70b6e --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v1-deletion-rules.yaml @@ -0,0 +1,93 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + thresholdValue: "0" + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest + ports: + - name: metrics-port + port: 9000 + appProtocol: http + networkPolicy: Cluster + monitoring: + scrapeConfig: + port: metrics-port + interval: 10s + path: /metrics + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" +status: + conditions: + - reason: WorkloadsReady + observedGeneration: 1 + status: "True" + type: Ready + observedGeneration: 1 + finishedJobs: + - test-cap-01-cav-v1-content + state: Ready diff --git a/internal/controller/testdata/version-monitoring/cav-v1-monitoring-port-missing.yaml b/internal/controller/testdata/version-monitoring/cav-v1-monitoring-port-missing.yaml new file mode 100644 index 0000000..fe4de36 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v1-monitoring-port-missing.yaml @@ -0,0 +1,88 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + creationTimestamp: "2022-03-18T22:14:33Z" + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v1 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + registrySecrets: + - regcred + version: 5.6.7 + workloads: + - name: cap-backend-srv + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:latest + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + thresholdValue: "0" + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:latest + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:latest + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:latest + monitoring: + scrapeConfig: + port: metrics-port + interval: 10s + path: /metrics + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" +status: + conditions: + - reason: ReadyForProcessing + status: "False" + observedGeneration: 1 + type: Ready + finishedJobs: + - test-cap-01-cav-v1-content + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/version-monitoring/cav-v2-deletion-rules.yaml b/internal/controller/testdata/version-monitoring/cav-v2-deletion-rules.yaml new file mode 100644 index 0000000..60c8624 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v2-deletion-rules.yaml @@ -0,0 +1,83 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v2 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "11371108" + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + version: 8.9.10 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v2 + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v2 + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:v2 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v2 + monitoring: + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: WorkloadsReady + status: "True" + type: Ready + finishedJobs: + - test-cap-01-cav-v2-content + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/version-monitoring/cav-v3-deletion-rules-processing.yaml b/internal/controller/testdata/version-monitoring/cav-v3-deletion-rules-processing.yaml new file mode 100644 index 0000000..045bc3e --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v3-deletion-rules-processing.yaml @@ -0,0 +1,83 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v3 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "113715468" + uid: 5e64489b-1234-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + version: 11.12.13 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v3 + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + thresholdValue: "0" + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v3 + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:v3 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v3 + monitoring: + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" +status: + conditions: + - reason: ReadyForProcessing + status: "False" + observedGeneration: 1 + type: Ready + finishedJobs: + - test-cap-01-cav-v3-content + observedGeneration: 1 + state: Processing diff --git a/internal/controller/testdata/version-monitoring/cav-v3-deletion-rules.yaml b/internal/controller/testdata/version-monitoring/cav-v3-deletion-rules.yaml new file mode 100644 index 0000000..9299d47 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/cav-v3-deletion-rules.yaml @@ -0,0 +1,84 @@ +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + generation: 1 + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01 + labels: + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/owner-generation: "2" + sme.sap.com/owner-identifier-hash: 1f74ae2fbff71a708786a4df4bb2ca87ec603581 + name: test-cap-01-cav-v3 + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplication + name: test-cap-01 + uid: 3c7ba7cb-dc04-4fd1-be86-3eb3a5c64a98 + resourceVersion: "113715468" + uid: 5e64489b-1234-4984-8617-e8c37338b3d8 + finalizers: + - sme.sap.com/capapplicationversion +spec: + capApplicationInstance: test-cap-01 + version: 11.12.13 + registrySecrets: + - regcred + workloads: + - name: cap-backend + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + deploymentDefinition: + type: CAP + image: docker.image.repo/srv/server:v3 + monitoring: + deletionRules: + metrics: + - name: total_http_requests + type: Counter + calculationPeriod: 2m + thresholdValue: "0.01" + - name: active_jobs + type: Gauge + calculationPeriod: 3m + thresholdValue: "0" + - name: content + consumedBTPServices: + - cap-uaa + jobDefinition: + type: Content + image: docker.image.repo/content/cap-content:v3 + - name: mtx + consumedBTPServices: + - cap-uaa + - cap-service-manager + - cap-saas-registry + jobDefinition: + type: "TenantOperation" + image: docker.image.repo/srv/server:v3 + - name: app-router + consumedBTPServices: + - cap-uaa + - cap-saas-registry + deploymentDefinition: + type: Router + image: docker.image.repo/approuter/approuter:v3 + monitoring: + deletionRules: + expression: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1" +status: + conditions: + - lastTransitionTime: "2022-03-18T23:07:47Z" + lastUpdateTime: "2022-03-18T23:07:47Z" + reason: WorkloadsReady + status: "True" + type: Ready + finishedJobs: + - test-cap-01-cav-v3-content + observedGeneration: 1 + state: Ready diff --git a/internal/controller/testdata/version-monitoring/servicemonitors-cav-v1.yaml b/internal/controller/testdata/version-monitoring/servicemonitors-cav-v1.yaml new file mode 100644 index 0000000..9ffbe98 --- /dev/null +++ b/internal/controller/testdata/version-monitoring/servicemonitors-cav-v1.yaml @@ -0,0 +1,38 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + annotations: + sme.sap.com/btp-app-identifier: btp-glo-acc-id.test-cap-01 + sme.sap.com/owner-identifier: default.test-cap-01-cav-v1 + labels: + app: test-cap-01 + sme.sap.com/category: ServiceMonitor + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router-svc + sme.sap.com/workload-type: Router + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/cav-version: "5.6.7" + sme.sap.com/owner-generation: "1" + sme.sap.com/owner-identifier-hash: e95e0682f33a657e75e1fc435972d19bd407ba3b + name: test-cap-01-cav-v1-app-router-svc + namespace: default + ownerReferences: + - apiVersion: sme.sap.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: CAPApplicationVersion + name: test-cap-01-cav-v1 + uid: 5e64489b-7346-4984-8617-e8c37338b3d8 +spec: + endpoints: + - interval: 10s + path: /metrics + port: metrics-port + namespaceSelector: {} + selector: + matchLabels: + app: test-cap-01 + sme.sap.com/btp-app-identifier-hash: f20cc8aeb2003b3abc33f749a16bd53544b6bab2 + sme.sap.com/category: Service + sme.sap.com/cav-version: "5.6.7" + sme.sap.com/workload-name: test-cap-01-cav-v1-app-router-svc + sme.sap.com/workload-type: Router diff --git a/internal/controller/version-monitoring.go b/internal/controller/version-monitoring.go new file mode 100644 index 0000000..1b9c472 --- /dev/null +++ b/internal/controller/version-monitoring.go @@ -0,0 +1,390 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + promapi "github.com/prometheus/client_golang/api" + promv1 "github.com/prometheus/client_golang/api/prometheus/v1" + prommodel "github.com/prometheus/common/model" + "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" + "golang.org/x/mod/semver" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" +) + +const ( + EnvPrometheusAddress = "PROMETHEUS_ADDRESS" + EnvPrometheusAcquireClientRetryDelay = "PROM_ACQUIRE_CLIENT_RETRY_DELAY" // Value should be a duration + EnvMetricsEvaluationInterval = "METRICS_EVAL_INTERVAL" +) + +const ( + CAPApplicationVersionEventReadForDeletion = "ReadyForDeletion" + EventActionEvaluateMetrics = "EvaluateMetrics" +) + +const ( + GaugeEvaluationExpression = "sum(avg_over_time(%s{job=\"%s\",namespace=\"%s\"}[%s]))" + CounterEvaluationExpression = "sum(rate(%s{job=\"%s\",namespace=\"%s\"}[%s]))" +) + +type cleanupOrchestrator struct { + api promv1.API + queue workqueue.TypedRateLimitingInterface[NamespacedResourceKey] + mEnv *monitoringEnv +} + +type monitoringEnv struct { + address string + acquireClientRetryDelay time.Duration + evaluationInterval time.Duration +} + +func parseMonitoringEnv() *monitoringEnv { + promAdd := strings.TrimSpace(os.Getenv(EnvPrometheusAddress)) + if promAdd == "" { + return nil + } + env := &monitoringEnv{address: promAdd} + + evalDurationEnv := func(envName string, fallback time.Duration) time.Duration { + if v, ok := os.LookupEnv(envName); ok && strings.TrimSpace(v) != "" { + dur, err := time.ParseDuration(strings.TrimSpace(v)) + if err == nil { + return dur + } + } + return fallback + } + env.acquireClientRetryDelay = evalDurationEnv(EnvPrometheusAcquireClientRetryDelay, time.Hour) + env.evaluationInterval = evalDurationEnv(EnvMetricsEvaluationInterval, 10*time.Minute) + return env +} + +func (c *Controller) startVersionCleanup(ctx context.Context) { + mEnv := parseMonitoringEnv() + if mEnv == nil { + return // no prometheus address + } + + restartSignal := make(chan bool, 1) + setup := func() context.CancelFunc { + for { + o := initializeVersionCleanupOrchestrator(ctx, mEnv) + if o == nil { + select { + case <-ctx.Done(): + return nil + case <-time.After(mEnv.acquireClientRetryDelay): // sleep a long time before attempting to setup the cleanup process + continue + } + } + child, cancelFn := context.WithCancel(ctx) + go func() { + <-child.Done() + o.queue.ShutDown() + }() + go c.scheduleVersionCollectionForCleanup(child, o, restartSignal) + go c.processVersionCleanupQueue(child, o, restartSignal) + return cancelFn + } + } + + for { + cancel := setup() + select { + case <-ctx.Done(): + return + case <-restartSignal: // restart broken routines + cancel() + } + } +} + +func recoverVersionCleanupRoutine(restart chan<- bool) { + if r := recover(); r != nil { + err := fmt.Errorf("panic@version-cleanup: %v", r) + klog.ErrorS(err, "recovered from panic") + select { // send restart signal restart process + case restart <- true: // send to channel if empty (channel size 1) + default: + } + } +} + +func initializeVersionCleanupOrchestrator(ctx context.Context, mEnv *monitoringEnv) *cleanupOrchestrator { + promClient, err := promapi.NewClient(promapi.Config{Address: mEnv.address}) + if err != nil { + klog.ErrorS(err, "could not create client", "address", mEnv.address) + return nil + } + v1api := promv1.NewAPI(promClient) + _, err = v1api.Runtimeinfo(ctx) + if err != nil { + klog.ErrorS(err, "could not fetch runtime info from prometheus server", "address", mEnv.address) + return nil + } + + // create orchestrator + return &cleanupOrchestrator{ + api: v1api, + queue: workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[NamespacedResourceKey]()), + mEnv: mEnv, + } +} + +func (c *Controller) scheduleVersionCollectionForCleanup(ctx context.Context, orc *cleanupOrchestrator, restart chan<- bool) { + defer recoverVersionCleanupRoutine(restart) + for { + if err := c.queueVersionsForCleanupEvaluation(ctx, orc); err != nil { + klog.ErrorS(err, "could not select applications for version cleanup evaluation") + } + select { + case <-ctx.Done(): + return + case <-time.After(orc.mEnv.evaluationInterval): // sleep for (say 10m) before reading versions again + continue + } + } +} + +func (c *Controller) queueVersionsForCleanupEvaluation(ctx context.Context, orc *cleanupOrchestrator) error { + lister := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister() + cas, err := lister.List(labels.Everything()) + if err != nil { + return err + } + + for i := range cas { + ca := cas[i] + if v, ok := ca.Annotations[AnnotationEnableCleanupMonitoring]; !ok || !(strings.ToLower(v) == "true" || strings.ToLower(v) == "dry-run") { + continue + } + outdated, err := c.getCleanupRelevantVersions(ctx, ca) + if err != nil || len(outdated) == 0 { + continue + } + for n := range outdated { + cav := outdated[n] + orc.queue.Add(NamespacedResourceKey{Namespace: cav.Namespace, Name: cav.Name}) + } + } + return nil +} + +func (c *Controller) getCleanupRelevantVersions(ctx context.Context, ca *v1alpha1.CAPApplication) ([]*v1alpha1.CAPApplicationVersion, error) { + excludedVersions := map[string]bool{} + excludedVersionNames := map[string]bool{} + + selector, _ := labels.ValidatedSelectorFromSet(map[string]string{ + LabelOwnerIdentifierHash: sha1Sum(ca.Namespace, ca.Name), + }) + tenantLister := c.crdInformerFactory.Sme().V1alpha1().CAPTenants().Lister() + cats, err := tenantLister.CAPTenants(ca.Namespace).List(selector) + if err != nil { + return nil, err + } + for i := range cats { + cat := cats[i] + if cat.Spec.Version != "" { + excludedVersions[cat.Spec.Version] = true + } + if cat.Status.CurrentCAPApplicationVersionInstance != "" { + excludedVersionNames[cat.Status.CurrentCAPApplicationVersionInstance] = true + } + } + + latestReadyVersion, err := c.getLatestReadyCAPApplicationVersion(ctx, ca, true) + if err != nil || latestReadyVersion == nil { + // if there are no Ready versions yet - do not initiate cleanup + return nil, err + } + + outdatedVersions := []*v1alpha1.CAPApplicationVersion{} + cavs, _ := c.getCachedCAPApplicationVersions(ctx, ca) // ignoring error as this is not critical + for i := range cavs { + cav := cavs[i] + // ignore all versions greater than latest Ready one + if semver.Compare("v"+cav.Spec.Version, "v"+latestReadyVersion.Spec.Version) == 1 { + continue + } + if excludedVersions[cav.Spec.Version] || excludedVersionNames[cav.Name] { + continue // filter out versions attached to tenants + } + outdatedVersions = append(outdatedVersions, cav) + } + + return outdatedVersions, nil +} + +func (c *Controller) processVersionCleanupQueue(ctx context.Context, orc *cleanupOrchestrator, restart chan<- bool) { + defer recoverVersionCleanupRoutine(restart) + for { + select { + case <-ctx.Done(): + return + default: + if stop := c.processVersionCleanupQueueItem(ctx, orc); stop { + return + } + } + } +} + +func (c *Controller) processVersionCleanupQueueItem(ctx context.Context, orc *cleanupOrchestrator) (stop bool) { + item, shutdown := orc.queue.Get() + if shutdown { + return true // stop processing + } + defer orc.queue.Done(item) + + if err := c.evaluateVersionForCleanup(ctx, item, orc.api); err != nil { + orc.queue.AddRateLimited(item) + } else { + orc.queue.Forget(item) + } + return false +} + +func (c *Controller) evaluateVersionForCleanup(ctx context.Context, item NamespacedResourceKey, promapi promv1.API) error { + lister := c.crdInformerFactory.Sme().V1alpha1().CAPApplicationVersions().Lister() + cav, err := lister.CAPApplicationVersions(item.Namespace).Get(item.Name) + if err != nil { + return handleOperatorResourceErrors(err) + } + + // read CAPApplication to determine dry-run mode + ca, err := c.crdInformerFactory.Sme().V1alpha1().CAPApplications().Lister().CAPApplications(cav.Namespace).Get(cav.Spec.CAPApplicationInstance) + if err != nil { + return err + } + + cleanup := true + for i := range cav.Spec.Workloads { + wl := cav.Spec.Workloads[i] + workloadEvaluation := true + if wl.DeploymentDefinition != nil && wl.DeploymentDefinition.Monitoring != nil && wl.DeploymentDefinition.Monitoring.DeletionRules != nil { + if wl.DeploymentDefinition.Monitoring.DeletionRules.ScalarExpression != nil { // evaluate provided expression + expr := strings.TrimSpace(*wl.DeploymentDefinition.Monitoring.DeletionRules.ScalarExpression) + if expr == "" { + workloadEvaluation = false + } else { + isRelevantForCleanup, err := evaluateExpression(ctx, expr, promapi) + if err != nil || !isRelevantForCleanup { + if err != nil { + klog.ErrorS(err, "could not evaluate PromQL expression for workload", "workload", wl.Name, "version", cav.Name) + } + workloadEvaluation = false + } + } + } else { + for j := range wl.DeploymentDefinition.Monitoring.DeletionRules.Metrics { + rule := wl.DeploymentDefinition.Monitoring.DeletionRules.Metrics[j] + isRelevantForCleanup, err := evaluateMetric(ctx, &rule, fmt.Sprintf("%s%s", getWorkloadName(cav.Name, wl.Name), ServiceSuffix), cav.Namespace, promapi) + if err != nil || !isRelevantForCleanup { + if err != nil { + klog.ErrorS(err, "could not evaluate metric for workload", "workload", wl.Name, "version", cav.Name) + } + workloadEvaluation = false + break + } + } + } + } + if !workloadEvaluation { + cleanup = false + break + } + } + + if cleanup { + klog.InfoS("version has been evaluated to be ready for deletion", "version", cav.Name) + c.Event(cav, nil, corev1.EventTypeNormal, CAPApplicationVersionEventReadForDeletion, EventActionEvaluateMetrics, fmt.Sprintf("version %s is now ready for deletion", cav.Name)) + + if v, ok := ca.Annotations[AnnotationEnableCleanupMonitoring]; ok && strings.ToLower(v) == "true" { + return c.crdClient.SmeV1alpha1().CAPApplicationVersions(cav.Namespace).Delete(ctx, cav.Name, v1.DeleteOptions{}) + } + } + + return nil +} + +func executePromQL(ctx context.Context, promapi promv1.API, query string) (prommodel.Value, error) { + // klog.InfoS("executing prometheus query", "query", query) + result, warnings, err := promapi.Query(ctx, query, time.Now()) + if err != nil { + klog.ErrorS(err, "prometheus query error", "query", query) + return nil, err + } + if len(warnings) > 0 { + klog.InfoS(fmt.Sprintf("query %s returned warnings [%s]", query, strings.Join(warnings, ", "))) + } + klog.InfoS(fmt.Sprintf("query %s returned result: %v", query, result)) + return result, nil +} + +func evaluateExpression(ctx context.Context, expr string, promapi promv1.API) (bool, error) { + result, err := executePromQL(ctx, promapi, expr) + if err != nil { + return false, err + } + + s, ok := result.(*prommodel.Scalar) + if !ok { + err := fmt.Errorf("result from query %s could not be casted as a scalar", expr) + klog.ErrorS(err, "error parsing query result") + return false, err + } + + return s.Value == 1, nil // expecting a boolean result +} + +func evaluateMetric(ctx context.Context, rule *v1alpha1.MetricRule, job, ns string, promapi promv1.API) (bool, error) { + query := "" + switch rule.Type { + case v1alpha1.MetricTypeGauge: + query = fmt.Sprintf(GaugeEvaluationExpression, rule.Name, job, ns, rule.CalculationPeriod) + case v1alpha1.MetricTypeCounter: + query = fmt.Sprintf(CounterEvaluationExpression, rule.Name, job, ns, rule.CalculationPeriod) + default: + return false, fmt.Errorf("metric %s has unsupported type %s", rule.Name, rule.Type) + } + + result, err := executePromQL(ctx, promapi, query) + if err != nil { + return false, err + } + + vec, ok := result.(prommodel.Vector) + if !ok { + err := fmt.Errorf("result from query %s could not be casted as a vector", query) + klog.ErrorS(err, "error parsing query result") + return false, err + } + if len(vec) > 0 { + sample := vec[0] // use the first one - expecting only one sample based on the expressions + var threshold prommodel.SampleValue + err = threshold.UnmarshalJSON([]byte(fmt.Sprintf("\"%s\"", rule.ThresholdValue))) + if err != nil { + klog.ErrorS(err, "error parsing threshold value", "value", rule.ThresholdValue, "metric", rule.Name) + return false, err + } + klog.InfoS("parsed prometheus query result and threshold", "threshold", threshold.String(), "result", sample.Value.String(), "query", query) + return sample.Value <= threshold, nil + } else { + // there could be no results if the version was not transmitting metrics for a very long time + return true, nil + } +} diff --git a/internal/controller/version-monitoring_test.go b/internal/controller/version-monitoring_test.go new file mode 100644 index 0000000..6693938 --- /dev/null +++ b/internal/controller/version-monitoring_test.go @@ -0,0 +1,604 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package controller + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "sync" + "testing" + "time" + + prommodel "github.com/prometheus/common/model" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" +) + +func TestMonitoringEnv(t *testing.T) { + expAdd := "http://prom.server.local" + expAcqRetryInt := "10s" + expEvalInt := "3h" + tests := []struct { + add *string + acqRetryInt *string + evalInt *string + }{ + {}, + {add: &expAdd, acqRetryInt: &expAcqRetryInt, evalInt: &expEvalInt}, + {add: &expAdd}, + } + + for _, tt := range tests { + t.Run("test monitoring env", func(t *testing.T) { + if tt.add != nil { + os.Setenv(EnvPrometheusAddress, *tt.add) + defer os.Unsetenv(EnvPrometheusAddress) + } + if tt.acqRetryInt != nil { + os.Setenv(EnvPrometheusAcquireClientRetryDelay, *tt.acqRetryInt) + defer os.Unsetenv(EnvPrometheusAcquireClientRetryDelay) + } + if tt.evalInt != nil { + os.Setenv(EnvMetricsEvaluationInterval, *tt.evalInt) + defer os.Unsetenv(EnvMetricsEvaluationInterval) + } + + mEnv := parseMonitoringEnv() + + if tt.add == nil { + if mEnv != nil { + t.Errorf("did not expect monitoring environment") + } + return + } + if tt.acqRetryInt != nil { + exp, _ := time.ParseDuration(*tt.acqRetryInt) + if mEnv.acquireClientRetryDelay != exp { + t.Errorf("expected acquire client retry interval to be %s", *tt.acqRetryInt) + } + } else { + if mEnv.acquireClientRetryDelay != time.Hour { + t.Errorf("expected default acquire client retry interval") + } + } + if tt.evalInt != nil { + exp, _ := time.ParseDuration(*tt.evalInt) + if mEnv.evaluationInterval != exp { + t.Errorf("expected evaluation interval to be %s", *tt.evalInt) + } + } else { + if mEnv.evaluationInterval != 10*time.Minute { + t.Errorf("expected default evaluation interval") + } + } + }) + } +} + +func setupTestControllerWithInitialResources(t *testing.T, initialResources []string) *Controller { + c := initializeControllerForReconciliationTests(t, []ResourceAction{}) + var wg sync.WaitGroup + work := func(file string) { + defer wg.Done() + raw, err := readYAMLResourcesFromFile(file) + if err != nil { + t.Errorf("error reading resources from file %s: %s", file, err.Error()) + return + } + for j := range raw { + err = addInitialObjectToStore(raw[j], c) + if err != nil { + t.Error(err) + return + } + } + } + + for i := range initialResources { + wg.Add(1) + go work(initialResources[i]) + } + wg.Wait() + + return c +} + +func TestGracefulShutdownMonitoringRoutines(t *testing.T) { + c := setupTestControllerWithInitialResources(t, []string{}) + + s, _ := getPromServer(false, []queryTestCase{}) + defer s.Close() + + os.Setenv(EnvPrometheusAddress, s.URL) + defer os.Unsetenv(EnvPrometheusAddress) + + testCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + c.startVersionCleanup(testCtx) + wg.Done() + }() + + wg.Wait() // check whether routines are closing - or test timeout +} + +func TestVersionSelectionForCleanup(t *testing.T) { + tests := []struct { + name string + resources []string + expectedVersions []string + expectError bool + }{ + { + name: "select versions not assigned to tenants", + resources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + "testdata/version-monitoring/cav-v3-deletion-rules.yaml", + "testdata/version-monitoring/cat-provider-v3-ready.yaml", + }, + expectedVersions: []string{"default.test-cap-01-cav-v1", "default.test-cap-01-cav-v2"}, + }, + { + name: "version cleanup must ignore CAPApplications without specified annotation", + resources: []string{ + "testdata/common/capapplication.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + "testdata/version-monitoring/cav-v3-deletion-rules.yaml", + "testdata/version-monitoring/cat-provider-v3-ready.yaml", + }, + expectedVersions: []string{}, + }, + { + name: "should not consider versions higher than the latest Ready version", + resources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + "testdata/version-monitoring/cav-v3-deletion-rules-processing.yaml", + "testdata/version-monitoring/cat-provider-v2-ready.yaml", + }, + expectedVersions: []string{"default.test-cap-01-cav-v1"}, + }, + { + name: "should not consider any version when there are no Ready versions", + resources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v3-deletion-rules-processing.yaml", + }, + expectedVersions: []string{}, + }, + { + name: "should not consider versions with tenants (using dry-run)", + resources: []string{ + "testdata/version-monitoring/ca-cleanup-dry-run-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + "testdata/version-monitoring/cav-v3-deletion-rules.yaml", + "testdata/version-monitoring/cat-consumer-v2-upgrading.yaml", + }, + expectedVersions: []string{"default.test-cap-01-cav-v1"}, + }, + } + + getQueuedItems := func(o *cleanupOrchestrator) []string { + res := []string{} + for { + i, stop := o.queue.Get() + if stop { + return res + } + o.queue.Done(i) + res = append(res, fmt.Sprintf("%s.%s", i.Namespace, i.Name)) + o.queue.Forget(i) + } + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + c := setupTestControllerWithInitialResources(t, tc.resources) + orc := &cleanupOrchestrator{queue: workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[NamespacedResourceKey]())} + defer orc.queue.ShutDown() + err := c.queueVersionsForCleanupEvaluation(context.TODO(), orc) + if err != nil { + if !tc.expectError { + t.Errorf("not expecting error for test case %s -> error: %s", tc.name, err.Error()) + } + return + } + evs := map[string]bool{} + for _, s := range tc.expectedVersions { + evs[s] = false + } + orc.queue.ShutDownWithDrain() // allows existing items to be processed before shutting down + results := getQueuedItems(orc) + for _, r := range results { + if _, ok := evs[r]; ok { + evs[r] = true + } else { + t.Errorf("unexpected version %s queued for cleanup", r) + } + } + for exp, found := range evs { + if !found { + t.Errorf("was expecting version %s to be queued for cleanup", exp) + } + } + }) + } +} + +type queryTestCase struct { + expectedQuery string + simulateError bool + simulateEmptyResult bool + simulatedResultType string // vector | scalar | invalid + simulatedValue float64 +} + +type evalTestConfig struct { + name string + evaluatedVersion string + startResources []string + expectCleanup bool + expectError bool + cases []queryTestCase +} + +func mockPromRuntimeInfoHandler(simError bool, w http.ResponseWriter) { + if simError { + w.WriteHeader(http.StatusServiceUnavailable) + } else { + io.WriteString(w, ` + { + "status": "success", + "data": { + "CWD": "/", + "goroutineCount": 48, + "GOMAXPROCS": 4 + } + } + `) + } +} + +func mockPromQueryHandler(testCases []queryTestCase, query string, w http.ResponseWriter) { + var tCase *queryTestCase + for i := range testCases { + tc := testCases[i] + if tc.expectedQuery == query { + tCase = &tc + break + } + } + if tCase == nil { + io.WriteString(w, ` + { + "status": "error", + "errorType": "TestCaseMismatch", + "error": "could not match received query to a specified test case" + } + `) + return + } + if tCase.simulateError { + io.WriteString(w, ` + { + "status": "error", + "errorType": "SimulatedError", + "error": "simulated error" + } + `) + return + } + if tCase.simulateEmptyResult { + io.WriteString(w, + fmt.Sprintf(`{ + "status": "success", + "data": { + "resultType": "%s", + "result": [] + } + }`, tCase.simulatedResultType), + ) + } + + getScalar := func() *prommodel.Scalar { + return &prommodel.Scalar{ + Timestamp: prommodel.Now(), + Value: prommodel.SampleValue(tCase.simulatedValue), + } + } + + getVector := func() *prommodel.Vector { + return &prommodel.Vector{{ + Timestamp: prommodel.Now(), + Value: prommodel.SampleValue(tCase.simulatedValue), + Metric: prommodel.Metric{}, + }} + } + + var ( + raw []byte + err error + ) + switch tCase.simulatedResultType { + case "scalar": + raw, err = getScalar().MarshalJSON() + case "vector": + raw, err = json.Marshal(getVector()) + case "invalid": + raw = []byte("{\"property\":\"invalid\"}") + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + val := string(raw) + + io.WriteString(w, + fmt.Sprintf(`{ + "status": "success", + "data": { + "resultType": "%s", + "result": %s + } + }`, tCase.simulatedResultType, val), + ) +} + +func getPromServer(unavailable bool, cases []queryTestCase) (*httptest.Server, func() map[string]bool) { + calledQueries := map[string]bool{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/v1/status/runtimeinfo" { + mockPromRuntimeInfoHandler(unavailable, w) + return + } + if r.URL.Path == "/api/v1/query" { + q := r.FormValue("query") + if q != "" { + calledQueries[q] = false + } + mockPromQueryHandler(cases, q, w) + return + } + w.WriteHeader(http.StatusInternalServerError) // unexpected path + })) + return server, func() map[string]bool { + return calledQueries + } +} + +func Test_initializeVersionCleanupOrchestrator(t *testing.T) { + tests := []struct { + name string + serverUnavailable bool + }{ + { + name: "initialize cleanup orchestrator and verify connection", + serverUnavailable: false, + }, + { + name: "ensure retry of cleanup orchestrator initialization", + serverUnavailable: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, _ := getPromServer(tt.serverUnavailable, []queryTestCase{}) + defer s.Close() + var o *cleanupOrchestrator + testCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + go func() { + o = initializeVersionCleanupOrchestrator(testCtx, &monitoringEnv{address: s.URL, evaluationInterval: 2 * time.Minute, acquireClientRetryDelay: 30 * time.Second}) + if o != nil { + cancel() + } + }() + <-testCtx.Done() + if tt.serverUnavailable { + if testCtx.Err() == nil || testCtx.Err() != context.DeadlineExceeded { + t.Error("expected to exceed test context deadline") + } + } else { + if o == nil { + t.Errorf("could not initialize prometheus client") + } + defer o.queue.ShutDown() + } + + }) + } +} + +func TestVersionCleanupEvaluation(t *testing.T) { + tests := []evalTestConfig{ + { + name: "evaluate version with missing application - expect error", + evaluatedVersion: "test-cap-01-cav-v1", + startResources: []string{ + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + }, + expectCleanup: false, + expectError: true, + cases: []queryTestCase{}, + }, + { + name: "evaluate version workloads - expecting deletion", + evaluatedVersion: "test-cap-01-cav-v1", + startResources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + }, + expectCleanup: true, + expectError: false, + cases: []queryTestCase{ + { + expectedQuery: "sum(rate(total_http_requests{job=\"test-cap-01-cav-v1-cap-backend-srv-svc\",namespace=\"default\"}[2m]))", + simulateError: false, + simulateEmptyResult: false, + simulatedResultType: "vector", + simulatedValue: 0.005, + }, + { + expectedQuery: "sum(avg_over_time(active_jobs{job=\"test-cap-01-cav-v1-cap-backend-srv-svc\",namespace=\"default\"}[3m]))", + simulateError: false, + simulateEmptyResult: false, + simulatedResultType: "vector", + simulatedValue: 0, + }, + { + expectedQuery: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1", + simulateError: false, + simulateEmptyResult: false, + simulatedResultType: "scalar", + simulatedValue: 1, + }, + }, + }, + { + name: "evaluate version workloads - prom query error - from metric rule", + evaluatedVersion: "test-cap-01-cav-v1", + startResources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + }, + expectCleanup: false, + expectError: false, + cases: []queryTestCase{ + { + expectedQuery: "sum(rate(total_http_requests{job=\"test-cap-01-cav-v1-cap-backend-srv-svc\",namespace=\"default\"}[2m]))", + simulateError: true, + simulateEmptyResult: false, + simulatedResultType: "vector", + simulatedValue: 0.005, + }, + }, + }, + { + name: "evaluate version workloads - prom query error - from expression", + evaluatedVersion: "test-cap-01-cav-v1", + startResources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + }, + expectCleanup: false, + expectError: false, + cases: []queryTestCase{ + { + expectedQuery: "sum(rate(total_http_requests{job=\"test-cap-01-cav-v1-cap-backend-srv-svc\",namespace=\"default\"}[2m]))", + simulateError: false, + simulateEmptyResult: false, + simulatedResultType: "vector", + simulatedValue: 0.005, + }, + { + expectedQuery: "sum(avg_over_time(active_jobs{job=\"test-cap-01-cav-v1-cap-backend-srv-svc\",namespace=\"default\"}[3m]))", + simulateError: false, + simulateEmptyResult: false, + simulatedResultType: "vector", + simulatedValue: 0, + }, + { + expectedQuery: "scalar(sum(avg_over_time(current_sessions{job=\"test-cap-01-cav-v1-app-router-svc\"}[12m]))) <= bool 1", + simulateError: true, + simulateEmptyResult: false, + simulatedResultType: "scalar", + simulatedValue: 1, + }, + }, + }, + { + name: "evaluate version workloads - prom query - invalid result type", + evaluatedVersion: "test-cap-01-cav-v1", + startResources: []string{ + "testdata/version-monitoring/ca-cleanup-enabled.yaml", + "testdata/version-monitoring/cav-v1-deletion-rules-error.yaml", + "testdata/version-monitoring/cav-v2-deletion-rules.yaml", + }, + expectCleanup: false, + expectError: false, + cases: []queryTestCase{ + { + expectedQuery: "sum(rate(total_http_requests{job=\"test-cap-01-cav-v1-cap-backend-srv-svc\",namespace=\"default\"}[2m]))", + simulateError: false, + simulateEmptyResult: false, + simulatedResultType: "invalid", + simulatedValue: 0.005, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, getActualQueries := getPromServer(false, tt.cases) + defer s.Close() + o := initializeVersionCleanupOrchestrator(context.TODO(), &monitoringEnv{address: s.URL, acquireClientRetryDelay: 1 * time.Minute}) + defer o.queue.ShutDown() + c := setupTestControllerWithInitialResources(t, tt.startResources) + item := NamespacedResourceKey{Namespace: "default", Name: tt.evaluatedVersion} + o.queue.Add(item) + _ = c.processVersionCleanupQueueItem(context.TODO(), o) + + // Verify error occurrence + if tt.expectError { + if o.queue.NumRequeues(item) == 0 { + t.Errorf("expected requeue for version %s", tt.evaluatedVersion) + } + } else { + if o.queue.NumRequeues(item) > 0 { + t.Errorf("expected no requeues for version %s", tt.evaluatedVersion) + } + } + + // check whether expected queries were called + act := getActualQueries() + for _, c := range tt.cases { + if _, ok := act[c.expectedQuery]; !ok { + t.Errorf("expected query %s to be called", c.expectedQuery) + } else { + act[c.expectedQuery] = true + } + } + for q, ok := range act { + if !ok { + t.Errorf("unexpected query %s was called", q) + } + } + + // verify version deletion + _, err := c.crdClient.SmeV1alpha1().CAPApplicationVersions("default").Get(context.TODO(), tt.evaluatedVersion, v1.GetOptions{}) + if tt.expectCleanup { + if err == nil || !errors.IsNotFound(err) { + t.Errorf("expected version %s to be deleted", tt.evaluatedVersion) + } + } else { + if err != nil { + t.Errorf("expected to fetch version %s", tt.evaluatedVersion) + } + } + + }) + } +} diff --git a/internal/util/log.go b/internal/util/log.go index 451b421..16f8ae7 100644 --- a/internal/util/log.go +++ b/internal/util/log.go @@ -107,3 +107,7 @@ func LogError(error error, msg string, step string, entity interface{}, child in overallArgs := logArgs(step, entity, child, args...) klog.ErrorS(error, msg, overallArgs...) } + +func LogWarning(args ...interface{}) { + klog.Warning(args...) +} diff --git a/internal/util/log_test.go b/internal/util/log_test.go index 56f0827..b70b4d7 100644 --- a/internal/util/log_test.go +++ b/internal/util/log_test.go @@ -32,17 +32,21 @@ func TestLogging(t *testing.T) { tests := []struct { name string error bool + warn bool args args }{ {name: "Test LogInfo", error: false, args: args{msg: "LogInfo basic test", step: "LogInfoStep0", entity: "LogInfo", child: "ChildLogInfo", args: []interface{}{"LogInfo", "No missing value"}}}, {name: "Test LogInfo", error: false, args: args{msg: "LogInfo pointer test", step: "LogInfoStep0", entity: "LogInfo", child: jobType, args: []interface{}{"LogInfo", "No missing value"}}}, {name: "Test LogError", error: true, args: args{err: errors.New("Test Error"), msg: "LogError basic test", step: "LogErrorStep0", entity: "LogError", child: "ChildLogError", args: []interface{}{"LogError", "No Missing value"}}}, {name: "Test LogError", error: true, args: args{err: errors.New("Test Error"), msg: "LogError LabelBTPApplicationIdentifierHash skip test", step: "LogErrorStep0", entity: "LogError", child: jobType, args: []interface{}{LabelBTPApplicationIdentifierHash, "some-test-hash", "Missing value"}}}, + {name: "Test LogWarn", warn: true, args: args{err: errors.New("Test Warn"), msg: "LogWarn LabelBTPApplicationIdentifierHash skip test"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.error { LogError(tt.args.err, tt.args.msg, tt.args.step, tt.args.entity, tt.args.child, tt.args.args...) + } else if tt.warn { + LogWarning(tt.args.err, tt.args.msg) } else { LogInfo(tt.args.msg, tt.args.step, tt.args.entity, tt.args.child, tt.args.args...) } diff --git a/pkg/apis/sme.sap.com/v1alpha1/types.go b/pkg/apis/sme.sap.com/v1alpha1/types.go index 1e65cbd..43e37ca 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/types.go +++ b/pkg/apis/sme.sap.com/v1alpha1/types.go @@ -96,6 +96,7 @@ type ApplicationDomains struct { // +kubebuilder:validation:MaxLength=62 // Primary application domain will be used to generate a wildcard TLS certificate. In project "Gardener" managed clusters this is (usually) a subdomain of the cluster domain Primary string `json:"primary"` + // +kubebuilder:validation:items:Pattern=^[a-z0-9-.]+$ // Customer specific domains to serve application endpoints (optional) Secondary []string `json:"secondary,omitempty"` // +kubebuilder:validation:Pattern=^[a-z0-9-.]*$ @@ -253,8 +254,76 @@ type DeploymentDetails struct { LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty"` // Readiness probe ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty"` + // Workload monitoring specification + Monitoring *WorkloadMonitoring `json:"monitoring,omitempty"` +} + +// WorkloadMonitoring specifies the metrics related to the workload +type WorkloadMonitoring struct { + // DeletionRules specify the metrics conditions that need to be satisfied for the version to be deleted automatically. + // Either a set of metrics based rules can be specified, or a PromQL expression which evaluates to a boolean scalar. + DeletionRules *DeletionRules `json:"deletionRules,omitempty"` + // Configuration to be used to create ServiceMonitor for the workload service. + // If not specified, CAP Operator will not attempt to create a ServiceMonitor for the workload + ScrapeConfig *MonitoringConfig `json:"scrapeConfig,omitempty"` +} + +type MonitoringConfig struct { + // Interval at which Prometheus scrapes the metrics from the target. + ScrapeInterval Duration `json:"interval,omitempty"` + // Name of the port (specified on the workload) which will be used by Prometheus server to scrape metrics + WorkloadPort string `json:"port"` + // HTTP path from which to scrape for metrics. + Path string `json:"path,omitempty"` + // Timeout after which Prometheus considers the scrape to be failed. + Timeout Duration `json:"scrapeTimeout,omitempty"` +} + +type DeletionRules struct { + Metrics []MetricRule `json:"metrics,omitempty"` + // A promQL expression that evaluates to a scalar boolean (1 or 0). + // Example: scalar(sum(avg_over_time(demo_metric{job="cav-demo-app-4-srv-svc",namespace="demo"}[2m]))) <= bool 0.1 + ScalarExpression *string `json:"expression,omitempty"` +} + +// MetricRule specifies a Prometheus metric and rule which represents a cleanup condition. Metrics of type Gauge and Counter are supported. +// +// Rule evaluation for Gauge type metric: The time series data of the metric (restricted to the current workload by setting `job` label as workload service name) is calculated as an average over the specified period. +// A sum of the calculated average from different time series is then compared to the provided threshold value to determine whether the rule has been satisfied. +// Evaluation: `sum(avg_over_time({job=}[])) <= ` +// +// Rule evaluation for Counter type metric: The time series data of the metric (restricted to the current workload by setting `job` label as workload service name) is calculated as rate of increase over the specified period. +// The sum of the calculated rates from different time series is then compared to the provided threshold value to determine whether the rule has been satisfied. +// Evaluation: `sum(rate({job=}[])) <= ` +type MetricRule struct { + // Prometheus metric. For example `http_request_count` + Name string `json:"name"` + // Type of Prometheus metric which can be either `Gauge` or `Counter` + // +kubebuilder:validation:Enum=Gauge;Counter + Type MetricType `json:"type"` + // Duration of time series data used for the rule evaluation + CalculationPeriod Duration `json:"calculationPeriod"` + // The threshold value which is compared against the calculated value. If calculated value is less than or equal to the threshold the rule condition is fulfilled. + // +kubebuilder:validation:Format:=double + ThresholdValue string `json:"thresholdValue"` } +// Duration is a valid time duration that can be parsed by Prometheus +// Supported units: y, w, d, h, m, s, ms +// Examples: `30s`, `1m`, `1h20m15s`, `15d` +// +kubebuilder:validation:Pattern:="^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$" +type Duration string + +// Type of Prometheus metric +type MetricType string + +const ( + // Prometheus Metric type Gauge + MetricTypeGauge MetricType = "Gauge" + // Prometheus Metric type Counter + MetricTypeCounter MetricType = "Counter" +) + // Type of deployment type DeploymentType string diff --git a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go index 6b27e3f..d9fc6a4 100644 --- a/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/sme.sap.com/v1alpha1/zz_generated.deepcopy.go @@ -687,6 +687,32 @@ func (in *CommonDetails) DeepCopy() *CommonDetails { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeletionRules) DeepCopyInto(out *DeletionRules) { + *out = *in + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]MetricRule, len(*in)) + copy(*out, *in) + } + if in.ScalarExpression != nil { + in, out := &in.ScalarExpression, &out.ScalarExpression + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeletionRules. +func (in *DeletionRules) DeepCopy() *DeletionRules { + if in == nil { + return nil + } + out := new(DeletionRules) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentDetails) DeepCopyInto(out *DeploymentDetails) { *out = *in @@ -713,6 +739,11 @@ func (in *DeploymentDetails) DeepCopyInto(out *DeploymentDetails) { *out = new(v1.Probe) (*in).DeepCopyInto(*out) } + if in.Monitoring != nil { + in, out := &in.Monitoring, &out.Monitoring + *out = new(WorkloadMonitoring) + (*in).DeepCopyInto(*out) + } return } @@ -776,6 +807,38 @@ func (in *JobDetails) DeepCopy() *JobDetails { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricRule) DeepCopyInto(out *MetricRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricRule. +func (in *MetricRule) DeepCopy() *MetricRule { + if in == nil { + return nil + } + out := new(MetricRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonitoringConfig) DeepCopyInto(out *MonitoringConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonitoringConfig. +func (in *MonitoringConfig) DeepCopy() *MonitoringConfig { + if in == nil { + return nil + } + out := new(MonitoringConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NameValue) DeepCopyInto(out *NameValue) { *out = *in @@ -920,3 +983,29 @@ func (in *WorkloadDetails) DeepCopy() *WorkloadDetails { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkloadMonitoring) DeepCopyInto(out *WorkloadMonitoring) { + *out = *in + if in.DeletionRules != nil { + in, out := &in.DeletionRules, &out.DeletionRules + *out = new(DeletionRules) + (*in).DeepCopyInto(*out) + } + if in.ScrapeConfig != nil { + in, out := &in.ScrapeConfig, &out.ScrapeConfig + *out = new(MonitoringConfig) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadMonitoring. +func (in *WorkloadMonitoring) DeepCopy() *WorkloadMonitoring { + if in == nil { + return nil + } + out := new(WorkloadMonitoring) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deletionrules.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deletionrules.go new file mode 100644 index 0000000..50f7994 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deletionrules.go @@ -0,0 +1,42 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// DeletionRulesApplyConfiguration represents a declarative configuration of the DeletionRules type for use +// with apply. +type DeletionRulesApplyConfiguration struct { + Metrics []MetricRuleApplyConfiguration `json:"metrics,omitempty"` + ScalarExpression *string `json:"expression,omitempty"` +} + +// DeletionRulesApplyConfiguration constructs a declarative configuration of the DeletionRules type for use with +// apply. +func DeletionRules() *DeletionRulesApplyConfiguration { + return &DeletionRulesApplyConfiguration{} +} + +// WithMetrics adds the given value to the Metrics field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Metrics field. +func (b *DeletionRulesApplyConfiguration) WithMetrics(values ...*MetricRuleApplyConfiguration) *DeletionRulesApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMetrics") + } + b.Metrics = append(b.Metrics, *values[i]) + } + return b +} + +// WithScalarExpression sets the ScalarExpression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ScalarExpression field is set to the value of the last call. +func (b *DeletionRulesApplyConfiguration) WithScalarExpression(value string) *DeletionRulesApplyConfiguration { + b.ScalarExpression = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go index d4fa6fb..0c55146 100644 --- a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/deploymentdetails.go @@ -16,11 +16,12 @@ import ( // with apply. type DeploymentDetailsApplyConfiguration struct { CommonDetailsApplyConfiguration `json:",inline"` - Type *smesapcomv1alpha1.DeploymentType `json:"type,omitempty"` - Replicas *int32 `json:"replicas,omitempty"` - Ports []PortsApplyConfiguration `json:"ports,omitempty"` - LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` - ReadinessProbe *v1.Probe `json:"readinessProbe,omitempty"` + Type *smesapcomv1alpha1.DeploymentType `json:"type,omitempty"` + Replicas *int32 `json:"replicas,omitempty"` + Ports []PortsApplyConfiguration `json:"ports,omitempty"` + LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` + ReadinessProbe *v1.Probe `json:"readinessProbe,omitempty"` + Monitoring *WorkloadMonitoringApplyConfiguration `json:"monitoring,omitempty"` } // DeploymentDetailsApplyConfiguration constructs a declarative configuration of the DeploymentDetails type for use with @@ -229,3 +230,11 @@ func (b *DeploymentDetailsApplyConfiguration) WithReadinessProbe(value v1.Probe) b.ReadinessProbe = &value return b } + +// WithMonitoring sets the Monitoring field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Monitoring field is set to the value of the last call. +func (b *DeploymentDetailsApplyConfiguration) WithMonitoring(value *WorkloadMonitoringApplyConfiguration) *DeploymentDetailsApplyConfiguration { + b.Monitoring = value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/metricrule.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/metricrule.go new file mode 100644 index 0000000..aec1378 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/metricrule.go @@ -0,0 +1,59 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +// MetricRuleApplyConfiguration represents a declarative configuration of the MetricRule type for use +// with apply. +type MetricRuleApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Type *v1alpha1.MetricType `json:"type,omitempty"` + CalculationPeriod *v1alpha1.Duration `json:"calculationPeriod,omitempty"` + ThresholdValue *string `json:"thresholdValue,omitempty"` +} + +// MetricRuleApplyConfiguration constructs a declarative configuration of the MetricRule type for use with +// apply. +func MetricRule() *MetricRuleApplyConfiguration { + return &MetricRuleApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *MetricRuleApplyConfiguration) WithName(value string) *MetricRuleApplyConfiguration { + b.Name = &value + return b +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *MetricRuleApplyConfiguration) WithType(value v1alpha1.MetricType) *MetricRuleApplyConfiguration { + b.Type = &value + return b +} + +// WithCalculationPeriod sets the CalculationPeriod field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CalculationPeriod field is set to the value of the last call. +func (b *MetricRuleApplyConfiguration) WithCalculationPeriod(value v1alpha1.Duration) *MetricRuleApplyConfiguration { + b.CalculationPeriod = &value + return b +} + +// WithThresholdValue sets the ThresholdValue field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ThresholdValue field is set to the value of the last call. +func (b *MetricRuleApplyConfiguration) WithThresholdValue(value string) *MetricRuleApplyConfiguration { + b.ThresholdValue = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/monitoringconfig.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/monitoringconfig.go new file mode 100644 index 0000000..814e64f --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/monitoringconfig.go @@ -0,0 +1,59 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/sap/cap-operator/pkg/apis/sme.sap.com/v1alpha1" +) + +// MonitoringConfigApplyConfiguration represents a declarative configuration of the MonitoringConfig type for use +// with apply. +type MonitoringConfigApplyConfiguration struct { + ScrapeInterval *v1alpha1.Duration `json:"interval,omitempty"` + WorkloadPort *string `json:"port,omitempty"` + Path *string `json:"path,omitempty"` + Timeout *v1alpha1.Duration `json:"scrapeTimeout,omitempty"` +} + +// MonitoringConfigApplyConfiguration constructs a declarative configuration of the MonitoringConfig type for use with +// apply. +func MonitoringConfig() *MonitoringConfigApplyConfiguration { + return &MonitoringConfigApplyConfiguration{} +} + +// WithScrapeInterval sets the ScrapeInterval field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ScrapeInterval field is set to the value of the last call. +func (b *MonitoringConfigApplyConfiguration) WithScrapeInterval(value v1alpha1.Duration) *MonitoringConfigApplyConfiguration { + b.ScrapeInterval = &value + return b +} + +// WithWorkloadPort sets the WorkloadPort field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the WorkloadPort field is set to the value of the last call. +func (b *MonitoringConfigApplyConfiguration) WithWorkloadPort(value string) *MonitoringConfigApplyConfiguration { + b.WorkloadPort = &value + return b +} + +// WithPath sets the Path field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Path field is set to the value of the last call. +func (b *MonitoringConfigApplyConfiguration) WithPath(value string) *MonitoringConfigApplyConfiguration { + b.Path = &value + return b +} + +// WithTimeout sets the Timeout field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Timeout field is set to the value of the last call. +func (b *MonitoringConfigApplyConfiguration) WithTimeout(value v1alpha1.Duration) *MonitoringConfigApplyConfiguration { + b.Timeout = &value + return b +} diff --git a/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloadmonitoring.go b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloadmonitoring.go new file mode 100644 index 0000000..7ae0b60 --- /dev/null +++ b/pkg/client/applyconfiguration/sme.sap.com/v1alpha1/workloadmonitoring.go @@ -0,0 +1,37 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cap-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// WorkloadMonitoringApplyConfiguration represents a declarative configuration of the WorkloadMonitoring type for use +// with apply. +type WorkloadMonitoringApplyConfiguration struct { + DeletionRules *DeletionRulesApplyConfiguration `json:"deletionRules,omitempty"` + ScrapeConfig *MonitoringConfigApplyConfiguration `json:"scrapeConfig,omitempty"` +} + +// WorkloadMonitoringApplyConfiguration constructs a declarative configuration of the WorkloadMonitoring type for use with +// apply. +func WorkloadMonitoring() *WorkloadMonitoringApplyConfiguration { + return &WorkloadMonitoringApplyConfiguration{} +} + +// WithDeletionRules sets the DeletionRules field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionRules field is set to the value of the last call. +func (b *WorkloadMonitoringApplyConfiguration) WithDeletionRules(value *DeletionRulesApplyConfiguration) *WorkloadMonitoringApplyConfiguration { + b.DeletionRules = value + return b +} + +// WithScrapeConfig sets the ScrapeConfig field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ScrapeConfig field is set to the value of the last call. +func (b *WorkloadMonitoringApplyConfiguration) WithScrapeConfig(value *MonitoringConfigApplyConfiguration) *WorkloadMonitoringApplyConfiguration { + b.ScrapeConfig = value + return b +} diff --git a/pkg/client/applyconfiguration/utils.go b/pkg/client/applyconfiguration/utils.go index 410f530..3287d83 100644 --- a/pkg/client/applyconfiguration/utils.go +++ b/pkg/client/applyconfiguration/utils.go @@ -59,12 +59,18 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &smesapcomv1alpha1.CAPTenantStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("CommonDetails"): return &smesapcomv1alpha1.CommonDetailsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DeletionRules"): + return &smesapcomv1alpha1.DeletionRulesApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("DeploymentDetails"): return &smesapcomv1alpha1.DeploymentDetailsApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("GenericStatus"): return &smesapcomv1alpha1.GenericStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("JobDetails"): return &smesapcomv1alpha1.JobDetailsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("MetricRule"): + return &smesapcomv1alpha1.MetricRuleApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("MonitoringConfig"): + return &smesapcomv1alpha1.MonitoringConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("NameValue"): return &smesapcomv1alpha1.NameValueApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Ports"): @@ -77,6 +83,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &smesapcomv1alpha1.TenantOperationWorkloadReferenceApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("WorkloadDetails"): return &smesapcomv1alpha1.WorkloadDetailsApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("WorkloadMonitoring"): + return &smesapcomv1alpha1.WorkloadMonitoringApplyConfiguration{} } return nil diff --git a/website/assets/scss/_styles_project.scss b/website/assets/scss/_styles_project.scss new file mode 100644 index 0000000..cf04905 --- /dev/null +++ b/website/assets/scss/_styles_project.scss @@ -0,0 +1 @@ +@import 'td/code-dark'; diff --git a/website/content/en/_index.md b/website/content/en/_index.md index 42db526..77c831a 100644 --- a/website/content/en/_index.md +++ b/website/content/en/_index.md @@ -7,10 +7,10 @@ description: A Kubernetes operator for managing the lifecycle of multi-tenant SA {{% blocks/cover title="Welcome to CAP Operator" image_anchor="top" height="full" color="primary" %}}
A Kubernetes operator for managing the lifecycle of multi-tenant CAP applications


- + Learn more - + Go to the source repository


diff --git a/website/content/en/docs/_index.md b/website/content/en/docs/_index.md index f114f99..fa7b68b 100644 --- a/website/content/en/docs/_index.md +++ b/website/content/en/docs/_index.md @@ -1,7 +1,7 @@ --- title: "Documentation" linkTitle: "Documentation" -weight: 20 +weight: 10 menu: main: weight: 10 diff --git a/website/content/en/docs/configuration/_index.md b/website/content/en/docs/configuration/_index.md index 0720601..dcab6b6 100644 --- a/website/content/en/docs/configuration/_index.md +++ b/website/content/en/docs/configuration/_index.md @@ -1,7 +1,7 @@ --- title: "Configuration" linkTitle: "Configuration" -weight: 30 +weight: 40 type: "docs" tags: ["setup"] description: > @@ -18,3 +18,6 @@ Here's a list of environment variables used by CAP Operator. - `DNS_MANAGER`: specifies the external DNS manager to be used. Possible values are: - `gardener`: ["Gardener" external DNS manager](https://github.com/gardener/external-dns-management) - `kubernetes`: [external DNS management from Kubernetes](https://github.com/kubernetes-sigs/external-dns) +- `PROMETHEUS_ADDRESS`: URL of the Prometheus server (or service) for executing PromQL queries e.g. `http://prometheus-operated.monitoring.svc.cluster.local:9090`. If no URL is supplied, the controller will not start the version monitoring function. +- `PROM_ACQUIRE_CLIENT_RETRY_DELAY`: Time delay between retries when a Prometheus client creation and connection check fails. +- `METRICS_EVAL_INTERVAL`: Time interval between subsequent iterations where outdated versions are identified and queued for evaluation. \ No newline at end of file diff --git a/website/content/en/docs/installation/_index.md b/website/content/en/docs/installation/_index.md index 809f893..a7466e9 100644 --- a/website/content/en/docs/installation/_index.md +++ b/website/content/en/docs/installation/_index.md @@ -1,7 +1,7 @@ --- title: "Installation" linkTitle: "Installation" -weight: 20 +weight: 30 type: "docs" description: > How to install CAP Operator in a Kubernetes cluster diff --git a/website/content/en/docs/usage/_index.md b/website/content/en/docs/usage/_index.md index 11a2965..9a2cc26 100644 --- a/website/content/en/docs/usage/_index.md +++ b/website/content/en/docs/usage/_index.md @@ -1,7 +1,7 @@ --- title: "Usage" linkTitle: "Usage" -weight: 40 +weight: 50 type: "docs" description: > How to manage the application with CAP Operator diff --git a/website/content/en/docs/usage/resources/_index.md b/website/content/en/docs/usage/resources/_index.md index 10be4d2..a41a4aa 100644 --- a/website/content/en/docs/usage/resources/_index.md +++ b/website/content/en/docs/usage/resources/_index.md @@ -1,7 +1,7 @@ --- title: "Resources" linkTitle: "Resources" -weight: 50 +weight: 60 type: "docs" description: > Detailed configuration of resources managed by CAP Operator diff --git a/website/content/en/docs/usage/resources/capapplicationversion.md b/website/content/en/docs/usage/resources/capapplicationversion.md index e35cfda..e766786 100644 --- a/website/content/en/docs/usage/resources/capapplicationversion.md +++ b/website/content/en/docs/usage/resources/capapplicationversion.md @@ -62,6 +62,11 @@ deploymentDefinition: routerDestinationName: cap-server-url - name: tech-port port: 4005 + monitoring: + scrapeConfig: + port: tech--port + deletionRules: + expression: scalar(sum(avg_over_time(current_sessions{job="cav-cap-app-v1-cap-backend-svc",namespace="cap-ns"}[2h]))) <= bool 5 ``` The `type` of the deployment is important to indicate how the operator handles this workload (for example, injection of `destinations` to be used by the approuter). Valid values are: @@ -85,6 +90,14 @@ The port configurations aren't mandatory and can be omitted. This would mean tha > NOTE: If multiple ports are configured for a workload of type `Router`, the first available port will be used to target external traffic to the application domain. +#### Monitoring configuration + +For each _workload of type deployment_ in a `CAPApplicationVersion`, it is possible to define: +1. Deletion rules: A criteria based on metrics which when satisfied signifies that the workload can be removed +2. Scrape configuration: Configuration which defines how metrics are scraped from the workload service. + +Details of how to configure workload monitoring can be found [here](../version-monitoring.md#configure-capapplicationversion). + ### Workloads with `jobDefinition` ```yaml @@ -211,6 +224,11 @@ spec: - name: tech-port port: 4005 appProtocol: grpc + monitoring: + scrapeConfig: + port: tech--port + deletionRules: + expression: scalar(sum(avg_over_time(current_sessions{job="cav-cap-app-v1-cap-backend-svc",namespace="cap-ns"}[2h]))) <= bool 5 livenessProbe: failureThreshold: 3 httpGet: diff --git a/website/content/en/docs/usage/version-monitoring.md b/website/content/en/docs/usage/version-monitoring.md new file mode 100644 index 0000000..08d54d0 --- /dev/null +++ b/website/content/en/docs/usage/version-monitoring.md @@ -0,0 +1,151 @@ +--- +title: "Version Monitoring" +linkTitle: "Version Monitoring" +weight: 50 +type: "docs" +description: > + How to monitor versions for automatic cleanup +--- + +In a continuous delivery environment where newer applications versions may be deployed frequently, monitoring and cleaning up older unused versions becomes important to conserve cluster resources (compute, memory, storage etc.) and operate a clutter free system. The CAP Operator now provides application developers and operations teams to define how an application version can be monitored for usage. + +## Integration with Prometheus + +[Prometheus](https://prometheus.io/) is the industry standard for monitoring application metrics and provides a wide variety of tools for managing and reporting metrics data. The CAP Operator (controller) can be connected to a [Prometheus](https://prometheus.io/) server by setting the `PROMETHEUS_ADDRESS` environment variable on the controller (see [Configuration](../configuration/_index.md)). The controller is then able to query application related metrics based on the workload specification of `CAPApplicationVersions`. If no Prometheus address is supplied, the version monitoring function of the controller is not started. + +## Configure `CAPApplication` + +To avoid incompatible changes, version cleanup monitoring must be enabled for CAP application using the annotation `sme.sap.com/enable-cleanup-monitoring`. The annotation can have the following values which affects the version cleanup behavior: + +|Value|Behavior| +|--|--| +|`dry-run`|When a `CAPApplicationVersion` is evaluated to be eligible for cleanup, an event of type `ReadyForDeletion` is emitted without performing the actual deletion of the version.| +|`true`|When a `CAPApplicationVersion` is evaluated to be eligible for cleanup, the version is deleted and an event of type `ReadyForDeletion` is emitted.| + +## Configure `CAPApplicationVersion` + +For each _workload of type deployment_ in a `CAPApplicationVersion`, it is possible to define: +1. Deletion rules: A criteria based on metrics which when satisfied signifies that the workload can be removed +2. Scrape configuration: Configuration which defines how metrics are scraped from the workload service. + +#### Deletion Rules (Variant 1) based on Metric Type + +The following example shows how a workload, named `backend`, is configured with deletion rules based on multiple metrics. +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + namespace: demo + name: cav-demo-app-1 +spec: + workloads: + - name: backend + deploymentDefinition: + monitoring: + deletionRules: + metrics: + - calculationPeriod: 90m + name: current_sessions + thresholdValue: "0" + type: Gauge + - calculationPeriod: 2h + name: total_http_requests + thresholdValue: "0.00005" + type: Counter +``` +This informs the CAP Operator that workload `backend` is supplying two metrics which can be monitored for usage. + +- Metric `current_sessions` is of type `Gauge` which indicates that it is an absolute value at any point of time. When evaluating this metric, the CAP operator queries Prometheus with a PromQL expression which calculates the average value of this metric over a specified calculation period. The average value from each time series is then added together to get the evaluated value. The evaluated value is then compared against the specified threshold value to determine usage (or eligibility for cleanup). + + |Evaluation steps for metric type `Gauge`| + |-| + |Execute PromQL expression `sum(avg_over_time(current_sessions{job="cav-demo-app-1-backend-svc",namespace="demo"}[90m]))` to get the evaluated value| + |Check whether evaluated value <= 0 (the specified `thresholdValue`)| + +- Similarly, metric `total_http_requests` is of type `Counter` which indicates that it is a cumulative value which can increment. When evaluating this metric, the CAP operator queries Prometheus with a PromQL expression which calculates the rate (of increase) of this metric over a specified calculation period. The rate of increase from each time series is then added together to get the evaluated value. The evaluated value is then compared against the specified threshold value to determine usage (or eligibility for cleanup). + + |Evaluation steps for metric type `Counter`| + |-| + |Execute PromQL expression `sum(rate(total_http_requests{job="cav-demo-app-1-backend-svc",namespace="demo"}[2h]))` to get the evaluated value| + |Check whether evaluated value <= 0.00005 (the specified `thresholdValue`)| + +{{% alert title="Prometheus Metrics Data" color="light" %}} +- Prometheus stores metric data as multiple time series by label set. The number of time series created from a single metric depends on the possible combination of labels. The label `job` represents the source of the metric and (within Kubernetes) is the service representing the workload. +- CAP Operator does not support Prometheus metric types other than `Gauge` and `Counter`. Lean more about metric types [here](https://prometheus.io/docs/concepts/metric_types/). +{{% /alert %}} + +All specified metrics of a workload must satisfy the evaluation criteria for the workload to be eligible for cleanup. + +#### Deletion Rules (Variant 2) as PromQL expression + +Another way to specify the deletion criteria for a workload is by providing a PromQL expression which results a boolean scalar. +```yaml +apiVersion: sme.sap.com/v1alpha1 +kind: CAPApplicationVersion +metadata: + namespace: demo + name: cav-demo-app-1 +spec: + workloads: + - name: backend + deploymentDefinition: + monitoring: + deletionRules: + expression: scalar(sum(avg_over_time(current_sessions{job="cav-demo-app-1-backend-svc",namespace="demo"}[2h]))) <= bool 5 +``` + +The supplied PromQL expression is executed as a Prometheus query by the CAP Operator. The expected result is a scalar boolean (`0` or `1`). Users may use [comparison binary operators](https://prometheus.io/docs/prometheus/latest/querying/operators/#comparison-binary-operators) with the `bool` modifier to achieve the expected result. If the evaluation result is true (`1`), the workload is eligible for removal. + +This variant can be useful when: +- the predefined evaluation based on metric types is not enough for determining usage of a workload. +- custom metrics scraping configurations are employed where the `job` label in the collected time series data does not mach the name of the (Kubernetes) Service created for the workload. + +### Scrape Configuration + +[Prometheus Operator](https://prometheus-operator.dev/docs/getting-started/introduction/) is a popular Kubernetes operator for managing Prometheus and related monitoring components. A common way to setup scrape targets for a Prometheus instance is by creating the [`ServiceMonitor`](https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.ServiceMonitor) resource which specifies which `Services` (and ports) that should be scraped for collecting application metrics. + +{{% alert title="Prerequisite" color="info" %}} +The `scrapeConfig` feature of a workload is usable only when the [`ServiceMonitor`](https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.ServiceMonitor) Custom Resource is available on the Kubernetes cluster. +{{% /alert %}} + +The CAP Operator provides an easy way to create `Service Monitors` which target the `Services` created for version workloads. The following sample shows how to configure this. +```yaml +kind: CAPApplicationVersion +metadata: + namespace: demo + name: cav-demo-app-1 +spec: + workloads: + - name: backend + deploymentDefinition: + ports: + - appProtocol: http + name: metrics-port + networkPolicy: Cluster + port: 9000 + monitoring: + deletionRules: + expression: scalar(sum(avg_over_time(current_sessions{job="cav-demo-app-1-backend-svc",namespace="demo"}[2h]))) <= bool 5 + scrapeConfig: + interval: 15s + path: /metrics + port: metrics-port +``` + +With this configuration the CAP Operator will create a `ServiceMonitor` which targets the workload `Service`. The `scrapeConfig.port` should match the name of one of the ports specified on the workload. + +{{% alert title="Use Case" color="secondary" %}} +The workload `scrapeConfig` aims to support a minimal configuration, creating a `ServiceMonitor` which supports the most common use case (i.e. scraping the workload service via. a defined workload port). To use complex configurations in `ServiceMonitors`, they should be created separately. If the `scrapeConfig` of a version workload is empty, the CAP Operator will not attempt to create the related `ServiceMonitor`. +{{% /alert %}} + +## Evaluating `CAPApplicationVersions` for cleanup + +At specified intervals (dictated by controller environment variable `METRICS_EVAL_INTERVAL`), the CAP Operator selects versions which are candidates for evaluation. +- Only versions for `CAPApplications` where annotation `sme.sap.com/enable-cleanup-monitoring` is set are considered. +- All versions (`spec.version`) higher than the highest version with `Ready` status are not considered for evaluation. If there is no version with status `Ready`, no versions are considered. +- All versions linked to a `CAPTenant` are excluded from evaluation. This includes versions where the following fields of a `CAPTenant` point to the version: + - `status.currentCAPApplicationVersionInstance` - current version of the tenant. + - `spec.version` - the version to which a tenant is upgrading. + +Workloads from the identified versions are then evaluated based on the defined `deletionRules`. Workloads without `deletionRules` are automatically eligible for cleanup. All workloads (with type deployment) of a version must satisfy the evaluation criteria for the version to be deleted. + diff --git a/website/content/en/docs/whats-new.md b/website/content/en/docs/whats-new.md new file mode 100644 index 0000000..8c03431 --- /dev/null +++ b/website/content/en/docs/whats-new.md @@ -0,0 +1,19 @@ +--- +title: "What's New" +linkTitle: "What's New" +weight: 20 +description: > + Discover new features added to CAP Operator +--- + +{{% cardpane %}} + {{% card header="Q3 2024" %}} + Define monitoring configuration on version workloads which allow outdated versions to be automatically cleaned up based on usage. Learn more about [Version Monitoring](./usage/version-monitoring.md). + {{% /card %}} + {{% card header="Q3 2024" %}} + New Custom Resource `CAPTenantOutput` can be used to record subscription related data from tenant operations. [Learn more](./usage/resources/captenantoutput.md). + {{% /card %}} + {{% card header="Q2 2024" %}} + `CAPApplicationVersion` now supports configuration of `initContainers`, `volumes`, `serviceAccountName`, [scheduling related configurations](https://kubernetes.io/docs/concepts/scheduling-eviction/) etc. on workloads. + {{% /card %}} +{{% /cardpane %}} diff --git a/website/go.mod b/website/go.mod index 71a0b32..acd99e1 100644 --- a/website/go.mod +++ b/website/go.mod @@ -1,5 +1,5 @@ module github.com/sap/cap-operator/website -go 1.23.0 +go 1.23.1 require github.com/google/docsy v0.10.0 // indirect diff --git a/website/hugo.yaml b/website/hugo.yaml index 51c9934..7cfb50d 100644 --- a/website/hugo.yaml +++ b/website/hugo.yaml @@ -41,7 +41,7 @@ permalinks: imaging: resampleFilter: "CatmullRom" quality: 90 - anchor: "Smart" + anchor: Smart # Language configuration languages: @@ -56,14 +56,13 @@ languages: markup: goldmark: + parser: + attribute: + block: true renderer: unsafe: true highlight: - # See a complete list of available styles at https://xyproto.github.io/splash/docs/all.html - style: "nord" - # Uncomment if you want your chosen highlight style used for code blocks without a specified language - guessSyntax: true - codeFences: true + noClasses: false # Required for dark-mode # Everything below this are Site Params @@ -76,7 +75,7 @@ params: # set taxonomyCloud = [] to hide taxonomy clouds taxonomyCloud: ["tags", "categories"] - copyright: "SAP" + copyright: SAP SE or an SAP affiliate company # privacy_policy = "https://policies.google.com/privacy" # Menu title if your navbar has a versions selector to access old versions of your site. @@ -112,8 +111,11 @@ params: # Enable Lunr.js offline search offlineSearch: true + prism_syntax_highlighting: false + # User interface configuration ui: + showLightDarkModeMenu: true # Set to true to disable breadcrumb navigation. breadcrumb_disable: false # Set to false to disable the About link in the site footer @@ -126,6 +128,7 @@ params: sidebar_menu_compact: true # Set to true to hide the sidebar search box (the top nav search box will still be displayed if search is enabled) sidebar_search_disable: true + sidebar_menu_foldable: true # Adds a H2 section titled "Feedback" to the bottom of each doc. The responses are sent to Google Analytics as events. # This feature depends on [services.googleAnalytics] and will be disabled if "services.googleAnalytics.id" is not set. diff --git a/website/includes/api-reference.html b/website/includes/api-reference.html index 0aeea08..9714079 100644 --- a/website/includes/api-reference.html +++ b/website/includes/api-reference.html @@ -1807,6 +1807,47 @@

CommonDetails +

DeletionRules +

+

+(Appears on: WorkloadMonitoring) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+metrics
+ + +[]MetricRule + + +
+
+expression
+ +string + +
+

A promQL expression that evaluates to a scalar boolean (1 or 0). +Example: scalar(sum(avg_over_time(demo_metric{job=“cav-demo-app-4-srv-svc”,namespace=“demo”}[2m]))) <= bool 0.1

+

DeploymentDetails

@@ -1901,6 +1942,19 @@

DeploymentDetails

Readiness probe

+ + +monitoring
+ + +WorkloadMonitoring + + + + +

Workload monitoring specification

+ +

DeploymentType @@ -1929,6 +1983,16 @@

DeploymentType +

Duration +(string alias)

+

+(Appears on: MetricRule, MonitoringConfig) +

+
+

Duration is a valid time duration that can be parsed by Prometheus +Supported units: y, w, d, h, m, s, ms +Examples: 30s, 1m, 1h20m15s, 15d

+

GenericStatus

@@ -2065,6 +2129,166 @@

JobType +

MetricRule +

+

+(Appears on: DeletionRules) +

+
+

MetricRule specifies a Prometheus metric and rule which represents a cleanup condition. Metrics of type Gauge and Counter are supported.

+

Rule evaluation for Gauge type metric: The time series data of the metric (restricted to the current workload by setting job label as workload service name) is calculated as an average over the specified period. +A sum of the calculated average from different time series is then compared to the provided threshold value to determine whether the rule has been satisfied. +Evaluation: sum(avg_over_time(<gauge-metric>{job=<workload-service-name>}[<lookback-duration>])) <= <lower0threshold-value>

+

Rule evaluation for Counter type metric: The time series data of the metric (restricted to the current workload by setting job label as workload service name) is calculated as rate of increase over the specified period. +The sum of the calculated rates from different time series is then compared to the provided threshold value to determine whether the rule has been satisfied. +Evaluation: sum(rate(<counter-metric>{job=<workload-service-name>}[<lookback-duration>])) <= <lower0threshold-value>

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Prometheus metric. For example http_request_count

+
+type
+ + +MetricType + + +
+

Type of Prometheus metric which can be either Gauge or Counter

+
+calculationPeriod
+ + +Duration + + +
+

Duration of time series data used for the rule evaluation

+
+thresholdValue
+ +string + +
+

The threshold value which is compared against the calculated value. If calculated value is less than or equal to the threshold the rule condition is fulfilled.

+
+

MetricType +(string alias)

+

+(Appears on: MetricRule) +

+
+

Type of Prometheus metric

+
+ + + + + + + + + + + + +
ValueDescription

"Counter"

Prometheus Metric type Counter

+

"Gauge"

Prometheus Metric type Gauge

+
+

MonitoringConfig +

+

+(Appears on: WorkloadMonitoring) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+interval
+ + +Duration + + +
+

Interval at which Prometheus scrapes the metrics from the target.

+
+port
+ +string + +
+

Name of the port (specified on the workload) which will be used by Prometheus server to scrape metrics

+
+path
+ +string + +
+

HTTP path from which to scrape for metrics.

+
+scrapeTimeout
+ + +Duration + + +
+

Timeout after which Prometheus considers the scrape to be failed.

+

NameValue

@@ -2473,8 +2697,54 @@

WorkloadDetails +

WorkloadMonitoring +

+

+(Appears on: DeploymentDetails) +

+
+

WorkloadMonitoring specifies the metrics related to the workload

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+deletionRules
+ + +DeletionRules + + +
+

DeletionRules specify the metrics conditions that need to be satisfied for the version to be deleted automatically. +Either a set of metrics based rules can be specified, or a PromQL expression which evaluates to a boolean scalar.

+
+scrapeConfig
+ + +MonitoringConfig + + +
+

Configuration to be used to create ServiceMonitor for the workload service. +If not specified, CAP Operator will not attempt to create a ServiceMonitor for the workload

+

Generated with gen-crd-api-reference-docs -on git commit 22e4ca7. +on git commit b632fa0.

diff --git a/website/includes/chart-values.md b/website/includes/chart-values.md index b06ae29..d9b0e1e 100644 --- a/website/includes/chart-values.md +++ b/website/includes/chart-values.md @@ -27,7 +27,12 @@ | controller.resources.limits.cpu | float | `0.2` | CPU limit | | controller.resources.requests.memory | string | `"50Mi"` | Memory request | | controller.resources.requests.cpu | float | `0.02` | CPU request | +| controller.volumes | list | `[]` | Optionally specify list of additional volumes for the controller pod(s) | +| controller.volumeMounts | list | `[]` | Optionally specify list of additional volumeMounts for the controller container(s) | | controller.dnsTarget | string | `""` | The dns target mentioned on the public ingress gateway service used in the cluster | +| controller.versionMonitoring.prometheusAddress | string | `""` | The URL of the Prometheus server from which metrics related to managed application versions can be queried | +| controller.versionMonitoring.metricsEvaluationInterval | string | `"1h"` | The duration (example 2h) after which versions are evaluated for deletion; based on specified workload metrics | +| controller.versionMonitoring.promClientAcquireRetryDelay | string | `"1h"` | The duration (example 10m) to wait before retrying to acquire Prometheus client and verify connection, after a failed attempt | | subscriptionServer.replicas | int | `1` | Replicas | | subscriptionServer.image.repository | string | `"ghcr.io/sap/cap-operator/server"` | Image repository | | subscriptionServer.image.tag | string | `""` | Image tag | @@ -44,6 +49,8 @@ | subscriptionServer.resources.limits.cpu | float | `0.1` | CPU limit | | subscriptionServer.resources.requests.memory | string | `"20Mi"` | Memory request | | subscriptionServer.resources.requests.cpu | float | `0.01` | CPU request | +| subscriptionServer.volumes | list | `[]` | Optionally specify list of additional volumes for the server pod(s) | +| subscriptionServer.volumeMounts | list | `[]` | Optionally specify list of additional volumeMounts for the server container(s) | | subscriptionServer.port | int | `4000` | Service port | | subscriptionServer.istioSystemNamespace | string | `"istio-system"` | The namespace in the cluster where istio system components are installed | | subscriptionServer.ingressGatewayLabels | object | `{"app":"istio-ingressgateway","istio":"ingressgateway"}` | Labels used to identify the istio ingress-gateway component | diff --git a/website/package-lock.json b/website/package-lock.json index d51fdcd..4a65df3 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -4,10 +4,9 @@ "requires": true, "packages": { "": { - "name": "website", "devDependencies": { "autoprefixer": "^10.4.20", - "hugo-extended": "^0.133.0", + "hugo-extended": "^0.134.2", "postcss-cli": "^11.0.0" } }, @@ -16,6 +15,7 @@ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -29,6 +29,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -38,6 +39,7 @@ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -53,6 +55,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -65,6 +68,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -79,6 +83,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -87,13 +92,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -107,6 +114,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -116,6 +124,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -129,6 +138,7 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -141,6 +151,7 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -153,6 +164,7 @@ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dev": true, + "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.1" }, @@ -164,19 +176,22 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -186,6 +201,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -201,6 +217,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -228,6 +245,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -264,13 +282,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -283,6 +303,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -293,6 +314,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -319,6 +341,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -351,6 +374,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -361,6 +385,7 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, + "license": "MIT", "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" @@ -370,13 +395,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -385,13 +412,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" } @@ -401,6 +430,7 @@ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", @@ -419,6 +449,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -427,9 +458,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001653", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", - "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==", + "version": "1.0.30001662", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", + "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==", "dev": true, "funding": [ { @@ -444,13 +475,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/careful-downloader": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/careful-downloader/-/careful-downloader-3.0.0.tgz", "integrity": "sha512-5KMIPa0Yoj+2tY6OK9ewdwcPebp+4XS0dMYvvF9/8fkFEfvnEpWmHWYs9JNcZ7RZUvY/v6oPzLpmmTzSIbroSA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4", "decompress": "^4.2.1", @@ -468,6 +501,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -480,6 +514,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -504,6 +539,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -518,6 +554,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -529,25 +566,29 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/crypto-random-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^1.0.1" }, @@ -563,6 +604,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -571,12 +613,13 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -592,6 +635,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -611,6 +655,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -626,6 +671,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -638,6 +684,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -652,6 +699,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -668,6 +716,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -677,6 +726,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -691,6 +741,7 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -706,6 +757,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -715,6 +767,7 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -724,27 +777,31 @@ "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", - "dev": true + "version": "1.5.26", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.26.tgz", + "integrity": "sha512-Z+OMe9M/V6Ep9n/52+b7lkvYEps26z4Yz3vjWL1V61W0q+VLF1pOHhMY17sa4roz4AWmULSI8E6SAojZA5L0YQ==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -754,15 +811,17 @@ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -772,6 +831,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -781,6 +841,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -797,6 +858,7 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -806,6 +868,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, + "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -815,6 +878,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -824,6 +888,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -836,6 +901,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" @@ -852,6 +918,7 @@ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.17" } @@ -861,6 +928,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -873,13 +941,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -895,6 +965,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -908,6 +979,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -917,6 +989,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -926,6 +999,7 @@ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -938,6 +1012,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -951,6 +1026,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -963,6 +1039,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.2", @@ -983,6 +1060,7 @@ "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", @@ -1008,6 +1086,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1019,13 +1098,15 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -1035,6 +1116,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1047,6 +1129,7 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -1058,13 +1141,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -1074,11 +1159,12 @@ } }, "node_modules/hugo-extended": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/hugo-extended/-/hugo-extended-0.133.0.tgz", - "integrity": "sha512-+C3I/0uUww04mcdkRdcdsfqHlCXMRQrvi7dUqlyPQViDqIMszPzKQorKEB3YddYEJLomLKoUtAoxs2JVqTXT8A==", + "version": "0.134.2", + "resolved": "https://registry.npmjs.org/hugo-extended/-/hugo-extended-0.134.2.tgz", + "integrity": "sha512-rCt3hrgYAUoGOfOnv1s4p2mID/TDHdad+FSgPtB7wVKeDOZZe0UJtlTkCzQZ4bVNHUCQb6YMDV7Dh/p3IjR3mQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "careful-downloader": "^3.0.0", "log-symbols": "^5.1.0", @@ -1110,13 +1196,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -1125,19 +1213,22 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1150,6 +1241,7 @@ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -1165,6 +1257,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1174,6 +1267,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1183,6 +1277,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1194,13 +1289,15 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1210,6 +1307,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1222,6 +1320,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1231,6 +1330,7 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1242,31 +1342,36 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -1279,6 +1384,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -1288,6 +1394,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -1299,13 +1406,15 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^6.0.0" }, @@ -1321,6 +1430,7 @@ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.0.0", "is-unicode-supported": "^1.1.0" @@ -1337,6 +1447,7 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1349,6 +1460,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -1361,6 +1473,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^3.0.0" }, @@ -1373,6 +1486,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -1382,6 +1496,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -1391,6 +1506,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1404,6 +1520,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1412,10 +1529,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.7", @@ -1428,6 +1546,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -1440,13 +1559,15 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", @@ -1462,6 +1583,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1471,6 +1593,7 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1480,6 +1603,7 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1492,6 +1616,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1501,6 +1626,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -1510,6 +1636,7 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.20" } @@ -1519,6 +1646,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^1.0.0" }, @@ -1534,6 +1662,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^4.0.0" }, @@ -1549,6 +1678,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -1567,6 +1697,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -1576,6 +1707,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1587,19 +1719,22 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1612,6 +1747,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1621,6 +1757,7 @@ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1630,6 +1767,7 @@ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -1638,9 +1776,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -1656,11 +1794,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1671,6 +1810,7 @@ "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-11.0.0.tgz", "integrity": "sha512-xMITAI7M0u1yolVcXJ9XTZiO9aO49mcoKQy6pCDFdMh9kGqhzLVpWxeD/32M/QBmkhcGypZFFOLNLmIW4Pg4RA==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.3.0", "dependency-graph": "^0.11.0", @@ -1710,6 +1850,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.1.1", "yaml": "^2.4.2" @@ -1749,6 +1890,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "picocolors": "^1.0.0", "thenby": "^1.3.4" @@ -1764,13 +1906,15 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1779,7 +1923,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -1799,13 +1944,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1818,6 +1965,7 @@ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.3.0" } @@ -1827,6 +1975,7 @@ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", "dev": true, + "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.1", "normalize-package-data": "^3.0.2", @@ -1845,6 +1994,7 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^6.3.0", "read-pkg": "^7.1.0", @@ -1862,6 +2012,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1872,11 +2023,19 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1889,6 +2048,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1897,13 +2057,15 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/responselike": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dev": true, + "license": "MIT", "dependencies": { "lowercase-keys": "^3.0.0" }, @@ -1919,6 +2081,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -1943,21 +2106,38 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, + "license": "MIT", "dependencies": { "commander": "^2.8.1" }, @@ -1971,6 +2151,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1983,6 +2164,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1991,10 +2173,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -2005,6 +2188,7 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -2014,13 +2198,15 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -2030,22 +2216,32 @@ "version": "3.0.20", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2060,6 +2256,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2072,6 +2269,7 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, + "license": "MIT", "dependencies": { "is-natural-number": "^4.0.1" } @@ -2081,6 +2279,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -2093,6 +2292,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -2111,6 +2311,7 @@ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" } @@ -2120,6 +2321,7 @@ "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", "dev": true, + "license": "MIT", "dependencies": { "is-stream": "^3.0.0", "temp-dir": "^3.0.0", @@ -2138,6 +2340,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -2149,25 +2352,29 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2180,6 +2387,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -2192,6 +2400,7 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -2202,6 +2411,7 @@ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2214,6 +2424,7 @@ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^4.0.0" }, @@ -2229,6 +2440,7 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -2252,6 +2464,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.1.2", "picocolors": "^1.0.1" @@ -2267,13 +2480,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -2284,6 +2499,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2300,13 +2516,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4" } @@ -2316,6 +2534,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -2324,13 +2543,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -2343,6 +2564,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2361,6 +2583,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -2370,6 +2593,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -2380,6 +2604,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.20" }, diff --git a/website/package.json b/website/package.json index 2082545..8c2f315 100644 --- a/website/package.json +++ b/website/package.json @@ -1,7 +1,7 @@ { "devDependencies": { "autoprefixer": "^10.4.20", - "hugo-extended": "^0.133.0", + "hugo-extended": "^0.134.2", "postcss-cli": "^11.0.0" } } diff --git a/website/static/img/activity-tenantprovisioning.drawio.svg b/website/static/img/activity-tenantprovisioning.drawio.svg index 4606bf7..0007137 100644 --- a/website/static/img/activity-tenantprovisioning.drawio.svg +++ b/website/static/img/activity-tenantprovisioning.drawio.svg @@ -1 +1,731 @@ -
subscribe to application
subscribe to applica...
SaaS Registry
SaaS Registry
identify CAPApplication
identify CAPApplicat...
read CAPTenant
read CAPTenant
create CAPTenant
create CAPTenant
does not exist
does not exist
exists
exists
Subscription Server
Subscription Server
Receive notification
(CAPTenant relevant)
Receive notification...
read CAPTenant
read CAPTenant
check status
check status
set status ReadyForProvisioning
set status ReadyForProv...
empty
empty
create CAPTenantOperation
create CAPTenantOperation
ReadyFor
Provisioning
ReadyFor...
set status to
Provisioning
set status to...
check status of CAPTenantOperation
check status of CAPTenant...
Provisioning
Provisioning
set status to
Ready
set status to...
set status to
ProvisioningError
set status to...
Completed
Completed
Failed
Failed
reconcile other status
reconcile other status
other status
other status
CAP Controller
CAP Controller
Reconciliation of CAPTenant with focus
on Provisioning
Reconciliation of CAPTenant with foc...
Consumer Tenant
Consumer Tenant
Subscription callback
route handling
Subscription callback...
check CAPTenant status
check CAPTenant stat...
update Saas Registry (async callback)
update Saas Registry...
respond with (202) accepted (sync)
respond with (202) a...
Ready / ProvisioningError
Ready / ProvisioningError
Provisioning
Provisioning
status tracking
routine
status tracking...
not empty
not empty
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + +
+
+
+ subscribe to application +
+
+
+
+ + subscribe to applica... + +
+
+ + + + + + + + + + +
+
+
+ + SaaS Registry + +
+
+
+
+ + SaaS Registry + +
+
+ + + + + + +
+
+
+ identify CAPApplication +
+
+
+
+ + identify CAPApplicat... + +
+
+ + + + + + +
+
+
+ read CAPTenant +
+
+
+
+ + read CAPTenant + +
+
+ + + + + + + +
+
+
+ create CAPTenant +
+
+
+
+ + create CAPTenant + +
+
+ + + + + + + + + + +
+
+
+ does not exist +
+
+
+
+ + does not exist + +
+
+ + + + +
+
+
+ exists +
+
+
+
+ + exists + +
+
+ + + + +
+
+
+ + Subscription Server + +
+
+
+
+ + Subscription Server + +
+
+ + + + + + + + +
+
+
+ Receive notification +
+ (CAPTenant relevant) +
+
+
+
+ + Receive notification... + +
+
+ + + + + + +
+
+
+ read CAPTenant +
+
+
+
+ + read CAPTenant + +
+
+ + + + + + +
+
+
+ check status +
+
+
+
+ + check status + +
+
+ + + + + +
+
+
+ set status ReadyForProvisioning +
+
+
+
+ + set status ReadyForProv... + +
+
+ + + + + + +
+
+
+ empty +
+
+
+
+ + empty + +
+
+ + + + + + +
+
+
+ create CAPTenantOperation +
+
+
+
+ + create CAPTenantOperation + +
+
+ + + + + + + +
+
+
+ ReadyFor +
+ Provisioning +
+
+
+
+ + ReadyFor... + +
+
+ + + + + + +
+
+
+ set status to +
+ Provisioning +
+
+
+
+ + set status to... + +
+
+ + + + + + + + + +
+
+
+ check status of CAPTenantOperation +
+
+
+
+ + check status of CAPTenant... + +
+
+ + + + + + +
+
+
+ Provisioning +
+
+
+
+ + Provisioning + +
+
+ + + + + +
+
+
+ set status to +
+ Ready +
+
+
+
+ + set status to... + +
+
+ + + + + + +
+
+
+ set status to +
+ ProvisioningError +
+
+
+
+ + set status to... + +
+
+ + + + + + + + + + + +
+
+
+ Completed +
+
+
+
+ + Completed + +
+
+ + + + +
+
+
+ Failed +
+
+
+
+ + Failed + +
+
+ + + + + + + + + + + + + + +
+
+
+ reconcile other status +
+
+
+
+ + reconcile other status + +
+
+ + + + + + + + + + + +
+
+
+ other status +
+
+
+
+ + other status + +
+
+ + + + + + + + + + + + + +
+
+
+ + CAP Controller + +
+
+
+
+ + CAP Controller + +
+
+ + + + +
+
+
+ Reconciliation of CAPTenant with focus +
+ on Provisioning +
+
+
+
+ + Reconciliation of CAPTenant with foc... + +
+
+ + + + +
+
+
+ Consumer Tenant +
+
+
+
+ + Consumer Tenant + +
+
+ + + + +
+
+
+ Subscription callback +
+ route handling +
+
+
+
+ + Subscription callback... + +
+
+ + + + + + + + + +
+
+
+ check CAPTenant status +
+
+
+
+ + check CAPTenant stat... + +
+
+ + + + + + + +
+
+
+ update Saas Registry (async callback) +
+
+
+
+ + update Saas Registry... + +
+
+ + + + + + +
+
+
+ respond with (202) accepted (sync) +
+
+
+
+ + respond with (202) a... + +
+
+ + + + + + + + +
+
+
+ Ready / ProvisioningError +
+
+
+
+ + Ready / ProvisioningError + +
+
+ + + + +
+
+
+ Provisioning +
+
+
+
+ + Provisioning + +
+
+ + + + + + +
+
+
+ status tracking +
+ routine +
+
+
+
+ + status tracking... + +
+
+ + + + + + + +
+
+
+ not empty +
+
+
+
+ + not empty + +
+
+ +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/website/static/img/block-cluster.drawio.svg b/website/static/img/block-cluster.drawio.svg index b1c0e9e..7629d8c 100644 --- a/website/static/img/block-cluster.drawio.svg +++ b/website/static/img/block-cluster.drawio.svg @@ -1,2 +1,444 @@ -Subscriptionweb-serverCAPcontrollerKubernetes API Server
 resources (etcd)
 resources (etcd)
AppRouterCAP ServerContent Deployer (UI5 apps, Portal)saas-registry(tenant subscription)xsuaa(authentication)
operator namespace
operator namespace
service-manager(resolve tenantschema)html5-repositoryservicedestinationservice
kube-system namespace
kube-system namespace
cf-service-operator / sap-btp-service-operator(manages service instances and bindings)...
application namespace
application namespace
Tenant Operation(provisioning/upgrade)
Provider sub-account
Provider sub-account
Service Manager(service broker for platforms)
Kubernetes Cluster
Kubernetes Cluster
SAP Business Technology Platform
SAP Business Technology Platform
Application Components
Application Components
create/remove
CAPTenant
create/remove...
reconcile custom
resource objects
reconcile custom...
SaaS Provisioning callback
SaaS Provisioning callback -
maintain service
instances
maintain service...
web-hook(validating)RRRRRRR
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + + + + + + Subscription + + + web-server + + + + + + CAP + + + controller + + + + + + Kubernetes + + + API Server + + + + + + +
+
+
+ resources (etcd) +
+
+
+
+ + resources (etcd) + +
+
+ + + + AppRouter + + + + + + CAP Server + + + + + + Content Deployer + + + (UI5 apps, Portal) + + + + + + saas-registry + + + (tenant + + + subscription) + + + + + + xsuaa + + + (authentication) + + + + + + +
+
+
+ operator namespace +
+
+
+
+ + operator namespace + +
+
+ + + + service-manager + + + (resolve tenant + + + schema) + + + + + + html5-repository + + + service + + + + + + destination + + + service + + + + + + +
+
+
+ kube-system namespace +
+
+
+
+ + kube-system namespace + +
+
+ + + + cf-service-operator / sap-btp-service-operator + + + (manages service instances and bindings) + + + + + + ... + + + + + + +
+
+
+ application namespace +
+
+
+
+ + application namespace + +
+
+ + + + Tenant Operation + + + (provisioning/ + + + upgrade) + + + + + + +
+
+
+ Provider sub-account +
+
+
+
+ + Provider sub-account + +
+
+ + + + Service Manager + + + (service broker for platforms) + + + + + + +
+
+
+ + Kubernetes Cluster + +
+
+
+
+ + Kubernetes Cluster + +
+
+ + + + +
+
+
+ + SAP Business Technology Platform + +
+
+
+
+ + SAP Business Technology Platform + +
+
+ + + + +
+
+
+ + Application Components + +
+
+
+
+ + Application Components + +
+
+ + + + + +
+
+
+ + create/remove +
+ CAPTenant +
+
+
+
+
+ + create/remove... + +
+
+ + + + +
+
+
+ reconcile custom +
+ resource objects +
+
+
+
+ + reconcile custom... + +
+
+ + + + +
+
+
+ SaaS Provisioning callback +
+
+
+
+
+ + SaaS Provisioning callback + +
+
+ + + + +
+
+
+ maintain service +
+ instances +
+
+
+
+ + maintain service... + +
+
+ + + + web-hook + + + (validating) + + + + + + + R + + + + + + + + R + + + + + + + + R + + + + + + + + R + + + + + + + + R + + + + + + + + R + + + + + + + + R + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/website/static/img/block-controller.drawio.svg b/website/static/img/block-controller.drawio.svg index 0838899..41f8725 100644 --- a/website/static/img/block-controller.drawio.svg +++ b/website/static/img/block-controller.drawio.svg @@ -1 +1,353 @@ -Kubernetes API Server
Change notification queues (one per CRD)
Change notification queues (one per CRD)
etcd     
etcd     
CAPApplication
CAPApplication
CAPApplicationVersion
CAPApplicationVersion
CAPTenant
CAPTenant
CAPTenantOperation
CAPTenantOperation
Change notification queues (one per CRD)
Change notification queues (one per CRD)
Identify relevant Custom Resource Object 
Queue processing
Queue processing
enqueue
modified
resource
enqueue...
Controller
Controller
read item
read item
requeue / forget
requeue / forget
reconcile Custom Resource Object (one per CRD)Kubernetes resource informer(s)
watch resources (actual state)
watch resources (actual state)
update resources (desired state)
update resources (desired state)
RRKubernetes resource informer(s)Identify relevant Custom Resource Object Rreconcile Custom Resource Object (one per CRD)
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + Kubernetes API Server + + + + + + +
+
+
+ Change notification queues (one per CRD) +
+
+
+
+ + Change notification queues (one per CRD) + +
+
+ + + + +
+
+
+ etcd +
+
+
+
+ + etcd + +
+
+ + + + +
+
+
+ CAPApplication +
+
+
+
+ + CAPApplication + +
+
+ + + + +
+
+
+ CAPApplicationVersion +
+
+
+
+ + CAPApplicationVersion + +
+
+ + + + +
+
+
+ CAPTenant +
+
+
+
+ + CAPTenant + +
+
+ + + + +
+
+
+ CAPTenantOperation +
+
+
+
+ + CAPTenantOperation + +
+
+ + + + +
+
+
+ Change notification queues (one per CRD) +
+
+
+
+ + Change notification queues (one per CRD) + +
+
+ + + + + + + + Identify relevant + + + Custom Resource Object + + + + + + +
+
+
+ Queue processing +
+
+
+
+ + Queue processing + +
+
+ + + + +
+
+
+ enqueue +
+ modified +
+ resource +
+
+
+
+ + enqueue... + +
+
+ + + + +
+
+
+ + Controller + +
+
+
+
+ + Controller + +
+
+ + + + +
+
+
+ read item +
+
+
+
+ + read item + +
+
+ + + + +
+
+
+ requeue / forget +
+
+
+
+ + requeue / forget + +
+
+ + + + + + reconcile Custom Resource Object (one per CRD) + + + + + + + + Kubernetes resource informer(s) + + + + + + +
+
+
+ watch resources (actual state) +
+
+
+
+ + watch resources (actual state) + +
+
+ + + + +
+
+
+ update resources (desired state) +
+
+
+
+ + update resources (desired state) + +
+
+ + + + + + + + + + + + + R + + + + + + + + R + + + + + + + + + Kubernetes resource informer(s) + + + + + + + + Identify relevant + + + Custom Resource Object + + + + + + + R + + + + + + + + + reconcile Custom Resource Object (one per CRD) + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/website/static/img/block-subscription.drawio.svg b/website/static/img/block-subscription.drawio.svg index 1f7eb1b..2890c83 100644 --- a/website/static/img/block-subscription.drawio.svg +++ b/website/static/img/block-subscription.drawio.svg @@ -1,13 +1,13 @@ - + - - - + + + -
+
Subscription server @@ -15,16 +15,16 @@
- + Subscription server - + -
+
Subscription server @@ -32,16 +32,16 @@
- + Subscription server - + -
+
Kubernetes cluster @@ -49,22 +49,22 @@
- + Kubernetes cluster - + - + Kubernetes API Server - + -
+
etcd @@ -72,16 +72,16 @@
- + etcd - + -
+
CAPTenant @@ -89,16 +89,16 @@
- + CAPTenant - + -
+
CAPApplication @@ -106,16 +106,16 @@
- + CAPApplication - + -
+
Secrets @@ -123,40 +123,40 @@
- + Secrets - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + R - - + + -
+
5 @@ -164,7 +164,7 @@
- + 5 @@ -172,7 +172,7 @@ -
+
create/delete: CAPTenant @@ -182,17 +182,17 @@
- + create/delete: CAPTenant... - - + + -
+
saas-registry @@ -200,7 +200,7 @@
- + saas-registry @@ -208,7 +208,7 @@ -
+
SAP BTP services - Provider sub-account @@ -216,24 +216,24 @@
- + SAP BTP services - Provider sub-accou... - - + + - + R - - + + -
+
asynchronous callback @@ -241,34 +241,34 @@
- + asynchronous callback - - - - - - - - - - - - + + + + + + + + + + + + - + R - - + + -
+
UAA @@ -276,7 +276,7 @@
- + UAA @@ -284,7 +284,7 @@ -
+
Tenant provisioning triggered: @@ -303,7 +303,7 @@
- + Tenant provisioning triggered:...