diff --git a/cmd/main.go b/cmd/main.go index ff30184c..6004474b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,11 +18,16 @@ package main import ( "flag" + "fmt" "os" + "time" + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + gardener_apis "github.com/gardener/gardener/pkg/client/core/clientset/versioned/typed/core/v1beta1" infrastructuremanagerv1 "github.com/kyma-project/infrastructure-manager/api/v1" "github.com/kyma-project/infrastructure-manager/internal/controller" "github.com/kyma-project/infrastructure-manager/internal/gardener" + "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -30,6 +35,7 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) @@ -46,11 +52,15 @@ func init() { //+kubebuilder:scaffold:scheme } +const defaultExpirationTime = 24 * time.Hour + func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string var gardenerKubeconfigPath string + var gardenerProjectName string + var expirationTime time.Duration flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -58,6 +68,8 @@ func main() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.StringVar(&gardenerKubeconfigPath, "gardener-kubeconfig-path", "/gardener/kubeconfig/kubeconfig", "Kubeconfig file for Gardener cluster") + flag.StringVar(&gardenerProjectName, "gardener-project-name", "gardener-project", "Name of the Gardener project") + flag.DurationVar(&expirationTime, "kubeconfig-expiration-time", defaultExpirationTime, "Dynamic kubeconfig expiration time") opts := zap.Options{ Development: true, @@ -93,9 +105,16 @@ func main() { os.Exit(1) } - provider := gardener.KubeconfigProvider{} + gardenerNamespace := fmt.Sprintf("garden-%s", gardenerProjectName) + expirationInSeconds := int64(expirationTime.Seconds()) + kubeconfigProvider, err := setupKubernetesKubeconfigProvider(gardenerKubeconfigPath, gardenerNamespace, expirationInSeconds) + + if err != nil { + setupLog.Error(err, "unable to initialize kubeconfig provider", "controller", "GardenerCluster") + os.Exit(1) + } - if err = (controller.NewGardenerClusterController(mgr, provider, logger)).SetupWithManager(mgr); err != nil { + if err = (controller.NewGardenerClusterController(mgr, kubeconfigProvider, logger)).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "GardenerCluster") os.Exit(1) } @@ -110,15 +129,39 @@ func main() { os.Exit(1) } - _, err = gardener.NewClientFromFile(gardenerKubeconfigPath) - if err != nil { - setupLog.Error(err, "failed to load Gardener kubeconfig") - os.Exit(1) - } - setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } + +func setupKubernetesKubeconfigProvider(kubeconfigPath string, namespace string, expirationInSeconds int64) (gardener.KubeconfigProvider, error) { + restConfig, err := gardener.NewRestConfigFromFile(kubeconfigPath) + if err != nil { + return gardener.KubeconfigProvider{}, err + } + + gardenerClientSet, err := gardener_apis.NewForConfig(restConfig) + if err != nil { + return gardener.KubeconfigProvider{}, err + } + + gardenerClient, err := client.New(restConfig, client.Options{}) + if err != nil { + return gardener.KubeconfigProvider{}, err + } + + shootClient := gardenerClientSet.Shoots(namespace) + dynamicKubeconfigAPI := gardenerClient.SubResource("adminkubeconfig") + + err = v1beta1.AddToScheme(gardenerClient.Scheme()) + if err != nil { + return gardener.KubeconfigProvider{}, errors.Wrap(err, "failed to register Gardener schema") + } + + return gardener.NewKubeconfigProvider(shootClient, + dynamicKubeconfigAPI, + namespace, + expirationInSeconds), nil +} diff --git a/config/default/manager_gardener_secret_patch.yaml b/config/default/manager_gardener_secret_patch.yaml index 72713b5c..4afb921f 100644 --- a/config/default/manager_gardener_secret_patch.yaml +++ b/config/default/manager_gardener_secret_patch.yaml @@ -19,6 +19,8 @@ spec: - /manager args: - --gardener-kubeconfig-path=/gardener/credentials/kubeconfig + - --gardener-project-name=kyma-dev + - --kubeconfig-expiration-time=24h volumeMounts: - name: gardener-kubeconfig mountPath: /gardener/credentials diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 78ec8c8b..0f9269f5 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,28 +5,31 @@ metadata: name: manager-role rules: - apiGroups: - - infrastructuremanager.kyma-project.io + - "" resources: - - gardenerclusters + - secrets verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - infrastructuremanager.kyma-project.io resources: - - gardenerclusters/finalizers + - gardenerclusters verbs: + - create + - delete + - get + - list + - patch - update + - watch - apiGroups: - infrastructuremanager.kyma-project.io resources: - - gardenerclusters/status + - gardenerclusters/finalizers verbs: - - get - - patch - update diff --git a/config/samples/clusterinventory_v1_cluster.yaml b/config/samples/clusterinventory_v1_cluster.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/config/samples/clusterinventory_v1_gardenercluster.yaml b/config/samples/clusterinventory_v1_gardenercluster.yaml index d571200b..a699ef82 100644 --- a/config/samples/clusterinventory_v1_gardenercluster.yaml +++ b/config/samples/clusterinventory_v1_gardenercluster.yaml @@ -1,4 +1,4 @@ -apiVersion: clusterinventory.kyma-project.io/v1 +apiVersion: infrastructuremanager.kyma-project.io/v1 kind: GardenerCluster metadata: labels: diff --git a/internal/controller/gardener_cluster_controller.go b/internal/controller/gardener_cluster_controller.go index 7193a072..e9b8c96f 100644 --- a/internal/controller/gardener_cluster_controller.go +++ b/internal/controller/gardener_cluster_controller.go @@ -62,7 +62,7 @@ type KubeconfigProvider interface { } //+kubebuilder:rbac:groups=infrastructuremanager.kyma-project.io,resources=gardenerclusters,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=infrastructuremanager.kyma-project.io,resources=gardenerclusters/status,verbs=get;update;patch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups=infrastructuremanager.kyma-project.io,resources=gardenerclusters/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -74,7 +74,7 @@ type KubeconfigProvider interface { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile func (r *GardenerClusterController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //nolint:revive - r.log.Info("Starting reconciliation loop") + r.log.Info(fmt.Sprintf("Starting reconciliation loop for GardenerCluster resource: %v", req.NamespacedName)) var cluster infrastructuremanagerv1.GardenerCluster @@ -95,8 +95,6 @@ func (r *GardenerClusterController) Reconcile(ctx context.Context, req ctrl.Requ secret, err := r.getSecret(cluster.Spec.Shoot.Name) if err != nil { - r.log.Error(err, "could not get the Secret for "+cluster.Spec.Shoot.Name) - if !k8serrors.IsNotFound(err) { return ctrl.Result{ Requeue: true, @@ -106,10 +104,7 @@ func (r *GardenerClusterController) Reconcile(ctx context.Context, req ctrl.Requ } if secret == nil { - r.log.Error(err, "Secret not found, and will be created") - err = r.createSecret(ctx, cluster) - if err != nil { return r.ResultWithoutRequeue(), err } diff --git a/internal/gardener/KubeConfigProvider.go b/internal/gardener/KubeConfigProvider.go index fefaeec5..83c81879 100644 --- a/internal/gardener/KubeConfigProvider.go +++ b/internal/gardener/KubeConfigProvider.go @@ -1,8 +1,59 @@ package gardener +import ( + "context" + + authenticationv1alpha1 "github.com/gardener/gardener/pkg/apis/authentication/v1alpha1" + "github.com/gardener/gardener/pkg/apis/core/v1beta1" + "github.com/pkg/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gardenerClient "sigs.k8s.io/controller-runtime/pkg/client" +) + type KubeconfigProvider struct { + shootNamespace string + shootClient ShootClient + dynamicKubeconfigAPI DynamicKubeconfigAPI + expirationInSeconds int64 +} + +type ShootClient interface { + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.Shoot, error) +} + +type DynamicKubeconfigAPI interface { + Create(ctx context.Context, obj gardenerClient.Object, subResource gardenerClient.Object, opts ...gardenerClient.SubResourceCreateOption) error } -func (receiver KubeconfigProvider) Fetch(shootName string) (string, error) { - return "kubeconfig-" + shootName, nil +func NewKubeconfigProvider( + shootClient ShootClient, + dynamicKubeconfigAPI DynamicKubeconfigAPI, + shootNamespace string, + expirationInSeconds int64) KubeconfigProvider { + return KubeconfigProvider{ + shootClient: shootClient, + dynamicKubeconfigAPI: dynamicKubeconfigAPI, + shootNamespace: shootNamespace, + expirationInSeconds: expirationInSeconds, + } +} + +func (kp KubeconfigProvider) Fetch(shootName string) (string, error) { + shoot, err := kp.shootClient.Get(context.Background(), shootName, v1.GetOptions{}) + if err != nil { + return "", errors.Wrap(err, "failed to get shoot") + } + + adminKubeconfigRequest := authenticationv1alpha1.AdminKubeconfigRequest{ + Spec: authenticationv1alpha1.AdminKubeconfigRequestSpec{ + ExpirationSeconds: &kp.expirationInSeconds, + }, + } + + err = kp.dynamicKubeconfigAPI.Create(context.Background(), shoot, &adminKubeconfigRequest) + if err != nil { + return "", errors.Wrap(err, "failed to create AdminKubeconfigRequest") + } + + return string(adminKubeconfigRequest.Status.Kubeconfig), nil } diff --git a/internal/gardener/client.go b/internal/gardener/client.go index 379fc0ff..7fab48a0 100644 --- a/internal/gardener/client.go +++ b/internal/gardener/client.go @@ -4,11 +4,11 @@ import ( "fmt" "os" - gardener_apis "github.com/gardener/gardener/pkg/client/core/clientset/versioned/typed/core/v1beta1" + restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) -func NewClientFromFile(kubeconfigFilePath string) (*gardener_apis.CoreV1beta1Client, error) { +func NewRestConfigFromFile(kubeconfigFilePath string) (*restclient.Config, error) { rawKubeconfig, err := os.ReadFile(kubeconfigFilePath) if err != nil { return nil, fmt.Errorf("failed to read Gardener Kubeconfig from path %s: %s", kubeconfigFilePath, err.Error()) @@ -19,10 +19,5 @@ func NewClientFromFile(kubeconfigFilePath string) (*gardener_apis.CoreV1beta1Cli return nil, err } - clientset, err := gardener_apis.NewForConfig(restConfig) - if err != nil { - return nil, err - } - - return clientset, nil + return restConfig, err }