github.com/metaprov/modela-operator@v0.0.0-20240118193048-f378be8b74d2/controllers/components/vault.go (about)

     1  package components
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	managementv1 "github.com/metaprov/modela-operator/api/v1alpha1"
     7  	"github.com/metaprov/modela-operator/pkg/helm"
     8  	"github.com/metaprov/modela-operator/pkg/kube"
     9  	"github.com/metaprov/modela-operator/pkg/vault"
    10  	"github.com/pkg/errors"
    11  	k8serr "k8s.io/apimachinery/pkg/api/errors"
    12  	"k8s.io/klog/v2"
    13  	"os"
    14  	"sigs.k8s.io/controller-runtime/pkg/log"
    15  	"time"
    16  
    17  	"github.com/hashicorp/vault/api"
    18  )
    19  
    20  const PolicyTemplate = `
    21  path "%s/*" {
    22    capabilities = ["create", "read", "update", "patch", "delete", "list"]
    23  }
    24  `
    25  
    26  // Modela system represent the model core system
    27  type Vault struct {
    28  	Namespace     string
    29  	Name          string
    30  	ReleaseName   string
    31  	PodNamePrefix string
    32  }
    33  
    34  func NewVault() *Vault {
    35  	return &Vault{
    36  		Namespace:     "modela-system",
    37  		Name:          "vault",
    38  		ReleaseName:   "modela-vault",
    39  		PodNamePrefix: "vault",
    40  	}
    41  }
    42  
    43  func (v Vault) GetInstallPhase() managementv1.ModelaPhase {
    44  	return managementv1.ModelaPhaseInstallingVault
    45  }
    46  
    47  func (v Vault) IsEnabled(modela managementv1.Modela) bool {
    48  	return modela.Spec.Vault.Install
    49  }
    50  
    51  func (v Vault) Installed(ctx context.Context) (bool, error) {
    52  	if belonging, err := kube.IsStatefulSetCreatedByModela(v.Namespace, "vault"); err == nil && !belonging {
    53  		return true, managementv1.ComponentNotInstalledByModelaError
    54  	}
    55  
    56  	if installed, err := helm.IsChartInstalled(ctx, v.Name, v.Namespace, v.ReleaseName); !installed {
    57  		return false, err
    58  	}
    59  
    60  	return true, nil
    61  }
    62  
    63  func (v Vault) Install(ctx context.Context, modela *managementv1.Modela) error {
    64  	logger := log.FromContext(ctx)
    65  
    66  	if err := kube.CreateNamespace(v.Namespace, modela.Name); err != nil && !k8serr.IsAlreadyExists(err) {
    67  		logger.Error(err, "failed to create namespace")
    68  		return err
    69  	}
    70  
    71  	logger.Info("Applying Vault Helm Chart")
    72  	values := modela.Spec.Vault.Values.Object
    73  	if values == nil {
    74  		values = make(map[string]interface{})
    75  		if _, ok := values["injector"]; !ok { // Disable the agent injector by default
    76  			injectorValues := make(map[string]interface{})
    77  			injectorValues["enabled"] = false
    78  			values["injector"] = injectorValues
    79  		}
    80  	}
    81  
    82  	return helm.InstallChart(ctx, v.Name, v.Namespace, v.ReleaseName, values)
    83  }
    84  
    85  func (v Vault) ConfigureVault(ctx context.Context, modela *managementv1.Modela) error {
    86  	client, err := vault.GetUnauthenticatedClient(modela)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	sys := client.Sys()
    92  	initialized, err := sys.InitStatus()
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	if !initialized {
    98  		initResponse, err := sys.Init(&api.InitRequest{
    99  			SecretShares:    1,
   100  			SecretThreshold: 1,
   101  		})
   102  
   103  		if err != nil {
   104  			return errors.Wrap(err, "Failed to initialize Vault server")
   105  		}
   106  
   107  		if err := kube.CreateOrUpdateSecret("modela-system", "vault-keys", map[string]string{
   108  			"key": initResponse.Keys[0],
   109  		}); err != nil {
   110  			return errors.Wrap(err, "Failed to create Vault keys secret")
   111  		}
   112  
   113  		if err := kube.CreateOrUpdateSecret("modela-system", "vault-root-token", map[string]string{
   114  			"token": initResponse.RootToken,
   115  		}); err != nil {
   116  			return errors.Wrap(err, "Failed to create Vault keys secret")
   117  		}
   118  
   119  		// Unseal the vault
   120  		if _, err := sys.Unseal(initResponse.Keys[0]); err != nil {
   121  			return errors.Wrap(err, "Failed to unseal Vault")
   122  		}
   123  
   124  		client.SetToken(initResponse.RootToken)
   125  
   126  		// Mount the KVv2 secret engine
   127  		if err := sys.Mount(modela.Spec.Vault.MountPath, &api.MountInput{
   128  			Type:    "kv",
   129  			Options: map[string]string{"version": "2"},
   130  		}); err != nil {
   131  			return errors.Wrap(err, "Failed to mount secret engine")
   132  		}
   133  
   134  		// Create the policy for the mount
   135  		var policy = fmt.Sprintf(PolicyTemplate, modela.Spec.Vault.MountPath)
   136  		if err := sys.PutPolicy("modela-policy", policy); err != nil {
   137  			return errors.Wrap(err, "Failed to create policy")
   138  		}
   139  
   140  		// Configure Kubernetes authentication
   141  		if err := sys.EnableAuthWithOptions("kubernetes", &api.EnableAuthOptions{Type: "kubernetes"}); err != nil {
   142  			return errors.Wrap(err, "Failed to enable Kubernetes authentication")
   143  		}
   144  
   145  		c := client.Logical()
   146  		if _, err := c.Write("/auth/kubernetes/config", map[string]interface{}{
   147  			"kubernetes_host": "https://kubernetes.default.svc",
   148  		}); err != nil {
   149  			return errors.Wrap(err, "Failed to configure Kubernetes authentication")
   150  		}
   151  
   152  		// Configure the Kubernetes auth method role
   153  		if _, err := c.Write("/auth/kubernetes/role/modela", map[string]interface{}{
   154  			"name": "modela",
   155  			"bound_service_account_names": []string{"lab-job-sa", "modela-api-gateway", "modela-data-plane",
   156  				"modela-control-plane", "servingsite-job-sa", "modela-operator-controller-manager"},
   157  			"bound_service_account_namespaces": []string{"*"},
   158  			"policies":                         []string{"modela-policy"},
   159  		}); err != nil {
   160  			return errors.Wrap(err, "Failed to configure Kubernetes authentication roles")
   161  		}
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  // Check if we are still installing the database
   168  func (v Vault) Installing(ctx context.Context) (bool, error) {
   169  	installed, err := v.Installed(ctx)
   170  	if !installed {
   171  		return installed, err
   172  	}
   173  	running, err := kube.IsPodRunning(v.Namespace, v.PodNamePrefix)
   174  	if err != nil {
   175  		return false, err
   176  	}
   177  	return !running, nil
   178  }
   179  
   180  func (v Vault) Ready(ctx context.Context) (bool, error) {
   181  	installing, err := v.Installing(ctx)
   182  	if err != nil && err != managementv1.ComponentNotInstalledByModelaError {
   183  		return false, err
   184  	}
   185  
   186  	return !installing, nil
   187  }
   188  
   189  func (v Vault) Uninstall(ctx context.Context, modela *managementv1.Modela) error {
   190  	return helm.UninstallChart(ctx, v.Name, v.Namespace, v.ReleaseName, map[string]interface{}{})
   191  }
   192  
   193  func performAutoUnseal() {
   194  	// Check if we are running inside the cluster. If not, abort as we have no way to communicate with Vault
   195  	if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); errors.Is(err, os.ErrNotExist) {
   196  		return
   197  	}
   198  
   199  	// Check if we have the Vault keys
   200  	if exists, err := kube.IsNamespaceCreated("modela-system"); !exists || err != nil {
   201  		return
   202  	}
   203  
   204  	secret, err := kube.GetSecret("modela-system", "vault-keys")
   205  	if err != nil {
   206  		return
   207  	}
   208  
   209  	key, ok := secret.Data["key"]
   210  	if !ok {
   211  		return
   212  	}
   213  
   214  	client, err := vault.GetUnauthenticatedClientInCluster()
   215  	if err != nil {
   216  		return
   217  	}
   218  
   219  	sys := client.Sys()
   220  	sealed, err := sys.SealStatus()
   221  	if err != nil {
   222  		klog.ErrorS(err, "Failed to check if Vault is sealed")
   223  		return
   224  	}
   225  
   226  	if sealed.Sealed {
   227  		klog.Info("Attempting to unseal Vault server")
   228  		_, _ = sys.Unseal(string(key))
   229  	}
   230  }
   231  
   232  func StartAutoUnseal(ctx context.Context) {
   233  	for {
   234  		select {
   235  		case <-ctx.Done():
   236  			return
   237  		case <-time.After(5 * time.Second):
   238  			performAutoUnseal()
   239  		}
   240  	}
   241  }