Added support for configuration per project

Shoots with type==Infrastructure will not be considered to be added to Fleet
This commit is contained in:
Jakub Vavřík
2021-02-18 08:08:06 +01:00
parent 46ab0c5eae
commit d2d7872f67
16 changed files with 378 additions and 117 deletions

View File

@@ -3,7 +3,6 @@ package config
import (
healthcheckconfig "github.com/gardener/gardener/extensions/pkg/controller/healthcheck/config"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
componentbaseconfig "k8s.io/component-base/config"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -12,15 +11,23 @@ import (
type FleetAgentConfig struct {
metav1.TypeMeta
// ClientConnection specifies the kubeconfig file and client connection
// settings for the proxy server to use when communicating with the apiserver.
ClientConnection *componentbaseconfig.ClientConnectionConfiguration
// DefaultConfiguration holds default config applied if no project config found
DefaultConfiguration ProjectConfig
// ProjectConfiguration holds configuration overrides for each project
ProjectConfiguration map[string]ProjectConfig
HealthCheckConfig *healthcheckconfig.HealthCheckConfig
}
// ProjectConfig holds configuration for single project
type ProjectConfig struct {
// Kubeconfig contains base64 encoded kubeconfig
Kubeconfig string
// labels to use in Fleet Cluster registration
Labels map[string]string
//namespace to store clusters registrations in Fleet managers cluster
Namespace string
HealthCheckConfig *healthcheckconfig.HealthCheckConfig
}

View File

@@ -3,7 +3,6 @@ package v1alpha1
import (
healthcheckconfig "github.com/gardener/gardener/extensions/pkg/controller/healthcheck/config"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
componentbaseconfigv1alpha1 "k8s.io/component-base/config/v1alpha1"
)
// +genclient
@@ -13,16 +12,23 @@ import (
type FleetAgentConfig struct {
metav1.TypeMeta `json:",inline"`
// ClientConnection specifies the kubeconfig file and client connection
// settings for the proxy server to use when communicating with the apiserver.
// +optional
ClientConnection *componentbaseconfigv1alpha1.ClientConnectionConfiguration `json:"clientConnection,omitempty"`
// DefaultConfiguration holds default config applied if no project config found
DefaultConfiguration ProjectConfig `json:"defaultConfig,omitempty"`
// ProjectConfiguration holds configuration overrides for each project
ProjectConfiguration map[string]ProjectConfig `json:"projectConfig,omitempty"`
HealthCheckConfig *healthcheckconfig.HealthCheckConfig `json:"healthCheckConfig,omitempty"`
}
// ProjectConfig holds configuration for single project
type ProjectConfig struct {
// Kubeconfig contains base64 encoded kubeconfig
Kubeconfig string `json:"kubeconfig,omitempty"`
// labels to use in Fleet Cluster registration
Labels map[string]string `json:"labels,omitempty"`
//namespace to store clusters registrations in Fleet managers cluster
Namespace string `json:"namespace,omitempty"`
HealthCheckConfig *healthcheckconfig.HealthCheckConfig `json:"healthCheckConfig,omitempty"`
}

View File

@@ -27,8 +27,6 @@ import (
config "github.com/javamachr/gardener-extension-shoot-fleet-agent/pkg/apis/config"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
componentbaseconfig "k8s.io/component-base/config"
configv1alpha1 "k8s.io/component-base/config/v1alpha1"
)
func init() {
@@ -48,13 +46,24 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*ProjectConfig)(nil), (*config.ProjectConfig)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_ProjectConfig_To_config_ProjectConfig(a.(*ProjectConfig), b.(*config.ProjectConfig), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.ProjectConfig)(nil), (*ProjectConfig)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_ProjectConfig_To_v1alpha1_ProjectConfig(a.(*config.ProjectConfig), b.(*ProjectConfig), scope)
}); err != nil {
return err
}
return nil
}
func autoConvert_v1alpha1_FleetAgentConfig_To_config_FleetAgentConfig(in *FleetAgentConfig, out *config.FleetAgentConfig, s conversion.Scope) error {
out.ClientConnection = (*componentbaseconfig.ClientConnectionConfiguration)(unsafe.Pointer(in.ClientConnection))
out.Labels = *(*map[string]string)(unsafe.Pointer(&in.Labels))
out.Namespace = in.Namespace
if err := Convert_v1alpha1_ProjectConfig_To_config_ProjectConfig(&in.DefaultConfiguration, &out.DefaultConfiguration, s); err != nil {
return err
}
out.ProjectConfiguration = *(*map[string]config.ProjectConfig)(unsafe.Pointer(&in.ProjectConfiguration))
out.HealthCheckConfig = (*healthcheckconfig.HealthCheckConfig)(unsafe.Pointer(in.HealthCheckConfig))
return nil
}
@@ -65,9 +74,10 @@ func Convert_v1alpha1_FleetAgentConfig_To_config_FleetAgentConfig(in *FleetAgent
}
func autoConvert_config_FleetAgentConfig_To_v1alpha1_FleetAgentConfig(in *config.FleetAgentConfig, out *FleetAgentConfig, s conversion.Scope) error {
out.ClientConnection = (*configv1alpha1.ClientConnectionConfiguration)(unsafe.Pointer(in.ClientConnection))
out.Labels = *(*map[string]string)(unsafe.Pointer(&in.Labels))
out.Namespace = in.Namespace
if err := Convert_config_ProjectConfig_To_v1alpha1_ProjectConfig(&in.DefaultConfiguration, &out.DefaultConfiguration, s); err != nil {
return err
}
out.ProjectConfiguration = *(*map[string]ProjectConfig)(unsafe.Pointer(&in.ProjectConfiguration))
out.HealthCheckConfig = (*healthcheckconfig.HealthCheckConfig)(unsafe.Pointer(in.HealthCheckConfig))
return nil
}
@@ -76,3 +86,27 @@ func autoConvert_config_FleetAgentConfig_To_v1alpha1_FleetAgentConfig(in *config
func Convert_config_FleetAgentConfig_To_v1alpha1_FleetAgentConfig(in *config.FleetAgentConfig, out *FleetAgentConfig, s conversion.Scope) error {
return autoConvert_config_FleetAgentConfig_To_v1alpha1_FleetAgentConfig(in, out, s)
}
func autoConvert_v1alpha1_ProjectConfig_To_config_ProjectConfig(in *ProjectConfig, out *config.ProjectConfig, s conversion.Scope) error {
out.Kubeconfig = in.Kubeconfig
out.Labels = *(*map[string]string)(unsafe.Pointer(&in.Labels))
out.Namespace = in.Namespace
return nil
}
// Convert_v1alpha1_ProjectConfig_To_config_ProjectConfig is an autogenerated conversion function.
func Convert_v1alpha1_ProjectConfig_To_config_ProjectConfig(in *ProjectConfig, out *config.ProjectConfig, s conversion.Scope) error {
return autoConvert_v1alpha1_ProjectConfig_To_config_ProjectConfig(in, out, s)
}
func autoConvert_config_ProjectConfig_To_v1alpha1_ProjectConfig(in *config.ProjectConfig, out *ProjectConfig, s conversion.Scope) error {
out.Kubeconfig = in.Kubeconfig
out.Labels = *(*map[string]string)(unsafe.Pointer(&in.Labels))
out.Namespace = in.Namespace
return nil
}
// Convert_config_ProjectConfig_To_v1alpha1_ProjectConfig is an autogenerated conversion function.
func Convert_config_ProjectConfig_To_v1alpha1_ProjectConfig(in *config.ProjectConfig, out *ProjectConfig, s conversion.Scope) error {
return autoConvert_config_ProjectConfig_To_v1alpha1_ProjectConfig(in, out, s)
}

View File

@@ -23,23 +23,18 @@ package v1alpha1
import (
config "github.com/gardener/gardener/extensions/pkg/controller/healthcheck/config"
runtime "k8s.io/apimachinery/pkg/runtime"
configv1alpha1 "k8s.io/component-base/config/v1alpha1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FleetAgentConfig) DeepCopyInto(out *FleetAgentConfig) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.ClientConnection != nil {
in, out := &in.ClientConnection, &out.ClientConnection
*out = new(configv1alpha1.ClientConnectionConfiguration)
**out = **in
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
in.DefaultConfiguration.DeepCopyInto(&out.DefaultConfiguration)
if in.ProjectConfiguration != nil {
in, out := &in.ProjectConfiguration, &out.ProjectConfiguration
*out = make(map[string]ProjectConfig, len(*in))
for key, val := range *in {
(*out)[key] = val
(*out)[key] = *val.DeepCopy()
}
}
if in.HealthCheckConfig != nil {
@@ -67,3 +62,26 @@ func (in *FleetAgentConfig) DeepCopyObject() runtime.Object {
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProjectConfig) DeepCopyInto(out *ProjectConfig) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectConfig.
func (in *ProjectConfig) DeepCopy() *ProjectConfig {
if in == nil {
return nil
}
out := new(ProjectConfig)
in.DeepCopyInto(out)
return out
}

View File

@@ -23,23 +23,18 @@ package config
import (
healthcheckconfig "github.com/gardener/gardener/extensions/pkg/controller/healthcheck/config"
runtime "k8s.io/apimachinery/pkg/runtime"
componentbaseconfig "k8s.io/component-base/config"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FleetAgentConfig) DeepCopyInto(out *FleetAgentConfig) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.ClientConnection != nil {
in, out := &in.ClientConnection, &out.ClientConnection
*out = new(componentbaseconfig.ClientConnectionConfiguration)
**out = **in
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
in.DefaultConfiguration.DeepCopyInto(&out.DefaultConfiguration)
if in.ProjectConfiguration != nil {
in, out := &in.ProjectConfiguration, &out.ProjectConfiguration
*out = make(map[string]ProjectConfig, len(*in))
for key, val := range *in {
(*out)[key] = val
(*out)[key] = *val.DeepCopy()
}
}
if in.HealthCheckConfig != nil {
@@ -67,3 +62,26 @@ func (in *FleetAgentConfig) DeepCopyObject() runtime.Object {
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProjectConfig) DeepCopyInto(out *ProjectConfig) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectConfig.
func (in *ProjectConfig) DeepCopy() *ProjectConfig {
if in == nil {
return nil
}
out := new(ProjectConfig)
in.DeepCopyInto(out)
return out
}

View File

@@ -16,20 +16,21 @@ package controller
import (
"context"
b64 "encoding/base64"
"fmt"
"reflect"
"strings"
projConfig "github.com/javamachr/gardener-extension-shoot-fleet-agent/pkg/apis/config"
"github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/gardener/gardener/pkg/extensions"
"github.com/go-logr/logr"
fleetv1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -51,42 +52,42 @@ const KubeconfigSecretName = "kubecfg"
// KubeconfigKey key in KubeconfigSecretName secret that holds kubeconfig for Shoot
const KubeconfigKey = "kubeconfig"
// DefaultConfigKey is the name of default config key.
const DefaultConfigKey = "default"
// NewActuator returns an actuator responsible for Extension resources.
func NewActuator(config config.Config) extension.Actuator {
fleetKubeConfig, _ := b64.StdEncoding.DecodeString(config.FleetAgentConfig.ClientConnection.Kubeconfig)
var kubeconfigPath string
var err error
if kubeconfigPath, err = writeKubeconfigToTempFile(fleetKubeConfig); err != nil {
panic(err)
}
fleetClientConfig, _ := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
var fleetNamespace = "clusters"
if len(config.Namespace) != 0 {
fleetNamespace = config.Namespace
}
fleetManager, err := NewManagerForConfig(fleetClientConfig, fleetNamespace)
if err != nil {
panic(err)
}
logger := log.Log.WithName(ActuatorName)
fleetManagers := initializeFleetManagers(config, logger)
return &actuator{
logger: log.Log.WithName(ActuatorName),
logger: logger,
serviceConfig: config,
fleetManager: fleetManager,
fleetManagers: fleetManagers,
}
}
type actuator struct {
client client.Client
config *rest.Config
decoder runtime.Decoder
fleetManager *FleetManager
client client.Client
config *rest.Config
decoder runtime.Decoder
fleetManagers map[string]*FleetManager
serviceConfig config.Config
logger logr.Logger
}
// initializeFleetManagers initializes fleet managers for given config
func initializeFleetManagers(config config.Config, logger logr.Logger) map[string]*FleetManager {
fleetManagers := make(map[string]*FleetManager)
fleetManagers[DefaultConfigKey] = createFleetManager(config.DefaultConfiguration, logger)
for name, projConfig := range config.ProjectConfiguration {
fleetManagers[name] = createFleetManager(projConfig, logger)
}
return fleetManagers
}
// Reconcile the Extension resource.
func (a *actuator) Reconcile(ctx context.Context, ex *extensionsv1alpha1.Extension) error {
namespace := ex.GetNamespace()
@@ -95,6 +96,9 @@ func (a *actuator) Reconcile(ctx context.Context, ex *extensionsv1alpha1.Extensi
if err != nil {
return err
}
if isShootedSeedCluster(cluster) {
return a.updateStatus(ctx, ex)
}
shootsConfigOverride := &config.Config{}
if ex.Spec.ProviderConfig != nil { //parse providerConfig defaults override for this Shoot
if _, _, err := a.decoder.Decode(ex.Spec.ProviderConfig.Raw, nil, shootsConfigOverride); err != nil {
@@ -112,12 +116,15 @@ func (a *actuator) Delete(ctx context.Context, ex *extensionsv1alpha1.Extension)
if err != nil {
return err
}
if isShootedSeedCluster(cluster) {
return nil
}
a.logger.Info("Component is being deleted", "component", "fleet-agent-management", "namespace", namespace, "cluster", buildCrdName(cluster))
err = a.fleetManager.DeleteKubeconfigSecret(ctx, buildCrdName(cluster))
err = a.getFleetManager(cluster).DeleteKubeconfigSecret(ctx, buildCrdName(cluster))
if err != nil {
a.logger.Error(err, "Failed to delete kubeconfig secret for Shoot cluster.", "cluster", buildCrdName(cluster))
}
err = a.fleetManager.DeleteCluster(ctx, buildCrdName(cluster))
err = a.getFleetManager(cluster).DeleteCluster(ctx, buildCrdName(cluster))
if err != nil {
a.logger.Error(err, "Failed to delete Cluster registration for Shoot cluster.", "cluster", buildCrdName(cluster))
}
@@ -157,23 +164,26 @@ func (a *actuator) InjectScheme(scheme *runtime.Scheme) error {
// ReconcileClusterInFleetManager reconciles cluster registration in remote fleet manager
func (a *actuator) ReconcileClusterInFleetManager(ctx context.Context, namespace string, cluster *extensions.Cluster, override *config.Config) {
a.logger.Info("Starting with already registered check")
labels := prepareLabels(cluster, a.serviceConfig, override)
registered, err := a.fleetManager.GetCluster(ctx, cluster.Shoot.Name)
if !errors.IsNotFound(err) {
labels := prepareLabels(cluster, getProjectConfig(cluster, &a.serviceConfig), getProjectConfig(cluster, override))
registered, err := a.getFleetManager(cluster).GetCluster(ctx, cluster.Shoot.Name)
if err != nil {
a.logger.Error(err, "Failed to get cluster registration for Shoot", "shoot", cluster.Shoot.Name)
}
if err == nil && registered != nil {
if reflect.DeepEqual(registered.Labels, labels) {
a.logger.Info("Cluster already registered - skipping registration", "clientId", registered.Spec.ClientID)
} else {
a.logger.Info("Updating labels of already registered cluster.", "clientId", registered.Spec.ClientID)
a.updateClusterLabelsInFleet(ctx, registered, labels)
a.updateClusterLabelsInFleet(ctx, registered, cluster, labels)
}
return
}
a.registerNewClusterInFleet(ctx, namespace, cluster, labels)
}
func (a *actuator) updateClusterLabelsInFleet(ctx context.Context, clusterRegistration *fleetv1alpha1.Cluster, labels map[string]string) {
func (a *actuator) updateClusterLabelsInFleet(ctx context.Context, clusterRegistration *fleetv1alpha1.Cluster, cluster *extensions.Cluster, labels map[string]string) {
clusterRegistration.Labels = labels
_, err := a.fleetManager.UpdateCluster(ctx, clusterRegistration)
_, err := a.getFleetManager(cluster).UpdateCluster(ctx, clusterRegistration)
if err != nil {
a.logger.Error(err, "Failed to update cluster labels in Fleet registration.", "clusterName", clusterRegistration.Name)
}
@@ -207,10 +217,10 @@ func (a *actuator) registerNewClusterInFleet(ctx context.Context, namespace stri
KubeConfigSecret: "kubecfg-" + buildCrdName(cluster),
},
}
if _, err = a.fleetManager.CreateKubeconfigSecret(ctx, &kubeconfigSecret); err != nil {
if _, err = a.getFleetManager(cluster).CreateKubeconfigSecret(ctx, &kubeconfigSecret); err != nil {
a.logger.Error(err, "Failed to create secret with kubeconfig for Fleet registration")
}
if _, err = a.fleetManager.CreateCluster(ctx, &clusterRegistration); err != nil {
if _, err = a.getFleetManager(cluster).CreateCluster(ctx, &clusterRegistration); err != nil {
a.logger.Error(err, "Failed to create Cluster for Fleet registration")
}
a.logger.Info("Registered shoot cluster in Fleet Manager ", "registration", clusterRegistration)
@@ -219,17 +229,18 @@ func (a *actuator) registerNewClusterInFleet(ctx context.Context, namespace stri
}
}
func prepareLabels(cluster *extensions.Cluster, serviceConfig config.Config, override *config.Config) map[string]string {
func prepareLabels(cluster *extensions.Cluster, serviceConfig projConfig.ProjectConfig, override projConfig.ProjectConfig) map[string]string {
labels := make(map[string]string)
labels["corebundle"] = "true"
labels["region"] = cluster.Shoot.Spec.Region
labels["cluster"] = cluster.Shoot.Name
labels["seed"] = cluster.Seed.Name
if len(override.Labels) > 0 { //adds labels from Shoot configuration
for key, value := range override.Labels {
labels[key] = value
}
} else {
if len(serviceConfig.FleetAgentConfig.Labels) > 0 { //adds labels from default configuration
if len(serviceConfig.Labels) > 0 { //adds labels from default configuration
for key, value := range serviceConfig.Labels {
labels[key] = value
}
@@ -238,13 +249,41 @@ func prepareLabels(cluster *extensions.Cluster, serviceConfig config.Config, ove
return labels
}
// buildCrdName creates a unique name for cluster registration resources in Fleet manager cluster
func buildCrdName(cluster *extensions.Cluster) string {
return cluster.Seed.Name + "" + cluster.Shoot.Name
}
func (a *actuator) updateStatus(ctx context.Context, ex *extensionsv1alpha1.Extension) error {
return controller.TryUpdateStatus(ctx, retry.DefaultBackoff, a.client, ex, func() error {
return nil
})
}
func (a *actuator) getFleetManager(cluster *extensions.Cluster) *FleetManager {
manager, present := a.fleetManagers[getProjectName(cluster)]
if !present {
return a.fleetManagers[DefaultConfigKey]
}
return manager
}
// getProjectConfig return project specific or default config
func getProjectConfig(cluster *extensions.Cluster, serviceConfig *config.Config) projConfig.ProjectConfig {
name := getProjectName(cluster)
projectConfig, present := serviceConfig.ProjectConfiguration[name]
if !present {
return serviceConfig.DefaultConfiguration
}
return projectConfig
}
// buildCrdName creates a unique name for cluster registration resources in Fleet manager cluster
func buildCrdName(cluster *extensions.Cluster) string {
return cluster.Seed.Name + "" + cluster.Shoot.Name
}
// isShootedSeedCluster checks if clusters purpose is Infrastructure
func isShootedSeedCluster(cluster *extensions.Cluster) bool {
return *cluster.Shoot.Spec.Purpose == v1beta1.ShootPurposeInfrastructure
}
// getProjectName extracts project name from Shoots namespace
func getProjectName(cluster *extensions.Cluster) string {
return cluster.Shoot.Namespace[strings.LastIndex(cluster.Shoot.Namespace, "-")+1:]
}

View File

@@ -2,7 +2,13 @@ package controller
import (
"context"
b64 "encoding/base64"
"github.com/go-logr/logr"
"k8s.io/client-go/tools/clientcmd"
fleetConfig "github.com/javamachr/gardener-extension-shoot-fleet-agent/pkg/apis/config"
clientset "github.com/javamachr/gardener-extension-shoot-fleet-agent/pkg/client/fleet/clientset/versioned"
"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
corev1 "k8s.io/api/core/v1"
@@ -66,3 +72,31 @@ func (f *FleetManager) CreateKubeconfigSecret(ctx context.Context, secret *corev
func (f *FleetManager) DeleteKubeconfigSecret(ctx context.Context, secretName string) error {
return f.secretClient.CoreV1().Secrets(f.namespace).Delete(ctx, secretName, metav1.DeleteOptions{})
}
// createFleetManager creates fleet manager for given configuration
func createFleetManager(config fleetConfig.ProjectConfig, logger logr.Logger) *FleetManager {
logger.Info("Creating Fleet manager for config", "config", config)
fleetKubeConfig, err := b64.StdEncoding.DecodeString(config.Kubeconfig)
if err != nil {
panic(err)
}
var kubeconfigPath string
if kubeconfigPath, err = writeKubeconfigToTempFile(fleetKubeConfig); err != nil {
panic(err)
}
logger.Info("Written kubeconfig to temp", "file", kubeconfigPath)
fleetClientConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
panic(err)
}
logger.Info("Fleet k8s client successfully built.")
var fleetNamespace = "clusters"
if len(config.Namespace) != 0 {
fleetNamespace = config.Namespace
}
fleetManager, err := NewManagerForConfig(fleetClientConfig, fleetNamespace)
if err != nil {
panic(err)
}
return fleetManager
}