Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create and add OpenSignal API credentials secret to OTelC pod #4507

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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: 1 addition & 1 deletion pkg/api/v1beta3/dynakube/dynakube_props.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (dk *DynaKube) ImagePullSecretReferences() []corev1.LocalObjectReference {
return imagePullSecrets
}

// Tokens returns the name of the Secret to be used for tokens.
// Tokens return the name of the Secret to be used for tokens.
func (dk *DynaKube) Tokens() string {
if tkns := dk.Spec.Tokens; tkns != "" {
return tkns
Expand Down
5 changes: 5 additions & 0 deletions pkg/api/v1beta3/dynakube/telemetryservice_props.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dynakube

import (
"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/telemetryservice"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (dk *DynaKube) TelemetryService() *telemetryservice.TelemetryService {
Expand All @@ -12,3 +13,7 @@ func (dk *DynaKube) TelemetryService() *telemetryservice.TelemetryService {

return ts
}

func (dk *DynaKube) TelemetryApiCredentialsSecretName() *metav1.LabelSelector {
return &dk.Spec.MetadataEnrichment.NamespaceSelector
}
3 changes: 3 additions & 0 deletions pkg/controllers/dynakube/otelc/consts/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package consts

const TelemetryApiCredentialsSecretName = "dynatrace-telemetry-api-credentials"
8 changes: 8 additions & 0 deletions pkg/controllers/dynakube/otelc/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/secret"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/service"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/statefulset"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -16,6 +17,7 @@ type Reconciler struct {
dk *dynakube.DynaKube
statefulsetReconciler controllers.Reconciler
serviceReconciler *service.Reconciler
secretReconciler *secret.Reconciler
}

type ReconcilerBuilder func(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) controllers.Reconciler
Expand All @@ -27,6 +29,7 @@ func NewReconciler(client client.Client, apiReader client.Reader, dk *dynakube.D
dk: dk,
statefulsetReconciler: statefulset.NewReconciler(client, apiReader, dk),
serviceReconciler: service.NewReconciler(client, apiReader, dk),
secretReconciler: secret.NewReconciler(client, apiReader, dk),
}
}

Expand All @@ -36,6 +39,11 @@ func (r *Reconciler) Reconcile(ctx context.Context) error {
return err
}

err = r.secretReconciler.Reconcile(ctx)
if err != nil {
return err
}

err = r.statefulsetReconciler.Reconcile(ctx)
if err != nil {
log.Info("failed to reconcile Dynatrace OTELc statefulset")
Expand Down
5 changes: 5 additions & 0 deletions pkg/controllers/dynakube/otelc/secret/conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package secret

const (
secretConditionType = "TelemetryApiCredentialsSecretCondition"
)
7 changes: 7 additions & 0 deletions pkg/controllers/dynakube/otelc/secret/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package secret

import "github.com/Dynatrace/dynatrace-operator/pkg/logd"

var (
log = logd.Get().WithName("open-signal-api-credentials-secret")
)
160 changes: 160 additions & 0 deletions pkg/controllers/dynakube/otelc/secret/reconciler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package secret

import (
"context"
"fmt"

"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube"
dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/token"
"github.com/Dynatrace/dynatrace-operator/pkg/util/conditions"
"github.com/Dynatrace/dynatrace-operator/pkg/util/hasher"
k8slabels "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/labels"
k8ssecret "github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type Reconciler struct {
client client.Client
apiReader client.Reader
dk *dynakube.DynaKube
}

type ReconcilerBuilder func(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler

func NewReconciler(client client.Client, apiReader client.Reader, dk *dynakube.DynaKube) *Reconciler {
return &Reconciler{
client: client,
dk: dk,
apiReader: apiReader,
}
}

func (r *Reconciler) Reconcile(ctx context.Context) error {
if r.dk.TelemetryService().IsEnabled() {
return r.ensureOpenSignalAPISecret(ctx)
}

return r.removeOpenSignalAPISecret(ctx)
}

func (r *Reconciler) ensureOpenSignalAPISecret(ctx context.Context) error {
query := k8ssecret.Query(r.client, r.apiReader, log)
_, err := query.Get(ctx, types.NamespacedName{Name: consts.TelemetryApiCredentialsSecretName, Namespace: r.dk.Namespace})

if err != nil && k8serrors.IsNotFound(err) {
log.Info("creating new secret for telemetry api credentials")

secretConfig, err := r.generateTelemetryApiCredentialsSecret(ctx, consts.TelemetryApiCredentialsSecretName)

if err != nil {
conditions.SetSecretGenFailed(r.dk.Conditions(), secretConditionType, err)

return err
}

_, err = hasher.GenerateHash(secretConfig.Data)
if err != nil {
conditions.SetSecretGenFailed(r.dk.Conditions(), secretConditionType, err)

return err
}

err = query.Create(ctx, secretConfig)
if err != nil {
log.Info("could not create secret for telemetry api credentials", "name", secretConfig.Name)
conditions.SetKubeApiError(r.dk.Conditions(), secretConditionType, err)

return err
}

conditions.SetSecretCreated(r.dk.Conditions(), secretConditionType, consts.TelemetryApiCredentialsSecretName)
}

return nil
}

func (r *Reconciler) getApiToken(ctx context.Context) ([]byte, error) {
tokenReader := token.NewReader(r.apiReader, r.dk)

tokens, err := tokenReader.ReadTokens(ctx)
if err != nil {
return nil, errors.Wrapf(err, "'%s:%s' secret is missing or invalid", r.dk.Namespace, r.dk.Tokens())
}

apiToken, hasApiToken := tokens[dtclient.ApiToken]
if !hasApiToken {
return nil, errors.New(fmt.Sprintf("'%s' token is missing in '%s:%s' secret", dtclient.ApiToken, r.dk.Namespace, r.dk.Tokens()))
}

return []byte(apiToken.Value), nil
}

func (r *Reconciler) getDtEndpoint() ([]byte, error) {
tenantUUID, err := r.dk.TenantUUID()
if err != nil {
return nil, err
}

if r.dk.ActiveGate().IsApiEnabled() {
return []byte(fmt.Sprintf("https://%s-activegate.dynatrace.svc/e/%s/api/v2/otlp", r.dk.Name, tenantUUID)), nil
}

return []byte(fmt.Sprintf("https://%s.dev.dynatracelabs.com/api/v2/otlp", tenantUUID)), nil
}

func (r *Reconciler) generateTelemetryApiCredentialsSecret(ctx context.Context, name string) (secret *corev1.Secret, err error) {
secretData := make(map[string][]byte)

apiToken, err := r.getApiToken(ctx)
if err != nil {
return nil, err
}

secretData["DT_API_TOKEN"] = apiToken

dtEndpoint, err := r.getDtEndpoint()
if err != nil {
return nil, err
}

secretData["DT_ENDPOINT"] = dtEndpoint

secretConfig, err := k8ssecret.Build(r.dk,
name,
secretData,
k8ssecret.SetLabels(k8slabels.NewCoreLabels(r.dk.Name, k8slabels.OtelCComponentLabel).BuildLabels()),
)

if err != nil {
return nil, err
}

return secretConfig, nil
}

func (r *Reconciler) removeOpenSignalAPISecret(ctx context.Context) error {
if meta.FindStatusCondition(*r.dk.Conditions(), secretConditionType) == nil {
return nil // no condition == nothing is there to clean up
}

query := k8ssecret.Query(r.client, r.apiReader, log)
err := query.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: consts.TelemetryApiCredentialsSecretName, Namespace: r.dk.Namespace}})

if err != nil {
log.Info("could not delete apiCredential secret", "name", consts.TelemetryApiCredentialsSecretName)

return err
}

meta.RemoveStatusCondition(r.dk.Conditions(), secretConditionType)

return nil
}
110 changes: 110 additions & 0 deletions pkg/controllers/dynakube/otelc/secret/reconciler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package secret

import (
"context"
"testing"

schemeFake "github.com/Dynatrace/dynatrace-operator/pkg/api/scheme/fake"
"github.com/Dynatrace/dynatrace-operator/pkg/api/shared/communication"
"github.com/Dynatrace/dynatrace-operator/pkg/api/status"
"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube"
"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/activegate"
"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube/telemetryservice"
dtclient "github.com/Dynatrace/dynatrace-operator/pkg/clients/dynatrace"
"github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts"
"github.com/Dynatrace/dynatrace-operator/pkg/util/conditions"
"github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/secret"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

const (
testApiToken = "apiTokenValue"
testTenantUUID = "abc12345"
testKubeSystemUUID = "12345"
)

func TestSecretCreation(t *testing.T) {
ctx := context.Background()

t.Run("creates secret if it does not exist", func(t *testing.T) {
dk := createDynaKube(true)

testSecret, err := secret.Build(&dk, dk.Name, map[string][]byte{
dtclient.ApiToken: []byte(testApiToken),
})
require.NoError(t, err)

clt := fake.NewFakeClient(testSecret)

r := NewReconciler(clt, clt, &dk)

err = r.ensureOpenSignalAPISecret(ctx)
require.NoError(t, err)

var apiCredsSecret corev1.Secret
err = clt.Get(ctx, types.NamespacedName{Name: consts.TelemetryApiCredentialsSecretName, Namespace: dk.Namespace}, &apiCredsSecret)
require.NoError(t, err)
assert.NotEmpty(t, apiCredsSecret)
require.NotNil(t, meta.FindStatusCondition(*dk.Conditions(), secretConditionType))
assert.Equal(t, conditions.SecretCreatedReason, meta.FindStatusCondition(*dk.Conditions(), secretConditionType).Reason)
})

t.Run("removes secret if exists but we don't need it", func(t *testing.T) {
dk := createDynaKube(false)
conditions.SetSecretCreated(dk.Conditions(), secretConditionType, consts.TelemetryApiCredentialsSecretName)

objs := []client.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: consts.TelemetryApiCredentialsSecretName,
Namespace: dk.Namespace,
},
},
}

clt := schemeFake.NewClient(objs...)
r := NewReconciler(clt, clt, &dk)

err := r.Reconcile(ctx)
require.NoError(t, err)

var apiTokenSecret corev1.Secret
err = clt.Get(ctx, types.NamespacedName{Name: consts.TelemetryApiCredentialsSecretName, Namespace: dk.Namespace}, &apiTokenSecret)

require.Error(t, err)
assert.Empty(t, apiTokenSecret)
})
}

