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  }