diff --git a/astraSDK/__init__.py b/astraSDK/__init__.py index 48825d5..fda0408 100644 --- a/astraSDK/__init__.py +++ b/astraSDK/__init__.py @@ -24,6 +24,7 @@ from . import common from . import entitlements from . import hooks +from . import k8s from . import namespaces from . import notifications from . import protections diff --git a/astraSDK/backups.py b/astraSDK/backups.py index 12dbb2c..d0806ed 100644 --- a/astraSDK/backups.py +++ b/astraSDK/backups.py @@ -43,7 +43,8 @@ def __init__(self, quiet=True, verbose=False, output="json"): def main(self, appFilter=None): if self.apps is False: - print("Call to getApps().main() failed") + if not self.quiet: + super().printError("Call to getApps() failed") return False """self.apps = {"items":[{"appDefnSource":"namespace","appLabels":[], diff --git a/astraSDK/buckets.py b/astraSDK/buckets.py index 0c4e8b3..cad4d7c 100644 --- a/astraSDK/buckets.py +++ b/astraSDK/buckets.py @@ -69,8 +69,8 @@ def main(self, nameFilter=None, provider=None): dataReturn = yaml.dump(bucketsCooked) elif self.output == "table": dataReturn = self.basicTable( - ["bucketID", "name", "credentialID", "provider", "state"], - ["id", "name", "credentialID", "provider", "state"], + ["bucketID", "name", "credentialID", "provider", "state", "retentionTime"], + ["id", "name", "credentialID", "provider", "state", "retentionTime"], bucketsCooked, ) if not self.quiet: diff --git a/astraSDK/common.py b/astraSDK/common.py index cb48abf..980f13f 100644 --- a/astraSDK/common.py +++ b/astraSDK/common.py @@ -16,6 +16,7 @@ """ import json +import kubernetes import os import sys import yaml @@ -69,15 +70,11 @@ def __init__(self): raise SystemExit(f"{item} is a required field in {configFile}") if "." in self.conf.get("astra_project"): - self.base = "https://%s/accounts/%s/" % ( - self.conf.get("astra_project"), - self.conf.get("uid"), - ) + self.domain = self.conf.get("astra_project") else: - self.base = "https://%s.astra.netapp.io/accounts/%s/" % ( - self.conf.get("astra_project"), - self.conf.get("uid"), - ) + self.domain = "%s.astra.netapp.io" % (self.conf.get("astra_project")) + self.account_id = self.conf.get("uid") + self.base = "https://%s/accounts/%s/" % (self.domain, self.account_id) self.headers = self.conf.get("headers") if self.conf.get("verifySSL") is False: @@ -91,11 +88,50 @@ def main(self): "base": self.base, "headers": self.headers, "verifySSL": self.verifySSL, + "domain": self.domain, + "account_id": self.account_id, } -class SDKCommon: +class BaseCommon: def __init__(self): + pass + + def printError(self, ret): + """Function to print relevant error information when a call fails""" + try: + sys.stderr.write(colored(json.dumps(json.loads(ret.text), indent=2), "red") + "\n") + except json.decoder.JSONDecodeError: + sys.stderr.write(colored(ret.text, "red")) + except AttributeError: + sys.stderr.write(colored(ret, "red")) + + def recursiveGet(self, k, item): + """Recursion function which is just a wrapper around dict.get(key), to handle cases + where there's a dict within a dict. A '.' in the key name ('metadata.creationTimestamp') + is used for identification purposes.""" + if len(k.split(".")) > 1: + return self.recursiveGet(k.split(".", 1)[1], item[k.split(".")[0]]) + else: + return item.get(k) + + def basicTable(self, tabHeader, tabKeys, dataDict): + """Function to create a basic tabulate table for terminal printing""" + tabData = [] + for item in dataDict["items"]: + # Generate a table row based on the keys list + row = [self.recursiveGet(k, item) for k in tabKeys] + # Handle cases where table row has a nested list + for c, r in enumerate(row): + if type(r) is list: + row[c] = ", ".join(r) + tabData.append(row) + return tabulate(tabData, tabHeader, tablefmt="grid") + + +class SDKCommon(BaseCommon): + def __init__(self): + super().__init__() self.conf = getConfig().main() self.base = self.conf.get("base") self.headers = self.conf.get("headers") @@ -175,33 +211,8 @@ def printVerbose(self, url, method, headers, data, params): print(colored(f"API data: {data}", "green")) print(colored(f"API params: {params}", "green")) - def printError(self, ret): - """Function to print relevant error information when a call fails""" - try: - sys.stderr.write(colored(json.dumps(json.loads(ret.text), indent=2), "red") + "\n") - except json.decoder.JSONDecodeError: - sys.stderr.write(colored(ret.text, "red")) - except AttributeError: - sys.stderr.write(colored(f"SDKCommon().printError: Unknown error: {ret}", "red")) - - def recursiveGet(self, k, item): - """Recursion function which is just a wrapper around dict.get(key), to handle cases - where there's a dict within a dict. A '.' in the key name ('metadata.creationTimestamp) - is used for identification purposes.""" - if len(k.split(".")) > 1: - return self.recursiveGet(k.split(".", 1)[1], item[k.split(".")[0]]) - else: - return item.get(k) - def basicTable(self, tabHeader, tabKeys, dataDict): - """Function to create a basic tabulate table for terminal printing""" - tabData = [] - for item in dataDict["items"]: - # Generate a table row based on the keys list - row = [self.recursiveGet(k, item) for k in tabKeys] - # Handle cases where table row has a nested list - for c, r in enumerate(row): - if type(r) is list: - row[c] = ", ".join(r) - tabData.append(row) - return tabulate(tabData, tabHeader, tablefmt="grid") +class KubeCommon(BaseCommon): + def __init__(self): + super().__init__() + self.kube_config = kubernetes.config.load_kube_config() diff --git a/astraSDK/k8s.py b/astraSDK/k8s.py new file mode 100644 index 0000000..a04d33b --- /dev/null +++ b/astraSDK/k8s.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" + Copyright 2023 NetApp, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import base64 +import copy +import json +import kubernetes +import yaml + +from .common import KubeCommon, SDKCommon + + +class getResources(KubeCommon): + def __init__(self, quiet=True, output="json"): + """quiet: Will there be CLI output or just return (datastructure) + output: json: (default) output in JSON + yaml: output in yaml""" + self.quiet = quiet + self.output = output + super().__init__() + + def main( + self, + plural, + version="v1", + group="trident.netapp.io", + keyFilter=None, + valFilter=None, + ): + with kubernetes.client.ApiClient(self.kube_config) as api_client: + api_instance = kubernetes.client.CustomObjectsApi(api_client) + try: + resp = api_instance.list_cluster_custom_object( + group=group, + version=version, + plural=plural, + ) + if keyFilter and valFilter: + filterCopy = copy.deepcopy(resp) + for counter, r in enumerate(filterCopy.get("items")): + if self.recursiveGet(keyFilter, r) != valFilter: + resp["items"].remove(filterCopy["items"][counter]) + + if self.output == "yaml": + resp = yaml.dump(resp) + + if not self.quiet: + print(json.dumps(resp) if type(resp) is dict else resp) + return resp + + except kubernetes.client.rest.ApiException as e: + self.printError(e) + + +class updateResource(KubeCommon): + def __init__(self, quiet=True): + """quiet: Will there be CLI output or just return (datastructure)""" + self.quiet = quiet + super().__init__() + + def main( + self, + plural, + name, + body, + version="v1", + group="trident.netapp.io", + ): + with kubernetes.client.ApiClient(self.kube_config) as api_client: + api_instance = kubernetes.client.CustomObjectsApi(api_client) + try: + resp = api_instance.patch_cluster_custom_object(group, version, plural, name, body) + + if not self.quiet: + print(json.dumps(resp) if type(resp) is dict else resp) + return resp + + except kubernetes.client.rest.ApiException as e: + self.printError(e) + + +class getNamespaces(KubeCommon): + def __init__(self, quiet=True, output="json"): + """quiet: Will there be CLI output or just return (datastructure) + output: json: (default) output in JSON + yaml: output in yaml""" + self.quiet = quiet + self.output = output + super().__init__() + + def main(self): + with kubernetes.client.ApiClient(self.kube_config) as api_client: + api_instance = kubernetes.client.CoreV1Api(api_client) + try: + resp = api_instance.list_namespace().to_dict() + systemNS = [ + "astra-connector-operator", + "kube-node-lease", + "kube-public", + "kube-system", + "neptune-system", + "trident", + ] + namespaces = copy.deepcopy(resp) + for counter, ns in enumerate(namespaces.get("items")): + if ns.get("metadata").get("name") in systemNS: + resp["items"].remove(namespaces["items"][counter]) + + if self.output == "yaml": + resp = yaml.dump(resp) + + if not self.quiet: + print(json.dumps(resp, default=str) if type(resp) is dict else resp) + return resp + + except kubernetes.client.rest.ApiException as e: + self.printError(e) + + +class getSecrets(KubeCommon): + def __init__(self, quiet=True, output="json"): + """quiet: Will there be CLI output or just return (datastructure) + output: json: (default) output in JSON + yaml: output in yaml""" + self.quiet = quiet + self.output = output + super().__init__() + + def main(self, namespace="trident"): + with kubernetes.client.ApiClient(self.kube_config) as api_client: + api_instance = kubernetes.client.CoreV1Api(api_client) + try: + resp = api_instance.list_namespaced_secret(namespace).to_dict() + + if self.output == "yaml": + resp = yaml.dump(resp) + + if not self.quiet: + print(json.dumps(resp, default=str) if type(resp) is dict else resp) + return resp + + except kubernetes.client.rest.ApiException as e: + self.printError(e) + + +class createRegCred(KubeCommon, SDKCommon): + def __init__(self, quiet=True): + """quiet: Will there be CLI output or just return (datastructure)""" + self.quiet = quiet + super().__init__() + + def main(self, name=None, registry=None, username=None, password=None, namespace="trident"): + if (not username and password) or (username and not password): + raise SystemExit( + "Either both or neither of (username and password) should be specified" + ) + if not registry: + registry = f"cr.{self.conf['domain']}" + if not username and not password: + username = self.conf["account_id"] + password = self.conf["headers"].get("Authorization").split(" ")[-1] + + regCred = { + "auths": { + registry: { + "username": username, + "password": password, + "auth": base64.b64encode(f"{username}:{password}".encode("utf-8")).decode( + "utf-8" + ), + } + } + } + regCredSecret = kubernetes.client.V1Secret( + metadata=( + kubernetes.client.V1ObjectMeta(name=name) + if name + else kubernetes.client.V1ObjectMeta( + generate_name="-".join(registry.split(".")) + "-" + ) + ), + type="kubernetes.io/dockerconfigjson", + data={ + ".dockerconfigjson": base64.b64encode(json.dumps(regCred).encode("utf-8")).decode( + "utf-8" + ) + }, + ) + + with kubernetes.client.ApiClient(self.kube_config) as api_client: + api_instance = kubernetes.client.CoreV1Api(api_client) + try: + resp = api_instance.create_namespaced_secret( + namespace=namespace, + body=regCredSecret, + ).to_dict() + if not self.quiet: + print(json.dumps(resp, default=str) if type(resp) is dict else resp) + return resp + except kubernetes.client.rest.ApiException as e: + self.printError(e) diff --git a/astraSDK/snapshots.py b/astraSDK/snapshots.py index f7ceabb..dfa2260 100644 --- a/astraSDK/snapshots.py +++ b/astraSDK/snapshots.py @@ -43,7 +43,8 @@ def __init__(self, quiet=True, verbose=False, output="json"): def main(self, appFilter=None): if self.apps is False: - print("Call to getApps() failed") + if not self.quiet: + super().printError("Call to getApps() failed") return False snaps = {} diff --git a/docs/toolkit/deploy/README.md b/docs/toolkit/deploy/README.md index 93919c0..cfc84d8 100644 --- a/docs/toolkit/deploy/README.md +++ b/docs/toolkit/deploy/README.md @@ -1,9 +1,33 @@ # Deploy -The `deploy` command has the following command syntax: +The `deploy` command allows you to deploy Kubernetes-based resources onto your *current context*, including: + +* [Astra Control Provisioner](#acp) +* [Helm Chart](#chart) + +```text +$ actoolkit deploy -h +usage: actoolkit deploy [-h] {acp,chart} ... + +options: + -h, --help show this help message and exit + +objectType: + {acp,chart} + acp deploy ACP (Astra Control Provisioner) + chart deploy a Helm chart +``` + +## ACP + +The `deploy acp` command allows you to install Astra Control Provisioner. Additional documentation upcoming. + +## Chart + +The `deploy chart` command allows you to deploy a helm chart with the following syntax: ```text -actoolkit deploy -n/--namespace \ +actoolkit deploy chart -n/--namespace \ -f/--values --set --set ``` diff --git a/docs/toolkit/list/README.md b/docs/toolkit/list/README.md index 5ac7507..d349764 100644 --- a/docs/toolkit/list/README.md +++ b/docs/toolkit/list/README.md @@ -266,37 +266,37 @@ Sample output: ```text $ actoolkit list buckets -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| bucketID | name | credentialID | provider | state | -+======================================+=================================+======================================+============+===========+ -| 361aa1e0-60bc-4f1b-ba3b-bdaa890b5bac | astra-gcp-backup-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| ceb69272-ee61-4876-aef5-d6cc21a3e20c | astra-azure-backup-fbe43be9aaa0 | ad544328-1fbb-48af-bff8-ebb21e874540 | azure | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| 225080bb-ff5b-4cb6-a834-50604904bfc9 | gcp-secondary-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| bucketID | name | credentialID | provider | state | retentionTime | ++======================================+=================================+======================================+============+===========+=================+ +| 361aa1e0-60bc-4f1b-ba3b-bdaa890b5bac | astra-gcp-backup-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | 259200 | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| ceb69272-ee61-4876-aef5-d6cc21a3e20c | astra-azure-backup-fbe43be9aaa0 | ad544328-1fbb-48af-bff8-ebb21e874540 | azure | available | | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| 225080bb-ff5b-4cb6-a834-50604904bfc9 | gcp-secondary-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ ``` ```text $ actoolkit list buckets --provider gcp -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| bucketID | name | credentialID | provider | state | -+======================================+=================================+======================================+============+===========+ -| 361aa1e0-60bc-4f1b-ba3b-bdaa890b5bac | astra-gcp-backup-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| 225080bb-ff5b-4cb6-a834-50604904bfc9 | gcp-secondary-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| bucketID | name | credentialID | provider | state | retentionTime | ++======================================+=================================+======================================+============+===========+=================+ +| 361aa1e0-60bc-4f1b-ba3b-bdaa890b5bac | astra-gcp-backup-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| 225080bb-ff5b-4cb6-a834-50604904bfc9 | gcp-secondary-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | 259200 | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ ``` ```text $ actoolkit list buckets --nameFilter backup -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| bucketID | name | credentialID | provider | state | -+======================================+=================================+======================================+============+===========+ -| 361aa1e0-60bc-4f1b-ba3b-bdaa890b5bac | astra-gcp-backup-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ -| ceb69272-ee61-4876-aef5-d6cc21a3e20c | astra-azure-backup-fbe43be9aaa0 | ad544328-1fbb-48af-bff8-ebb21e874540 | azure | available | -+--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+ ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| bucketID | name | credentialID | provider | state | retentionTime | ++======================================+=================================+======================================+============+===========+=================+ +| 361aa1e0-60bc-4f1b-ba3b-bdaa890b5bac | astra-gcp-backup-fbe43be9aaa0 | 987ab72d-3e48-4b9f-879f-1d14059efa8e | gcp | available | 259200 | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ +| ceb69272-ee61-4876-aef5-d6cc21a3e20c | astra-azure-backup-fbe43be9aaa0 | ad544328-1fbb-48af-bff8-ebb21e874540 | azure | available | | ++--------------------------------------+---------------------------------+--------------------------------------+------------+-----------+-----------------+ ``` ## Clouds diff --git a/tkSrc/classes.py b/tkSrc/classes.py index 62d7d47..8fe88cd 100644 --- a/tkSrc/classes.py +++ b/tkSrc/classes.py @@ -61,13 +61,39 @@ def needsattr(self, name): if not getattr(self, name, False): return True + def recursiveGet(self, k, item): + """Recursion function which is just a wrapper around dict.get(key), to handle cases + where there's a dict within a dict. A '.' in the key name ('metadata.name') + is used for identification purposes.""" + if len(k.split(".")) > 1: + return self.recursiveGet(k.split(".", 1)[1], item[k.split(".")[0]]) + else: + return item.get(k) + def buildList(self, name, key, fKey=None, fVal=None): """Generates a list for use in argparse choices""" try: # return a list of resource values based on 'key' if not fKey or not fVal: - return [x[key] for x in getattr(self, name)["items"]] + return [self.recursiveGet(key, x) for x in getattr(self, name)["items"]] # return a list of resource values based on 'key' only if some other 'fKey' == 'fVal' - return [x[key] for x in (y for y in getattr(self, name)["items"] if y[fKey] == fVal)] + return [ + self.recursiveGet(key, x) + for x in ( + y for y in getattr(self, name)["items"] if self.recursiveGet(fKey, y) == fVal + ) + ] except TypeError: return [] + + def getSingleDict(self, name, key, value, parser): + """Returns a single dict within the "items" list of the main resource dict, based on a + matching key/value pair""" + try: + return next( + x for x in getattr(self, name)["items"] if self.recursiveGet(key, x) == value + ) + except StopIteration: + parser.error( + f"A resource with a '{key}:{value}' pair in the '{name}' dict was not found" + ) diff --git a/tkSrc/create.py b/tkSrc/create.py index 885dd87..2a858af 100644 --- a/tkSrc/create.py +++ b/tkSrc/create.py @@ -25,15 +25,16 @@ import tkSrc -def doProtectionTask(protectionType, appID, name, background, pollTimer, quiet, verbose): - """Take a snapshot/backup of appID giving it name - Return the snapshotID/backupID of the backup taken or False if the protection task fails""" - if protectionType == "backup": - protectionID = astraSDK.backups.takeBackup(quiet=quiet, verbose=verbose).main(appID, name) - elif protectionType == "snapshot": - protectionID = astraSDK.snapshots.takeSnap(quiet=quiet, verbose=verbose).main(appID, name) +def monitorProtectionTask(protectionID, protectionType, appID, background, pollTimer, parser): + """Ensure backup/snapshot task was created successfully, then monitor""" if protectionID is False: return False + if protectionType == "backup": + protection_class = astraSDK.backups.getBackups() + elif protectionType == "snapshot": + protection_class = astraSDK.snapshots.getSnaps() + else: + parser.error(f"unknown protection type: {protectionType}") print(f"Starting {protectionType} of {appID}") if background: @@ -44,42 +45,50 @@ def doProtectionTask(protectionType, appID, name, background, pollTimer, quiet, print(f"Waiting for {protectionType} to complete.", end="") sys.stdout.flush() - while True: - if protectionType == "backup": - objects = astraSDK.backups.getBackups().main() - elif protectionType == "snapshot": - objects = astraSDK.snapshots.getSnaps().main() - if not objects: - # This isn't technically true. Trying to list the backups/snapshots after taking - # the protection job failed. The protection job itself may eventually succeed. - print(f"Taking {protectionType} failed") - return False - for obj in objects["items"]: - # Just because the API call to create a backup/snapshot succeeded, that doesn't - # mean the actual backup will succeed. So loop to show completed or failed. - if obj["id"] == protectionID: - if obj["state"] == "completed": - print("complete!") - sys.stdout.flush() - return protectionID - elif obj["state"] == "failed": - print(f"{protectionType} job failed") - return False - time.sleep(pollTimer) - print(".", end="") - sys.stdout.flush() + err_counter = [] + while len(err_counter) < 3: + try: + objects = protection_class.main(appFilter=appID) + if not objects: + raise Exception(f"astraSDK.{protectionType}s.get{protectionType}s().main() failed") + protection_found = False + for obj in objects["items"]: + if obj["id"] == protectionID: + protection_found = True + if obj["state"] == "completed": + print("complete!") + sys.stdout.flush() + return protectionID + elif obj["state"] == "failed": + print(f"{protectionType} job failed") + return False + if not protection_found: + raise Exception(f"Protection ID {protectionID} not found") + time.sleep(pollTimer) + print(".", end="") + sys.stdout.flush() + except Exception as err: + err_counter.append(err) + for err in set([str(e) for e in err_counter]): + protection_class.printError(err + "\n") + return False def main(args, parser, ard): if args.objectType == "backup": - rc = doProtectionTask( - args.objectType, + protectionID = astraSDK.backups.takeBackup(quiet=args.quiet, verbose=args.verbose).main( args.appID, tkSrc.helpers.isRFC1123(args.name), + bucketID=args.bucketID, + snapshotID=args.snapshotID, + ) + rc = monitorProtectionTask( + protectionID, + args.objectType, + args.appID, args.background, args.pollTimer, - args.quiet, - args.verbose, + parser, ) if rc is False: raise SystemExit("doProtectionTask() failed") @@ -232,14 +241,17 @@ def main(args, parser, ard): if rc is False: raise SystemExit("astraSDK.scripts.createScript() failed") elif args.objectType == "snapshot": - rc = doProtectionTask( - args.objectType, + protectionID = astraSDK.snapshots.takeSnap(quiet=args.quiet, verbose=args.verbose).main( args.appID, tkSrc.helpers.isRFC1123(args.name), + ) + rc = monitorProtectionTask( + protectionID, + args.objectType, + args.appID, args.background, args.pollTimer, - args.quiet, - args.verbose, + parser, ) if rc is False: raise SystemExit("doProtectionTask() failed") diff --git a/tkSrc/deploy.py b/tkSrc/deploy.py index a6feb2d..655c99e 100644 --- a/tkSrc/deploy.py +++ b/tkSrc/deploy.py @@ -15,6 +15,7 @@ limitations under the License. """ +import base64 import json import kubernetes import sys @@ -26,7 +27,7 @@ import tkSrc -def doDeploy(chart, appName, namespace, setValues, fileValues, verbose, quiet): +def deployHelm(chart, appName, namespace, setValues, fileValues, verbose, quiet): """Deploy a helm chart , naming the app into """ setStr = tkSrc.helpers.createHelmStr("set", setValues) @@ -150,13 +151,63 @@ def doDeploy(chart, appName, namespace, setValues, fileValues, verbose, quiet): raise SystemExit(f"cpp.main({period}...) returned False") -def main(args): - doDeploy( - args.chart, - tkSrc.helpers.isRFC1123(args.app), - args.namespace, - args.set, - args.values, - args.verbose, - args.quiet, - ) +def main(args, parser, ard): + if args.objectType == "acp": + # Ensure the trident orchestrator is already running + torc = astraSDK.k8s.getResources().main( + "tridentorchestrators", version="v1", group="trident.netapp.io" + ) + if len(torc["items"]) == 0: + parser.error("trident operator not found on current Kubernetes context") + elif len(torc["items"]) > 1: + parser.error("multiple trident operators found on current Kubernetes context") + # Handle the registry secret + if not args.regCred: + cred = astraSDK.k8s.createRegCred(quiet=args.quiet).main(registry=args.registry) + if not cred: + raise SystemExit("astraSDK.k8s.createRegCred() failed") + args.regCred = cred["metadata"]["name"] + else: + if ard.needsattr("credentials"): + ard.credentials = astraSDK.k8s.getSecrets().main(namespace="trident") + cred = ard.getSingleDict("credentials", "metadata.name", args.regCred, parser) + # Handle default registry + if not args.registry: + try: + args.registry = next( + iter( + json.loads(base64.b64decode(cred["data"][".dockerconfigjson"]).decode())[ + "auths" + ].keys() + ) + ) + except KeyError as err: + parser.error( + f"{args.regCred} does not appear to be a Docker secret: {err} key not found" + ) + # Create the patch spec + torc_name = torc["items"][0]["metadata"]["name"] + torc_version = torc["items"][0]["status"]["version"][1:] + torc_spec = {"spec": torc["items"][0]["spec"]} + torc_spec["spec"]["enableACP"] = True + torc_spec["spec"]["acpImage"] = f"{args.registry}/astra/trident-acp:{torc_version}" + torc_spec["spec"]["imagePullSecrets"] = [args.regCred] + # Make the update + astraSDK.k8s.updateResource().main( + "tridentorchestrators", + torc_name, + torc_spec, + version="v1", + group="trident.netapp.io", + ) + print(f"tridentorchestrator.trident.netapp.io/{torc_name} edited") + elif args.objectType == "chart": + deployHelm( + args.chart, + tkSrc.helpers.isRFC1123(args.app), + args.namespace, + args.set, + args.values, + args.verbose, + args.quiet, + ) diff --git a/tkSrc/parser.py b/tkSrc/parser.py index 0c0edb7..8905dfe 100644 --- a/tkSrc/parser.py +++ b/tkSrc/parser.py @@ -60,7 +60,7 @@ def top_level_commands(self): ) self.parserDeploy = self.subparsers.add_parser( "deploy", - help="Deploy a helm chart", + help="Deploy kubernetes resources into current context", ) self.parserClone = self.subparsers.add_parser( "clone", @@ -102,8 +102,11 @@ def top_level_commands(self): ) def sub_commands(self): - """'list', 'create', 'manage', 'destroy', 'unmanage', and 'update' all have - subcommands, for example, 'list apps' or 'manage cluster'.""" + """'deploy', 'list', 'create', 'manage', 'destroy', 'unmanage', and 'update' + all have subcommands, for example, 'list apps' or 'manage cluster'.""" + self.subparserDeploy = self.parserDeploy.add_subparsers( + title="objectType", dest="objectType", required=True + ) self.subparserList = self.parserList.add_subparsers( title="objectType", dest="objectType", required=True ) @@ -126,6 +129,17 @@ def sub_commands(self): title="objectType", dest="objectType", required=True ) + def sub_deploy_commands(self): + """deploy 'X'""" + self.subparserDeployAcp = self.subparserDeploy.add_parser( + "acp", + help="deploy ACP (Astra Control Provisioner)", + ) + self.subparserDeployChart = self.subparserDeploy.add_parser( + "chart", + help="deploy a Helm chart", + ) + def sub_list_commands(self): """list 'X'""" self.subparserListApiResources = self.subparserList.add_parser( @@ -344,39 +358,6 @@ def sub_update_commands(self): help="update script", ) - def deploy_args(self): - """deploy args and flags""" - self.parserDeploy.add_argument( - "app", - help="name of app", - ) - self.parserDeploy.add_argument( - "chart", - choices=(None if self.plaidMode else self.acl.charts), - help="chart to deploy", - ) - self.parserDeploy.add_argument( - "-n", - "--namespace", - required=True, - help="Namespace to deploy into (must not already exist)", - ) - self.parserDeploy.add_argument( - "-f", - "--values", - required=False, - action="append", - nargs="*", - help="Specify Helm values in a YAML file", - ) - self.parserDeploy.add_argument( - "--set", - required=False, - action="append", - nargs="*", - help="Individual helm chart parameters", - ) - def clone_args(self): """clone args and flags""" self.parserClone.add_argument( @@ -527,6 +508,55 @@ def restore_args(self): "PersistentVolumeClaim --filterSet label=app.kubernetes.io/tier=backend,name=mysql", ) + def deploy_acp_args(self): + """deploy ACP args and flags""" + self.subparserDeployAcp.add_argument( + "--regCred", + choices=(None if self.plaidMode else self.acl.credentials), + default=None, + help="optionally specify the name of the existing registry credential " + "(rather than automatically creating a new secret)", + ) + self.subparserDeployAcp.add_argument( + "--registry", + default=None, + help="optionally specify the FQDN of the ACP image source registry " + "(defaults to cr.)", + ) + + def deploy_chart_args(self): + """deploy helm chart args and flags""" + self.subparserDeployChart.add_argument( + "app", + help="name of app", + ) + self.subparserDeployChart.add_argument( + "chart", + choices=(None if self.plaidMode else self.acl.charts), + help="chart to deploy", + ) + self.subparserDeployChart.add_argument( + "-n", + "--namespace", + required=True, + help="Namespace to deploy into (must not already exist)", + ) + self.subparserDeployChart.add_argument( + "-f", + "--values", + required=False, + action="append", + nargs="*", + help="Specify Helm values in a YAML file", + ) + self.subparserDeployChart.add_argument( + "--set", + required=False, + action="append", + nargs="*", + help="Individual helm chart parameters", + ) + def list_apiresources_args(self): """list api resources args and flags""" self.subparserListApiResources.add_argument( @@ -1479,6 +1509,7 @@ def main(self): self.sub_commands() # Of those top-level commands with sub-commands, create those sub-command parsers + self.sub_deploy_commands() self.sub_list_commands() self.sub_copy_commands() self.sub_create_commands() @@ -1488,10 +1519,12 @@ def main(self): self.sub_update_commands() # Create arguments for all commands - self.deploy_args() self.clone_args() self.restore_args() + self.deploy_acp_args() + self.deploy_chart_args() + self.list_apiresources_args() self.list_apps_args() self.list_assets_args() diff --git a/toolkit.py b/toolkit.py index facc2a4..e1bc78b 100755 --- a/toolkit.py +++ b/toolkit.py @@ -112,9 +112,20 @@ def main(argv=sys.argv): if not plaidMode: # It isn't intuitive, however only one key in verbs can be True - if verbs["deploy"]: + if ( + verbs["deploy"] + and len(argv) - verbPosition >= 2 + and argv[verbPosition + 1] == "chart" + ): ard.charts = tkSrc.helpers.updateHelm() acl.charts = ard.buildList("charts", "name") + elif ( + verbs["deploy"] + and len(argv) - verbPosition >= 2 + and argv[verbPosition + 1] == "acp" + ): + ard.credentials = astraSDK.k8s.getSecrets().main(namespace="trident") + acl.credentials = ard.buildList("credentials", "metadata.name") elif verbs["clone"]: ard.apps = astraSDK.apps.getApps().main() @@ -393,7 +404,7 @@ def main(argv=sys.argv): args = parser.parse_args(args=argv) if args.subcommand == "deploy": - tkSrc.deploy.main(args) + tkSrc.deploy.main(args, parser, ard) elif args.subcommand == "clone": tkSrc.clone.main(args, parser, ard) elif args.subcommand == "restore":