Apps: Add Example App to ./apps (#112069)

* [API Server] Add Example App for reference use.

* Remove Printlns.

* Upgrade app-sdk to v0.46.0, update apps to handle breaking changes.

* Only start the reconciler for the example app if the v1alpha1 API version is enabled.

* Some comment doc updates.

* Run make update-workspace

* Set codeowner for /apps/example

* Run make gofmt and make update-workspace

* Run prettier on apps/example/README.md

* Add COPY apps/example to Dockerfile

* Add an authorizer to the example app.

* Fix import ordering.

* Update apps/example/kinds/manifest.cue

Co-authored-by: Owen Diehl <ow.diehl@gmail.com>

* Run make update-workspace

* Re-run make gen-go for enterprise import updates

* Run make update-workspace

---------

Co-authored-by: Owen Diehl <ow.diehl@gmail.com>
This commit is contained in:
Austin Pond
2025-10-27 12:01:10 -04:00
committed by GitHub
co-authored by Owen Diehl
parent d25f5199c7
commit bf65c43783
71 changed files with 3744 additions and 4 deletions
@@ -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,
}
)
@@ -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)
}
@@ -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{}
@@ -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{},
}
}
@@ -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)
}
@@ -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
@@ -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{}
}
@@ -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"
)
@@ -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,
}
)
@@ -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
}
@@ -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{}
@@ -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{}
}
@@ -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{}
}
@@ -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()
@@ -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{}
}
@@ -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{}
}
@@ -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()
@@ -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{},
}
}
@@ -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)
}
@@ -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
@@ -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{}
}
@@ -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"
)
@@ -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()
@@ -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{}
}
@@ -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{}
}
@@ -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()
@@ -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{}
}
@@ -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{}
}
@@ -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()
+374
View File
@@ -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||<namespace>/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 "<namespace>/" 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)
}
+143
View File
@@ -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()},
}
}
+55
View File
@@ -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
},
)
}
+7
View File
@@ -0,0 +1,7 @@
package app
// ExampleConfig is an example app-specific config type
type ExampleConfig struct {
EnableReconciler bool
EnableSomeFeature bool
}
+126
View File
@@ -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)
}
+31
View File
@@ -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
}
+58
View File
@@ -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
}
+50
View File
@@ -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,
})
}
+28
View File
@@ -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
}