diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 33b13a06b..444059a68 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -17,6 +17,7 @@ import ( "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" + "github.com/portainer/portainer/api/internal/endpointutils" ) type endpointCreatePayload struct { @@ -244,6 +245,22 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * for _, stackID := range relatedEdgeStacks { relationObject.EdgeStacks[stackID] = true } + } else if endpointutils.IsKubernetesEndpoint(endpoint) { + endpointutils.InitialIngressClassDetection( + endpoint, + handler.DataStore.Endpoint(), + handler.K8sClientFactory, + ) + endpointutils.InitialMetricsDetection( + endpoint, + handler.DataStore.Endpoint(), + handler.K8sClientFactory, + ) + endpointutils.InitialStorageDetection( + endpoint, + handler.DataStore.Endpoint(), + handler.K8sClientFactory, + ) } err = handler.DataStore.EndpointRelation().Create(relationObject) diff --git a/api/http/models/kubernetes/metrics.go b/api/http/models/kubernetes/metrics.go new file mode 100644 index 000000000..5d095cf84 --- /dev/null +++ b/api/http/models/kubernetes/metrics.go @@ -0,0 +1,13 @@ +package kubernetes + +type K8sMetrics struct { + Resources []K8sMetricsResources `json:"resources"` +} + +type K8sMetricsResources struct { + Kind string `json:"Kind,omitempty"` + Name string `json:"Name,omitempty"` + Namespaced bool `json:"Namespaced,omitempty"` + SingularName string `json:"SingularName,omitempty"` + Verbs []string `json:"Verbs,omitempty"` +} diff --git a/api/internal/endpointutils/endpointutils.go b/api/internal/endpointutils/endpointutils.go index 356a600e6..8d0cdaa0e 100644 --- a/api/internal/endpointutils/endpointutils.go +++ b/api/internal/endpointutils/endpointutils.go @@ -4,6 +4,9 @@ import ( "strings" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/kubernetes/cli" + log "github.com/rs/zerolog/log" ) // IsLocalEndpoint returns true if this is a local environment(endpoint) @@ -69,3 +72,79 @@ func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bo return set } + +func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) { + cli, err := factory.GetKubeClient(endpoint) + if err != nil { + log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection") + return + } + controllers, err := cli.GetIngressControllers() + if err != nil { + log.Debug().Err(err).Msg("failed to fetch ingressclasses") + return + } + + var updatedClasses []portainer.KubernetesIngressClassConfig + for i := range controllers { + var updatedClass portainer.KubernetesIngressClassConfig + updatedClass.Name = controllers[i].ClassName + updatedClass.Type = controllers[i].Type + updatedClasses = append(updatedClasses, updatedClass) + } + + endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses + err = endpointService.UpdateEndpoint( + portainer.EndpointID(endpoint.ID), + endpoint, + ) + if err != nil { + log.Debug().Err(err).Msg("unable to store found IngressClasses inside the database") + return + } +} + +func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) { + cli, err := factory.GetKubeClient(endpoint) + if err != nil { + log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection") + return + } + _, err = cli.GetMetrics() + if err != nil { + log.Debug().Err(err).Msg("unable to fetch metrics: leaving metrics collection disabled.") + return + } + endpoint.Kubernetes.Configuration.UseServerMetrics = true + err = endpointService.UpdateEndpoint( + portainer.EndpointID(endpoint.ID), + endpoint, + ) + if err != nil { + log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database") + return + } +} + +func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) { + cli, err := factory.GetKubeClient(endpoint) + if err != nil { + log.Debug().Err(err).Msg("unable to create Kubernetes client for initial storage detection") + return + } + + storage, err := cli.GetStorage() + if err != nil { + log.Debug().Err(err).Msg("unable to fetch storage classes: leaving storage classes disabled") + return + } + endpoint.Kubernetes.Configuration.StorageClasses = storage + err = endpointService.UpdateEndpoint( + portainer.EndpointID(endpoint.ID), + endpoint, + ) + if err != nil { + log.Debug().Err(err).Msg("unable to enable storage class inside the database") + return + } +} diff --git a/api/kubernetes/cli/metrics.go b/api/kubernetes/cli/metrics.go new file mode 100644 index 000000000..669ce27b3 --- /dev/null +++ b/api/kubernetes/cli/metrics.go @@ -0,0 +1,19 @@ +package cli + +import ( + "context" + "encoding/json" + + models "github.com/portainer/portainer/api/http/models/kubernetes" +) + +func (kcl *KubeClient) GetMetrics() (models.K8sMetrics, error) { + var metrics models.K8sMetrics + resp, err := kcl.cli.CoreV1().RESTClient().Get().AbsPath("apis/metrics.k8s.io/v1beta1/nodes").DoRaw(context.Background()) + if err != nil { + return metrics, err + } + + err = json.Unmarshal(resp, &metrics) + return metrics, err +} diff --git a/api/kubernetes/cli/storage.go b/api/kubernetes/cli/storage.go new file mode 100644 index 000000000..4b8ca7e85 --- /dev/null +++ b/api/kubernetes/cli/storage.go @@ -0,0 +1,37 @@ +package cli + +import ( + "context" + + portainer "github.com/portainer/portainer/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (kcl *KubeClient) GetStorage() ([]portainer.KubernetesStorageClassConfig, error) { + var storages []portainer.KubernetesStorageClassConfig + + storageClient := kcl.cli.StorageV1().StorageClasses() + storageList, err := storageClient.List(context.Background(), metav1.ListOptions{}) + if err != nil { + return storages, err + } + + for _, s := range storageList.Items { + var storage portainer.KubernetesStorageClassConfig + + v, ok := s.Annotations["storageclass.kubernetes.io/is-default-class"] + if !ok || v != "true" { + continue + } + storage.Name = s.Name + storage.Provisioner = s.Provisioner + storage.AccessModes = []string{"RWO"} + if s.AllowVolumeExpansion != nil { + storage.AllowVolumeExpansion = *s.AllowVolumeExpansion + } + + storages = append(storages, storage) + } + + return storages, nil +} diff --git a/api/portainer.go b/api/portainer.go index 2b5ce55b0..773d5662e 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1375,6 +1375,8 @@ type ( DeleteNamespace(namespace string) error GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) GetIngressControllers() (models.K8sIngressControllers, error) + GetMetrics() (models.K8sMetrics, error) + GetStorage() ([]KubernetesStorageClassConfig, error) CreateIngress(namespace string, info models.K8sIngressInfo) error UpdateIngress(namespace string, info models.K8sIngressInfo) error GetIngresses(namespace string) ([]models.K8sIngressInfo, error) diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index fb2bce797..d49a3a88e 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -225,10 +225,8 @@ class KubernetesConfigureController { } }); await Promise.all(storagePromises); - + this.$state.reload(); this.Notifications.success('Success', 'Configuration successfully applied'); - - this.$state.go('portainer.home'); } catch (err) { this.Notifications.error('Failure', err, 'Unable to apply configuration'); } finally {