/* Copyright © The ESO Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package generator import ( "os" "strings" "time" //nolint . "github.com/onsi/gomega" "sigs.k8s.io/controller-runtime/pkg/client" // nolint . "github.com/onsi/ginkgo/v2" v1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" "github.com/external-secrets/external-secrets-e2e/framework" esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1" genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" grafanaclient "github.com/grafana/grafana-openapi-client-go/client" grafanasearch "github.com/grafana/grafana-openapi-client-go/client/search" grafanasa "github.com/grafana/grafana-openapi-client-go/client/service_accounts" ) var _ = Describe("grafana generator", Label("grafana"), func() { f := framework.New("grafana") const grafanaCredsSecretName = "grafana-creds" grafanaClient := newGrafanaClient() BeforeEach(func() { // grafana instance may need to load for a bit // we'll wake it up here and wait for it to be ready Eventually(func() error { _, err := grafanaClient.Search.Search(&grafanasearch.SearchParams{}) return err }).WithPolling(time.Second * 15).WithTimeout(time.Minute * 5).ShouldNot(HaveOccurred()) }) AfterEach(func() { // ESO does clean up tokens, but not the service accounts. accounts, err := grafanaClient.ServiceAccounts.SearchOrgServiceAccountsWithPaging(&grafanasa.SearchOrgServiceAccountsWithPagingParams{ Perpage: ptr.To(int64(100)), Page: ptr.To(int64(1)), Query: ptr.To(f.Namespace.Name), }) Expect(err).ToNot(HaveOccurred()) if accounts.GetPayload().ServiceAccounts != nil && len(accounts.GetPayload().ServiceAccounts) > 0 { for _, sa := range accounts.GetPayload().ServiceAccounts { _, err := grafanaClient.ServiceAccounts.DeleteServiceAccount(sa.ID) Expect(err).ToNot(HaveOccurred()) } } }) setupGenerator := func(tc *testCase) { err := f.CRClient.Create(GinkgoT().Context(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: grafanaCredsSecretName, Namespace: f.Namespace.Name, }, Data: map[string][]byte{ "grafana-token": []byte(os.Getenv("GRAFANA_TOKEN")), }, }) Expect(err).ToNot(HaveOccurred()) tc.Generator = &genv1alpha1.Grafana{ TypeMeta: metav1.TypeMeta{ APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version, Kind: genv1alpha1.GrafanaKind, }, ObjectMeta: metav1.ObjectMeta{ Name: generatorName, Namespace: f.Namespace.Name, }, Spec: genv1alpha1.GrafanaSpec{ URL: os.Getenv("GRAFANA_URL"), ServiceAccount: genv1alpha1.GrafanaServiceAccount{ Name: f.Namespace.Name, Role: "Viewer", }, Auth: genv1alpha1.GrafanaAuth{ Token: &genv1alpha1.SecretKeySelector{ Name: grafanaCredsSecretName, Key: "grafana-token", }, }, }, } tc.ExternalSecret.Spec.DataFrom = []esv1.ExternalSecretDataFromRemoteRef{ { SourceRef: &esv1.StoreGeneratorSourceRef{ GeneratorRef: &esv1.GeneratorRef{ Kind: "Grafana", Name: generatorName, }, }, }, } } ensureExternalSecretPurgesGeneratorState := func(tc *testCase) { // delete ES to trigger cleanup of generator state err := f.CRClient.Delete(GinkgoT().Context(), tc.ExternalSecret) Expect(err).ToNot(HaveOccurred()) By("waiting for generator state to be cleaned up") // wait for generator state to be cleaned up Eventually(func() int { generatorStates := &genv1alpha1.GeneratorStateList{} err := f.CRClient.List(GinkgoT().Context(), generatorStates, client.InNamespace(f.Namespace.Name)) if err != nil { return -1 } GinkgoLogr.Info("found generator states", "states", generatorStates.Items) return len(generatorStates.Items) }).WithPolling(time.Second * 1).WithTimeout(time.Minute * 2).Should(BeZero()) } tokenIsUsable := func(tc *testCase) { tc.AfterSync = func(secret *v1.Secret) { // ensure token exists and is usable Expect(string(secret.Data["token"])).ToNot(BeEmpty()) _, err := grafanaClient.Search.Search(&grafanasearch.SearchParams{ Query: ptr.To(""), }) Expect(err).ToNot(HaveOccurred()) ensureExternalSecretPurgesGeneratorState(tc) } } cleanupServiceAccountsAfterDeletion := func(tc *testCase) { tc.ExternalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second * 10} tc.AfterSync = func(secret *v1.Secret) { // Wait for ES to be rotated a couple of times, // this should create a couple of service accounts. // This allows us to verify that the service accounts are cleaned up // after the generator is deleted. Eventually(func() bool { generatorStates := &genv1alpha1.GeneratorStateList{} err := f.CRClient.List(GinkgoT().Context(), generatorStates, client.InNamespace(f.Namespace.Name)) Expect(err).ToNot(HaveOccurred()) GinkgoLogr.Info("generator states", "states", generatorStates.Items) return len(generatorStates.Items) > 2 }).WithPolling(time.Second * 10).WithTimeout(time.Minute * 5).Should(BeTrue()) ensureExternalSecretPurgesGeneratorState(tc) // ensure service accounts are cleaned up saList, err := grafanaClient.ServiceAccounts.SearchOrgServiceAccountsWithPaging(&grafanasa.SearchOrgServiceAccountsWithPagingParams{ Perpage: ptr.To(int64(100)), Page: ptr.To(int64(1)), Query: ptr.To(f.Namespace.Name), }) Expect(err).ToNot(HaveOccurred()) Expect(saList.GetPayload().ServiceAccounts).To(HaveLen(1)) tokens, err := grafanaClient.ServiceAccounts.ListTokensWithParams(&grafanasa.ListTokensParams{ ServiceAccountID: saList.GetPayload().ServiceAccounts[0].ID, }) Expect(err).ToNot(HaveOccurred()) Expect(tokens.GetPayload()).To(BeEmpty()) } } DescribeTable("generate secrets with grafana generator", generatorTableFunc, Entry("should generate a token that can be used to access the API", f, setupGenerator, tokenIsUsable), Entry("deleting a generator should cleanup the generated service accounts", f, setupGenerator, cleanupServiceAccountsAfterDeletion), ) }) func newGrafanaClient() *grafanaclient.GrafanaHTTPAPI { url := strings.TrimPrefix(os.Getenv("GRAFANA_URL"), "https://") return grafanaclient.NewHTTPClientWithConfig(nil, &grafanaclient.TransportConfig{ Host: url, BasePath: "/api", Schemes: []string{"https"}, APIKey: os.Getenv("GRAFANA_TOKEN"), NumRetries: 15, RetryTimeout: time.Second * 6, }) }