github.com/metaprov/modela-operator@v0.0.0-20240118193048-f378be8b74d2/controllers/components/modelasystem.go (about) 1 package components 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "github.com/Masterminds/goutils" 8 managementv1 "github.com/metaprov/modela-operator/api/v1alpha1" 9 "github.com/metaprov/modela-operator/pkg/kube" 10 "github.com/metaprov/modela-operator/pkg/vault" 11 infra "github.com/metaprov/modelaapi/pkg/apis/infra/v1alpha1" 12 "golang.org/x/mod/semver" 13 "io/ioutil" 14 v1 "k8s.io/api/core/v1" 15 k8serr "k8s.io/apimachinery/pkg/api/errors" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "net/http" 18 "sigs.k8s.io/controller-runtime/pkg/log" 19 "sigs.k8s.io/kustomize/kyaml/kio" 20 ) 21 22 // ModelaSystem represents an installation of the Modela core system (control plane, API gateway, etc.) 23 type ModelaSystem struct { 24 ModelaVersion string 25 Namespace string 26 CatalogNamespace string 27 SystemManifestPath string 28 CatalogManifestPath string 29 CrdUrl string 30 VersionMatrixUrl string 31 PodNamePrefix string 32 } 33 34 func (m ModelaSystem) GetInstallPhase() managementv1.ModelaPhase { 35 return managementv1.ModelaPhaseInstallingModela 36 } 37 38 func (m ModelaSystem) IsEnabled(_ managementv1.Modela) bool { 39 return true 40 } 41 42 func NewModelaSystem(version string) *ModelaSystem { 43 return &ModelaSystem{ 44 ModelaVersion: version, 45 Namespace: "modela-system", 46 CatalogNamespace: "modela-catalog", 47 SystemManifestPath: "modela-system", 48 CatalogManifestPath: "modela-catalog", 49 CrdUrl: "assets/crds/manifests/%s/base/crd", 50 VersionMatrixUrl: "https://raw.githubusercontent.com/metaprov/modelaapi/main/version_matrix.json", 51 PodNamePrefix: "modela-control-plane", 52 } 53 } 54 55 func (ms ModelaSystem) Installed(ctx context.Context) (bool, error) { 56 if created, err := kube.IsNamespaceCreated("modela-system"); !created || err != nil { 57 return created, err 58 } 59 if resc, missing, err := kube.LoadResources(ms.SystemManifestPath, []kio.Filter{kube.SkipCertManagerFilter{}}, false); missing > 0 { 60 log.FromContext(ctx).Info("Resources detected as missing from the modela-system namespace", "count", missing) 61 fmt.Println(string(resc)) 62 return false, managementv1.ComponentMissingResourcesError 63 } else if err != nil { 64 return false, err 65 } 66 return true, nil 67 } 68 69 func (ms ModelaSystem) CatalogInstalled(ctx context.Context) (bool, error) { 70 if created, err := kube.IsNamespaceCreated("modela-catalog"); !created || err != nil { 71 return created, err 72 } 73 if _, missing, err := kube.LoadResources(ms.CatalogManifestPath, nil, false); missing > 0 { 74 log.FromContext(ctx).Info("Resources detected as missing from the modela-catalog namespace", "count", missing) 75 return false, managementv1.ComponentMissingResourcesError 76 } else if err != nil { 77 return false, err 78 } 79 return true, nil 80 } 81 82 func (ms ModelaSystem) InstallCRD(ctx context.Context, modela *managementv1.Modela) error { 83 logger := log.FromContext(ctx) 84 85 // Download the version matrix, which associates a minimum Modela version for each API version 86 resp, err := http.Get(ms.VersionMatrixUrl) 87 if err != nil { 88 return err 89 } 90 defer resp.Body.Close() 91 data, _ := ioutil.ReadAll(resp.Body) 92 93 var jsonData interface{} 94 if err := json.Unmarshal(data, &jsonData); err != nil { 95 return err 96 } 97 98 // Determine the required version based on the version closest to the Modela version 99 versionData := jsonData.(map[string]interface{}) 100 101 var versions []string 102 for version, _ := range versionData { 103 versions = append(versions, version) 104 } 105 semver.Sort(versions) 106 107 var finalVersion string 108 for _, version := range versions { 109 if semver.Compare(ms.ModelaVersion, version) >= 0 { 110 finalVersion = versionData[version].(string) 111 } 112 } 113 114 if ms.ModelaVersion == "develop" || ms.ModelaVersion == "stable" { 115 finalVersion = "v1alpha1" 116 } 117 118 // Check if the version is already installed 119 if version, _ := kube.GetCRDVersion("tenants.infra.modela.ai"); version == finalVersion { 120 logger.Info(fmt.Sprintf("CRD version %s already installed; skipping CRD installation", finalVersion)) 121 return nil 122 } 123 124 // Install the determined CRD version using Kustomize 125 logger.Info(fmt.Sprintf("Installing CRD version %s", finalVersion)) 126 return kube.ApplyUrlKustomize(fmt.Sprintf(ms.CrdUrl, finalVersion)) 127 } 128 129 func (ms ModelaSystem) InstallManagedImages(ctx context.Context, modela *managementv1.Modela) error { 130 logger := log.FromContext(ctx) 131 132 yaml, _, err := kube.LoadResources(ms.CatalogManifestPath+"/managedimages", []kio.Filter{ 133 kube.LabelFilter{Labels: map[string]string{"management.modela.ai/operator": modela.Name}}, 134 kube.ManagedImageFilter{Version: ms.ModelaVersion}, 135 }, true) 136 if err != nil { 137 return err 138 } 139 140 logger.Info("Applying modela-catalog ManagedImage resources", "length", len(yaml)) 141 if err := kube.ApplyYaml(string(yaml)); err != nil { 142 return err 143 } 144 145 return nil 146 } 147 148 func (ms ModelaSystem) InstallLicense(ctx context.Context, modela *managementv1.Modela) error { 149 logger := log.FromContext(ctx) 150 151 if modela.Spec.License.LinkLicense == nil { 152 return nil 153 } 154 155 if err := kube.CreateOrUpdateSecret("modela-system", "license-secret", map[string]string{ 156 "token": *modela.Spec.License.LicenseKey, 157 }); err != nil { 158 logger.Error(err, "Failed to update license secret") 159 return err 160 } 161 162 now := metav1.Now() 163 if err := kube.CreateOrUpdateLicense("modela-system", "modela-license", &infra.License{ 164 ObjectMeta: metav1.ObjectMeta{ 165 Name: "modela-license", 166 Namespace: "modela-system", 167 }, 168 Spec: infra.LicenseSpec{ 169 SecretRef: v1.SecretReference{ 170 Namespace: "modela-system", 171 Name: "license-secret", 172 }, 173 }, 174 Status: infra.LicenseStatus{ 175 LastUpdated: &now, 176 }, 177 }); err != nil { 178 logger.Error(err, "Failed to update license object") 179 return err 180 } 181 182 return nil 183 } 184 185 func (ms ModelaSystem) InstallCatalog(ctx context.Context, modela *managementv1.Modela) error { 186 logger := log.FromContext(ctx) 187 188 yaml, _, err := kube.LoadResources(ms.CatalogManifestPath, []kio.Filter{ 189 kube.LabelFilter{Labels: map[string]string{"management.modela.ai/operator": modela.Name}}, 190 }, false) 191 if err != nil { 192 return err 193 } 194 195 if err := kube.CreateNamespace(ms.CatalogNamespace, modela.Name); err != nil && !k8serr.IsAlreadyExists(err) { 196 logger.Error(err, "failed to create namespace") 197 return err 198 } 199 200 logger.Info("Applying modela-catalog resources", "length", len(yaml)) 201 if err := kube.ApplyYaml(string(yaml)); err != nil { 202 return err 203 } 204 205 if err := ms.InstallLicense(ctx, modela); err != nil { 206 return err 207 } 208 return ms.InstallManagedImages(ctx, modela) 209 } 210 211 func (ms ModelaSystem) Install(ctx context.Context, modela *managementv1.Modela) error { 212 logger := log.FromContext(ctx) 213 if err := ms.InstallCRD(ctx, modela); err != nil { 214 return err 215 } 216 217 if err := kube.CreateNamespace(ms.Namespace, modela.Name); err != nil && !k8serr.IsAlreadyExists(err) { 218 logger.Error(err, "failed to create namespace") 219 return err 220 } 221 222 var vaultAddress string 223 if modela.Spec.Vault.VaultAddress == nil || *modela.Spec.Vault.VaultAddress == "" { 224 vaultAddress = "http://modela-vault.modela-system.svc.cluster.local:8200" 225 } else { 226 vaultAddress = *modela.Spec.Vault.VaultAddress 227 } 228 229 yaml, _, err := kube.LoadResources(ms.SystemManifestPath, []kio.Filter{ 230 kube.SkipCertManagerFilter{}, 231 kube.ModelaConfigFilter{VaultAddress: vaultAddress, VaultMountPath: modela.Spec.Vault.MountPath}, 232 kube.ContainerVersionFilter{Version: ms.ModelaVersion}, 233 kube.LabelFilter{Labels: map[string]string{"management.modela.ai/operator": modela.Name}}, 234 kube.OwnerReferenceFilter{Owner: modela.GetName(), OwnerNamespace: modela.GetNamespace(), UID: string(modela.GetUID())}, 235 }, false) 236 if err != nil { 237 return err 238 } 239 240 logger.Info("Applying modela-system resources", "length", len(yaml)) 241 if err := kube.ApplyYaml(string(yaml)); err != nil { 242 return err 243 } 244 245 modela.Status.InstalledVersion = ms.ModelaVersion 246 token, _ := goutils.RandomAlphaNumeric(32) 247 if err != nil { 248 return err 249 } 250 return vault.ApplySecret(modela, "jwt-secret", map[string]interface{}{"token": token}) 251 } 252 253 func (ms ModelaSystem) InstallNewVersion(ctx context.Context, modela *managementv1.Modela) error { 254 logger := log.FromContext(ctx) 255 256 yaml, _, err := kube.LoadResources(ms.SystemManifestPath, []kio.Filter{ 257 kube.ContainerVersionFilter{Version: ms.ModelaVersion}, 258 kube.LabelFilter{Labels: map[string]string{"management.modela.ai/operator": modela.Name}}, 259 kube.JwtSecretFilter{}, 260 kube.OwnerReferenceFilter{Owner: modela.GetName(), OwnerNamespace: modela.GetNamespace(), UID: string(modela.GetUID())}, 261 }, true) 262 if err != nil { 263 return err 264 } 265 266 logger.Info("Applying modela-system resources", "length", len(yaml)) 267 if err := kube.ApplyYaml(string(yaml)); err != nil { 268 return err 269 } 270 return ms.InstallManagedImages(ctx, modela) 271 } 272 273 func (ms ModelaSystem) Installing(ctx context.Context) (bool, error) { 274 installed, err := ms.Installed(ctx) 275 if !installed { 276 return installed, err 277 } 278 running, err := kube.IsPodRunning(ms.Namespace, ms.PodNamePrefix) 279 if err != nil { 280 return false, err 281 } 282 return !running, nil 283 } 284 285 func (ms ModelaSystem) Ready(ctx context.Context) (bool, error) { 286 installing, err := ms.Installing(ctx) 287 if err != nil { 288 return false, err 289 } else if err == managementv1.ComponentMissingResourcesError { 290 return false, nil 291 } 292 return !installing, nil 293 } 294 295 func (ms ModelaSystem) Uninstall(ctx context.Context, modela *managementv1.Modela) error { 296 if created, err := kube.IsNamespaceCreatedByOperator(ms.Namespace, modela.Name); !created { 297 return managementv1.ComponentNotInstalledByModelaError 298 } else if err != nil { 299 return err 300 } 301 302 if err := kube.DeleteNamespace(ms.Namespace); err != nil { 303 return err 304 } 305 306 return nil 307 }