PK!盶tulips/__init__.pyfrom tulips.resource.deployment import Deployment # noqa: F401 from tulips.resource.ingress import Ingress # noqa: F401 from tulips.resource.issuer import Issuer # noqa: F401 from tulips.resource.persistentvolumeclaim import ( # noqa: F401 PersistentVolumeClaim, ) from tulips.resource.secret import Secret # noqa: F401 from tulips.resource.service import Service # noqa: F401 from tulips.resource.statefullset import StatefulSet # noqa: F401 from tulips.resource.configmap import ConfigMap # noqa: F401 from tulips.resource.cronjob import CronJob # noqa: F401 from tulips.resource.job import Job # noqa: F401 from tulips.resource.networkpolicy import NetworkPolicy # noqa: F401 PK!Htulips/__main__.pyimport click import structlog import yaml from kubernetes import client as k8s from kubernetes.client.rest import ApiException from .tulip import Tulip log = structlog.get_logger("tulip") __version__ = "0.0.3" @click.group() def cli(): pass @click.command( context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), help=( "You can pass chart variables via foo=bar, " "for example '$ tulip push app.yaml foo=bar'" ), ) @click.argument("chart", type=click.Path(exists=True)) @click.option("--namespace", default="default", help="Kubernetes namespace") @click.option("--release", help="Name of the release") @click.option( "--kubeconfig", help="Path to kubernetes config", default="$HOME/.kube/config", type=click.Path(exists=True), ) @click.pass_context def push(ctx, chart, namespace, release, kubeconfig): options = { ".Release.Name": release, "namespace": namespace, "chart": chart, } for item in ctx.args: options.update([item.split("=")]) click.echo(options) client = Tulip(kubeconfig, namespace, options, chart) for resource in client.resources(): try: resource.create() log.info( "Created", name=resource.name, Resource=resource.__class__.__name__, ) except ApiException as e: log.error( "Failed creating", name=resource.name, Resource=chart.__class__.__name__, reason=e.reason, ) @click.command( context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), help=( "You can pass chart variables via foo=bar, " "for example '$ tulip push app.yaml foo=bar'" ), ) @click.argument("chart", type=click.Path(exists=True)) @click.option("--namespace", default="default", help="Kubernetes namespace") @click.option("--release", help="Name of the release") @click.option( "--kubeconfig", help="Path to kubernetes config", default="$HOME/.kube/config", type=click.Path(exists=True), ) @click.pass_context def status(ctx, chart, namespace, release, kubeconfig): options = { ".Release.Name": release, "namespace": namespace, "chart": chart, } for item in ctx.args: options.update([item.split("=")]) client = Tulip(kubeconfig, namespace, options, chart) for resource in client.resources(): try: click.echo(resource.status().status) except ApiException as e: log.error( "Failed getting info", name=resource.name, Resource=chart.__class__.__name__, reason=e.reason, ) @click.command( context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), help=( "You can pass chart variables via foo=bar, " "for example '$ tulip push app.yaml foo=bar'" ), ) @click.argument("chart", type=click.Path(exists=True)) @click.option("--namespace", default="default", help="Kubernetes namespace") @click.option("--release", help="Name of the release") @click.option( "--kubeconfig", help="Path to kubernetes config", default="$HOME/.kube/config", type=click.Path(exists=True), ) @click.pass_context def echo(ctx, chart, namespace, release, kubeconfig): options = { ".Release.Name": release, "namespace": namespace, "chart": chart, } for item in ctx.args: options.update([item.split("=")]) click.echo(f"### meta={options}") client = Tulip(kubeconfig, namespace, options, chart) for resource in client.resources(): click.echo(yaml.dump(resource.resource)) click.echo("---") @click.command( context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), help=( "You can pass chart variables via foo=bar, " "for example '$ tulip rm app.yaml foo=bar'" ), ) @click.argument("chart", type=click.Path(exists=True)) @click.option("--namespace", default="default", help="Kubernetes namespace") @click.option("--release", help="Name of the release") @click.option( "--kubeconfig", help="Path to kubernetes config", type=click.Path(exists=True), ) @click.pass_context def rm(ctx, chart, namespace, release, kubeconfig): options = {"release": release, "namespace": namespace, "chart": chart} for item in ctx.args: options.update([item.split("=")]) click.echo(options) client = Tulip(kubeconfig, namespace, options, chart) delete = k8s.V1DeleteOptions( propagation_policy="Foreground", grace_period_seconds=5 ) for chart in client.resources(): try: chart.delete(body=delete) log.info( "Deleted", name=chart.name, Resource=chart.__class__.__name__ ) except ApiException as e: log.error( "Failed deleting", name=chart.name, Resource=chart.__class__.__name__, reason=e.reason, ) def main(): cli.add_command(rm) cli.add_command(push) cli.add_command(echo) cli.add_command(status) cli() if __name__ == "__main__": main() PK!|)y y tulips/resource/__init__.pyimport abc from kubernetes import client as k8s import typing as t class UndefinedResource(Exception): """Resource was not defined error.""" def __init__(self, kind: str) -> None: self.str = kind Exception.__init__( self, f"{kind} is not yet defined[{ResourceRegistry.REGISTRY}]" ) class ResourceRegistry(type): REGISTRY: dict = {} def __new__(cls, name, bases, attrs): new_cls = type.__new__(cls, name, bases, attrs) cls.REGISTRY[new_cls.__name__] = new_cls return new_cls @classmethod def get_cls(cls, kind: str) -> t.Callable: """Get Class for provided kind. Args: kind (str): Name of the kind that implements Resource. Raises: UndefinedResource: Resource class for provided kind is not defined. Returns: t.Callable: [Resource] class that implements resource. """ kind_cls = cls.REGISTRY.get(kind) if not kind_cls: raise UndefinedResource(kind) return kind_cls class Resource(metaclass=ResourceRegistry): """Resource is interface that describes Kubernetes Resource defintion.""" resource: dict client: k8s.ApiClient namespace: str def __init__( self, client: k8s.ApiClient, namespace: str, resource: dict, source_file: str, ) -> None: """Initializes resource or CRD. Args: client (k8s.ApiClient): Instance of the Kubernetes client. namespace (str): Namespace where Workload should be deployed. resource (dict): Kubernetes resource or CRD. source_file (str): Source file from which this resource was generated from. """ self.client = client self.namespace = namespace self.resource = resource self.source_file = resource @abc.abstractmethod def create(self): """Create Resource.""" pass @abc.abstractmethod def delete(self, body: k8s.V1DeleteOptions): """Delete Resource.""" pass @abc.abstractmethod def status(self): """Info about Resource.""" pass @property def name(self): """Returns the class kind. Returns: [str]: Base name of the class and kind """ return self.resource["metadata"]["name"] PK!F  tulips/resource/configmap.pyfrom kubernetes import client as k8s from . import Resource class ConfigMap(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.CoreV1Api(self.client).delete_namespaced_config_map( body=body, namespace=self.namespace, name=self.name ) def create(self): return k8s.CoreV1Api(self.client).create_namespaced_config_map( body=self.resource, namespace=self.namespace ) def status(self): return k8s.CoreV1Api(self.client).read_namespaced_config_map( name=self.name, namespace=self.namespace ) def patch(self): return k8s.CoreV1Api(self.client).patch_namespaced_config_map( name=self.name, body=self.resource, namespace=self.namespace ) PK! tulips/resource/cronjob.pyfrom kubernetes import client as k8s from kubernetes.client.models.v1beta1_cron_job import V1beta1CronJob from . import Resource class CronJob(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.BatchV1beta1Api(self.client).delete_namespaced_cron_job( body=body, namespace=self.namespace, name=self.name ) def create(self) -> V1beta1CronJob: return k8s.BatchV1beta1Api(self.client).create_namespaced_cron_job( body=self.resource, namespace=self.namespace ) def status(self) -> V1beta1CronJob: return k8s.BatchV1beta1Api( self.client ).read_namespaced_cron_job_status( name=self.name, namespace=self.namespace ) def patch(self): return k8s.BatchV1beta1Api(self.client).patch_namespaced_cron_job( name=self.name, body=self.resource, namespace=self.namespace ) PK!utulips/resource/deployment.pyfrom kubernetes import client as k8s from . import Resource class Deployment(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.AppsV1Api(self.client).delete_namespaced_deployment( body=body, namespace=self.namespace, name=self.name ) def create(self): return k8s.AppsV1Api(self.client).create_namespaced_deployment( body=self.resource, namespace=self.namespace ) def status(self): return k8s.AppsV1Api(self.client).read_namespaced_stateful_set_status( name=self.name, namespace=self.namespace ) def patch(self): return k8s.AppsV1Api(self.client).patch_namespaced_deployment( name=self.name, body=self.resource, namespace=self.namespace ) PK!: FFtulips/resource/ingress.pyfrom kubernetes import client as k8s from . import Resource class Ingress(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.ExtensionsV1beta1Api(self.client).delete_namespaced_ingress( body=body, namespace=self.namespace, name=self.name ) def create(self): return k8s.ExtensionsV1beta1Api(self.client).create_namespaced_ingress( body=self.resource, namespace=self.namespace ) def status(self): return k8s.ExtensionsV1beta1Api( self.client ).read_namespaced_ingress_status( name=self.name, namespace=self.namespace ) def patch(self): return k8s.ExtensionsV1beta1Api(self.client).patch_namespaced_ingress( name=self.name, body=self.resource, namespace=self.namespace ) PK!Ctulips/resource/issuer.pyfrom kubernetes import client as k8s from . import Resource class Issuer(Resource): """A `cert-manager` Issuer resource.""" version = "v1alpha1" group = "certmanager.k8s.io" plural = "issuers" def delete(self, body: k8s.V1DeleteOptions): return k8s.CustomObjectsApi( self.client ).delete_namespaced_custom_object( body=body, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, name=self.name, ) def create(self): return k8s.CustomObjectsApi( self.client ).create_namespaced_custom_object( body=self.resource, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) def status(self): return k8s.CustomObjectsApi(self.client).get_namespaced_custom_object( name=self.name, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) def patch(self): return k8s.CustomObjectsApi( self.client ).patch_namespaced_custom_object( body=self.resource, name=self.name, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) PK!D.HHtulips/resource/job.pyfrom kubernetes import client as k8s from kubernetes.client.models.v1_job import V1Job from . import Resource class Job(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.BatchV1Api(self.client).delete_namespaced_job( body=body, namespace=self.namespace, name=self.name ) def create(self) -> V1Job: return k8s.BatchV1Api(self.client).create_namespaced_job( body=self.resource, namespace=self.namespace ) def status(self) -> V1Job: return k8s.BatchV1Api(self.client).read_namespaced_job_status( name=self.name, namespace=self.namespace ) def patch(self) -> V1Job: return k8s.BatchV1Api(self.client).patch_namespaced_job_status( body=self.resource, name=self.name, namespace=self.namespace ) PK!! tulips/resource/networkpolicy.pyfrom kubernetes import client as k8s from . import Resource class NetworkPolicy(Resource): """A `Calico` network policy resource.""" version = "v1" group = "networking.k8s.io" plural = "networkpolicies" def delete(self, body: k8s.V1DeleteOptions): return k8s.CustomObjectsApi( self.client ).delete_namespaced_custom_object( body=body, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, name=self.name, ) def create(self): return k8s.CustomObjectsApi( self.client ).create_namespaced_custom_object( body=self.resource, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) def status(self): return k8s.CustomObjectsApi(self.client).get_namespaced_custom_object( name=self.name, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) def patch(self): return k8s.CustomObjectsApi( self.client ).patch_namespaced_custom_object( body=self.resource, name=self.name, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) PK!f(tulips/resource/persistentvolumeclaim.pyfrom kubernetes import client as k8s from . import Resource class PersistentVolumeClaim(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.CoreV1Api( self.client ).delete_namespaced_persistent_volume_claim( body=body, namespace=self.namespace, name=self.name ) def create(self): return k8s.CoreV1Api( self.client ).create_namespaced_persistent_volume_claim( body=self.resource, namespace=self.namespace ) def status(self): return k8s.CoreV1Api( self.client ).read_namespaced_persistent_volume_claim_status( name=self.name, namespace=self.namespace ) def patch(self): return k8s.CoreV1Api( self.client ).patch_namespaced_persistent_volume_claim_status( body=self.resource, name=self.name, namespace=self.namespace ) PK! tulips/resource/secret.pyfrom kubernetes import client as k8s from . import Resource class Secret(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.CoreV1Api(self.client).delete_namespaced_secret( body=body, namespace=self.namespace, name=self.name ) def create(self): return k8s.CoreV1Api(self.client).create_namespaced_secret( body=self.resource, namespace=self.namespace ) def status(self): return k8s.CoreV1Api(self.client).read_namespaced_secret( name=self.name, namespace=self.namespace ) def patch(self): return k8s.CoreV1Api(self.client).patch_namespaced_secret( body=self.resource, name=self.name, namespace=self.namespace ) PK!(;  tulips/resource/service.pyfrom kubernetes import client as k8s from . import Resource class Service(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.CoreV1Api(self.client).delete_namespaced_service( body=body, namespace=self.namespace, name=self.name ) def create(self): return k8s.CoreV1Api(self.client).create_namespaced_service( body=self.resource, namespace=self.namespace ) def status(self): return k8s.CoreV1Api(self.client).read_namespaced_service_status( name=self.name, namespace=self.namespace ) def patch(self): return k8s.CoreV1Api(self.client).patch_namespaced_service_status( body=self.resource, name=self.name, namespace=self.namespace ) PK!ӘEtulips/resource/statefullset.pyfrom kubernetes import client as k8s from kubernetes.client.models.v1_stateful_set import V1StatefulSet from . import Resource class StatefulSet(Resource): def delete(self, body: k8s.V1DeleteOptions): return k8s.AppsV1Api(self.client).delete_namespaced_stateful_set( body=body, namespace=self.namespace, name=self.name ) def create(self) -> V1StatefulSet: return k8s.AppsV1Api(self.client).create_namespaced_stateful_set( body=self.resource, namespace=self.namespace ) def status(self) -> V1StatefulSet: return k8s.AppsV1Api(self.client).read_namespaced_stateful_set_status( name=self.name, namespace=self.namespace ) def patch(self) -> V1StatefulSet: return k8s.AppsV1Api(self.client).patch_namespaced_stateful_set_status( body=self.resource, name=self.name, namespace=self.namespace ) PK!tulips/tulip.pyimport base64 import re import typing as t from pathlib import Path import yaml from kubernetes import client as k8s from kubernetes import config from passlib import pwd from tulips.resource import Resource, ResourceRegistry class Tulip: def __init__( self, conf: str, namespace: str, meta: t.Dict, spec_path: str, override: str = None, ) -> None: """Manages deployment. Args: conf (str): Path to Kubernetes config. namespace (str): Kubernetes namespace. meta (t.Dict): Spec variables. spec_path (str): Location of chart to deploy. override (str): Override specific resource file, defaults to `resource.override.yaml` where one has `resource.yaml`. """ self.client: k8s.ApiClient = config.new_client_from_config(conf) # Disable threadding pool inside swagger client # because it causes problems with other multithreading workers # as we don't use any async calls we are safe to do so. self.client.pool.close() self.client.pool.join() self.meta = meta self.namespace = namespace self.override = override if override else "override" self.spec_path = spec_path def create_namespace(self) -> k8s.V1NamespaceStatus: """Create namespace. Returns: k8s.V1NamespaceStatus """ body = k8s.V1Namespace( metadata=k8s.V1ObjectMeta( name=self.namespace, labels={"store_id": self.namespace} ) ) return k8s.CoreV1Api(self.client).create_namespace(body) def delete_namespace(self) -> k8s.V1NamespaceStatus: """Delete namespace. Returns: k8s.V1NamespaceStatus """ body = k8s.V1DeleteOptions( propagation_policy="Foreground", grace_period_seconds=5 ) return k8s.CoreV1Api(self.client).delete_namespace( self.namespace, body ) def resources(self, only_resource=None) -> t.Iterator[Resource]: """Deployment specification. Returns: t.Iterator[Resource]: Iterator over specifications """ maps = {"@pwd": lambda: base64.b64encode(pwd.genword(length=24))} maps.update(self.meta) path = Path(self.spec_path).joinpath("templates") for res in path.glob("*.yaml"): base_name = str(res.name)[:-5] # strip .yaml override = res.parent.joinpath( "overrides", f"{base_name}.{self.override}.yaml" ) # use override if it is found else use original file source_file = override if override.is_file() else res text = self.prepare(source_file, maps) for spec in yaml.load_all(text): cls = ResourceRegistry.get_cls(spec["kind"]) # Skip if resource in defined in only_resource if only_resource and cls not in only_resource: continue yield cls(self.client, self.namespace, spec, source_file) def prepare(self, f: Path, maps: dict) -> str: """Replace {{}} with values passed from dictionary. Arguments: f {Path} -- Path to the file. maps {dict} -- Mappings to be replaced. Returns: str -- Updated yaml string of the resource. """ text: str = f.read_text() pattern = re.compile(r"{{(?:\s+)?(.*)(?:\s+)?}}") def replace(match): name = match.groups()[0].strip() val = maps[name] if name.startswith("@"): val = val() return str(val) return re.sub(pattern, replace, text) PK!HW"TTtulips-1.2.5.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!H6Ntulips-1.2.5.dist-info/METADATAX[s6~8tGԤ,qM[Un'Mbd2HB" .3if|H;;[3\!& +l#Em 9MFӦ, gk`Z5UwMu-7q&[*zJl, kk3Md翟T~Jd2|}9 !UwiU 9G]6m#[#4jp$o&'D2cBppWK׵V+7 nHZjVZ+V-<$zKS<BK_.tpNڻAwdw9Kxb;F!jS #cuYu _ LbUY x)pպw{}#dSlcwxx2Y-jVeŨdr3d=5Q9G75I%v\&\&|~z(bGݬLW}uԐ<#z Pd "2} ^tİB7*nd->xr:xqOKxc,8Q6@{,/ w}LzLv@"Ł>N>Nmuո',zQfXEc0%և<(I.DVRa {Y5 |?( j F` faY/,= [S ` كpQSH%O`DIqǡ@_q0a' )6Lg\PҴF!$ӦxWˉjtѼ1\ZM)+,vY-fZ9m^6.YSpVɦXpEskG<42ZhU6l 7|ƣJiy3pwn> RB,( QXI*kDz뗻bGCA1˻='0q bs.yp-qS\v{eJc:Q<SŇ,vGbT]j߰E.DUp m{{" Or@@N0ûstt4~ Ff9jd]XbִL#ԲL:! !8p8T8%+7==h~~߽zmPθ!g%t|3e׆KsJG%kl7 o~]XE nqa%Cd[)@]Q徥 y`|j/ɨޜ6ֵOsb&Bl2 „Koq{5?8O]]^͌"Wld+a8,LrC|:!xZt'BS60`/3v&oRc3¹1l:H\c%5jC8-P1,Q_PK!Hc8CQtulips-1.2.5.dist-info/RECORDu˲:y? =`sـ(B$s)%/MՓIR&G?3Liߠ E4h=ۃ9M"u*~tYڴ¿I,2a!V`!OM/\#FE !J1u=*wk׸+ Fff.K/hnj;WMqHݏl&Nͧ4&ٔUe:\w1|\Pg2Gk.v þbEK&j|otd9D Y~6YyYN^DvӮjefNTD9"՛W"*"!#^[qz0)piڞB0x5]XH3;D({0n@}T:I6U&CKyל8?Z+c "̍K)@*Bfla֤UD讴u{TxO쭡b\<[J`6#ֶuLf:LiֲGxTeE>^D!/o!n.|R_c M),Ʀ!].v&'gn.XeonZ\ʆ~K[l>w02uWnn σmCsG95_ @b^.wXceݧȕjjߪ. %Ǎaq]j{`gN܍2L}3?PK!盶tulips/__init__.pyPK!Htulips/__main__.pyPK!|)y y tulips/resource/__init__.pyPK!F  !tulips/resource/configmap.pyPK! $tulips/resource/cronjob.pyPK!u(tulips/resource/deployment.pyPK!: FF ,tulips/resource/ingress.pyPK!C/tulips/resource/issuer.pyPK!D.HHz5tulips/resource/job.pyPK!! 8tulips/resource/networkpolicy.pyPK!f(>tulips/resource/persistentvolumeclaim.pyPK! Btulips/resource/secret.pyPK!(;   Ftulips/resource/service.pyPK!ӘEcItulips/resource/statefullset.pyPK!9Mtulips/tulip.pyPK!HW"TTE\tulips-1.2.5.dist-info/WHEELPK!H6N\tulips-1.2.5.dist-info/METADATAPK!Hc8CQdtulips-1.2.5.dist-info/RECORDPK?h