diff --git a/api/v1beta1/grafanadashboard_types.go b/api/v1beta1/grafanadashboard_types.go
index 41e4dbf51..261ffb350 100644
--- a/api/v1beta1/grafanadashboard_types.go
+++ b/api/v1beta1/grafanadashboard_types.go
@@ -45,6 +45,15 @@ type GrafanaDashboardDatasource struct {
DatasourceName string `json:"datasourceName"`
}
+type GrafanaDashboardUrlBasicAuth struct {
+ Username *v1.SecretKeySelector `json:"username,omitempty"`
+ Password *v1.SecretKeySelector `json:"password,omitempty"`
+}
+
+type GrafanaDashboardUrlAuthorization struct {
+ BasicAuth *GrafanaDashboardUrlBasicAuth `json:"basicAuth,omitempty"`
+}
+
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
@@ -64,6 +73,10 @@ type GrafanaDashboardSpec struct {
// +optional
Url string `json:"url,omitempty"`
+ // authorization options for dashboard from url
+ // +optional
+ UrlAuthorization *GrafanaDashboardUrlAuthorization `json:"urlAuthorization,omitempty"`
+
// Jsonnet
// +optional
Jsonnet string `json:"jsonnet,omitempty"`
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index df6db0651..d2cfdbe44 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -830,6 +830,11 @@ func (in *GrafanaDashboardSpec) DeepCopyInto(out *GrafanaDashboardSpec) {
*out = make([]byte, len(*in))
copy(*out, *in)
}
+ if in.UrlAuthorization != nil {
+ in, out := &in.UrlAuthorization, &out.UrlAuthorization
+ *out = new(GrafanaDashboardUrlAuthorization)
+ (*in).DeepCopyInto(*out)
+ }
if in.JsonnetProjectBuild != nil {
in, out := &in.JsonnetProjectBuild, &out.JsonnetProjectBuild
*out = new(JsonnetProjectBuild)
@@ -921,6 +926,51 @@ func (in *GrafanaDashboardStatus) DeepCopy() *GrafanaDashboardStatus {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GrafanaDashboardUrlAuthorization) DeepCopyInto(out *GrafanaDashboardUrlAuthorization) {
+ *out = *in
+ if in.BasicAuth != nil {
+ in, out := &in.BasicAuth, &out.BasicAuth
+ *out = new(GrafanaDashboardUrlBasicAuth)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardUrlAuthorization.
+func (in *GrafanaDashboardUrlAuthorization) DeepCopy() *GrafanaDashboardUrlAuthorization {
+ if in == nil {
+ return nil
+ }
+ out := new(GrafanaDashboardUrlAuthorization)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GrafanaDashboardUrlBasicAuth) DeepCopyInto(out *GrafanaDashboardUrlBasicAuth) {
+ *out = *in
+ if in.Username != nil {
+ in, out := &in.Username, &out.Username
+ *out = new(v1.SecretKeySelector)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Password != nil {
+ in, out := &in.Password, &out.Password
+ *out = new(v1.SecretKeySelector)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardUrlBasicAuth.
+func (in *GrafanaDashboardUrlBasicAuth) DeepCopy() *GrafanaDashboardUrlBasicAuth {
+ if in == nil {
+ return nil
+ }
+ out := new(GrafanaDashboardUrlBasicAuth)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaDatasource) DeepCopyInto(out *GrafanaDatasource) {
*out = *in
diff --git a/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml b/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
index 71b2856ff..114203fa5 100644
--- a/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
+++ b/config/crd/bases/grafana.integreatly.org_grafanadashboards.yaml
@@ -339,6 +339,65 @@ spec:
url:
description: dashboard url
type: string
+ urlAuthorization:
+ description: authorization options for dashboard from url
+ properties:
+ basicAuth:
+ properties:
+ password:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ username:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ type: object
required:
- instanceSelector
type: object
diff --git a/controllers/client/common.go b/controllers/client/common.go
new file mode 100644
index 000000000..520409ca6
--- /dev/null
+++ b/controllers/client/common.go
@@ -0,0 +1,36 @@
+package client
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ v1 "k8s.io/api/core/v1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func GetValueFromSecretKey(ctx context.Context, ref *v1.SecretKeySelector, c client.Client, namespace string) ([]byte, error) {
+ if ref == nil {
+ return nil, errors.New("empty secret key selector")
+ }
+
+ secret := &v1.Secret{}
+ selector := client.ObjectKey{
+ Name: ref.Name,
+ Namespace: namespace,
+ }
+ err := c.Get(ctx, selector, secret)
+ if err != nil {
+ return nil, err
+ }
+
+ if secret.Data == nil {
+ return nil, fmt.Errorf("empty credential secret: %v/%v", namespace, ref.Name)
+ }
+
+ if val, ok := secret.Data[ref.Key]; ok {
+ return val, nil
+ }
+
+ return nil, fmt.Errorf("credentials not found in secret: %v/%v", namespace, ref.Name)
+}
diff --git a/controllers/client/grafana_client.go b/controllers/client/grafana_client.go
index 7113b3b1d..a01b941ca 100644
--- a/controllers/client/grafana_client.go
+++ b/controllers/client/grafana_client.go
@@ -7,12 +7,10 @@ import (
"net/url"
"time"
- "github.com/grafana/grafana-operator/v5/controllers/metrics"
- v1 "k8s.io/api/core/v1"
-
genapi "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-operator/v5/api/v1beta1"
"github.com/grafana/grafana-operator/v5/controllers/config"
+ "github.com/grafana/grafana-operator/v5/controllers/metrics"
"github.com/grafana/grafana-operator/v5/controllers/model"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -25,32 +23,11 @@ type grafanaAdminCredentials struct {
func getAdminCredentials(ctx context.Context, c client.Client, grafana *v1beta1.Grafana) (*grafanaAdminCredentials, error) {
credentials := &grafanaAdminCredentials{}
- getValueFromSecret := func(ref *v1.SecretKeySelector) ([]byte, error) {
- secret := &v1.Secret{}
- selector := client.ObjectKey{
- Name: ref.Name,
- Namespace: grafana.Namespace,
- }
- err := c.Get(ctx, selector, secret)
- if err != nil {
- return nil, err
- }
-
- if secret.Data == nil {
- return nil, fmt.Errorf("empty credential secret: %v/%v", grafana.Namespace, ref.Name)
- }
-
- if val, ok := secret.Data[ref.Key]; ok {
- return val, nil
- }
-
- return nil, fmt.Errorf("admin credentials not found: %v/%v", grafana.Namespace, ref.Name)
- }
if grafana.IsExternal() {
// prefer api key if present
if grafana.Spec.External.ApiKey != nil {
- apikey, err := getValueFromSecret(grafana.Spec.External.ApiKey)
+ apikey, err := GetValueFromSecretKey(ctx, grafana.Spec.External.ApiKey, c, grafana.Namespace)
if err != nil {
return nil, err
}
@@ -59,12 +36,12 @@ func getAdminCredentials(ctx context.Context, c client.Client, grafana *v1beta1.
}
// rely on username and password otherwise
- username, err := getValueFromSecret(grafana.Spec.External.AdminUser)
+ username, err := GetValueFromSecretKey(ctx, grafana.Spec.External.AdminUser, c, grafana.Namespace)
if err != nil {
return nil, err
}
- password, err := getValueFromSecret(grafana.Spec.External.AdminPassword)
+ password, err := GetValueFromSecretKey(ctx, grafana.Spec.External.AdminPassword, c, grafana.Namespace)
if err != nil {
return nil, err
}
@@ -95,7 +72,7 @@ func getAdminCredentials(ctx context.Context, c client.Client, grafana *v1beta1.
if env.ValueFrom != nil {
if env.ValueFrom.SecretKeyRef != nil {
- usernameFromSecret, err := getValueFromSecret(env.ValueFrom.SecretKeyRef)
+ usernameFromSecret, err := GetValueFromSecretKey(ctx, env.ValueFrom.SecretKeyRef, c, grafana.Namespace)
if err != nil {
return nil, err
}
@@ -111,7 +88,7 @@ func getAdminCredentials(ctx context.Context, c client.Client, grafana *v1beta1.
if env.ValueFrom != nil {
if env.ValueFrom.SecretKeyRef != nil {
- passwordFromSecret, err := getValueFromSecret(env.ValueFrom.SecretKeyRef)
+ passwordFromSecret, err := GetValueFromSecretKey(ctx, env.ValueFrom.SecretKeyRef, c, grafana.Namespace)
if err != nil {
return nil, err
}
diff --git a/controllers/dashboard_controller.go b/controllers/dashboard_controller.go
index c126a3c53..45ad774ba 100644
--- a/controllers/dashboard_controller.go
+++ b/controllers/dashboard_controller.go
@@ -481,7 +481,7 @@ func (r *GrafanaDashboardReconciler) fetchDashboardJson(ctx context.Context, das
case v1beta1.DashboardSourceTypeGzipJson:
return v1beta1.Gunzip([]byte(dashboard.Spec.GzipJson))
case v1beta1.DashboardSourceTypeUrl:
- return fetchers.FetchDashboardFromUrl(dashboard, client2.InsecureTLSConfiguration)
+ return fetchers.FetchDashboardFromUrl(ctx, dashboard, r.Client, client2.InsecureTLSConfiguration)
case v1beta1.DashboardSourceTypeJsonnet:
envs, err := r.getDashboardEnvs(ctx, dashboard)
if err != nil {
@@ -495,7 +495,7 @@ func (r *GrafanaDashboardReconciler) fetchDashboardJson(ctx context.Context, das
}
return fetchers.BuildProjectAndFetchJsonnetFrom(dashboard, envs)
case v1beta1.DashboardSourceTypeGrafanaCom:
- return fetchers.FetchDashboardFromGrafanaCom(dashboard)
+ return fetchers.FetchDashboardFromGrafanaCom(ctx, dashboard, r.Client)
case v1beta1.DashboardSourceConfigMap:
return fetchers.FetchDashboardFromConfigMap(dashboard, r.Client)
default:
diff --git a/controllers/fetchers/grafana_com_fetcher.go b/controllers/fetchers/grafana_com_fetcher.go
index 2c330439a..a41fc8769 100644
--- a/controllers/fetchers/grafana_com_fetcher.go
+++ b/controllers/fetchers/grafana_com_fetcher.go
@@ -1,11 +1,14 @@
package fetchers
import (
+ "context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
"github.com/grafana/grafana-operator/v5/api/v1beta1"
client2 "github.com/grafana/grafana-operator/v5/controllers/client"
"github.com/grafana/grafana-operator/v5/controllers/metrics"
@@ -13,7 +16,7 @@ import (
const grafanaComDashboardApiUrlRoot = "https://grafana.com/api/dashboards"
-func FetchDashboardFromGrafanaCom(dashboard *v1beta1.GrafanaDashboard) ([]byte, error) {
+func FetchDashboardFromGrafanaCom(ctx context.Context, dashboard *v1beta1.GrafanaDashboard, c client.Client) ([]byte, error) {
cache := dashboard.GetContentCache()
if len(cache) > 0 {
return cache, nil
@@ -33,7 +36,7 @@ func FetchDashboardFromGrafanaCom(dashboard *v1beta1.GrafanaDashboard) ([]byte,
dashboard.Spec.Url = fmt.Sprintf("%s/%d/revisions/%d/download", grafanaComDashboardApiUrlRoot, source.Id, *source.Revision)
- return FetchDashboardFromUrl(dashboard, tlsConfig)
+ return FetchDashboardFromUrl(ctx, dashboard, c, tlsConfig)
}
func getLatestGrafanaComRevision(dashboard *v1beta1.GrafanaDashboard, tlsConfig *tls.Config) (int, error) {
diff --git a/controllers/fetchers/grafana_com_fetcher_test.go b/controllers/fetchers/grafana_com_fetcher_test.go
index 178112b6b..dc68f3b7a 100644
--- a/controllers/fetchers/grafana_com_fetcher_test.go
+++ b/controllers/fetchers/grafana_com_fetcher_test.go
@@ -1,6 +1,7 @@
package fetchers
import (
+ "context"
"testing"
"github.com/grafana/grafana-operator/v5/api/v1beta1"
@@ -17,7 +18,7 @@ func TestFetchDashboardFromGrafanaCom(t *testing.T) {
Status: v1beta1.GrafanaDashboardStatus{},
}
- fetchedDashboard, err := FetchDashboardFromGrafanaCom(dashboard)
+ fetchedDashboard, err := FetchDashboardFromGrafanaCom(context.Background(), dashboard, k8sClient)
assert.Nil(t, err)
assert.NotNil(t, fetchedDashboard, "Fetched dashboard shouldn't be empty")
assert.GreaterOrEqual(t, *dashboard.Spec.GrafanaCom.Revision, 30, "At least 30 revisions exist for dashboard 1860 as of 2023-03-29")
diff --git a/controllers/fetchers/suite_test.go b/controllers/fetchers/suite_test.go
new file mode 100644
index 000000000..7f6d977a0
--- /dev/null
+++ b/controllers/fetchers/suite_test.go
@@ -0,0 +1,62 @@
+package fetchers
+
+import (
+ "testing"
+
+ grafanav1beta1 "github.com/grafana/grafana-operator/v5/api/v1beta1"
+
+ "github.com/onsi/ginkgo"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var (
+ cfg *rest.Config
+ k8sClient client.Client
+ testEnv *envtest.Environment
+)
+
+func TestAPIs(t *testing.T) {
+ RegisterFailHandler(ginkgo.Fail)
+
+ RunSpecs(t, "Fetchers Suite")
+}
+
+var _ = ginkgo.BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{}
+
+ cfg, err := testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ err = grafanav1beta1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = grafanav1beta1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = grafanav1beta1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ //+kubebuilder:scaffold:scheme
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+}, 60)
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
diff --git a/controllers/fetchers/url_fetcher.go b/controllers/fetchers/url_fetcher.go
index 5196d3428..a87b34f72 100644
--- a/controllers/fetchers/url_fetcher.go
+++ b/controllers/fetchers/url_fetcher.go
@@ -1,6 +1,7 @@
package fetchers
import (
+ "context"
"crypto/tls"
"fmt"
"io"
@@ -8,14 +9,17 @@ import (
"net/url"
"time"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
"github.com/grafana/grafana-operator/v5/api/v1beta1"
- client2 "github.com/grafana/grafana-operator/v5/controllers/client"
+ grafanaClient "github.com/grafana/grafana-operator/v5/controllers/client"
"github.com/grafana/grafana-operator/v5/controllers/metrics"
+ client2 "github.com/grafana/grafana-operator/v5/controllers/client"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
-func FetchDashboardFromUrl(dashboard *v1beta1.GrafanaDashboard, tlsConfig *tls.Config) ([]byte, error) {
+func FetchDashboardFromUrl(ctx context.Context, dashboard *v1beta1.GrafanaDashboard, c client.Client, tlsConfig *tls.Config) ([]byte, error) {
url, err := url.Parse(dashboard.Spec.Url)
if err != nil {
return nil, err
@@ -32,6 +36,25 @@ func FetchDashboardFromUrl(dashboard *v1beta1.GrafanaDashboard, tlsConfig *tls.C
}
client := client2.NewInstrumentedRoundTripper(fmt.Sprintf("%v/%v", dashboard.Namespace, dashboard.Name), metrics.DashboardUrlRequests, true, tlsConfig)
+ // basic auth is supported for dashboards from url
+ if dashboard.Spec.UrlAuthorization != nil && dashboard.Spec.UrlAuthorization.BasicAuth != nil {
+ username, err := grafanaClient.GetValueFromSecretKey(ctx, dashboard.Spec.UrlAuthorization.BasicAuth.Username, c, dashboard.Namespace)
+ if err != nil {
+ return nil, err
+ }
+
+ password, err := grafanaClient.GetValueFromSecretKey(ctx, dashboard.Spec.UrlAuthorization.BasicAuth.Password, c, dashboard.Namespace)
+ if err != nil {
+ return nil, err
+ }
+
+ if username != nil && password != nil {
+ request.SetBasicAuth(string(username), string(password))
+ } else {
+ return nil, fmt.Errorf("basic auth username and/or password are missing for dashboard %s/%s", dashboard.Namespace, dashboard.Name)
+ }
+ }
+
response, err := client.RoundTrip(request)
if err != nil {
return nil, err
diff --git a/controllers/fetchers/url_fetcher_test.go b/controllers/fetchers/url_fetcher_test.go
index 9cbf5a514..42d6e5f20 100644
--- a/controllers/fetchers/url_fetcher_test.go
+++ b/controllers/fetchers/url_fetcher_test.go
@@ -1,38 +1,111 @@
package fetchers
import (
+ "context"
"net/http"
- "net/http/httptest"
- "testing"
+
+ "github.com/onsi/gomega/ghttp"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana-operator/v5/api/v1beta1"
- "github.com/stretchr/testify/assert"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
)
-func TestFetchDashboardFromUrl(t *testing.T) {
+var _ = Describe("Fetching dashboards from URL", func() {
dashboardJSON := []byte(`{"dummyField": "dummyData"}`)
compressedJSON, err := v1beta1.Gzip(dashboardJSON)
- assert.Nil(t, err, "Failed to compress a dashboard")
-
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- _, err := w.Write(dashboardJSON)
- assert.NoError(t, err)
- }))
- defer ts.Close()
-
- dashboard := &v1beta1.GrafanaDashboard{
- Spec: v1beta1.GrafanaDashboardSpec{
- Url: ts.URL,
- },
- Status: v1beta1.GrafanaDashboardStatus{},
- }
-
- fetchedDashboard, err := FetchDashboardFromUrl(dashboard, nil)
- assert.Nil(t, err)
- assert.Equal(t, dashboardJSON, fetchedDashboard, "Fetched dashboard doesn't match the original")
-
- assert.False(t, dashboard.Status.ContentTimestamp.Time.IsZero(), "ContentTimestamp should have been updated")
- assert.Equal(t, compressedJSON, dashboard.Status.ContentCache, "ContentCache should have been updated")
- assert.Equal(t, ts.URL, dashboard.Status.ContentUrl, "ContentUrl should have been updated")
-}
+ Expect(err).NotTo(HaveOccurred())
+
+ var server *ghttp.Server
+
+ BeforeEach(func() {
+ server = ghttp.NewServer()
+ })
+
+ When("using no authentication", func() {
+ BeforeEach(func() {
+ server.AppendHandlers(ghttp.CombineHandlers(
+ ghttp.RespondWith(http.StatusOK, dashboardJSON),
+ ))
+ })
+
+ It("fetches the correct url", func() {
+ dashboard := &v1beta1.GrafanaDashboard{
+ Spec: v1beta1.GrafanaDashboardSpec{
+ Url: server.URL(),
+ },
+ Status: v1beta1.GrafanaDashboardStatus{},
+ }
+
+ fetchedDashboard, err := FetchDashboardFromUrl(context.Background(), dashboard, k8sClient, nil)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(fetchedDashboard).To(Equal(fetchedDashboard))
+ Expect(dashboard.Status.ContentTimestamp.Time.IsZero()).To(BeFalse())
+ Expect(dashboard.Status.ContentCache).To(Equal(compressedJSON))
+ Expect(dashboard.Status.ContentUrl).To(Equal(server.URL()))
+ })
+ })
+ When("using authentication", func() {
+ basicAuthUsername := "admin"
+ basicAuthPassword := "admin"
+ BeforeEach(func() {
+ server.AppendHandlers(ghttp.CombineHandlers(
+ ghttp.VerifyBasicAuth(basicAuthUsername, basicAuthPassword),
+ ghttp.RespondWith(http.StatusOK, dashboardJSON),
+ ))
+ })
+
+ It("fetches the correct url", func() {
+ dashboard := &v1beta1.GrafanaDashboard{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "default",
+ },
+ Spec: v1beta1.GrafanaDashboardSpec{
+ Url: server.URL(),
+ UrlAuthorization: &v1beta1.GrafanaDashboardUrlAuthorization{
+ BasicAuth: &v1beta1.GrafanaDashboardUrlBasicAuth{
+ Username: &v1.SecretKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "credentials",
+ },
+ Key: "USERNAME",
+ Optional: nil,
+ },
+ Password: &v1.SecretKeySelector{
+ LocalObjectReference: v1.LocalObjectReference{
+ Name: "credentials",
+ },
+ Key: "PASSWORD",
+ Optional: nil,
+ },
+ },
+ },
+ },
+ Status: v1beta1.GrafanaDashboardStatus{},
+ }
+
+ credentialsSecret := &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "credentials",
+ Namespace: "default",
+ },
+ StringData: map[string]string{
+ "USERNAME": "admin",
+ "PASSWORD": "admin",
+ },
+ }
+ err = k8sClient.Create(context.Background(), credentialsSecret)
+ Expect(err).NotTo(HaveOccurred())
+ fetchedDashboard, err := FetchDashboardFromUrl(context.Background(), dashboard, k8sClient, nil)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(fetchedDashboard).To(Equal(fetchedDashboard))
+ Expect(dashboard.Status.ContentTimestamp.Time.IsZero()).To(BeFalse())
+ Expect(dashboard.Status.ContentCache).To(Equal(compressedJSON))
+ Expect(dashboard.Status.ContentUrl).To(Equal(server.URL()))
+ })
+ })
+})
diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml
index 71b2856ff..114203fa5 100644
--- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml
+++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanadashboards.yaml
@@ -339,6 +339,65 @@ spec:
url:
description: dashboard url
type: string
+ urlAuthorization:
+ description: authorization options for dashboard from url
+ properties:
+ basicAuth:
+ properties:
+ password:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ username:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ type: object
required:
- instanceSelector
type: object
diff --git a/deploy/kustomize/base/crds.yaml b/deploy/kustomize/base/crds.yaml
index 88e56efa5..7f80bbfd8 100644
--- a/deploy/kustomize/base/crds.yaml
+++ b/deploy/kustomize/base/crds.yaml
@@ -864,6 +864,65 @@ spec:
url:
description: dashboard url
type: string
+ urlAuthorization:
+ description: authorization options for dashboard from url
+ properties:
+ basicAuth:
+ properties:
+ password:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ username:
+ description: SecretKeySelector selects a key of a Secret.
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ TODO: Add other useful fields. apiVersion, kind, uid?
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
+ type: string
+ optional:
+ description: Specify whether the Secret or its key must
+ be defined
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ type: object
required:
- instanceSelector
type: object
diff --git a/docs/docs/api.md b/docs/docs/api.md
index 54847f08f..3691d10d0 100644
--- a/docs/docs/api.md
+++ b/docs/docs/api.md
@@ -1166,6 +1166,13 @@ GrafanaDashboardSpec defines the desired state of GrafanaDashboard
dashboard url
Name | +Type | +Description | +Required | +
---|---|---|---|
basicAuth | +object | +
+ + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
password | +object | +
+ SecretKeySelector selects a key of a Secret. + |
+ false | +
username | +object | +
+ SecretKeySelector selects a key of a Secret. + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
key | +string | +
+ The key of the secret to select from. Must be a valid secret key. + |
+ true | +
name | +string | +
+ Name of the referent.
+This field is effectively required, but due to backwards compatibility is
+allowed to be empty. Instances of this type with an empty value here are
+almost certainly wrong.
+TODO: Add other useful fields. apiVersion, kind, uid?
+More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + + Default: + |
+ false | +
optional | +boolean | +
+ Specify whether the Secret or its key must be defined + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
key | +string | +
+ The key of the secret to select from. Must be a valid secret key. + |
+ true | +
name | +string | +
+ Name of the referent.
+This field is effectively required, but due to backwards compatibility is
+allowed to be empty. Instances of this type with an empty value here are
+almost certainly wrong.
+TODO: Add other useful fields. apiVersion, kind, uid?
+More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + + Default: + |
+ false | +
optional | +boolean | +
+ Specify whether the Secret or its key must be defined + |
+ false | +