diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1568a383809..ce7089273ef 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -96,6 +96,7 @@ /apps/iam/ @grafana/access-squad /apps/sdk.mk @grafana/grafana-app-platform-squad /apps/correlations @grafana/datapro +/apps/example/ @grafana/grafana-app-platform-squad /apps/logsdrilldown/ @grafana/observability-logs /pkg/api/ @grafana/grafana-backend-group /pkg/apis/ @grafana/grafana-app-platform-squad diff --git a/Dockerfile b/Dockerfile index 16efe807bfc..0edb989ac76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -114,6 +114,7 @@ COPY apps/alerting/notifications apps/alerting/notifications COPY apps/alerting/rules apps/alerting/rules COPY pkg/codegen pkg/codegen COPY pkg/plugins/codegen pkg/plugins/codegen +COPY apps/example apps/example RUN go mod download diff --git a/apps/example/Makefile b/apps/example/Makefile new file mode 100644 index 00000000000..230bfd4149a --- /dev/null +++ b/apps/example/Makefile @@ -0,0 +1,9 @@ +include ../sdk.mk + +.PHONY: generate # Run Grafana App SDK code generation +generate: install-app-sdk update-app-sdk + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none \ No newline at end of file diff --git a/apps/example/README.md b/apps/example/README.md new file mode 100644 index 00000000000..8a69389c3fa --- /dev/null +++ b/apps/example/README.md @@ -0,0 +1,120 @@ +# Example App + +This App is an example of general app capabilities when developing on the grafana app platform. + +## Enabling the App + +By default, the example app is disabled. To enable this App, add the following to your `conf/custom.ini`: + +``` +[grafana-apiserver] +runtime_config = example.grafana.app/v0alpha1=true,example.grafana.app/v1alpha1=true +``` + +## Manifest + +The source of the app's schemas and list of capabilities is the manifest, which is generated from [kinds/manifest.cue](./kinds/manifest.cue). +The `Example` kind is defined for [v0alpha1 here](./kinds/example_v0alpha1) and [v1alpha1 (default) here](./kinds/example_v1alpha1.cue). +The root definition of the `Example` kind that both versions share is defined [here](./kinds/example.cue). + +The CUE is used to generate code (and the AppManifest) when `make generate` is run. + +## Code + +All of the app's code is located in [pkg/app](./pkg/app/). The [New() function](./pkg/app/app.go#20) in `pkg/app/app.go` is the entry point of the app, +and everything should be discoverable from there. + +The code to register the app with the grafana API server (including inserting the app-specific config [ExampleConfig](./pkg/app/config.go)) +is located in [/pkg/registry/apps/example/register.go](../../pkg/registry/apps/example/register.go). + +Any app must also have its installer listed in [WireSet](../../pkg/registry/apps/wireset.go) and added to `installers` in [ProvideAppInstallers](../../pkg/registry/apps/apps.go). When building a new app, make to to regerate wire (`make build` in the root of the repo does this). + +### Generated Code + +The [pkg/apis](./pkg/apis/) package, and all its subdirectories, contain code generated by `make generate`. +This code should not be edited, but it can be useful to look at when working through the flow of the app. + +## Sample Swagger Payloads + +Navigate to [localhost:3000/swagger?api=example.grafana.app-v1alpha1](http://localhost:3000/swagger?api=example.grafana.app-v1alpha1) to view the swagger for the app's `v1alpha1` version +(this version has the most capabilities/endpoints). You can use the `Execute` button to make requests via the swagger UI. + +Create a new `Example` resource with via swagger with: + +```json +{ + "apiVersion": "example.grafana.app/v1alpha1", + "kind": "Example", + "metadata": { + "name": "test", + "namespace": "default" + }, + "spec": { + "firstField": "test", + "secondField": 0, + "list": { + "info": "foo", + "next": { + "info": "bar" + } + } + } +} +``` + +Create an invalid object which will be rejected by validation: + +```json +{ + "apiVersion": "example.grafana.app/v1alpha1", + "kind": "Example", + "metadata": { + "name": "invalid", + "namespace": "default" + }, + "spec": { + "firstField": "test", + "secondField": 0, + "list": { + "info": "foo", + "next": { + "info": "bar" + } + } + } +} +``` + +Update `custom` subresource: + +```json +{ + "apiVersion": "example.grafana.app/v1alpha1", + "kind": "Example", + "metadata": { + "namespace": "default", + "name": "test", + "resourceVersion": "" + }, + "custom": { + "myField": "foo", + "otherField": "bar" + } +} +``` + +(`metadata.resourceVersion` is required for an update, use the value you get from a GET request) + +## cURL + +You can also interact with the grafana API server via a kubeconfig set up for it, or via `curl` using the `-u :` flag. +Currently, cluster-scoped custom routes are erased from the swagger as part of grafana's APIServer code, but can still be called via `curl`, like so: + +```bash +curl -u admin:admin http://localhost:3000/apis/example.grafana.app/v1alpha1/other +``` + +``` +% curl -u admin:admin http://localhost:3000/apis/example.grafana.app/v1alpha1/other +{"message":"This is a cluster route"} +``` diff --git a/apps/example/go.mod b/apps/example/go.mod new file mode 100644 index 00000000000..2a3e75d75c0 --- /dev/null +++ b/apps/example/go.mod @@ -0,0 +1,100 @@ +module github.com/grafana/grafana/apps/example + +go 1.25.3 + +require ( + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe + k8s.io/apimachinery v0.34.1 + k8s.io/apiserver v0.34.1 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect + github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/onsi/gomega v1.36.2 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/example/go.sum b/apps/example/go.sum new file mode 100644 index 00000000000..fcab8eb625f --- /dev/null +++ b/apps/example/go.sum @@ -0,0 +1,256 @@ +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/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +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= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= +github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= +github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= +github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= +github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe/go.mod h1:zn/yoxKpWA2KUsxOhQbSbL8OCkF2JNLpSEHs+hQYvdM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +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/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/example/kinds/cue.mod/module.cue b/apps/example/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..b2f49fc6dc6 --- /dev/null +++ b/apps/example/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/example/kinds" +language: version: "v0.8.2" diff --git a/apps/example/kinds/example.cue b/apps/example/kinds/example.cue new file mode 100644 index 00000000000..12e7f5a496d --- /dev/null +++ b/apps/example/kinds/example.cue @@ -0,0 +1,52 @@ +package kinds + +// This is our ExampleKind definition, which contains kind metadata. It is the same across all versions of the kind. +exampleKind: { + // Name is the human-readable name which is used for generated type names. + kind: "Example" + // Scope determines the scope of the kind in the API server. It currently allows two values: + // * Namespaced - resources for this kind are created inside namespaces + // * Cluster - resource for this kind are always cluster-wide (this can be thought of as a "global" namespace) + // If not present, this defaults to "Namespaced" + scope: "Namespaced" + // [OPTIONAL] + // The human-readable plural form of the "name" field. + // Will default to +"s" if not present. + pluralName: "Examples" + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + mutation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + conversion: true + // [OPTIONAL] + // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. + // If not present, default values within the codegen trait are used. + // If you wish to specify codegen per-version, put this section in the version's object + // (for example, exampleKindv1alpha1) instead. + codegen: { + // [OPTIONAL] + // ts contains TypeScript code generation properties for the kind + ts: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate front-end TypeScript code for the kind. + // Defaults to true if not present. + enabled: true + } + // [OPTIONAL] + // go contains go code generation properties for the kind + go: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate back-end go code for the kind. + // Defaults to true if not present. + enabled: true + } + } +} diff --git a/apps/example/kinds/example_v0alpha1.cue b/apps/example/kinds/example_v0alpha1.cue new file mode 100644 index 00000000000..645189f7dc2 --- /dev/null +++ b/apps/example/kinds/example_v0alpha1.cue @@ -0,0 +1,15 @@ +package kinds + +// This is the v0alpha1 version of the kind. Please see v1alpha1 for more complete comments +// and a more complex schema and set of capabilities. +examplev0alpha1: exampleKind & { + schema: { + // Spec is the schema of our resource. The spec should include all the user-editable information for the kind. + spec: { + firstField: int + } + status: { + lastObservedGeneration: int64 + } + } +} \ No newline at end of file diff --git a/apps/example/kinds/example_v1alpha1.cue b/apps/example/kinds/example_v1alpha1.cue new file mode 100644 index 00000000000..bb54c59d56c --- /dev/null +++ b/apps/example/kinds/example_v1alpha1.cue @@ -0,0 +1,76 @@ +package kinds + +// This is the v1alpha1 version of the kind, which joins the kind metadata and +// version-specific information for the kind, such as the schema +examplev1alpha1: exampleKind & { + // schema is the schema for this version of the kind + // As an API server-expressable resource, the schema has a restricted format: + // { + // spec: { ... } + // status: { ... } // optional + // metadata: { ... } // optional + // } + // `spec` must always be present, and is the schema for the object. + // `status` is optional, and should contain status or state information which is typically not user-editable + // (controlled by controllers/operators). The kind system adds some implicit status information which is + // common across all kinds, and becomes present in the unified lineage used for code generation and other tooling. + // `metadata` is optional, and should contain kind- or schema-specific metadata. The kind system adds + // an explicit set of common metadata which can be found in the definition file for a CUE kind at + // [https://github.com/grafana/grafana-app-sdk/blob/main/codegen/cuekind/def.cue] + // additional metadata fields cannot conflict with the common metadata field names + schema: { + // #DefinedType is a re-usable definition for us to use in our schema. + // Fields leading with # are definitions in CUE and won't be included in the generated types. + #DefinedType: { + // Info is information about this entry. This comment, like all comments + // on fields or definitions, will be copied into the generated types as well. + info: string + // Next is an optional next element in the DefinedType, allowing for a self-referential + // linked-list like structure. The ? in the field makes this optional. + next?: #DefinedType + } + // Spec is the schema of our resource. The spec should include all the user-editable information for the kind. + spec: { + // Example fields + firstField: string + secondField: int + list?: #DefinedType + } + // status is where state and status information which may be used or updated by the operator or back-end should be placed + // If you do not have any such information, you do not need to include this field, + // however, as mentioned above, certain fields will be added by the kind system regardless. + status: { + lastObservedGeneration: int64 + } + // Custom is a subresource that will be stored the same way status is stored, + // and requires using the /custom route to update. + // Its content is returned as part of a GET to the resource itself, just like with status. + // To route a subresource to an arbitrary handler, use the 'routes' field instead (see below). + custom: { + myField: string + otherField: string + } + // metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations + // with getters and setters by the code generation. + //metadata: { + // kindSpecificField: string + //} + } + + // routes contains subresource routes for the kind, which are exposed as HTTP handlers on 'examples//'. + // This allows you to add additional non-storage-based handlers to your kind. + // These should only be used if the behavior cannot be accomplished by reconciliation on storage events. + routes: { + // This will add a handler for /foo on the resource + "foo": { + // GET request handler. A subresource route can have multiple methods attached to it. + // Allowed values are GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS + "GET": { + // The response type for the GET /foo method. This will generate a go type, and will also be used for the OpenAPI definition for the route. + response: { + message: string + } + } + } + } +} \ No newline at end of file diff --git a/apps/example/kinds/manifest.cue b/apps/example/kinds/manifest.cue new file mode 100644 index 00000000000..934d419623d --- /dev/null +++ b/apps/example/kinds/manifest.cue @@ -0,0 +1,116 @@ +package kinds + +manifest: { + // appName is the unique name of your app. It is used to reference the app from other config objects, + // and to generate the group used by your app in the app platform API. + appName: "example" + // groupOverride can be used to specify a non-appName-based API group. + // By default, an app's API group is LOWER(REPLACE(appName, '-', '')).ext.grafana.com, + // but there are cases where this needs to be changed. + // Keep in mind that changing this after an app is deployed can cause problems with clients and/or kind data. + groupOverride: "example.grafana.app" + + // versions is a map of versions supported by your app. Version names should follow the format "v" or + // "v(alpha|beta)". Each version contains the kinds your app manages for that version. + // If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access. + versions: { + "v0alpha1": v0alpha1 + "v1alpha1": v1alpha1 + } + // extraPermissions contains any additional permissions your app may require to function. + // Your app will always have all permissions for each kind it manages (the items defined in 'kinds'). + extraPermissions: { + // If your app needs access to additional kinds supplied by other apps, you can list them here + accessKinds: [ + // Here is an example for your app accessing the playlist kind for reads and watch + // { + // group: "playlist.grafana.app" + // resource: "playlists" + // actions: ["get","list","watch"] + // } + ] + } +} + +v0alpha1: { + kinds: [examplev0alpha1] + // This is explicitly set to false to keep the example app disabled by default. + // It can be enabled via conf overrides, or by setting this value to true and regenerating. + served: false +} + +// v1alpha1 is the v1alpha1 version of the app's API. +// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version. +v1alpha1: { + // kinds is the list of kinds served by this version + kinds:[examplev1alpha1] + // [OPTIONAL] + // served indicates whether this particular version is served by the API server. + // served should be set to false before a version is removed from the manifest entirely. + // served defaults to true if not present. + // This is explicitly set to false to keep the example app disabled by default. + // It can be enabled via conf overrides, or by setting this value to true and regenerating. + served: false + // routes contains resource routes for the version, which are split into 'namespaced' and 'cluster' scoped routes. + // This allows you to add additional non-storage- and non-kind- based handlers for your app. + // These should only be used if the behavior cannot be accomplished by reconciliation on storage events or subresource routes on a kind. + routes: { + // namespaced contains namespace-scoped resource routes for the version, + // which are exposed as HTTP handlers on '/namespaces//'. + namespaced: { + "/something": { + "GET": { + response: { + namespace: string + message: string + } + request: { + query: { + message?: string + } + } + } + } + } + // cluster contains cluster-scoped resource routes for the version, + // which are exposed as HTTP handlers on '/'. + cluster: { + "/other": { + "GET": { + response: { + message: string + } + request: { + query: { + message?: string + } + } + responseMetadata: typeMeta: false // Don't generate or return kubernetes type metadata for this object + } + } + } + } + // [OPTIONAL] + // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. + // If not present, default values within the codegen trait are used. + // If you wish to specify codegen per-version, put this section in the version's object + // (for example, v1alpha1) instead. + codegen: { + // [OPTIONAL] + // ts contains TypeScript code generation properties for the kind + ts: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate front-end TypeScript code for the kind. + // Defaults to true if not present. + enabled: true + } + // [OPTIONAL] + // go contains go code generation properties for the kind + go: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate back-end go code for the kind. + // Defaults to true if not present. + enabled: true + } + } +} \ No newline at end of file diff --git a/apps/example/pkg/apis/example/v0alpha1/constants.go b/apps/example/pkg/apis/example/v0alpha1/constants.go new file mode 100644 index 00000000000..edbbe6d97c2 --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/constants.go @@ -0,0 +1,18 @@ +package v0alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "example.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v0alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/example/pkg/apis/example/v0alpha1/example_client_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_client_gen.go new file mode 100644 index 00000000000..e528b6b5cbc --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ExampleClient struct { + client *resource.TypedClient[*Example, *ExampleList] +} + +func NewExampleClient(client resource.Client) *ExampleClient { + return &ExampleClient{ + client: resource.NewTypedClient[*Example, *ExampleList](client, ExampleKind()), + } +} + +func NewExampleClientFromGenerator(generator resource.ClientGenerator) (*ExampleClient, error) { + c, err := generator.ClientFor(ExampleKind()) + if err != nil { + return nil, err + } + return NewExampleClient(c), nil +} + +func (c *ExampleClient) Get(ctx context.Context, identifier resource.Identifier) (*Example, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ExampleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ExampleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ExampleClient) Create(ctx context.Context, obj *Example, opts resource.CreateOptions) (*Example, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ExampleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ExampleClient) Update(ctx context.Context, obj *Example, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ExampleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Example, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ExampleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ExampleStatus, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, &Example{ + TypeMeta: metav1.TypeMeta{ + Kind: ExampleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ExampleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/example/pkg/apis/example/v0alpha1/example_codec_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_codec_gen.go new file mode 100644 index 00000000000..67bb330bfe6 --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// ExampleJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ExampleJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*ExampleJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*ExampleJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &ExampleJSONCodec{} diff --git a/apps/example/pkg/apis/example/v0alpha1/example_metadata_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_metadata_gen.go new file mode 100644 index 00000000000..1e390bb070f --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type ExampleMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewExampleMetadata creates a new ExampleMetadata object. +func NewExampleMetadata() *ExampleMetadata { + return &ExampleMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/example/pkg/apis/example/v0alpha1/example_object_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_object_gen.go new file mode 100644 index 00000000000..9b846649ebd --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Example struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Example + Spec ExampleSpec `json:"spec" yaml:"spec"` + + Status ExampleStatus `json:"status" yaml:"status"` +} + +func (o *Example) GetSpec() any { + return o.Spec +} + +func (o *Example) SetSpec(spec any) error { + cast, ok := spec.(ExampleSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Example) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Example) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Example) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(ExampleStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type ExampleStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Example) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Example) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Example) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Example) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Example) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Example) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Example) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Example) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Example) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Example) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Example) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Example) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Example) DeepCopy() *Example { + cpy := &Example{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Example) DeepCopyInto(dst *Example) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Example{} + +// +k8s:openapi-gen=true +type ExampleList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Example `json:"items" yaml:"items"` +} + +func (o *ExampleList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExampleList) Copy() resource.ListObject { + cpy := &ExampleList{ + TypeMeta: o.TypeMeta, + Items: make([]Example, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Example); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ExampleList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ExampleList) SetItems(items []resource.Object) { + o.Items = make([]Example, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Example) + } +} + +func (o *ExampleList) DeepCopy() *ExampleList { + cpy := &ExampleList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ExampleList) DeepCopyInto(dst *ExampleList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ExampleList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *ExampleSpec) DeepCopy() *ExampleSpec { + cpy := &ExampleSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *ExampleSpec) DeepCopyInto(dst *ExampleSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ExampleStatus +func (s *ExampleStatus) DeepCopy() *ExampleStatus { + cpy := &ExampleStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ExampleStatus into another ExampleStatus object +func (s *ExampleStatus) DeepCopyInto(dst *ExampleStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/example/pkg/apis/example/v0alpha1/example_schema_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_schema_gen.go new file mode 100644 index 00000000000..1feaa8d42d1 --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaExample = resource.NewSimpleSchema("example.grafana.app", "v0alpha1", &Example{}, &ExampleList{}, resource.WithKind("Example"), + resource.WithPlural("examples"), resource.WithScope(resource.NamespacedScope)) + kindExample = resource.Kind{ + Schema: schemaExample, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ExampleJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ExampleKind() resource.Kind { + return kindExample +} + +// Schema returns a resource.SimpleSchema representation of Example +func ExampleSchema() *resource.SimpleSchema { + return schemaExample +} + +// Interface compliance checks +var _ resource.Schema = kindExample diff --git a/apps/example/pkg/apis/example/v0alpha1/example_spec_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_spec_gen.go new file mode 100644 index 00000000000..f0e408554ab --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_spec_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// +k8s:openapi-gen=true +type ExampleSpec struct { + FirstField int64 `json:"firstField"` +} + +// NewExampleSpec creates a new ExampleSpec object. +func NewExampleSpec() *ExampleSpec { + return &ExampleSpec{} +} diff --git a/apps/example/pkg/apis/example/v0alpha1/example_status_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_status_gen.go new file mode 100644 index 00000000000..75c78737ada --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_status_gen.go @@ -0,0 +1,45 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type ExamplestatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State ExampleStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewExamplestatusOperatorState creates a new ExamplestatusOperatorState object. +func NewExamplestatusOperatorState() *ExamplestatusOperatorState { + return &ExamplestatusOperatorState{} +} + +// +k8s:openapi-gen=true +type ExampleStatus struct { + LastObservedGeneration int64 `json:"lastObservedGeneration"` + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]ExamplestatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewExampleStatus creates a new ExampleStatus object. +func NewExampleStatus() *ExampleStatus { + return &ExampleStatus{} +} + +// +k8s:openapi-gen=true +type ExampleStatusOperatorStateState string + +const ( + ExampleStatusOperatorStateStateSuccess ExampleStatusOperatorStateState = "success" + ExampleStatusOperatorStateStateInProgress ExampleStatusOperatorStateState = "in_progress" + ExampleStatusOperatorStateStateFailed ExampleStatusOperatorStateState = "failed" +) diff --git a/apps/example/pkg/apis/example/v1alpha1/constants.go b/apps/example/pkg/apis/example/v1alpha1/constants.go new file mode 100644 index 00000000000..68a0c69a614 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "example.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/example/pkg/apis/example/v1alpha1/example_client_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_client_gen.go new file mode 100644 index 00000000000..686c663c12c --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_client_gen.go @@ -0,0 +1,144 @@ +package v1alpha1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ExampleClient struct { + client *resource.TypedClient[*Example, *ExampleList] +} + +func NewExampleClient(client resource.Client) *ExampleClient { + return &ExampleClient{ + client: resource.NewTypedClient[*Example, *ExampleList](client, ExampleKind()), + } +} + +func NewExampleClientFromGenerator(generator resource.ClientGenerator) (*ExampleClient, error) { + c, err := generator.ClientFor(ExampleKind()) + if err != nil { + return nil, err + } + return NewExampleClient(c), nil +} + +func (c *ExampleClient) Get(ctx context.Context, identifier resource.Identifier) (*Example, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ExampleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ExampleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ExampleClient) Create(ctx context.Context, obj *Example, opts resource.CreateOptions) (*Example, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ExampleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ExampleClient) Update(ctx context.Context, obj *Example, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ExampleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Example, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ExampleClient) UpdateCustom(ctx context.Context, identifier resource.Identifier, newCustom ExampleCustom, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, &Example{ + TypeMeta: metav1.TypeMeta{ + Kind: ExampleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Custom: newCustom, + }, resource.UpdateOptions{ + Subresource: "custom", + ResourceVersion: opts.ResourceVersion, + }) +} +func (c *ExampleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ExampleStatus, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, &Example{ + TypeMeta: metav1.TypeMeta{ + Kind: ExampleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ExampleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} + +type GetFooRequest struct { + Params GetFooRequestParams + Headers http.Header +} + +func (c *ExampleClient) GetFoo(ctx context.Context, identifier resource.Identifier, request GetFooRequest) (*GetFoo, error) { + params := url.Values{} + resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{ + Path: "foo", + Verb: "GET", + Query: params, + Headers: request.Headers, + }) + if err != nil { + return nil, err + } + cast := GetFoo{} + err = json.Unmarshal(resp, &cast) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response bytes into GetFoo: %w", err) + } + return &cast, nil +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_codec_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_codec_gen.go new file mode 100644 index 00000000000..80cee2b1cfc --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// ExampleJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ExampleJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*ExampleJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*ExampleJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &ExampleJSONCodec{} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_custom_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_custom_gen.go new file mode 100644 index 00000000000..bcb4f4bb363 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_custom_gen.go @@ -0,0 +1,25 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// Custom is a subresource that will be stored the same way status is stored, +// and requires using the /custom route to update. +// Its content is returned as part of a GET to the resource itself, just like with status. +// To route a subresource to an arbitrary handler, use the 'routes' field instead (see below). +// metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations +// with getters and setters by the code generation. +// +// metadata: { +// kindSpecificField: string +// } +// +// +k8s:openapi-gen=true +type ExampleCustom struct { + MyField string `json:"myField"` + OtherField string `json:"otherField"` +} + +// NewExampleCustom creates a new ExampleCustom object. +func NewExampleCustom() *ExampleCustom { + return &ExampleCustom{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_body_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_body_types_gen.go new file mode 100644 index 00000000000..42344f1cb6d --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_body_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetFooRequestBody struct { + Bar string `json:"bar"` +} + +// NewGetFooRequestBody creates a new GetFooRequestBody object. +func NewGetFooRequestBody() *GetFooRequestBody { + return &GetFooRequestBody{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_object_gen.go new file mode 100644 index 00000000000..bf6a51b1815 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetFooRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetFooRequestParams `json:",inline"` +} + +func NewGetFooRequestParamsObject() *GetFooRequestParamsObject { + return &GetFooRequestParamsObject{} +} + +func (o *GetFooRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetFooRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetFooRequestParamsObject) DeepCopyInto(dst *GetFooRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetFooRequestParams := GetFooRequestParams{} + _ = resource.CopyObjectInto(&dstGetFooRequestParams, &o.GetFooRequestParams) +} + +var _ runtime.Object = NewGetFooRequestParamsObject() diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_types_gen.go new file mode 100644 index 00000000000..f0f04d7d0a2 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetFooRequestParams struct { + Message *string `json:"message,omitempty"` +} + +// NewGetFooRequestParams creates a new GetFooRequestParams object. +func NewGetFooRequestParams() *GetFooRequestParams { + return &GetFooRequestParams{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_body_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_body_types_gen.go new file mode 100644 index 00000000000..ab77d0905b1 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_body_types_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// The response type for the GET /foo method. This will generate a go type, and will also be used for the OpenAPI definition for the route. +// +k8s:openapi-gen=true +type GetFooBody struct { + Message string `json:"message"` +} + +// NewGetFooBody creates a new GetFooBody object. +func NewGetFooBody() *GetFooBody { + return &GetFooBody{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_object_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_object_types_gen.go new file mode 100644 index 00000000000..ba41aba79c5 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetFoo struct { + metav1.TypeMeta `json:",inline"` + GetFooBody `json:",inline"` +} + +func NewGetFoo() *GetFoo { + return &GetFoo{} +} + +func (t *GetFooBody) DeepCopyInto(dst *GetFooBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetFoo) DeepCopyObject() runtime.Object { + dst := NewGetFoo() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetFoo) DeepCopyInto(dst *GetFoo) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetFooBody.DeepCopyInto(&dst.GetFooBody) +} + +var _ runtime.Object = NewGetFoo() diff --git a/apps/example/pkg/apis/example/v1alpha1/example_metadata_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_metadata_gen.go new file mode 100644 index 00000000000..e3974409f20 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type ExampleMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewExampleMetadata creates a new ExampleMetadata object. +func NewExampleMetadata() *ExampleMetadata { + return &ExampleMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_object_gen.go new file mode 100644 index 00000000000..efd1374e855 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_object_gen.go @@ -0,0 +1,347 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Example struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Example + Spec ExampleSpec `json:"spec" yaml:"spec"` + + Status ExampleStatus `json:"status" yaml:"status"` + + Custom ExampleCustom `json:"custom" yaml:"custom"` +} + +func (o *Example) GetSpec() any { + return o.Spec +} + +func (o *Example) SetSpec(spec any) error { + cast, ok := spec.(ExampleSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Example) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + + "custom": o.Custom, + } +} + +func (o *Example) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + + case "custom": + return o.Custom, true + default: + return nil, false + } +} + +func (o *Example) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(ExampleStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type ExampleStatus", value) + } + o.Status = cast + return nil + + case "custom": + cast, ok := value.(ExampleCustom) + if !ok { + return fmt.Errorf("cannot set custom type %#v, not of type ExampleCustom", value) + } + o.Custom = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Example) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Example) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Example) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Example) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Example) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Example) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Example) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Example) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Example) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Example) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Example) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Example) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Example) DeepCopy() *Example { + cpy := &Example{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Example) DeepCopyInto(dst *Example) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) + o.Custom.DeepCopyInto(&dst.Custom) +} + +// Interface compliance compile-time check +var _ resource.Object = &Example{} + +// +k8s:openapi-gen=true +type ExampleList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Example `json:"items" yaml:"items"` +} + +func (o *ExampleList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExampleList) Copy() resource.ListObject { + cpy := &ExampleList{ + TypeMeta: o.TypeMeta, + Items: make([]Example, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Example); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ExampleList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ExampleList) SetItems(items []resource.Object) { + o.Items = make([]Example, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Example) + } +} + +func (o *ExampleList) DeepCopy() *ExampleList { + cpy := &ExampleList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ExampleList) DeepCopyInto(dst *ExampleList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ExampleList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *ExampleSpec) DeepCopy() *ExampleSpec { + cpy := &ExampleSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *ExampleSpec) DeepCopyInto(dst *ExampleSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ExampleStatus +func (s *ExampleStatus) DeepCopy() *ExampleStatus { + cpy := &ExampleStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ExampleStatus into another ExampleStatus object +func (s *ExampleStatus) DeepCopyInto(dst *ExampleStatus) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ExampleCustom +func (s *ExampleCustom) DeepCopy() *ExampleCustom { + cpy := &ExampleCustom{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ExampleCustom into another ExampleCustom object +func (s *ExampleCustom) DeepCopyInto(dst *ExampleCustom) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_schema_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_schema_gen.go new file mode 100644 index 00000000000..e1f7dca0b02 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaExample = resource.NewSimpleSchema("example.grafana.app", "v1alpha1", &Example{}, &ExampleList{}, resource.WithKind("Example"), + resource.WithPlural("examples"), resource.WithScope(resource.NamespacedScope)) + kindExample = resource.Kind{ + Schema: schemaExample, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ExampleJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ExampleKind() resource.Kind { + return kindExample +} + +// Schema returns a resource.SimpleSchema representation of Example +func ExampleSchema() *resource.SimpleSchema { + return schemaExample +} + +// Interface compliance checks +var _ resource.Schema = kindExample diff --git a/apps/example/pkg/apis/example/v1alpha1/example_spec_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_spec_gen.go new file mode 100644 index 00000000000..3f73653ac1c --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_spec_gen.go @@ -0,0 +1,34 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// #DefinedType is a re-usable definition for us to use in our schema. +// Fields leading with # are definitions in CUE and won't be included in the generated types. +// +k8s:openapi-gen=true +type ExampleDefinedType struct { + // Info is information about this entry. This comment, like all comments + // on fields or definitions, will be copied into the generated types as well. + Info string `json:"info"` + // Next is an optional next element in the DefinedType, allowing for a self-referential + // linked-list like structure. The ? in the field makes this optional. + Next *ExampleDefinedType `json:"next,omitempty"` +} + +// NewExampleDefinedType creates a new ExampleDefinedType object. +func NewExampleDefinedType() *ExampleDefinedType { + return &ExampleDefinedType{} +} + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// +k8s:openapi-gen=true +type ExampleSpec struct { + // Example fields + FirstField string `json:"firstField"` + SecondField int64 `json:"secondField"` + List *ExampleDefinedType `json:"list,omitempty"` +} + +// NewExampleSpec creates a new ExampleSpec object. +func NewExampleSpec() *ExampleSpec { + return &ExampleSpec{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_status_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_status_gen.go new file mode 100644 index 00000000000..ced0f82949e --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_status_gen.go @@ -0,0 +1,48 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type ExamplestatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State ExampleStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewExamplestatusOperatorState creates a new ExamplestatusOperatorState object. +func NewExamplestatusOperatorState() *ExamplestatusOperatorState { + return &ExamplestatusOperatorState{} +} + +// status is where state and status information which may be used or updated by the operator or back-end should be placed +// If you do not have any such information, you do not need to include this field, +// however, as mentioned above, certain fields will be added by the kind system regardless. +// +k8s:openapi-gen=true +type ExampleStatus struct { + LastObservedGeneration int64 `json:"lastObservedGeneration"` + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]ExamplestatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewExampleStatus creates a new ExampleStatus object. +func NewExampleStatus() *ExampleStatus { + return &ExampleStatus{} +} + +// +k8s:openapi-gen=true +type ExampleStatusOperatorStateState string + +const ( + ExampleStatusOperatorStateStateSuccess ExampleStatusOperatorStateState = "success" + ExampleStatusOperatorStateStateInProgress ExampleStatusOperatorStateState = "in_progress" + ExampleStatusOperatorStateStateFailed ExampleStatusOperatorStateState = "failed" +) diff --git a/apps/example/pkg/apis/example/v1alpha1/getother_request_params_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_object_gen.go new file mode 100644 index 00000000000..25fa13336e2 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetOtherRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetOtherRequestParams `json:",inline"` +} + +func NewGetOtherRequestParamsObject() *GetOtherRequestParamsObject { + return &GetOtherRequestParamsObject{} +} + +func (o *GetOtherRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetOtherRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetOtherRequestParamsObject) DeepCopyInto(dst *GetOtherRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetOtherRequestParams := GetOtherRequestParams{} + _ = resource.CopyObjectInto(&dstGetOtherRequestParams, &o.GetOtherRequestParams) +} + +var _ runtime.Object = NewGetOtherRequestParamsObject() diff --git a/apps/example/pkg/apis/example/v1alpha1/getother_request_params_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_types_gen.go new file mode 100644 index 00000000000..09b886b1df7 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetOtherRequestParams struct { + Message *string `json:"message,omitempty"` +} + +// NewGetOtherRequestParams creates a new GetOtherRequestParams object. +func NewGetOtherRequestParams() *GetOtherRequestParams { + return &GetOtherRequestParams{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getother_response_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getother_response_types_gen.go new file mode 100644 index 00000000000..e6c7bdf6ad8 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getother_response_types_gen.go @@ -0,0 +1,13 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type GetOther struct { + Message string `json:"message"` +} + +// NewGetOther creates a new GetOther object. +func NewGetOther() *GetOther { + return &GetOther{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_object_gen.go new file mode 100644 index 00000000000..83cdd19ad7f --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetSomethingRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetSomethingRequestParams `json:",inline"` +} + +func NewGetSomethingRequestParamsObject() *GetSomethingRequestParamsObject { + return &GetSomethingRequestParamsObject{} +} + +func (o *GetSomethingRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetSomethingRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSomethingRequestParamsObject) DeepCopyInto(dst *GetSomethingRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetSomethingRequestParams := GetSomethingRequestParams{} + _ = resource.CopyObjectInto(&dstGetSomethingRequestParams, &o.GetSomethingRequestParams) +} + +var _ runtime.Object = NewGetSomethingRequestParamsObject() diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_types_gen.go new file mode 100644 index 00000000000..30ff91085ce --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetSomethingRequestParams struct { + Message *string `json:"message,omitempty"` +} + +// NewGetSomethingRequestParams creates a new GetSomethingRequestParams object. +func NewGetSomethingRequestParams() *GetSomethingRequestParams { + return &GetSomethingRequestParams{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_response_body_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_body_types_gen.go new file mode 100644 index 00000000000..c785f9c4ecd --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_body_types_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type GetSomethingBody struct { + Namespace string `json:"namespace"` + Message string `json:"message"` +} + +// NewGetSomethingBody creates a new GetSomethingBody object. +func NewGetSomethingBody() *GetSomethingBody { + return &GetSomethingBody{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_response_object_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_object_types_gen.go new file mode 100644 index 00000000000..2e6ce668030 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetSomething struct { + metav1.TypeMeta `json:",inline"` + GetSomethingBody `json:",inline"` +} + +func NewGetSomething() *GetSomething { + return &GetSomething{} +} + +func (t *GetSomethingBody) DeepCopyInto(dst *GetSomethingBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetSomething) DeepCopyObject() runtime.Object { + dst := NewGetSomething() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSomething) DeepCopyInto(dst *GetSomething) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetSomethingBody.DeepCopyInto(&dst.GetSomethingBody) +} + +var _ runtime.Object = NewGetSomething() diff --git a/apps/example/pkg/apis/example_manifest.go b/apps/example/pkg/apis/example_manifest.go new file mode 100644 index 00000000000..eab0ee792ab --- /dev/null +++ b/apps/example/pkg/apis/example_manifest.go @@ -0,0 +1,374 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + v0alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v0alpha1" + v1alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +var ( + rawSchemaExamplev0alpha1 = []byte(`{"Example":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"spec":{"additionalProperties":false,"description":"Spec is the schema of our resource. The spec should include all the user-editable information for the kind.","properties":{"firstField":{"type":"integer"}},"required":["firstField"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"lastObservedGeneration":{"type":"integer"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"required":["lastObservedGeneration"],"type":"object"}}`) + versionSchemaExamplev0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaExamplev0alpha1, &versionSchemaExamplev0alpha1) + rawSchemaExamplev1alpha1 = []byte(`{"DefinedType":{"additionalProperties":false,"description":"#DefinedType is a re-usable definition for us to use in our schema.\nFields leading with # are definitions in CUE and won't be included in the generated types.","properties":{"info":{"description":"Info is information about this entry. This comment, like all comments\non fields or definitions, will be copied into the generated types as well.","type":"string"},"next":{"$ref":"#/components/schemas/DefinedType","description":"Next is an optional next element in the DefinedType, allowing for a self-referential\nlinked-list like structure. The ? in the field makes this optional."}},"required":["info"],"type":"object"},"Example":{"properties":{"custom":{"$ref":"#/components/schemas/custom"},"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"custom":{"additionalProperties":false,"description":"Custom is a subresource that will be stored the same way status is stored,\nand requires using the /custom route to update.\nIts content is returned as part of a GET to the resource itself, just like with status.\nTo route a subresource to an arbitrary handler, use the 'routes' field instead (see below).\nmetadata if where kind- and schema-specific metadata goes. This is converted into typed annotations\nwith getters and setters by the code generation.\nmetadata: {\n\tkindSpecificField: string\n}","properties":{"myField":{"type":"string"},"otherField":{"type":"string"}},"required":["myField","otherField"],"type":"object"},"spec":{"additionalProperties":false,"description":"Spec is the schema of our resource. The spec should include all the user-editable information for the kind.","properties":{"firstField":{"description":"Example fields","type":"string"},"list":{"$ref":"#/components/schemas/DefinedType"},"secondField":{"type":"integer"}},"required":["firstField","secondField"],"type":"object"},"status":{"additionalProperties":false,"description":"status is where state and status information which may be used or updated by the operator or back-end should be placed\nIf you do not have any such information, you do not need to include this field,\nhowever, as mentioned above, certain fields will be added by the kind system regardless.","properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"lastObservedGeneration":{"type":"integer"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"required":["lastObservedGeneration"],"type":"object"}}`) + versionSchemaExamplev1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaExamplev1alpha1, &versionSchemaExamplev1alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "example", + Group: "example.grafana.app", + PreferredVersion: "v1alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: false, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Example", + Plural: "Examples", + Scope: "Namespaced", + Conversion: true, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + Mutation: &app.MutationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaExamplev0alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, + }, + + { + Name: "v1alpha1", + Served: false, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Example", + Plural: "Examples", + Scope: "Namespaced", + Conversion: true, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + Mutation: &app.MutationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaExamplev1alpha1, + Routes: map[string]spec3.PathProps{ + "foo": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getFoo", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Description: "The response type for the GET /foo method. This will generate a go type, and will also be used for the OpenAPI definition for the route.", + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "message", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{ + "/something": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getSomething", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "namespace", + "message", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + Cluster: map[string]spec3.PathProps{ + "/other": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getOther", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "message", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("example") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Example/v0alpha1": v0alpha1.ExampleKind(), + "Example/v1alpha1": v1alpha1.ExampleKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{ + + "v1alpha1|Example|foo|GET": v1alpha1.GetFoo{}, + + "v1alpha1||/something|GET": v1alpha1.GetSomething{}, + "v1alpha1||other|GET": v1alpha1.GetOther{}, +} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{ + "v1alpha1|Example|foo|GET": &v1alpha1.GetFooRequestParamsObject{}, +} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/example/pkg/app/app.go b/apps/example/pkg/app/app.go new file mode 100644 index 00000000000..3d189ca6155 --- /dev/null +++ b/apps/example/pkg/app/app.go @@ -0,0 +1,143 @@ +package app + +import ( + "fmt" + "log/slog" + "os" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + "k8s.io/apimachinery/pkg/runtime/schema" + + examplev0alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v0alpha1" + examplev1alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +// New creates a new instance of the Example App. It gets called after the app's APIs have been registered, +// and is used for routing non-storage API requests, admission control, conversion, and can run +// reconcilers on kinds. +func New(cfg app.Config) (app.App, error) { + // APIPath needs to be set to `/apis`, as it defaults to empty + cfg.KubeConfig.APIPath = "/apis" + // We create a client to work with our Example kind in our reconciler + client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).ClientFor(examplev1alpha1.ExampleKind()) + if err != nil { + return nil, fmt.Errorf("unable to create example client: %w", err) + } + var reconciler operator.Reconciler + exampleConfig, ok := cfg.SpecificConfig.(*ExampleConfig) + if ok && exampleConfig.EnableReconciler { + reconciler = NewExampleReconciler(client) + // Set the default logger if the reconciler is enabled--this should be done in grafana's API server handling instead, + // and will be corrected in a future PR + logging.DefaultLogger = logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, // Temporarily hardcoded to debug for the example + })) + } + + // This is the configuration for our App. + simpleConfig := simple.AppConfig{ + Name: "example", + KubeConfig: cfg.KubeConfig, + // ManagedKinds is the list of all kinds our app manages (the kinds owned by our app). + // Here, a Kind is defined as a distinct Group, Version, and Kind combination, + // so for each version of our Example kind, we need to add it to this list. + // Each kind can also have admission control attached to it--different versions can have different admission control attached. + // Handlers for custom routes defined in the manifest for the kind go here--this is where they actuall get routed, + // they are only defined in the manifest. + // Reconcilers and/or Watchers are also attached here, though they should only be attached to a single version per kind. + ManagedKinds: []simple.AppManagedKind{ + { + Kind: examplev0alpha1.ExampleKind(), + // Validator is run on ingress and is it returns an error the request is rejected + Validator: NewValidator(), + // Mutator is run on ingress and makes changes to the input object + Mutator: NewMutator(), + }, + { + Kind: examplev1alpha1.ExampleKind(), + // We only want the reconciler on one version of our kind, and it's usually best to use the latest + // We'll receive events for every example object, regardless of version used in the API, + // it will convert them to the version used for the reconciler. + Reconciler: reconciler, + // By default, reconcilers for ManagedKinds are wrapped in + ReconcileOptions: simple.BasicReconcileOptions{ + // Namespace is the namespace your reconciler will watch. + // It defaults to all, so this isn't necessary to specify the way we do here. + Namespace: resource.NamespaceAll, + // We can optionally filter our reconciler to only get events for Example resources which + // satisfy the following label filters + // LabelFilters: []string{"foo=bar"}, + // By default, reconcilers for ManagedKinds are wrapped in the app-sdk's OpinionatedReconciler. + // To turn this functionality off, you can set UsePlain to false + // UsePlain: true, + }, + // Validator is run on ingress and is it returns an error the request is rejected + Validator: NewValidator(), + // Mutator is run on ingress and makes changes to the input object + Mutator: NewMutator(), + // We defined this route in our CUE, but we need to actually define the HTTP handler for it. + CustomRoutes: simple.AppCustomRouteHandlers{ + { + Path: "foo", + Method: "GET", + }: ExampleGetFooHandler, + }, + }, + }, + // Conversion for kinds is defined for all versions of a kind at once. + // This interface may change in the future, see https://github.com/grafana/grafana-app-sdk/issues/617 + Converters: map[schema.GroupKind]simple.Converter{ + { + Group: cfg.ManifestData.Group, + Kind: examplev0alpha1.ExampleKind().Kind(), + }: NewExampleConverter(), + }, + // VersionedCustomRoutes are the custom route handlers for routes defined at the version level of the manifest + // instead of for a specific kind. This are sometimes referred to as "resource routes" + // (as opposed to "subresource routes" which are attached to kinds). + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + "v1alpha1": { + { + Namespaced: true, + Path: "something", + Method: "GET", + }: GetSomethingHandler, + { + Namespaced: false, + Path: "other", + Method: "GET", + }: GetOtherHandler, + }, + }, + } + + a, err := simple.NewApp(simpleConfig) + if err != nil { + return nil, err + } + + // This makes it easier to catch problems at startup, rather than when something doesn't behave as expected. + // ValidateManifest will ensure that the capabilities you define in your simple.AppConfig + // match the capabilities described in the AppManifest. + err = a.ValidateManifest(cfg.ManifestData) + if err != nil { + return nil, err + } + + return a, nil +} + +func GetKinds() map[schema.GroupVersion][]resource.Kind { + gv := schema.GroupVersion{ + Group: examplev1alpha1.ExampleKind().Group(), + Version: examplev1alpha1.ExampleKind().Version(), + } + return map[schema.GroupVersion][]resource.Kind{ + gv: {examplev1alpha1.ExampleKind()}, + } +} diff --git a/apps/example/pkg/app/authorizer.go b/apps/example/pkg/app/authorizer.go new file mode 100644 index 00000000000..1848d47898b --- /dev/null +++ b/apps/example/pkg/app/authorizer.go @@ -0,0 +1,55 @@ +package app + +import ( + "context" + "fmt" + "regexp" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +var namespacedSomethingRouteMatcher = regexp.MustCompile(fmt.Sprintf(`^/apis/%s/%s/namespaces/([^\/]+)/something$`, v1alpha1.APIGroup, v1alpha1.APIVersion)) + +// GetAuthorizer returns an authorizer for all kinds managed by the example app. +// It must be added to the installer in pkg/registry/apps/example/register.go to be used +func GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // require a user + u, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + // check if is admin + if u.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + + // Only allow admins to call the custom subresource + if attr.GetSubresource() == "custom" { + return authorizer.DecisionDeny, "forbidden", nil + } + + // Only allow admins to call the namespaced and cluster routes + // There's no easy way to check that from attrs like with GetSubresource(), + // so we look at the full path and check + if namespacedSomethingRouteMatcher.MatchString(attr.GetPath()) { + return authorizer.DecisionDeny, "forbidden", nil + } + if attr.GetPath() == fmt.Sprintf("/apis/%s/%s/other", v1alpha1.APIGroup, v1alpha1.APIVersion) { + return authorizer.DecisionDeny, "forbidden", nil + } + + // Otherwise, allow + return authorizer.DecisionAllow, "", nil + }, + ) +} diff --git a/apps/example/pkg/app/config.go b/apps/example/pkg/app/config.go new file mode 100644 index 00000000000..bbbedf54202 --- /dev/null +++ b/apps/example/pkg/app/config.go @@ -0,0 +1,7 @@ +package app + +// ExampleConfig is an example app-specific config type +type ExampleConfig struct { + EnableReconciler bool + EnableSomeFeature bool +} diff --git a/apps/example/pkg/app/conversion.go b/apps/example/pkg/app/conversion.go new file mode 100644 index 00000000000..1bd7f6addd9 --- /dev/null +++ b/apps/example/pkg/app/conversion.go @@ -0,0 +1,126 @@ +package app + +import ( + "bytes" + "errors" + "fmt" + "strconv" + + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ simple.Converter = NewExampleConverter() + +type ExampleConverter struct{} + +func NewExampleConverter() *ExampleConverter { + return &ExampleConverter{} +} + +// Convert converts an object from an arbitrary input version slice of bytes +// to a target version, and returns the JSON bytes of that version. +func (e *ExampleConverter) Convert(obj k8s.RawKind, targetAPIVersion string) ([]byte, error) { + srcGVK := schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) + dstGVK := schema.FromAPIVersionAndKind(targetAPIVersion, v1alpha1.ExampleKind().Kind()) + if srcGVK.Group != v1alpha1.APIGroup { + // This should never happen, but check just in case + return nil, fmt.Errorf("wrong group to convert example.grafana.app, got %s", srcGVK.Group) + } + if srcGVK.Kind != v1alpha1.ExampleKind().Kind() { + // This should also never happen, but check just in case + return nil, fmt.Errorf("wrong kind to convert Example, got %s", srcGVK.Kind) + } + if srcGVK == dstGVK { + // This should never happen, but if it does no conversion is necessary, we can return the input + return obj.Raw, nil + } + + // Check source version + switch srcGVK.Version { + case v0alpha1.APIVersion: + srcKind := v0alpha1.ExampleKind() + uncastSrcObj, err := srcKind.Read(bytes.NewReader(obj.Raw), resource.KindEncodingJSON) + if err != nil { + return nil, fmt.Errorf("unable to parse JSON bytes into %s: %w", srcGVK.String(), err) + } + srcObj, ok := uncastSrcObj.(*v0alpha1.Example) + if !ok { + return nil, errors.New("read object was not of type *v0alpha1.Example") + } + switch dstGVK.Version { + case v1alpha1.APIVersion: + dstObj := &v1alpha1.Example{} + // Set Type metadata + dstObj.SetGroupVersionKind(dstGVK) + // Copy Object metadata + srcObj.ObjectMeta.DeepCopyInto(&dstObj.ObjectMeta) + // Copy spec and status + dstObj.Spec.FirstField = strconv.Itoa(int(srcObj.Spec.FirstField)) + dstObj.Status.LastObservedGeneration = srcObj.Status.LastObservedGeneration + dstObj.Status.AdditionalFields = srcObj.Status.AdditionalFields + if srcObj.Status.OperatorStates != nil { + dstObj.Status.OperatorStates = make(map[string]v1alpha1.ExamplestatusOperatorState) + for k, v := range srcObj.Status.OperatorStates { + dstObj.Status.OperatorStates[k] = v1alpha1.ExamplestatusOperatorState{ + LastEvaluation: v.LastEvaluation, + State: v1alpha1.ExampleStatusOperatorStateState(v.State), + DescriptiveState: v.DescriptiveState, + Details: v.Details, + } + } + } + dstKind := v1alpha1.ExampleKind() + buf := &bytes.Buffer{} + err := dstKind.Write(dstObj, buf, resource.KindEncodingJSON) + return buf.Bytes(), err + default: + return nil, fmt.Errorf("unknown target version %s", dstGVK.Version) + } + case v1alpha1.APIVersion: + srcKind := v1alpha1.ExampleKind() + uncastSrcObj, err := srcKind.Read(bytes.NewReader(obj.Raw), resource.KindEncodingJSON) + if err != nil { + return nil, fmt.Errorf("unable to parse JSON bytes into %s: %w", srcGVK.String(), err) + } + srcObj, ok := uncastSrcObj.(*v1alpha1.Example) + if !ok { + return nil, errors.New("read object was not of type *v1alpha1.Example") + } + switch dstGVK.Version { + case v0alpha1.APIVersion: + dstObj := &v0alpha1.Example{} + // Set Type metadata + dstObj.SetGroupVersionKind(dstGVK) + // Copy Object metadata + srcObj.ObjectMeta.DeepCopyInto(&dstObj.ObjectMeta) + // Copy spec and status + castInt, _ := strconv.Atoi(srcObj.Spec.FirstField) // Lossy backwards conversion + dstObj.Spec.FirstField = int64(castInt) + dstObj.Status.LastObservedGeneration = srcObj.Status.LastObservedGeneration + dstObj.Status.AdditionalFields = srcObj.Status.AdditionalFields + if srcObj.Status.OperatorStates != nil { + dstObj.Status.OperatorStates = make(map[string]v0alpha1.ExamplestatusOperatorState) + for k, v := range srcObj.Status.OperatorStates { + dstObj.Status.OperatorStates[k] = v0alpha1.ExamplestatusOperatorState{ + LastEvaluation: v.LastEvaluation, + State: v0alpha1.ExampleStatusOperatorStateState(v.State), + DescriptiveState: v.DescriptiveState, + Details: v.Details, + } + } + } + dstKind := v0alpha1.ExampleKind() + buf := &bytes.Buffer{} + err := dstKind.Write(dstObj, buf, resource.KindEncodingJSON) + return buf.Bytes(), err + default: + return nil, fmt.Errorf("unknown target version %s", dstGVK.Version) + } + } + return nil, fmt.Errorf("unknown source version %s", srcGVK.Version) +} diff --git a/apps/example/pkg/app/mutation.go b/apps/example/pkg/app/mutation.go new file mode 100644 index 00000000000..a04c2b9f312 --- /dev/null +++ b/apps/example/pkg/app/mutation.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" +) + +var _ simple.KindMutator = NewMutator() + +type Mutator struct{} + +func NewMutator() *Mutator { + return &Mutator{} +} + +// Mutate makes modifications to an input object from the API, and returns the changed object. +// This mutation will be done on every request, so it can be used to add or update things like labels +// or annotations. Here, we add an annotation noting the last resourceVersion this was called for. +func (m *Mutator) Mutate(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + annotations := req.Object.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations["example.grafana.app/mutated"] = req.Object.GetResourceVersion() + req.Object.SetAnnotations(annotations) + return &app.MutatingResponse{ + UpdatedObject: req.Object, + }, nil +} diff --git a/apps/example/pkg/app/reconciler.go b/apps/example/pkg/app/reconciler.go new file mode 100644 index 00000000000..66399c62bf8 --- /dev/null +++ b/apps/example/pkg/app/reconciler.go @@ -0,0 +1,58 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +// ExampleReconciler wraps TypedReconciler to simplify some of our reconciliation logic, +// as TypedReconciler will handle type checking of the input object for us. +type ExampleReconciler struct { + operator.TypedReconciler[*v1alpha1.Example] + client resource.Client +} + +func NewExampleReconciler(client resource.Client) *ExampleReconciler { + reconciler := ExampleReconciler{ + TypedReconciler: operator.TypedReconciler[*v1alpha1.Example]{}, + client: client, + } + reconciler.ReconcileFunc = reconciler.doReconcile + return &reconciler +} + +// doReconcile is the main reconciliation loop for our app's Example reconciler. +// All it does is print a log message and then update the last observed generation in the status +// (if the request is a DELETE, it doesn't try to update the status, as the update would fail). +func (e *ExampleReconciler) doReconcile(ctx context.Context, req operator.TypedReconcileRequest[*v1alpha1.Example]) (operator.ReconcileResult, error) { + if req.Object.GetGeneration() == req.Object.Status.LastObservedGeneration { + // Skip if we've already processed this spec + return operator.ReconcileResult{}, nil + } + + logging.FromContext(ctx).Info("reconciling example", "name", req.Object.GetName(), "namespace", req.Object.GetNamespace(), "action", operator.ResourceActionFromReconcileAction(req.Action)) + + // If this is a delete, we don't need to do anything + if req.Action == operator.ReconcileActionDeleted { + return operator.ReconcileResult{}, nil + } + + // Update the status. + // We use resource.UpdateObject here to handle conflicts when doing the update, + // as it gets the current state, performs our update function, then pushes to the remote + _, err := resource.UpdateObject(ctx, e.client, req.Object.GetStaticMetadata().Identifier(), func(obj *v1alpha1.Example, _ bool) (*v1alpha1.Example, error) { + obj.Status.LastObservedGeneration = req.Object.GetGeneration() + return obj, nil + }, resource.UpdateOptions{ + Subresource: "status", + }) + if err != nil { + return operator.ReconcileResult{}, err + } + + return operator.ReconcileResult{}, nil +} diff --git a/apps/example/pkg/app/routes.go b/apps/example/pkg/app/routes.go new file mode 100644 index 00000000000..eb9206b0ce3 --- /dev/null +++ b/apps/example/pkg/app/routes.go @@ -0,0 +1,50 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +// ExampleGetFooHandler handles requests for the GET /foo subresource route +func ExampleGetFooHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + message := "Hello, world!" + return json.NewEncoder(writer).Encode(v1alpha1.GetFoo{ + GetFooBody: v1alpha1.GetFooBody{ + Message: message, + }, + }) +} + +// GetSomethingHandler handles requests for the GET /something resource route +func GetSomethingHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + message := "This is a namespaced route" + if request.URL.Query().Has("message") { + message = request.URL.Query().Get("message") + } + return json.NewEncoder(writer).Encode(v1alpha1.GetSomething{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.APIGroup, v1alpha1.APIVersion), + }, + GetSomethingBody: v1alpha1.GetSomethingBody{ + Namespace: request.ResourceIdentifier.Namespace, + Message: message, + }, + }) +} + +// GetOtherHandler handles requests for the GET /other cluster-scoped resource route +func GetOtherHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + message := "This is a cluster route" + if request.URL.Query().Has("message") { + message = request.URL.Query().Get("message") + } + return json.NewEncoder(writer).Encode(v1alpha1.GetOther{ + Message: message, + }) +} diff --git a/apps/example/pkg/app/validation.go b/apps/example/pkg/app/validation.go new file mode 100644 index 00000000000..74dad42f9ca --- /dev/null +++ b/apps/example/pkg/app/validation.go @@ -0,0 +1,28 @@ +package app + +import ( + "context" + "errors" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" +) + +var _ simple.KindValidator = NewValidator() + +// Validator implements simple.KindValidator +type Validator struct{} + +func NewValidator() *Validator { + return &Validator{} +} + +// Validate runs any kind of validation on incoming objects, +// and returns an error to reject the request. +// Here, we just reject any Example resource which is named "invalid" +func (v *Validator) Validate(ctx context.Context, req *app.AdmissionRequest) error { + if req.Object.GetName() == "invalid" { + return errors.New("example cannot be named 'invalid'") + } + return nil +} diff --git a/apps/example/plugin/src/generated/example/v0alpha1/example_object_gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/example_object_gen.ts new file mode 100644 index 00000000000..dd344a9b8b2 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/example_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Example { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/example/plugin/src/generated/example/v0alpha1/types.metadata.gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/example/plugin/src/generated/example/v0alpha1/types.spec.gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..aee6e7ddc96 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/types.spec.gen.ts @@ -0,0 +1,11 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +export interface Spec { + firstField: number; +} + +export const defaultSpec = (): Spec => ({ + firstField: 0, +}); + diff --git a/apps/example/plugin/src/generated/example/v0alpha1/types.status.gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..2b1c7ecb645 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/types.status.gen.ts @@ -0,0 +1,32 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + lastObservedGeneration: number; + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ + lastObservedGeneration: 0, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/example_object_gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/example_object_gen.ts new file mode 100644 index 00000000000..1d45847ce0f --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/example_object_gen.ts @@ -0,0 +1,51 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; +import { Custom } from './types.custom.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Example { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; + custom: Custom; +} diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.custom.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.custom.gen.ts new file mode 100644 index 00000000000..5de21c36556 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.custom.gen.ts @@ -0,0 +1,21 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// Custom is a subresource that will be stored the same way status is stored, +// and requires using the /custom route to update. +// Its content is returned as part of a GET to the resource itself, just like with status. +// To route a subresource to an arbitrary handler, use the 'routes' field instead (see below). +// metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations +// with getters and setters by the code generation. +// metadata: { +// kindSpecificField: string +// } +export interface Custom { + myField: string; + otherField: string; +} + +export const defaultCustom = (): Custom => ({ + myField: "", + otherField: "", +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.metadata.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.routes.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.routes.gen.ts new file mode 100644 index 00000000000..145a7070c14 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.routes.gen.ts @@ -0,0 +1,35 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// routes contains subresource routes for the kind, which are exposed as HTTP handlers on `examples//`. +// This allows you to add additional non-storage-based handlers to your kind. +// These should only be used if the behavior cannot be accomplished by reconciliation on storage events. +export interface Routes { + // This will add a handler for /foo on the resource + foo: { + // GET request handler. A subresource route can have multiple methods attached to it. + // Allowed values are GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS + GET: { + // The response type for the GET /foo method. + // This will generate a go type, and will also be used for the OpenAPI definition for the route. + response: { + message: string; + }; + request: { + message?: string; + }; + }; + }; +} + +export const defaultRoutes = (): Routes => ({ + foo: { + GET: { + response: { + message: "", +}, + request: { +}, +}, +}, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.spec.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..1a490c64cc7 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.spec.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// #DefinedType is a re-usable definition for us to use in our schema. +// Fields leading with # are definitions in CUE and won't be included in the generated types. +export interface DefinedType { + // Info is information about this entry. This comment, like all comments + // on fields or definitions, will be copied into the generated types as well. + info: string; + // Next is an optional next element in the DefinedType, allowing for a self-referential + // linked-list like structure. The ? in the field makes this optional. + next?: DefinedType; +} + +export const defaultDefinedType = (): DefinedType => ({ + info: "", +}); + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +export interface Spec { + // Example fields + firstField: string; + secondField: number; + list?: DefinedType; +} + +export const defaultSpec = (): Spec => ({ + firstField: "", + secondField: 0, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.status.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..712de3d9076 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.status.gen.ts @@ -0,0 +1,35 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +// status is where state and status information which may be used or updated by the operator or back-end should be placed +// If you do not have any such information, you do not need to include this field, +// however, as mentioned above, certain fields will be added by the kind system regardless. +export interface Status { + lastObservedGeneration: number; + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ + lastObservedGeneration: 0, +}); + diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/examplekind_object_gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/examplekind_object_gen.ts new file mode 100644 index 00000000000..c77f8842a68 --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/examplekind_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface ExampleKind { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/types.metadata.gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/types.spec.gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..adc7830a1ac --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.spec.gen.ts @@ -0,0 +1,25 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// status is where state and status information which may be used or updated by the operator or back-end should be placed +// If you do not have any such information, you do not need to include this field, +// however, as mentioned above, certain fields will be added by the kind system regardless. +// status: { +// currentState: string +// } +// metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations +// with getters and setters by the code generation. +// metadata: { +// kindSpecificField: string +// } +export interface Spec { + // Example fields + firstField: string; + secondField: number; +} + +export const defaultSpec = (): Spec => ({ + firstField: "", + secondField: 0, +}); + diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/types.status.gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/go.work b/go.work index 1b60b90eae9..3fa612c2fce 100644 --- a/go.work +++ b/go.work @@ -11,6 +11,7 @@ use ( ./apps/alerting/rules ./apps/correlations ./apps/dashboard + ./apps/example ./apps/folder ./apps/iam ./apps/investigations diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 9c052688d8f..2dbdad4a9d6 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -57,5 +57,4 @@ import ( _ "github.com/grafana/tempo/pkg/traceql" _ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1" - _ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1" ) diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index ad28e417e4a..6b48f15d708 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/investigations" "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -38,10 +39,12 @@ func ProvideAppInstallers( correlationsAppInstaller *correlations.AppInstaller, alertingNotificationAppInstaller *notifications.AlertingNotificationsAppInstaller, logsdrilldownAppInstaller *logsdrilldown.LogsDrilldownAppInstaller, + exampleAppInstaller *example.ExampleAppInstaller, ) []appsdkapiserver.AppInstaller { installers := []appsdkapiserver.AppInstaller{ playlistAppInstaller, pluginsApplInstaller, + exampleAppInstaller, } //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) { diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index 83c337636c0..a27819b726b 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/registry/apps/plugins" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -19,6 +20,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { rulesInstaller := &rules.AlertingRulesAppInstaller{} correlationsAppInstaller := &correlations.AppInstaller{} notificationsAppInstaller := ¬ifications.AlertingNotificationsAppInstaller{} + exampleAppInstaller := &example.ExampleAppInstaller{} tests := []struct { name string @@ -35,7 +37,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { features := featuremgmt.WithFeatures(tt.flags...) - got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil) + got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, exampleAppInstaller) if tt.expectRulesApp { require.Contains(t, got, tt.rulesInst) } else { diff --git a/pkg/registry/apps/example/register.go b/pkg/registry/apps/example/register.go new file mode 100644 index 00000000000..1839d97b307 --- /dev/null +++ b/pkg/registry/apps/example/register.go @@ -0,0 +1,80 @@ +package example + +import ( + "fmt" + "strings" + + "k8s.io/apiserver/pkg/authorization/authorizer" + restclient "k8s.io/client-go/rest" + + "github.com/grafana/grafana-app-sdk/app" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/simple" + "github.com/grafana/grafana/apps/example/pkg/apis" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" + exampleapp "github.com/grafana/grafana/apps/example/pkg/app" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ appsdkapiserver.AppInstaller = (*ExampleAppInstaller)(nil) +) + +type ExampleAppInstaller struct { + appsdkapiserver.AppInstaller + cfg *setting.Cfg +} + +func (e ExampleAppInstaller) GetAuthorizer() authorizer.Authorizer { + return exampleapp.GetAuthorizer() +} + +func RegisterAppInstaller( + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, +) (*ExampleAppInstaller, error) { + installer := &ExampleAppInstaller{ + cfg: cfg, + } + // Config specific to the app. This can pull from feature flags or setting.Cfg. + specificConfig := &exampleapp.ExampleConfig{ + EnableSomeFeature: true, + } + + // Set specificConfig.EnableReconciler to true IFF the v1alpha1 API is enabled in the runtime config. + // This is example-app-specific, as the version the reconciler uses is not served by default and must be enabled via an override. + apiserverRuntimeCfg := cfg.SectionWithEnvOverrides("grafana-apiserver").Key("runtime_config").String() + for _, s := range strings.Split(apiserverRuntimeCfg, ",") { + if len(s) == 0 { + continue + } + arr := strings.SplitN(s, "=", 2) + if len(arr) == 2 { + if arr[0] == fmt.Sprintf("%s/%s", v1alpha1.APIGroup, v1alpha1.APIVersion) { + specificConfig.EnableReconciler = strings.EqualFold("true", arr[1]) + break + } + } + } + + // Provider is the app provider, which contains the AppManifest, app-specific-config, and the New function for the app + provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, exampleapp.New) + + // appConfig is used alongside the provider for registrion. + // Most of the data is redunant, this may be more optimized in the future. + appConfig := app.Config{ + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: specificConfig, + } + // NewDefaultInstaller gets us the installer we need to underly the ExampleAppInstaller type. + // It does all the hard work of installing our app to the grafana API server + i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator()) + if err != nil { + return nil, err + } + installer.AppInstaller = i + + return installer, nil +} diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index b1bedf15ad2..5961b0caf85 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/investigations" "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -26,4 +27,5 @@ var WireSet = wire.NewSet( rules.RegisterAppInstaller, notifications.RegisterAppInstaller, logsdrilldown.RegisterAppInstaller, + example.RegisterAppInstaller, ) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 62435221671..feed7394400 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -81,6 +81,7 @@ import ( notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" correlations2 "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/investigations" "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -783,7 +784,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller) + exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, exampleAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -1400,7 +1405,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller) + exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, exampleAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { diff --git a/pkg/services/apiserver/appinstaller/server.go b/pkg/services/apiserver/appinstaller/server.go index b6b32be48e9..2e5d46b2b3d 100644 --- a/pkg/services/apiserver/appinstaller/server.go +++ b/pkg/services/apiserver/appinstaller/server.go @@ -116,6 +116,13 @@ func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSuppo return statusStore } + // if the storage is a subresource store, we need to extract the underlying generic registry store + if subresourceStore, ok := storage.(*appsdkapiserver.SubresourceREST); ok { + subresourceStore.Store.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr) + subresourceStore.Store.KeyRootFunc = grafanaregistry.KeyRootFunc(gr) + return subresourceStore + } + return storage }