mirror of https://github.com/portainer/portainer
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
8.5 KiB
240 lines
8.5 KiB
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
|
"github.com/rs/zerolog/log"
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
const (
|
|
labelPortainerKubeConfigOwner = "io.portainer.kubernetes.configuration.owner"
|
|
labelPortainerKubeConfigOwnerId = "io.portainer.kubernetes.configuration.owner.id"
|
|
)
|
|
|
|
// GetSecrets gets all the Secrets for a given namespace in a k8s endpoint.
|
|
// if the user is an admin, all secrets in the current k8s environment(endpoint) are fetched using the getSecrets function.
|
|
// otherwise, namespaces the non-admin user has access to will be used to filter the secrets based on the allowed namespaces.
|
|
func (kcl *KubeClient) GetSecrets(namespace string) ([]models.K8sSecret, error) {
|
|
if kcl.IsKubeAdmin {
|
|
return kcl.getSecrets(namespace)
|
|
}
|
|
return kcl.getSecretsForNonAdmin(namespace)
|
|
}
|
|
|
|
// getSecretsForNonAdmin fetches the secrets in the namespaces the user has access to.
|
|
// This function is called when the user is not an admin.
|
|
func (kcl *KubeClient) getSecretsForNonAdmin(namespace string) ([]models.K8sSecret, error) {
|
|
log.Debug().Msgf("Fetching volumes for non-admin user: %v", kcl.NonAdminNamespaces)
|
|
|
|
if len(kcl.NonAdminNamespaces) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
secrets, err := kcl.getSecrets(namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
|
results := make([]models.K8sSecret, 0)
|
|
for _, secret := range secrets {
|
|
if _, ok := nonAdminNamespaceSet[secret.Namespace]; ok {
|
|
results = append(results, secret)
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// getSecrets gets all the Secrets for a given namespace in a k8s endpoint.
|
|
// the result is a list of secrets parsed into a K8sSecret struct.
|
|
func (kcl *KubeClient) getSecrets(namespace string) ([]models.K8sSecret, error) {
|
|
secrets, err := kcl.cli.CoreV1().Secrets(namespace).List(context.Background(), metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := []models.K8sSecret{}
|
|
for _, secret := range secrets.Items {
|
|
results = append(results, parseSecret(&secret, false))
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// GetSecret gets a Secret by name for a given namespace.
|
|
// the result is a secret parsed into a K8sSecret struct.
|
|
func (kcl *KubeClient) GetSecret(namespace string, secretName string) (models.K8sSecret, error) {
|
|
secret, err := kcl.cli.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return models.K8sSecret{}, err
|
|
}
|
|
|
|
return parseSecret(secret, true), nil
|
|
}
|
|
|
|
// parseSecret parses a k8s Secret object into a K8sSecret struct.
|
|
// for get operation, withData will be set to true.
|
|
// otherwise, only metadata will be parsed.
|
|
func parseSecret(secret *corev1.Secret, withData bool) models.K8sSecret {
|
|
result := models.K8sSecret{
|
|
K8sConfiguration: models.K8sConfiguration{
|
|
UID: string(secret.UID),
|
|
Name: secret.Name,
|
|
Namespace: secret.Namespace,
|
|
CreationDate: secret.CreationTimestamp.Time.UTC().Format(time.RFC3339),
|
|
Annotations: secret.Annotations,
|
|
Labels: secret.Labels,
|
|
ConfigurationOwner: secret.Labels[labelPortainerKubeConfigOwner],
|
|
ConfigurationOwnerId: secret.Labels[labelPortainerKubeConfigOwnerId],
|
|
},
|
|
SecretType: string(secret.Type),
|
|
}
|
|
|
|
if withData {
|
|
secretData := secret.Data
|
|
secretDataMap := make(map[string]string, len(secretData))
|
|
for key, value := range secretData {
|
|
secretDataMap[key] = string(value)
|
|
}
|
|
|
|
result.Data = secretDataMap
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// CombineSecretsWithApplications combines the secrets with the applications that use them.
|
|
// the function fetches all the pods and replica sets in the cluster and checks if the secret is used by any of the pods.
|
|
// if the secret is used by a pod, the application that uses the pod is added to the secret.
|
|
// otherwise, the secret is returned as is.
|
|
func (kcl *KubeClient) CombineSecretsWithApplications(secrets []models.K8sSecret) ([]models.K8sSecret, error) {
|
|
updatedSecrets := make([]models.K8sSecret, len(secrets))
|
|
|
|
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
|
}
|
|
|
|
for index, secret := range secrets {
|
|
updatedSecret := secret
|
|
|
|
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromSecret(secret, pods, replicaSets)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to get applications from secret. Error: %w", err)
|
|
}
|
|
|
|
if len(applicationConfigurationOwners) > 0 {
|
|
updatedSecret.ConfigurationOwnerResources = applicationConfigurationOwners
|
|
}
|
|
|
|
updatedSecrets[index] = updatedSecret
|
|
}
|
|
|
|
return updatedSecrets, nil
|
|
}
|
|
|
|
// CombineSecretWithApplications combines the secret with the applications that use it.
|
|
// the function fetches all the pods in the cluster and checks if the secret is used by any of the pods.
|
|
// it needs to check if the pods are owned by a replica set to determine if the pod is part of a deployment.
|
|
func (kcl *KubeClient) CombineSecretWithApplications(secret models.K8sSecret) (models.K8sSecret, error) {
|
|
pods, err := kcl.cli.CoreV1().Pods(secret.Namespace).List(context.Background(), metav1.ListOptions{})
|
|
if err != nil {
|
|
return models.K8sSecret{}, fmt.Errorf("an error occurred during the CombineSecretWithApplications operation, unable to get pods. Error: %w", err)
|
|
}
|
|
|
|
containsReplicaSetOwner := false
|
|
for _, pod := range pods.Items {
|
|
containsReplicaSetOwner = isReplicaSetOwner(pod)
|
|
break
|
|
}
|
|
|
|
if containsReplicaSetOwner {
|
|
replicaSets, err := kcl.cli.AppsV1().ReplicaSets(secret.Namespace).List(context.Background(), metav1.ListOptions{})
|
|
if err != nil {
|
|
return models.K8sSecret{}, fmt.Errorf("an error occurred during the CombineSecretWithApplications operation, unable to get replica sets. Error: %w", err)
|
|
}
|
|
|
|
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromSecret(secret, pods.Items, replicaSets.Items)
|
|
if err != nil {
|
|
return models.K8sSecret{}, fmt.Errorf("an error occurred during the CombineSecretWithApplications operation, unable to get applications from secret. Error: %w", err)
|
|
}
|
|
|
|
if len(applicationConfigurationOwners) > 0 {
|
|
secret.ConfigurationOwnerResources = applicationConfigurationOwners
|
|
}
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) error {
|
|
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
|
|
|
|
serviceAccountSecret := &corev1.Secret{
|
|
TypeMeta: metav1.TypeMeta{},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: serviceAccountSecretName,
|
|
Annotations: map[string]string{
|
|
"kubernetes.io/service-account.name": serviceAccountName,
|
|
},
|
|
},
|
|
Type: "kubernetes.io/service-account-token",
|
|
}
|
|
|
|
_, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Create(context.TODO(), serviceAccountSecret, metav1.CreateOptions{})
|
|
if err != nil && !k8serrors.IsAlreadyExists(err) {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (kcl *KubeClient) getServiceAccountToken(serviceAccountName string) (string, error) {
|
|
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
|
|
|
|
secret, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Get(context.TODO(), serviceAccountSecretName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// API token secret is populated asynchronously.
|
|
// Is it created by the controller and will depend on the environment(endpoint)/secret-store:
|
|
// https://github.com/kubernetes/kubernetes/issues/67882#issuecomment-422026204
|
|
// as a work-around, we wait for up to 5 seconds for the secret to be populated.
|
|
timeout := time.After(5 * time.Second)
|
|
searchingForSecret := true
|
|
for searchingForSecret {
|
|
select {
|
|
case <-timeout:
|
|
return "", errors.New("unable to find secret token associated to user service account (timeout)")
|
|
default:
|
|
secret, err = kcl.cli.CoreV1().Secrets(portainerNamespace).Get(context.TODO(), serviceAccountSecretName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(secret.Data) > 0 {
|
|
searchingForSecret = false
|
|
break
|
|
}
|
|
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
secretTokenData, ok := secret.Data["token"]
|
|
if ok {
|
|
return string(secretTokenData), nil
|
|
}
|
|
|
|
return "", errors.New("unable to find secret token associated to user service account")
|
|
}
|