diff --git a/api/v1alpha1/k8sgpt_types.go b/api/v1alpha1/k8sgpt_types.go index d20c0924..48c2e179 100644 --- a/api/v1alpha1/k8sgpt_types.go +++ b/api/v1alpha1/k8sgpt_types.go @@ -101,9 +101,12 @@ type K8sGPTSpec struct { Filters []string `json:"filters,omitempty"` ExtraOptions *ExtraOptionsRef `json:"extraOptions,omitempty"` Sink *WebhookRef `json:"sink,omitempty"` - AI *AISpec `json:"ai,omitempty"` - RemoteCache *RemoteCacheRef `json:"remoteCache,omitempty"` - Integrations *Integrations `json:"integrations,omitempty"` + // Define the kubeconfig the Deployment must use. + // If empty, the Deployment will use the ServiceAccount provided by Kubernetes itself. + Kubeconfig *SecretRef `json:"kubeconfig,omitempty"` + AI *AISpec `json:"ai,omitempty"` + RemoteCache *RemoteCacheRef `json:"remoteCache,omitempty"` + Integrations *Integrations `json:"integrations,omitempty"` } const ( diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f145463d..613a75b2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -242,6 +242,11 @@ func (in *K8sGPTSpec) DeepCopyInto(out *K8sGPTSpec) { *out = new(WebhookRef) **out = **in } + if in.Kubeconfig != nil { + in, out := &in.Kubeconfig, &out.Kubeconfig + *out = new(SecretRef) + **out = **in + } if in.AI != nil { in, out := &in.AI, &out.AI *out = new(AISpec) diff --git a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml index 70517509..2919180b 100644 --- a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml +++ b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml @@ -93,6 +93,16 @@ spec: type: boolean type: object type: object + kubeconfig: + description: Define the kubeconfig the Deployment must use. If empty, + the Deployment will use the ServiceAccount provided by Kubernetes + itself. + properties: + key: + type: string + name: + type: string + type: object noCache: type: boolean remoteCache: diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index dccfd350..9f9e615b 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -124,7 +124,7 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // Check and see if the instance is new or has a K8sGPT deployment in flight deployment := v1.Deployment{} err = r.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace, - Name: "k8sgpt-deployment"}, &deployment) + Name: k8sgptConfig.Name}, &deployment) if client.IgnoreNotFound(err) != nil { k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) diff --git a/pkg/client/client.go b/pkg/client/client.go index 8bf2ffd9..bd490907 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -54,7 +54,7 @@ func GenerateAddress(ctx context.Context, cli client.Client, k8sgptConfig *v1alp // Get service IP and port for k8sgpt-deployment svc := &corev1.Service{} err := cli.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace, - Name: "k8sgpt"}, svc) + Name: k8sgptConfig.Name}, svc) if err != nil { return "", nil } diff --git a/pkg/resources/k8sgpt.go b/pkg/resources/k8sgpt.go index bda75ca5..3cfe2e5a 100644 --- a/pkg/resources/k8sgpt.go +++ b/pkg/resources/k8sgpt.go @@ -17,6 +17,7 @@ package resources import ( "context" err "errors" + "fmt" "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1" "github.com/k8sgpt-ai/k8sgpt-operator/pkg/utils" @@ -29,6 +30,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -39,7 +41,6 @@ type SyncOrDestroy int const ( SyncOp SyncOrDestroy = iota DestroyOp - DeploymentName = "k8sgpt-deployment" ) // GetService Create service for K8sGPT @@ -47,7 +48,7 @@ func GetService(config v1alpha1.K8sGPT) (*corev1.Service, error) { // Create service service := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: "k8sgpt", + Name: config.Name, Namespace: config.Namespace, OwnerReferences: []metav1.OwnerReference{ { @@ -62,7 +63,7 @@ func GetService(config v1alpha1.K8sGPT) (*corev1.Service, error) { }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ - "app": DeploymentName, + "app": config.Name, }, Ports: []corev1.ServicePort{ { @@ -171,13 +172,13 @@ func GetClusterRole(config v1alpha1.K8sGPT) (*r1.ClusterRole, error) { } // GetDeployment Create deployment with the latest K8sGPT image -func GetDeployment(config v1alpha1.K8sGPT) (*appsv1.Deployment, error) { +func GetDeployment(config v1alpha1.K8sGPT, outOfClusterMode bool) (*appsv1.Deployment, error) { // Create deployment replicas := int32(1) deployment := appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: DeploymentName, + Name: config.Name, Namespace: config.Namespace, OwnerReferences: []metav1.OwnerReference{ { @@ -194,13 +195,13 @@ func GetDeployment(config v1alpha1.K8sGPT) (*appsv1.Deployment, error) { Replicas: &replicas, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - "app": DeploymentName, + "app": config.Name, }, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - "app": DeploymentName, + "app": config.Name, }, }, Spec: corev1.PodSpec{ @@ -264,6 +265,35 @@ func GetDeployment(config v1alpha1.K8sGPT) (*appsv1.Deployment, error) { }, }, } + if outOfClusterMode { + // No need of ServiceAccount since the Deployment will use + // a kubeconfig pointing to an external cluster. + deployment.Spec.Template.Spec.ServiceAccountName = "" + deployment.Spec.Template.Spec.AutomountServiceAccountToken = ptr.To(false) + + kubeconfigPath := fmt.Sprintf("/tmp/%s", config.Name) + + deployment.Spec.Template.Spec.Containers[0].Args = append(deployment.Spec.Template.Spec.Containers[0].Args, fmt.Sprintf("--kubeconfig=%s/kubeconfig", kubeconfigPath)) + deployment.Spec.Template.Spec.Containers[0].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ + Name: "kubeconfig", + ReadOnly: true, + MountPath: kubeconfigPath, + }) + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: "kubeconfig", + VolumeSource: v1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: config.Spec.Kubeconfig.Name, + Items: []corev1.KeyToPath{ + { + Key: config.Spec.Kubeconfig.Key, + Path: "kubeconfig", + }, + }, + }, + }, + }) + } if config.Spec.AI.Secret != nil { password := corev1.EnvVar{ Name: "K8SGPT_PASSWORD", @@ -338,35 +368,39 @@ func Sync(ctx context.Context, c client.Client, var objs []client.Object - svc, er := GetService(config) - if er != nil { - return er - } + outOfClusterMode := config.Spec.Kubeconfig != nil - objs = append(objs, svc) + if !outOfClusterMode { + svcAcc, er := GetServiceAccount(config) + if er != nil { + return er + } - svcAcc, er := GetServiceAccount(config) - if er != nil { - return er - } + objs = append(objs, svcAcc) - objs = append(objs, svcAcc) + clusterRole, er := GetClusterRole(config) + if er != nil { + return er + } - clusterRole, er := GetClusterRole(config) - if er != nil { - return er - } + objs = append(objs, clusterRole) + + clusterRoleBinding, er := GetClusterRoleBinding(config) + if er != nil { + return er + } - objs = append(objs, clusterRole) + objs = append(objs, clusterRoleBinding) + } - clusterRoleBinding, er := GetClusterRoleBinding(config) + svc, er := GetService(config) if er != nil { return er } - objs = append(objs, clusterRoleBinding) + objs = append(objs, svc) - deployment, er := GetDeployment(config) + deployment, er := GetDeployment(config, outOfClusterMode) if er != nil { return er }