PK!aXNNtulips/__init__.pyimport importlib import typing as t from .kind import Kind from kubernetes import client as k8s def class_for_kind(kind: str) -> t.Callable[[k8s.ApiClient, str, dict], Kind]: """Return Kubernetes ResourceDefintion class. Args: kind (t.AnyStr): Resource name. Raises: ImportError: Resource is not defined. Example: Missing persistentvolumeclaim.py AttributeError: Resource has no Kind class. Example: Missing persistentvolumeclaim.PersistentVolumeClaim Returns: t.Callable[Kind]: Resource operator. """ # load the module, will raise ImportError if module cannot be loaded m = importlib.import_module(f".kind.{kind.lower()}", package=__package__) # get the class, will raise AttributeError if class cannot be found c = getattr(m, kind) return c PK!wL* tulips/__main__.pyimport click import structlog from kubernetes import client as k8s from kubernetes.client.rest import ApiException from tulip import Helm log = structlog.get_logger("tulip") __version__ = "0.0.1" @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", type=click.Path(exists=True), ) @click.pass_context def push(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 = Helm(kubeconfig, namespace, options, chart) for chart in client.specs(): try: chart.create() log.info("Created", name=chart.name, kind=chart.__class__.__name__) except ApiException as e: log.error( "Failed creating", name=chart.name, kind=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 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 = Helm(kubeconfig, namespace, options, chart) delete = k8s.V1DeleteOptions( propagation_policy="Foreground", grace_period_seconds=5 ) for chart in client.specs(): try: chart.delete(body=delete) log.info("Deleted", name=chart.name, kind=chart.__class__.__name__) except ApiException as e: log.error( "Failed deleting", name=chart.name, kind=chart.__class__.__name__, reason=e.reason, ) if __name__ == "__main__": cli.add_command(rm) cli.add_command(push) cli() PK! tulips/kind/__init__.pyimport abc from kubernetes import client as k8s class Kind(metaclass=abc.ABCMeta): """Kind is interface that describes Kubernetes Resource defintion.""" chart: dict client: k8s.ApiClient namespace: str def __init__( self, client: k8s.ApiClient, namespace: str, chart: dict ) -> None: """Initializes chart or CRD. Args: client (k8s.ApiClient): Instance of the Kubernetes client. namespace (str): Namespace where Workload should be deployed. chart (dict): Kubernetes chart or CRD. """ self.client = client self.namespace = namespace self.chart = chart @abc.abstractmethod def create(self): pass @abc.abstractmethod def delete(self, options: k8s.V1DeleteOptions): pass @property def name(self): return self.chart["metadata"]["name"] PK!Xtulips/kind/deployment.pyfrom kubernetes import client as k8s from . import Kind class Deployment(Kind): 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.chart, namespace=self.namespace ) PK! tulips/kind/ingress.pyfrom kubernetes import client as k8s from . import Kind class Ingress(Kind): 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.chart, namespace=self.namespace ) PK!VVtulips/kind/issuer.pyfrom kubernetes import client as k8s from . import Kind class Issuer(Kind): """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.chart, namespace=self.namespace, version=self.version, group=self.group, plural=self.plural, ) PK! $tulips/kind/persistentvolumeclaim.pyfrom kubernetes import client as k8s from . import Kind class PersistentVolumeClaim(Kind): 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.chart, namespace=self.namespace ) PK!5ɖtulips/kind/secret.pyfrom kubernetes import client as k8s from . import Kind class Secret(Kind): 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.chart, namespace=self.namespace ) PK!>Itulips/kind/service.pyfrom kubernetes import client as k8s from . import Kind class Service(Kind): 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.chart, namespace=self.namespace ) PK![  tulips/tulip.py import io import re import typing as t import structlog import yaml from kubernetes import client as k8s from kubernetes import config from passlib import pwd from tulips import class_for_kind from tulips.kind import Kind log = structlog.get_logger("helm") __version__ = "0.0.2" class Helm: def __init__( self, conf: str, namespace: str, meta: t.Dict, spec_path: str ) -> None: """Manages deployment. Args: conf (t.AnyStr): Path to Kubernetes config. namespace (t.AnyStr): Kubernetes namespace. meta (t.Dict): Spec variables spec_path (t.AnyStr): Location of chart to deploy. """ self.meta: dict = meta self.namespace: str = namespace self.spec_path: str = spec_path self.client: k8s.ApiClient = config.new_client_from_config(conf) def specs(self) -> t.Iterator[Kind]: """Deployment specification. Returns: t.Iterator[Kind]: Iterator over specifications """ pattern = re.compile(r"^(.*)<%=(?:\s+)?(\S*)(?:\s+)?=%>(.*)$") yaml.add_implicit_resolver("!meta", pattern) maps = {"@pwd": lambda: pwd.genword(length=16)} 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) with io.open(self.spec_path) as f: for spec in yaml.load_all(f.read()): yield class_for_kind(spec["kind"])( self.client, self.namespace, spec ) PK!H"6+-'tulips-0.1.2.dist-info/entry_points.txtN+I/N.,()*),V񹉙yz9\\PK!HƣxSTtulips-0.1.2.dist-info/WHEEL A н#J;/"d&F]xzw>@Zpy3F ]n2H%_60{8&baPa>PK!HS/% tulips-0.1.2.dist-info/METADATAVmsF~b;б3 i:0I`vL!'im]}ҩbc~}N0H{vݽg|gj#T9~coxNʰN7}6uEf5*reK.ѢR)SmlJv * t: asĩ*:ٗn4\"|Ɔ%~V9o",ĕF"z$>:|4ƈ@`9 Jf۷L&Z-4/ Q. Gx|ߞ(>>a7) c}~.&<]zH8~qY<xKGWc/?9}VQbR$ֻ?1VJhm:EjQY]4R%5EM5mԺdꧫ?L-|lof5_ Rج%i~RpcQ|ف )Ħ7X&(3 IxNJ`~>=ڽvEh`YCc)6 1E{{:gݧ b !BYmQ Fz/Fg,O[0fu!K cpM&X"́hbY^fksmML}aJČ|o;#J"؜[X+Gȳ^CLNP,*+X@"1LKeOz֔'DHa7Z QH𴠤ٖsL]U)m))%F%k*qXI)f1EIGo傺҄T<7Q ^0>NIW M0Q ^}z]__7Q=).BVEs@iJ|١!7<,04g햨RT|JX'wX_[PSU"wj-K OZιH+-(-҄y0rP׼Q CQCw0 *o쌔AE3!%8!Ivi"8hu:g1D/O_M]'G @ Ny`ŵV\Cz0uǵhEk h7v٫f^RSPf rpv0ԡԙ{B^ZS:; ? [ =>7P iPc墤Uhwg_0weBPK!Hdb=8tulips-0.1.2.dist-info/RECORDuɲX}= Z,z!ʨ@8AQx6:VhEU3r\w?u=]RpsVdxȢ"y4/Z%F͏K*B\~H^vJ{mi_[I)_h7*PQ$]BPM=puBU4lcӹmo4ma[I惋QWs ;O:ON:m اX 32%E-zp(]djoUQ5 =ЏPq4jU3fe8|.'-H3/wkK1TSq(p Ki1v0qڙ!Gkj[^|EA]KAgGmxo^^퓺8=BU# 헊:O`JWwz-N0r47< BεdhI_PK!aXNNtulips/__init__.pyPK!wL* ~tulips/__main__.pyPK! tulips/kind/__init__.pyPK!Xktulips/kind/deployment.pyPK! Wtulips/kind/ingress.pyPK!VVMtulips/kind/issuer.pyPK! $tulips/kind/persistentvolumeclaim.pyPK!5ɖtulips/kind/secret.pyPK!>Itulips/kind/service.pyPK![  tulips/tulip.pyPK!H"6+-''tulips-0.1.2.dist-info/entry_points.txtPK!HƣxST'tulips-0.1.2.dist-info/WHEELPK!HS/%  (tulips-0.1.2.dist-info/METADATAPK!Hdb=8,tulips-0.1.2.dist-info/RECORDPK/