Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

# Binaries for programs and plugins
__debug_bin*
*.exe
*.exe~
*.dll
Expand All @@ -26,6 +27,7 @@ vendor
*~

.vscode/
.mirrord/
.DS_Store

# Audit lab
Expand Down
124 changes: 110 additions & 14 deletions api/v1beta1/grafana_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,54 @@ type OperatorReconcileVars struct {
Plugins string
}

// GrafanaServiceAccountTokenSpec describes a token to create.
type GrafanaServiceAccountTokenSpec struct {
// Name is the name of the Kubernetes Secret (and token identifier in Grafana). The secret will contain the token value.
// +kubebuilder:validation:Required
Name string `json:"name"`

// Expires is the optional expiration time for the token. After this time, the operator may rotate the token.
// +kubebuilder:validation:Optional
Expires *metav1.Time `json:"expires,omitempty"`
}
Comment on lines +56 to +64
Copy link
Collaborator

@Baarsgaard Baarsgaard Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect some users would want to have secrets created outside the current namespace.
Example: allowing teams access to a Grafana instance by creating External instances with limited permissions on the GrafanaServiceAccount.


// GrafanaServiceAccountSpec defines the desired state of a GrafanaServiceAccount.
type GrafanaServiceAccountSpec struct {
// ID is a kind of unique identifier to distinguish between service accounts if the name is changed.
// +kubebuilder:validation:Required
ID string `json:"id"`

// Name is the desired name of the service account in Grafana.
// +kubebuilder:validation:Required
Name string `json:"name"`

// Role is the Grafana role for the service account (Viewer, Editor, Admin).
// +kubebuilder:validation:Required
// +kubebuilder:validation:Enum=Viewer;Editor;Admin
Role string `json:"role"`

// IsDisabled indicates if the service account should be disabled in Grafana.
// +kubebuilder:validation:Optional
IsDisabled bool `json:"isDisabled,omitempty"`

// Tokens defines API tokens to create for this service account. Each token will be stored in a Kubernetes Secret with the given name.
// +kubebuilder:validation:Optional
Tokens []GrafanaServiceAccountTokenSpec `json:"tokens,omitempty"`
}

type GrafanaServiceAccounts struct {
// Accounts lists Grafana service accounts to manage.
// Each service account is uniquely identified by its ID.
// +listType=map
// +listMapKey=id
Accounts []GrafanaServiceAccountSpec `json:"accounts,omitempty"`

// GenerateTokenSecret, if true, will create one default API token in a Secret if no Tokens are specified.
// If false, no token is created unless explicitly listed in Tokens.
// +kubebuilder:default=true
GenerateTokenSecret bool `json:"generateTokenSecret,omitempty"`
Comment on lines +97 to +100
Copy link
Collaborator

@Baarsgaard Baarsgaard Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot imagine a scenario where a default service account is wanted as opposed to fully defining the spec?
The GrafanaServiceAccounts struct is nice to have for potential future configuration values, if we can think of extra aside from GenerateTokenSecret.

Copy link
Contributor Author

@ndk ndk Jul 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, maybe it's time to revise the proposal? :)
https://github.com/grafana/grafana-operator/blob/master/docs/docs/proposals/003-grafanaserviceaccount-crd.md

apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana
metadata:
  name: grafana-sa
  namespace: grafana-namespace
spec:
  grafanaServiceAccounts: #Not sure if this is the right place to place it but thats easily fixed when implementing.
    createServiceAccount:
        generateTokenSecret: [true/false] #Will create the k8s-secret with a default name if true. Defaults to true.
    accounts: #Since its possible today to have multiple service accounts it should be a list of accounts.
        - id: grafana-sa
          name: grafana-service-account
          roles: [Viewer/Editor/Admin]
          tokens: #This is a list of the tokens that belongs to this GSA and that the operator should create k8s-secrets with tokens for with the names specified. If not specified it would default to creating a token in a k8s-secret with a default name if spec.createServiceAccount.generateTokenSecret is true.
              - Name: grafana-sa-token-<name-of-GSA>
              expires: <Absolute date for expiration, defaults to Never>
          permissions:    #This is to try and match what values can be set when creating GSA in the GUI where you can set different permissions for users and groups.
              - user: <users in the cluster/root user etc>
              permission: [Edit/Admin]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the proposal is just a general idea on how we thought it'd be good to implement it at the time of writing. We're not bound to implement it as-is if we find areas to improve upon

}

