Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[minor] Add support for various SLS configurations #1438

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Binary file added image/cli/install/ibm-mas_devops.tar.gz
Binary file not shown.
Binary file added image/cli/install/mas_devops.tar.gz
Binary file not shown.
77 changes: 72 additions & 5 deletions python/src/mas/cli/install/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@
WorkspaceNameFormatValidator,
TimeoutFormatValidator,
StorageClassValidator,
OptimizerInstallPlanValidator
OptimizerInstallPlanValidator,
SLSConfigValidator,
NewNamespaceValidator
)

from mas.devops.ocp import createNamespace, getStorageClasses
from mas.devops.mas import getCurrentCatalog, getDefaultStorageClasses
from mas.devops.sls import listSLSInstances
from mas.devops.data import getCatalog
from mas.devops.tekton import (
installOpenShiftPipelines,
Expand Down Expand Up @@ -252,13 +255,70 @@ def configCatalog(self):
@logMethodCall
def configSLS(self) -> None:
self.printH1("Configure Product License")
self.slsLicenseFileLocal = self.promptForFile("License file", mustExist=True, envVar="SLS_LICENSE_FILE_LOCAL")

if self.showAdvancedOptions:
self.slsLicenseFileLocal = None
self.slsCertsDir = None
self.existingSLSInstances = listSLSInstances(self.dynamicClient)
self.slsConfigOptions = []
self.slsInstanceOptions = []

numSLSInstances = len(self.existingSLSInstances)

description = [
"IBM Suite License Service (SLS) is the licensing enforcement for Maximo Application Suite. Choose how to configure SLS:",
" - New: Deploy a new instance on the cluster.",
" - External: Point to an external instance outside of the cluster",
""
]

self.slsConfigOptions.append("New")
self.slsConfigOptions.append("External")

if numSLSInstances > 0:
self.slsConfigOptions.insert(1, "Existing")
description.insert(2, " - Existing: Select an existing instance on the cluster. This is useful for sharing SLS with multiple MAS instances.")

slsConfigCompleter = WordCompleter(self.slsConfigOptions)

self.printDescription(description)
slsConfigSelection = self.promptForString("Select SLS config option", completer=slsConfigCompleter, validator=SLSConfigValidator())

if slsConfigSelection == "New":
if numSLSInstances > 0:
self.promptForString("SLS namespace", "sls_namespace", validator=NewNamespaceValidator())
self.slsLicenseFileLocal = self.promptForFile("License file", mustExist=True, envVar="SLS_LICENSE_FILE_LOCAL")
self.setParam("sls_action", "install")

if slsConfigSelection == "Existing":
print_formatted_text(HTML("<LightSlateGrey>Select an existing SLS instance from the list below:</LightSlateGrey>"))

for slsInstance in self.existingSLSInstances:
print_formatted_text(HTML(f"- <u>{slsInstance['metadata']['namespace']}</u> | {slsInstance['metadata']['name']}) | v{slsInstance['status']['versions']['reconciled']}"))
self.slsInstanceOptions.append(slsInstance['metadata']['namespace'])

slsInstanceCompleter = WordCompleter(self.slsInstanceOptions)
print()
self.promptForString("Select SLS namespace", "sls_namespace", completer=slsInstanceCompleter)
self.setParam("sls_action", "gencfg")

if slsConfigSelection == "External":
self.promptForString("SLS url", "sls_url")
self.promptForString("SLS registrationKey", "sls_registration_key")
self.slsCertsDir = self.promptForDir("Enter the path containing the SLS certificate(s)", mustExist=True)
self.setParam("sls_action", "gencfg")
# Improvement Idea: Use SLS client to verify if endpoint exists based on data provided
else:
self.slsLicenseFileLocal = self.promptForFile("License file", mustExist=True, envVar="SLS_LICENSE_FILE_LOCAL")
self.setParam("sls_action", "install")

@logMethodCall
def configDRO(self) -> None:
self.promptForString("Contact e-mail address", "uds_contact_email")
self.promptForString("Contact first name", "uds_contact_firstname")
self.promptForString("Contact last name", "uds_contact_lastname")

if self.showAdvancedOptions:
self.promptForString("IBM Suite License Services (SLS) Namespace", "sls_namespace", default="ibm-sls")
self.promptForString("IBM Data Reporter Operator (DRO) Namespace", "dro_namespace", default="redhat-marketplace")

@logMethodCall
Expand Down Expand Up @@ -730,6 +790,7 @@ def interactiveMode(self, simplified: bool, advanced: bool) -> None:

# Licensing (SLS and DRO)
self.configSLS()
self.configDRO()
self.configICRCredentials()

# MAS Core
Expand Down Expand Up @@ -900,6 +961,7 @@ def nonInteractiveMode(self) -> None:
if value is None:
self.fatalError(f"{key} must be set")
self.slsLicenseFileLocal = value
# ToDo: Review SLS options in noninteractive mode

elif key.startswith("approval_"):
if key not in self.approvals:
Expand Down Expand Up @@ -1008,8 +1070,13 @@ def install(self, argv):
self.configCP4D()

# The entitlement file for SLS is mounted as a secret in /workspace/entitlement
entitlementFileBaseName = path.basename(self.slsLicenseFileLocal)
self.setParam("sls_entitlement_file", f"/workspace/entitlement/{entitlementFileBaseName}")
if self.slsLicenseFileLocal:
entitlementFileBaseName = path.basename(self.slsLicenseFileLocal)
self.setParam("sls_entitlement_file", f"/workspace/entitlement/{entitlementFileBaseName}")

# The ca cert for SLS is mounted as a secret in /workspace/certificates
if self.slsCaCertFileLocal:
self.setParam("sls_tls_crt_local_file_base64_path", "/workspace/certificates/sls.ca.crt")

# Set up the secrets for additional configs, podtemplates and manual certificates
self.additionalConfigs()
Expand Down
4 changes: 4 additions & 0 deletions python/src/mas/cli/install/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
"mas_domain",
# SLS
"sls_namespace",
"sls_url",
"sls_action",
"sls_registration_key",
"sls_tls_crt_local_file_base64_path",
# DNS Providers
# TODO: Add CloudFlare and Route53 support
"dns_provider",
Expand Down
30 changes: 20 additions & 10 deletions python/src/mas/cli/install/settings/additionalConfigs.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,18 @@ def podTemplates(self) -> None:
self.podTemplatesSecret = podTemplatesSecret

def manualCertificates(self) -> None:

if self.getParam("mas_manual_cert_mgmt"):
certsSecret = {
"apiVersion": "v1",
"kind": "Secret",
"type": "Opaque",
"metadata": {
"name": "pipeline-certificates"
}
certsSecret = {
"apiVersion": "v1",
"kind": "Secret",
"type": "Opaque",
"metadata": {
"name": "pipeline-certificates"
}
}

extensions = ["key", "crt"]
extensions = ["key", "crt"]

if self.getParam("mas_manual_cert_mgmt"):
apps = {
"mas_app_channel_assist": {
"dir": self.manualCertsDir + "/assist/",
Expand Down Expand Up @@ -182,6 +181,17 @@ def manualCertificates(self) -> None:

self.certsSecret = certsSecret


if self.slsCertsDir:
# Currently SLS only needs ca.crt
for file in ["ca.crt"]:
if file not in map(path.basename, glob(f'{self.slsCertsDir}/*')):
self.fatalError(f'{file} is not present in {self.slsCertsDir}/')
for ext in extensions:
certsSecret = self.addFilesToSecret(certsSecret, self.slsCertsDir, ext, "sls.")

self.certsSecret = certsSecret

def addFilesToSecret(self, secretDict: dict, configPath: str, extension: str, keyPrefix: str = '') -> dict:
"""
Add file (or files) to pipeline-additional-configs
Expand Down
43 changes: 42 additions & 1 deletion python/src/mas/cli/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@

from prompt_toolkit.validation import Validator, ValidationError

from mas.devops.ocp import getStorageClass
from mas.devops.ocp import getStorageClass, getNamespace
from mas.devops.mas import verifyMasInstance
from mas.devops.sls import listSLSInstances

import logging

Expand Down Expand Up @@ -136,3 +137,43 @@ def validate(self, document):
response = document.text
if response not in ["full", "limited"]:
raise ValidationError(message='Enter a valid response: full, limited', cursor_position=len(response))


class SLSConfigValidator(Validator):
def validate(self, document):
"""
Validate that a response is a valid config plan for SLS
"""
response = document.text
validOptions = ["New", "External"]

dynClient = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
existingSLSInstances = listSLSInstances(dynClient)
numSLSInstances = len(existingSLSInstances)
if numSLSInstances > 0:
validOptions.insert(1, "Existing")

if response not in validOptions:
raise ValidationError(
message=f"Enter a valid response: {', '.join(validOptions)}",
cursor_position=len(response),
)


class NewNamespaceValidator(Validator):
def validate(self, document):
"""
Validate that a namespace does not exist
"""
namespace = document.text

dynClient = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)

# ToDo: Add namespace regex validation

if getNamespace(dynClient, namespace):
raise ValidationError(message=f"Namespace {namespace} already exists on the cluster, please chose a unique name", cursor_position=len(namespace))
14 changes: 13 additions & 1 deletion tekton/src/params/install.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
default: ""
- name: sls_entitlement_file
type: string
default: "/workspace/entitlement/entitlement.lic"
default: ""
- name: sls_mongodb_cfg_file
type: string
# The default value works for the default in-cluster install, it will need
Expand All @@ -42,6 +42,18 @@
- name: sls_icr_cpopen
type: string
default: ""
- name: sls_action
type: string
default: ""
- name: sls_url
type: string
default: ""
- name: sls_registration_key
type: string
default: ""
- name: sls_tls_crt_local_file_base64_path
type: string
default: ""

# Dependencies - MongoDb
# -----------------------------------------------------------------------------
Expand Down
11 changes: 11 additions & 0 deletions tekton/src/pipelines/taskdefs/dependencies/sls.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@
value: $(params.sls_channel)
- name: sls_icr_cpopen
value: $(params.sls_icr_cpopen)
- name: sls_action
value: $(params.sls_action)

- name: sls_url
value: $(params.sls_url)
- name: sls_registration_key
value: $(params.sls_registration_key)
- name: sls_tls_crt_local_file_base64_path
value: $(params.sls_tls_crt_local_file_base64_path)

# New way of bootstrapping license file
- name: sls_entitlement_file
Expand All @@ -44,3 +53,5 @@
workspace: shared-entitlement
- name: pod-templates
workspace: shared-pod-templates
- name: certificates
workspace: shared-certificates
19 changes: 19 additions & 0 deletions tekton/src/tasks/dependencies/sls.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ spec:
type: string
default: ""

- name: sls_url
type: string
default: ""
- name: sls_registration_key
type: string
default: ""
- name: sls_tls_crt_local_file_base64_path
type: string
default: ""

# New way of bootstrapping license file since SLS 3.7.0
- name: sls_entitlement_file
type: string
Expand Down Expand Up @@ -80,6 +90,13 @@ spec:
- name: SLS_ICR_CPOPEN
value: $(params.sls_icr_cpopen)

- name: SLS_URL
value: $(params.sls_url)
- name: SLS_REGISTRATION_KEY
value: $(params.sls_registration_key)
- name: SLS_TLS_CERT_LOCAL_FILE_BASE64_PATH
value: $(params.sls_tls_crt_local_file_base64_path)

# New way of bootstrapping license file since SLS 3.7.0
- name: SLS_ENTITLEMENT_FILE
value: $(params.sls_entitlement_file)
Expand Down Expand Up @@ -118,3 +135,5 @@ spec:
optional: true
- name: pod-templates
optional: true
- name: certificates
optional: true
Loading