From 139b793f4ef3fa247d6bb445481cd061a4b808f7 Mon Sep 17 00:00:00 2001 From: Austin Pond Date: Fri, 4 Aug 2023 10:18:55 -0400 Subject: [PATCH] Initial scratch work on new Grafana Resource format. --- kindcat_custom.cue | 56 ++++++++-------- resource.go | 158 +++++++++++++++++++++++++++++++++------------ unstructured.go | 42 +++++------- 3 files changed, 160 insertions(+), 96 deletions(-) diff --git a/kindcat_custom.cue b/kindcat_custom.cue index 170074c..7039181 100644 --- a/kindcat_custom.cue +++ b/kindcat_custom.cue @@ -9,7 +9,9 @@ import ( // _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. // It is not exhaustive and only includes fields which may be relevant to a kind's implementation, // As it is also intended to be generic enough to function with any API Server. -_kubeObjectMetadata: { +KubeObjectMetadata: { + namespace: string + name: string uid: string creationTimestamp: string & time.Time deletionTimestamp?: string & time.Time @@ -18,12 +20,21 @@ _kubeObjectMetadata: { labels: { [string]: string } + annotations: { + [string]: string + } +} + +GrafanaMetadata: { + updateTimestamp: string & time.Time + createdBy: string + updatedBy: string } // CommonMetadata is a combination of API Server metadata and additional metadata // intended to exist commonly across all kinds, but may have varying implementations as to its storage mechanism(s). CommonMetadata: { - _kubeObjectMetadata + KubeObjectMetadata updateTimestamp: string & time.Time createdBy: string @@ -37,37 +48,20 @@ CommonMetadata: { } } -// _crdSchema is the schema format for a CRD. -_crdSchema: { - // 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. - metadata: { - _kubeObjectMetadata - - updateTimestamp: string & time.Time - createdBy: string - updatedBy: string - - // TODO: additional metadata fields? - // Additional metadata can be added at any future point, as it is allowed to be constant across lineage versions - - // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - extraFields: { - [string]: _ - } - } & { - // All extensions to this metadata need to have string values (for APIServer encoding-to-annotations purposes) - // Can't use this as it's not yet enforced CUE: - //...string - // Have to do this gnarly regex instead - [!~"^(uid|creationTimestamp|deletionTimestamp|finalizers|resourceVersion|labels|updateTimestamp|createdBy|updatedBy|extraFields)$"]: string - } +_grdSchema: { + // metadata is the kubernetes metadata + metadata: KubeObjectMetadata + // grafanaMetadata is the grafana metadata + grafanaMetadata: GrafanaMetadata + // kindMetadata is kind-specfic metadata. It may be empty. + kindMetadata?: _ + // spec is the resource's body spec: _ // cuetsy is not happy creating spec with the MinFields constraint directly _specIsNonEmpty: spec & struct.MinFields(0) + // status is the status subresource status: { #OperatorState: { // lastEvaluation is the ResourceVersion last evaluated @@ -120,11 +114,13 @@ Custom: S={ if isCRD { // If the crd trait is defined, the schemas in the lineage must follow the format: // { - // "metadata": CommonMetadata & {...string} + // "metadata": KubeMetadata + // "grafanaMetadata": GrafanaMetadata + // "kindMetadata": {...} // "spec": {...} // "status": {...} // } - lineage: joinSchema: _crdSchema + lineage: joinSchema: _grdSchema } // crd contains properties specific to converting this kind to a Kubernetes CRD. diff --git a/resource.go b/resource.go index 426fc8d..88aa323 100644 --- a/resource.go +++ b/resource.go @@ -1,7 +1,9 @@ package kindsys import ( + "fmt" "reflect" + "strings" "time" ) @@ -29,6 +31,16 @@ type UnmarshalConfig struct { // A Resource is a single instance of a Grafana [Kind], either [Core] or [Custom]. // +// A Resource is broadly composed of metadata, spec, and additional "subresources." +// Metadata is split into three sub-components: +// - KubeMetadata is the metadata provided in the "metadata" component of a kubernetes resource +// - GrafanaMetadata is additional, standard Grafana Resource metadata +// - KindMetadata is metadata specific to the Kind +// Spec is the Resource's main payload, what could classicly be considered the "body" of the Resource. +// Subresources are additional components of the Resource which are not considered part of either the metadata or +// the spec ("body"). For properly-defined Grafana Resources, this will always include the "status" subresource, +// and may include others on a per-Kind basis. +// // The relationship between Resource and [Kind] is similar to the // relationship between objects and classes in conventional object oriented // design: @@ -48,13 +60,24 @@ type UnmarshalConfig struct { // to produce a struct that implements [Resource] for each kind. Such a struct // can be used as the generic type parameter to create a [TypedCore] or [TypedCustom] type Resource interface { - // CommonMetadata returns the Resource's CommonMetadata - CommonMetadata() CommonMetadata + // KubeMetadata returns the kubernetes standard resource metadata + KubeMetadata() KubeMetadata + + // GrafanaMetadata returns the grafana metadata for the resource + GrafanaMetadata() GrafanaMetadata + + // CustomMetadata returns metadata unique to this Resource's kind, as opposed to Common and Static metadata, + // which are the same across all kinds. An object may have no kind-specific CustomMetadata. + // CustomMetadata can only be read from this interface, for use with resource.Client implementations, + // those who wish to set CustomMetadata should use the interface's underlying type. + KindMetadata() CustomMetadata // SetCommonMetadata overwrites the CommonMetadata of the object. // Implementations should always overwrite, rather than attempt merges of the metadata. // Callers wishing to merge should get current metadata with CommonMetadata() and set specific values. - SetCommonMetadata(metadata CommonMetadata) + SetKubeMetadata(metadata KubeMetadata) + + SetGrafanaMetadata(metadata GrafanaMetadata) // StaticMetadata returns the Resource's StaticMetadata StaticMetadata() StaticMetadata @@ -65,17 +88,12 @@ type Resource interface { // Note that StaticMetadata is only mutable in an object create context. SetStaticMetadata(metadata StaticMetadata) - // CustomMetadata returns metadata unique to this Resource's kind, as opposed to Common and Static metadata, - // which are the same across all kinds. An object may have no kind-specific CustomMetadata. - // CustomMetadata can only be read from this interface, for use with resource.Client implementations, - // those who wish to set CustomMetadata should use the interface's underlying type. - CustomMetadata() CustomMetadata - // SpecObject returns the actual "schema" object, which holds the main body of data SpecObject() any // Subresources returns a map of subresource name(s) to the object value for that subresource. // Spec is not considered a subresource, and should only be returned by SpecObject + // No guarantees are made that mutations to objects in the map will affect the underlying resource. Subresources() map[string]any // Copy returns a full copy of the Resource with all its data @@ -160,14 +178,12 @@ type ListMetadata struct { ExtraFields map[string]any `json:"extraFields"` } -// CommonMetadata is the system-defined common metadata associated with a [Resource]. -// It combines Kubernetes standard metadata with certain Grafana-specific additions. -// -// It is analogous to [k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta] in vanilla Kubernetes. -// -// TODO generate this from the CUE definition -// TODO review this for optionality -type CommonMetadata struct { +// KubeMetadata is kubernetes object metadata. It does not directly import kubernetes go structs to avoid requiring kubernetes dependencies in projects +// which use kindsys. +// TODO: does this matter? Could we just import kubernetes' struct and attach methods we need to it? +type KubeMetadata struct { + Name string + Namespace string // UID is the unique ID of the object. This can be used to uniquely identify objects, // but is not guaranteed to be usable for lookups. UID string `json:"uid"` @@ -179,6 +195,8 @@ type CommonMetadata struct { // Labels are string key/value pairs attached to the object. They can be used for filtering, // or as additional metadata. Labels map[string]string `json:"labels"` + // Annotations + Annotations map[string]string `json:"annotations"` // CreationTimestamp indicates when the resource has been created. CreationTimestamp time.Time `json:"creationTimestamp"` // DeletionTimestamp indicates that the resource is pending deletion as of the provided time if non-nil. @@ -190,6 +208,32 @@ type CommonMetadata struct { // DeletionTimestamp is set to the time of the "delete," and the resource will continue to exist // until the finalizers list is cleared. Finalizers []string `json:"finalizers"` +} + +func (k KubeMetadata) Copy() KubeMetadata { + n := KubeMetadata{ + Name: k.Name, + Namespace: k.Namespace, + UID: k.UID, + ResourceVersion: k.ResourceVersion, + CreationTimestamp: k.CreationTimestamp, + } + if k.DeletionTimestamp != nil { + *n.DeletionTimestamp = *(k.DeletionTimestamp) + } + copy(n.Finalizers, k.Finalizers) + for key, val := range k.Annotations { + n.Annotations[key] = val + } + for key, val := range k.Labels { + k.Labels[key] = val + } + return n +} + +// TODO +// On encoding to kubernetes, fields in GrafanaMetadata MUST be encoded into annotations with the name "grafana.com/X", where X is the JSON name of the field. +type GrafanaMetadata struct { // UpdateTimestamp is the timestamp of the last update to the resource. UpdateTimestamp time.Time `json:"updateTimestamp"` // CreatedBy is a string which indicates the user or process which created the resource. @@ -198,12 +242,10 @@ type CommonMetadata struct { // UpdatedBy is a string which indicates the user or process which last updated the resource. // Implementations may choose what this indicator should be. UpdatedBy string `json:"updatedBy"` +} - // ExtraFields stores implementation-specific metadata. - // Not all Client implementations are required to honor all ExtraFields keys. - // Generally, this field should be shied away from unless you know the specific - // Client implementation you're working with and wish to track or mutate extra information. - ExtraFields map[string]any `json:"extraFields"` +func (g GrafanaMetadata) Copy() GrafanaMetadata { + return GrafanaMetadata{} } // TODO guard against skew, use indirection through an internal package @@ -221,34 +263,70 @@ func (s SimpleCustomMetadata) MapFields() map[string]any { // BasicMetadataObject provides a Metadata field composed of StaticMetadata and ObjectMetadata, as well as the // ObjectMetadata(),SetObjectMetadata(), StaticMetadata(), and SetStaticMetadata() receiver functions. type BasicMetadataObject struct { - StaticMeta StaticMetadata `json:"staticMetadata"` - CommonMeta CommonMetadata `json:"commonMetadata"` - CustomMeta SimpleCustomMetadata `json:"customMetadata"` -} - -// CommonMetadata returns the object's CommonMetadata -func (b *BasicMetadataObject) CommonMetadata() CommonMetadata { - return b.CommonMeta -} - -// SetCommonMetadata overwrites the ObjectMetadata.Common() supplied by BasicMetadataObject.ObjectMetadata() -func (b *BasicMetadataObject) SetCommonMetadata(m CommonMetadata) { - b.CommonMeta = m + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + Metadata KubeMetadata `json:"metadata"` + GrafanaMeta GrafanaMetadata `json:"grafanaMetadata"` + KindMeta SimpleCustomMetadata `json:"kindMetadata"` } // StaticMetadata returns the object's StaticMetadata func (b *BasicMetadataObject) StaticMetadata() StaticMetadata { - return b.StaticMeta + gv := strings.Split(b.APIVersion, "/") + g := "" + v := "" + if len(gv) > 1 { + g = gv[0] + v = gv[1] + } else if len(gv) == 1 { + // For kubernetes core resources, the group is empty and only the version appears in the APIVersion string + v = gv[0] + } + return StaticMetadata{ + Kind: b.Kind, + Group: g, + Version: v, + Namespace: b.Metadata.Namespace, + Name: b.Metadata.Name, + } } // SetStaticMetadata overwrites the StaticMetadata supplied by BasicMetadataObject.StaticMetadata() +// Note that in implementations, this may impact the KubeMetadata, as they have overlapping information. func (b *BasicMetadataObject) SetStaticMetadata(m StaticMetadata) { - b.StaticMeta = m + b.Kind = m.Kind + if m.Group != "" { + b.APIVersion = fmt.Sprintf("%s/%s", m.Group, m.Version) + } else { + b.APIVersion = m.Version + } + b.Metadata.Namespace = m.Namespace + b.Metadata.Name = m.Name +} + +// KubeMetadata returns the object's KubeMetadata +func (b *BasicMetadataObject) KubeMetadata() KubeMetadata { + return b.Metadata +} + +// SetKubeMetadata overwrites the KubeMetadata supplied by BasicMetadataObject.KubeMetadata() +func (b *BasicMetadataObject) SetKubeMetadata(m KubeMetadata) { + b.Metadata = m +} + +// GrafanaMetadata returns the object's GrafanaMetadata +func (b *BasicMetadataObject) GrafanaMetadata() GrafanaMetadata { + return b.GrafanaMeta +} + +// SetGrafanaMetadata overwrites the GrafanaMetadata supplied by BasicMetadataObject.GrafanaMetadata() +func (b *BasicMetadataObject) SetGrafanaMetadata(m GrafanaMetadata) { + b.GrafanaMeta = m } -// CustomMetadata returns the object's CustomMetadata -func (b *BasicMetadataObject) CustomMetadata() CustomMetadata { - return b.CustomMeta +// KindMetadata returns the object's CustomMetadata (kind-specific metadata) +func (b *BasicMetadataObject) KindMetadata() CustomMetadata { + return b.KindMeta } // TODO delete these? diff --git a/unstructured.go b/unstructured.go index a62fbaf..da88347 100644 --- a/unstructured.go +++ b/unstructured.go @@ -8,9 +8,10 @@ var _ Resource = &UnstructuredResource{} // strongly typed, and lacks any user-defined methods that may exist on a // kind-specific struct that implements [Resource]. type UnstructuredResource struct { - BasicMetadataObject - Spec map[string]any `json:"spec,omitempty"` - Status map[string]any `json:"status,omitempty"` + BasicMetadataObject `json:",inline"` + Spec map[string]any `json:"spec,omitempty"` + Status map[string]any `json:"status,omitempty"` + // TODO: is there value in storing other subresources in UnstructuredResource? } func (u *UnstructuredResource) SpecObject() any { @@ -24,32 +25,21 @@ func (u *UnstructuredResource) Subresources() map[string]any { } func (u *UnstructuredResource) Copy() Resource { - com := CommonMetadata{ - UID: u.CommonMeta.UID, - ResourceVersion: u.CommonMeta.ResourceVersion, - CreationTimestamp: u.CommonMeta.CreationTimestamp.UTC(), - UpdateTimestamp: u.CommonMeta.UpdateTimestamp.UTC(), - CreatedBy: u.CommonMeta.CreatedBy, - UpdatedBy: u.CommonMeta.UpdatedBy, - } - - copy(u.CommonMeta.Finalizers, com.Finalizers) - if u.CommonMeta.DeletionTimestamp != nil { - *com.DeletionTimestamp = *(u.CommonMeta.DeletionTimestamp) - } - for k, v := range u.CommonMeta.Labels { - com.Labels[k] = v - } - com.ExtraFields = mapcopy(u.CommonMeta.ExtraFields) - - cp := UnstructuredResource{ + n := UnstructuredResource{ + BasicMetadataObject: BasicMetadataObject{ + Kind: u.Kind, + APIVersion: u.APIVersion, + Metadata: u.Metadata.Copy(), + GrafanaMeta: u.GrafanaMeta.Copy(), + KindMeta: SimpleCustomMetadata{}, + }, Spec: mapcopy(u.Spec), Status: mapcopy(u.Status), } - - cp.CommonMeta = com - cp.CustomMeta = mapcopy(u.CustomMeta) - return &cp + for k, v := range u.KindMeta { + n.KindMeta[k] = v + } + return &n } func mapcopy(m map[string]any) map[string]any {