// GrafanaSpec defines the desired state of Grafana
type GrafanaSpec struct {
// +kubebuilder:pruning:PreserveUnknownFields
Expand Down Expand Up @@ -83,6 +131,8 @@ type GrafanaSpec struct {
// DisableDefaultSecurityContext prevents the operator from populating securityContext on deployments
// +kubebuilder:validation:Enum=Pod;Container;All
DisableDefaultSecurityContext string `json:"disableDefaultSecurityContext,omitempty"`
// Grafana Service Accounts
GrafanaServiceAccounts *GrafanaServiceAccounts `json:"grafanaServiceAccounts,omitempty"`
}

type External struct {
Expand Down Expand Up @@ -134,22 +184,68 @@ type GrafanaPreferences struct {
HomeDashboardUID string `json:"homeDashboardUid,omitempty"`
}

type GrafanaServiceAccountSecretStatus struct {
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
}

// GrafanaServiceAccountTokenStatus describes a token created in Grafana.
type GrafanaServiceAccountTokenStatus struct {
// Name is the name of the Kubernetes Secret. The secret will contain the token value.
Name string `json:"name"`

// Expires is the expiration time for the token.
// N.B. There's possible discrepancy with the expiration time in spec.
// It happens because Grafana API accepts TTL in seconds then calculates the expiration time against the current time.
Expires *metav1.Time `json:"expires,omitempty"`

// ID is the Grafana-assigned ID of the token.
ID int64 `json:"tokenId"`

// Secret is the Kubernetes Secret that stores the actual token value.
// This may seem redundant if the Secret name usually matches the token's Name,
// but it's stored explicitly in Status for clarity and future flexibility.
Secret *GrafanaServiceAccountSecretStatus `json:"secret,omitempty"`
}

// GrafanaServiceAccountStatus holds status for one Grafana instance.
type GrafanaServiceAccountStatus struct {
// SpecID is a kind of unique identifier to distinguish between service accounts if the name is changed.
SpecID string `json:"specId"`

// Name is the name of the service account in Grafana.
Name string `json:"name"`

// ServiceAccountID is the numeric ID of the service account in this Grafana.
ServiceAccountID int64 `json:"serviceAccountId"`

// Role is the Grafana role for the service account (Viewer, Editor, Admin).
Role string `json:"role"`

// IsDisabled indicates if the service account is disabled.
IsDisabled bool `json:"isDisabled,omitempty"`

// Tokens is the status of tokens for this service account in Grafana.
Tokens []GrafanaServiceAccountTokenStatus `json:"tokens,omitempty"`
}

// GrafanaStatus defines the observed state of Grafana
type GrafanaStatus struct {
Stage OperatorStageName `json:"stage,omitempty"`
StageStatus OperatorStageStatus `json:"stageStatus,omitempty"`
LastMessage string `json:"lastMessage,omitempty"`
AdminURL string `json:"adminUrl,omitempty"`
AlertRuleGroups NamespacedResourceList `json:"alertRuleGroups,omitempty"`
ContactPoints NamespacedResourceList `json:"contactPoints,omitempty"`
Dashboards NamespacedResourceList `json:"dashboards,omitempty"`
Datasources NamespacedResourceList `json:"datasources,omitempty"`
Folders NamespacedResourceList `json:"folders,omitempty"`
LibraryPanels NamespacedResourceList `json:"libraryPanels,omitempty"`
MuteTimings NamespacedResourceList `json:"muteTimings,omitempty"`
NotificationTemplates NamespacedResourceList `json:"notificationTemplates,omitempty"`
Version string `json:"version,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
Stage OperatorStageName `json:"stage,omitempty"`
StageStatus OperatorStageStatus `json:"stageStatus,omitempty"`
LastMessage string `json:"lastMessage,omitempty"`
AdminURL string `json:"adminUrl,omitempty"`
AlertRuleGroups NamespacedResourceList `json:"alertRuleGroups,omitempty"`
ContactPoints NamespacedResourceList `json:"contactPoints,omitempty"`
Dashboards NamespacedResourceList `json:"dashboards,omitempty"`
Datasources NamespacedResourceList `json:"datasources,omitempty"`
Folders NamespacedResourceList `json:"folders,omitempty"`
LibraryPanels NamespacedResourceList `json:"libraryPanels,omitempty"`
MuteTimings NamespacedResourceList `json:"muteTimings,omitempty"`
NotificationTemplates NamespacedResourceList `json:"notificationTemplates,omitempty"`
Version string `json:"version,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
GrafanaServiceAccounts []GrafanaServiceAccountStatus `json:"serviceAccounts,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
136 changes: 136 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading