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 }