func createDynaKube(telemetryServiceEnabled bool) dynakube.DynaKube {
dk := dynakube.DynaKube{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dk",
},
Spec: dynakube.DynaKubeSpec{},
Status: dynakube.DynaKubeStatus{
ActiveGate: activegate.Status{
ConnectionInfo: communication.ConnectionInfo{
TenantUUID: testTenantUUID,
},
VersionStatus: status.VersionStatus{},
},
KubeSystemUUID: testKubeSystemUUID,
},
}

if telemetryServiceEnabled {
dk.TelemetryService().Spec = &telemetryservice.Spec{}
} else {
dk.TelemetryService().Spec = nil
}

return dk
}
8 changes: 8 additions & 0 deletions pkg/controllers/dynakube/otelc/statefulset/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta3/dynakube"
"github.com/Dynatrace/dynatrace-operator/pkg/consts"
otelcConsts "github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/otelc/consts"
corev1 "k8s.io/api/core/v1"
)

Expand All @@ -27,6 +28,7 @@ const (
envK8sClusterName = "K8S_CLUSTER_NAME"
envK8sClusterUid = "K8S_CLUSTER_UID"
envDTentityK8sCluster = "DT_ENTITY_KUBERNETES_CLUSTER"
envDTendpoint = "DT_ENDPOINT"
// certDirEnv is the environment variable that identifies which directory
// to check for SSL certificate files. If set, this overrides the system default.
// It is a colon separated list of directories.
Expand Down Expand Up @@ -77,6 +79,12 @@ func getEnvs(dk *dynakube.DynaKube) []corev1.EnvVar {
{Name: envK8sClusterName, Value: dk.Name},
{Name: envK8sClusterUid, Value: dk.Status.KubeSystemUUID},
{Name: envDTentityK8sCluster, Value: dk.Status.KubernetesClusterMEID},
{Name: envDTendpoint, ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: otelcConsts.TelemetryApiCredentialsSecretName},
Key: envDTendpoint,
},
}},
}
if dk.Spec.TrustedCAs != "" {
envs = append(envs, corev1.EnvVar{Name: envTrustedCAs, Value: trustedCAVolumePath})
Expand Down
Loading