PK!Qyytulips/__init__.pyimport importlib import typing as t from .Resource import Resource from kubernetes import client as k8s def class_for_resource( kind: str ) -> t.Callable[[k8s.ApiClient, str, dict], Resource]: """Return Kubernetes ResourceDefintion class. Args: kind (str): Resource name. Raises: ImportError: Resource is not defined. Example: Missing persistentvolumeclaim.py AttributeError: Resource has no Resource class. Example: Missing persistentvolumeclaim.PersistentVolumeClaim Returns: t.Callable[Resource]: Resource operator. """ # load the module, will raise ImportError if module cannot be loaded m = importlib.import_module( f".resource.{kind.lower()}", package=__package__ ) # get the class, will raise AttributeError if class cannot be found c = getattr(m, kind) return c PK!)ԇ tulips/__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", 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 = 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 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() PK!FHtulips/resource/__init__.pyimport abc from kubernetes import client as k8s class Resource(metaclass=abc.ABCMeta): """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): pass @abc.abstractmethod def delete(self, options: k8s.V1DeleteOptions): pass @property def name(self): 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/tulip.py import io import re import typing as t import yaml from kubernetes import client as k8s from kubernetes import config from passlib import pwd from tulips import class_for_resource from tulips.resource import Resource 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: 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_resource(spec["kind"])( self.client, self.namespace, spec ) PK!H"6+-'tulips-0.1.3.dist-info/entry_points.txtN+I/N.,()*),V񹉙yz9\\PK!HƣxSTtulips-0.1.3.dist-info/WHEEL A н#J;/"d&F]xzw>@Zpy3F ]n2H%_60{8&baPa>PK!Hlo? tulips-0.1.3.dist-info/METADATAVms8_$@)]i ͐aVbMeHroW%z+<ϸa4z{,[ǃ WEj-/Ka[S DX-pQʕύfQ2t^o.}^%qj^WֹLv|{6aG!͵ krޜER !Y%U${/TҢ`y/}vsr&jlp38*Kk"w4~%2fnyQH=sC==U|7!H㧣Х@{!ҝo15Aӈ2q=X,]ݏ|~bSֿVb`J&,ڻ㼭R̼qn:u"\jevѱMV%qR:3KoURe0Ws%|!bX 3b;dd^)du#)yr)Tbiz 梗"^?HDA 9evx,z,RԐ"&rd#D4x}u:#Fx#ejxtw|XTTcaRw>;Wp`y{I\9N,u YӜ[b4>+@ a;O>s(sKS! U4j\ Q7ˌSc+(lRZ@p `O~EVz+SRrmRvɸ*Kc=Fqd o;f* Lo'TfU`e,>9\8KgUB7G8h8FU%2Wd鴙 ͬ)yJjfUiښ1k$]39g5$byU]U}퐊^:HEWEXu;NMLGhQ#wεYN G`qvg4Z $;J.|U^twqxn. %ZIxc,Drؤ/(*)ǡ z8T!{M+6uE{{z^ `pmvy1]ӣ>a\S),Brsp[Ԣ;-EXAn_h])on5[@ZL`hMNFgR}ifJvhRƓ. yݜ qXU]3Xr*,$@=v.пbvf52&ǯ"`/J-͂П2C E- j͵~ȅHKx"T(l(ǽI8-!b- P7.Mh)YHa4N  xnn"ZkAdV: ݔЫWNAX??d`=!JZ`\ݟ#C8:HJ;60}-a=o7քEث nLsx&CRSNh'~0EOݍNq)RWxgBXآ8$UaH92+y-PlX7ZXx5[6.==m.Sxk L=;/N.h^!0.)CuB^n -*[PK!HᛟTtulips-0.1.3.dist-info/RECORDuɒ@< :a 4 R*@x1&;py?I_L@$n%eWp]o,Kz1 |>]ֻ{0s]mf|JSS#Ij:{狊VvV͘nic{7GS,0Qs;;wG懕 ;Du6VGSv4%s j4 U)AˍtST;)gyMH8>%|;6CNgU[Ӫo&{ 7_/=˱~k f5XcP&dT)"DC[5Ȫ+v܅3EՉq%b'w)_| 2; [eun@e[|L\fN)w+)? 0ObzIp'q+lf9iHa+"Ois![<в5>0 U{%㇅S̊: z,e(qMI†%O_oQ;w٬yOآX I3: 顩WX=k$/wo"suc6\ƹv|;Z9( B;W(e,YtkEO<}Q7$e:/ȅZRc~ 7PK!Qyytulips/__init__.pyPK!)ԇ tulips/__main__.pyPK!FH`tulips/resource/__init__.pyPK!tdN,Atulips/resource/deployment.pyPK!<tulips/resource/ingress.pyPK!r?xaaAtulips/resource/issuer.pyPK!:b(tulips/resource/persistentvolumeclaim.pyPK!yL0tulips/resource/secret.pyPK!2-Otulips/resource/service.pyPK!ݚ !tulips/tulip.pyPK!H"6+-''tulips-0.1.3.dist-info/entry_points.txtPK!HƣxSTb(tulips-0.1.3.dist-info/WHEELPK!Hlo? (tulips-0.1.3.dist-info/METADATAPK!HᛟTk/tulips-0.1.3.dist-info/RECORDPKE2