diff --git a/cmd/agent/run.go b/cmd/agent/run.go index 54a9269..e2d4aaa 100644 --- a/cmd/agent/run.go +++ b/cmd/agent/run.go @@ -9,6 +9,7 @@ import ( "runtime" "time" + "castai-agent/internal/services/metadata" "github.com/samber/lo" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -174,6 +175,18 @@ func runAgentMode(ctx context.Context, castaiclient castai.Client, log *logrus.E return fmt.Errorf("getting provider: %w", err) } + metadataStore := metadata.New(clientset, cfg) + + clusterIDChangedHandler := func(clusterID string) { + if err := metadataStore.StoreMetadataSecret(ctx, &metadata.Metadata{ + ClusterID: clusterID, + }); err != nil { + log.Warnf("failed to store metadata in a secret: %v", err) + } + + clusterIDChanged(clusterID) + } + log.Data["provider"] = provider.Name() log.Infof("using provider %q", provider.Name()) @@ -189,10 +202,10 @@ func runAgentMode(ctx context.Context, castaiclient castai.Client, log *logrus.E return fmt.Errorf("registering cluster: %w", err) } clusterID = reg.ClusterID - clusterIDChanged(clusterID) + clusterIDChangedHandler(clusterID) log.Infof("cluster registered: %v, clusterID: %s", reg, clusterID) } else { - clusterIDChanged(clusterID) + clusterIDChangedHandler(clusterID) log.Infof("clusterID: %s provided by env variable", clusterID) } diff --git a/internal/config/config.go b/internal/config/config.go index 1347a2e..10efbdb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,6 +34,8 @@ type Config struct { PprofPort int `mapstructure:"pprof.port"` HealthzPort int `mapstructure:"healthz_port"` + MetadataStore *MetadataStoreConfig `mapstructure:"metadata_store"` + LeaderElection LeaderElectionConfig `mapstructure:"leader_election"` MonitorMetadata string `mapstructure:"monitor_metadata"` @@ -110,6 +112,12 @@ type Static struct { ClusterID string `mapstructure:"cluster_id"` } +type MetadataStoreConfig struct { + Enabled bool `mapstructure:"enabled"` + Namespace string `mapstructure:"namespace"` + SecretName string `mapstructure:"secret_name"` +} + type Anywhere struct { ClusterName string `mapstructure:"cluster_name"` } @@ -169,6 +177,10 @@ func Get() Config { viper.SetDefault("leader_election.lock_name", "agent-leader-election-lock") viper.SetDefault("leader_election.namespace", "castai-agent") + viper.SetDefault("metadata_store.enabled", false) + viper.SetDefault("metadata_store.secret_name", "castai-agent-metadata") + viper.SetDefault("metadata_store.namespace", "castai-agent") + viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AllowEmptyEnv(true) diff --git a/internal/services/metadata/store.go b/internal/services/metadata/store.go new file mode 100644 index 0000000..af7b8f4 --- /dev/null +++ b/internal/services/metadata/store.go @@ -0,0 +1,54 @@ +package metadata + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/errors" + + "castai-agent/internal/config" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type Store interface { + // StoreMetadataSecret stores relevant agent runtime metadata in a secret. + StoreMetadataSecret(ctx context.Context, metadata *Metadata) error +} + +var _ Store = (*StoreImpl)(nil) + +type StoreImpl struct { + clientset kubernetes.Interface + cfg config.Config +} + +func New(clientset kubernetes.Interface, cfg config.Config) *StoreImpl { + return &StoreImpl{ + clientset: clientset, + cfg: cfg, + } +} + +func (s *StoreImpl) StoreMetadataSecret(ctx context.Context, metadata *Metadata) error { + if !s.cfg.MetadataStore.Enabled { + return nil + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.cfg.MetadataStore.SecretName, + Namespace: s.cfg.MetadataStore.Namespace, + }, + StringData: map[string]string{ + "CLUSTER_ID": metadata.ClusterID, + }, + } + + _, err := s.clientset.CoreV1().Secrets(s.cfg.MetadataStore.Namespace).Update(ctx, secret, metav1.UpdateOptions{}) + if errors.IsNotFound(err) { + _, err = s.clientset.CoreV1().Secrets(s.cfg.MetadataStore.Namespace).Create(ctx, secret, metav1.CreateOptions{}) + } + + return err +} diff --git a/internal/services/metadata/store_test.go b/internal/services/metadata/store_test.go new file mode 100644 index 0000000..73f3a8e --- /dev/null +++ b/internal/services/metadata/store_test.go @@ -0,0 +1,97 @@ +package metadata + +import ( + "context" + "testing" + + "castai-agent/internal/config" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakeclientset "k8s.io/client-go/kubernetes/fake" +) + +func TestStoreImpl_StoreMetadataSecret(t *testing.T) { + tests := []struct { + name string + cfg config.Config + metadata *Metadata + existingSecret *corev1.Secret + expectedError bool + }{ + { + name: "should store metadata successfully when secret does not exist", + cfg: config.Config{ + MetadataStore: &config.MetadataStoreConfig{ + Enabled: true, + SecretName: "castai-agent-metadata", + Namespace: "default", + }, + }, + metadata: &Metadata{ + ClusterID: "test-cluster-id", + }, + expectedError: false, + }, + { + name: "should store metadata successfully when secret exists", + cfg: config.Config{ + MetadataStore: &config.MetadataStoreConfig{ + Enabled: true, + SecretName: "castai-agent-metadata", + Namespace: "default", + }, + }, + metadata: &Metadata{ + ClusterID: "test-cluster-id", + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "castai-agent-metadata", + Namespace: "default", + }, + StringData: map[string]string{ + "CLUSTER_ID": "original-test-cluster-id", + }, + }, + expectedError: false, + }, + { + name: "should not store metadata when store is disabled", + cfg: config.Config{ + MetadataStore: &config.MetadataStoreConfig{ + Enabled: false, + }, + }, + metadata: &Metadata{ + ClusterID: "test-cluster-id", + }, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + clientset := fakeclientset.NewSimpleClientset() + store := New(clientset, tt.cfg) + + if tt.existingSecret != nil { + _, err := clientset.CoreV1().Secrets(tt.cfg.MetadataStore.Namespace).Create(context.Background(), tt.existingSecret, metav1.CreateOptions{}) + r.NoError(err) + } + + err := store.StoreMetadataSecret(context.Background(), tt.metadata) + if tt.expectedError { + r.Error(err) + } else { + r.NoError(err) + if tt.cfg.MetadataStore.Enabled { + secret, err := clientset.CoreV1().Secrets(tt.cfg.MetadataStore.Namespace).Get(context.Background(), tt.cfg.MetadataStore.SecretName, metav1.GetOptions{}) + r.NoError(err) + r.Equal(tt.metadata.ClusterID, secret.StringData["CLUSTER_ID"]) + } + } + }) + } +} diff --git a/internal/services/metadata/types.go b/internal/services/metadata/types.go new file mode 100644 index 0000000..66c4d27 --- /dev/null +++ b/internal/services/metadata/types.go @@ -0,0 +1,5 @@ +package metadata + +type Metadata struct { + ClusterID string +}