PK!9tulips/__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 PK!Dtulips/__main__.pyimport click import structlog from kubernetes import client as k8s from kubernetes.client.rest import ApiException from tulip import Tulip log = structlog.get_logger("tulip") __version__ = "0.0.2" @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/dz0ny/.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/dz0ny/.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(options) client = Tulip(kubeconfig, namespace, options, chart) for resource in client.resources(): click.echo(resource.resource) @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, ) if __name__ == "__main__": cli.add_command(rm) cli.add_command(push) cli.add_command(echo) cli() PK!;$``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 ) -> 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. """ self.client = client self.namespace = namespace self.resource = resource @abc.abstractmethod def create(self): """Create Resource.""" pass @abc.abstractmethod def delete(self, options: k8s.V1DeleteOptions): """Delete 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!tdN,tulips/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 ) PK!tulips/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 ) PK!r?xaatulips/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, ) PK!:b(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 ) PK!yLtulips/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 ) PK!2-Otulips/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 ) PK!}#tulips/resource/statefullset.pyfrom kubernetes import client as k8s 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): return k8s.AppsV1Api(self.client).create_namespaced_stateful_set( body=self.resource, namespace=self.namespace ) PK! tulips/tulip.py import 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 ) -> 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. """ self.meta = meta self.namespace = namespace self.spec_path = spec_path self.client: k8s.ApiClient = config.new_client_from_config(conf) def resources(self) -> t.Iterator[Resource]: """Deployment specification. Returns: t.Iterator[Resource]: Iterator over specifications """ pattern = re.compile(r"^(.*)<%=(?:\s+)?(\S*)(?:\s+)?=%>(.*)$") yaml.add_implicit_resolver("!meta", pattern) maps = {"@pwd": lambda: base64.b64encode(pwd.genword(length=24))} maps.update(self.meta) def meta_constructor(loader, node): value = loader.construct_scalar(node) start, name, end = pattern.match(value).groups() val = maps[name] if name.startswith("@"): val = val() return start + val + end yaml.add_constructor("!meta", meta_constructor) path = Path(self.spec_path).joinpath("templates") for f in path.glob("*.yaml"): # yaml parser trips at {{}} so we replace it with custom # constructor text = f.read_text().replace("{{", "<%=").replace("}}", "=%>") for spec in yaml.load_all(text): yield ResourceRegistry.get_cls(spec["kind"])( self.client, self.namespace, spec ) PK!H"6+-'tulips-1.1.7.dist-info/entry_points.txtN+I/N.,()*),V񹉙yz9\\PK!H_zTTtulips-1.1.7.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcAPK!Hutulips-1.1.7.dist-info/METADATAW[s6~8tGҬHIn8xZMbMX X RNΤهՃ-߹;̲Fj (zJ>HQh3JFh֔%1Y]s LI&Τ5zk UET㚭PeammƃJآILa[E+7hҠO\L18lуkgy ?6y4FSɌKQc01\.aRZyNp}$rJ ^jy-ѷo I *M8l RX,r?ɚL['KK81 '[PQ֒-jW^qYy@P7@/*dF6)E)+JLCF, X*[Rrت긠i/l̚V)73`wL5pk%q*.Œ[4Kc2ws VhX}weZ$; n _Zp> ܝςT(<$[HG)ijHnG+Jh72z▶}NTyCBnp;Շ t睜/Y#mg'`mwas.ypb1Z.<'^ t">S J;#\냪K-CR]% 6ࢷψυr<3l۞f!DD@M0mw{srr2!; B#'d=rut6V5-.AbZ6I6ab >.}HU#ix7rʁSWI-;}Henr`~9-ʩ9VAӟ1*\DŽw~}=^@J]]e׆KsJG%kl7 oq-\XEKnqa-Cd[)@]Q徥 ?^6Qq9mk /,:ɛ a/1'>g3^ekP̉ibkQ؏fZ0R)}WNy(po^4ODQH}m=Gojn!5&83;=@-]æ5&_\3M06XxgڠRrA9!v z#| FV4 ┵S sEuDSG.]X垶>}a{rOm :MQGØAG)k=h ơk#BKèʐ?|Z6n?&mNjm)Jac \ȉh8 6oV7 PCr/;nhoj11 Puĺ 1 CZAD7 CA 1m[Zn ?9+s̽MKEzo$^>?}b)jI%$dw iYt e#N4-3{ |bIPK!H tulips-1.1.7.dist-info/RECORDuI@tXs`׶AeB -ek(_?f'vNOo#]w(Z^cx Tlj"=S|<<";K")Ζ nTۭzbKi2y.{ BRf'ՠ}`q+`JQ:L }ex3STTJrFl۫( |׬CÝw  (ރ/"@L@5*}*Mi= b8IdŶPӜƺL \;4r)o`Qw_] ssKF"0gjQ<+-J1+]#6@_ grSx*1wP߀M"L;-}i:W3› ;a챙7ILPy_С*񴮝/;~{ ?h(0.煙>Vᛤ/~_f,Y3͙"FVѕQ%yqIx`RX,pUjpI%ӗGwB釣Tms7\ Ik҃>Iys Ͱ׳:쌳VC[sҍ=$~"-ݓ5ٓꇱؓqa/7hnf&;{NDT菠utuh/PK!9tulips/__init__.pyPK!Dtulips/__main__.pyPK!;$``tulips/resource/__init__.pyPK!tdN,ztulips/resource/deployment.pyPK!utulips/resource/ingress.pyPK!r?xaaztulips/resource/issuer.pyPK!:b("tulips/resource/persistentvolumeclaim.pyPK!yLi$tulips/resource/secret.pyPK!2-OT&tulips/resource/service.pyPK!}#C(tulips/resource/statefullset.pyPK! E*tulips/tulip.pyPK!H"6+-'*2tulips-1.1.7.dist-info/entry_points.txtPK!H_zTT2tulips-1.1.7.dist-info/WHEELPK!Hu(3tulips-1.1.7.dist-info/METADATAPK!H :tulips-1.1.7.dist-info/RECORDPKI=