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
false + + urlAuthorization + object + + authorization options for dashboard from url
+ + false @@ -1751,6 +1758,165 @@ Jsonnet project build +### GrafanaDashboard.spec.urlAuthorization +[↩ Parent](#grafanadashboardspec) + + + +authorization options for dashboard from url + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
basicAuthobject +
+
false
+ + +### GrafanaDashboard.spec.urlAuthorization.basicAuth +[↩ Parent](#grafanadashboardspecurlauthorization) + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
passwordobject + SecretKeySelector selects a key of a Secret.
+
false
usernameobject + SecretKeySelector selects a key of a Secret.
+
false
+ + +### GrafanaDashboard.spec.urlAuthorization.basicAuth.password +[↩ Parent](#grafanadashboardspecurlauthorizationbasicauth) + + + +SecretKeySelector selects a key of a Secret. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + The key of the secret to select from. Must be a valid secret key.
+
true
namestring + 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
optionalboolean + Specify whether the Secret or its key must be defined
+
false
+ + +### GrafanaDashboard.spec.urlAuthorization.basicAuth.username +[↩ Parent](#grafanadashboardspecurlauthorizationbasicauth) + + + +SecretKeySelector selects a key of a Secret. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
keystring + The key of the secret to select from. Must be a valid secret key.
+
true
namestring + 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
optionalboolean + Specify whether the Secret or its key must be defined
+
false
+ + ### GrafanaDashboard.status [↩ Parent](#grafanadashboard)