From d2b0f9c388f0ae6bda8865efc9e185d5ce83a696 Mon Sep 17 00:00:00 2001 From: sungil Date: Mon, 26 Aug 2024 15:04:10 +0000 Subject: [PATCH] new render --- Dockerfile | 41 -- docker-build.sh | 8 + helm2yaml/applib/helm.py | 487 ++++++++---------- helm2yaml/applib/helmdeploy.py | 179 +++++++ helm2yaml/applib/parameter_aware_yamlmerge.py | 64 +++ helm2yaml/applib/repo.py | 79 ++- helm2yaml/check_repo.py | 4 +- helm2yaml/template | 43 ++ package/Dockerfile | 26 + 9 files changed, 564 insertions(+), 367 deletions(-) delete mode 100755 Dockerfile create mode 100755 docker-build.sh create mode 100644 helm2yaml/applib/helmdeploy.py create mode 100644 helm2yaml/applib/parameter_aware_yamlmerge.py create mode 100755 helm2yaml/template create mode 100755 package/Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100755 index 6b66647..0000000 --- a/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM harbor.taco-cat.xyz/tks/decapod-kustomize:v2.0.1 AS gobuilder -LABEL AUTHOR sungil(usnexp@gmail.com) - -FROM python:3.10.5-alpine3.16 AS builder -LABEL AUTHOR sungil(usnexp@gmail.com) - -ENV HELM_VER v3.9.0 -ENV GH_VER 2.11.3 - -RUN apk update && apk add curl -RUN curl -sL -o helm.tar.gz https://get.helm.sh/helm-${HELM_VER}-linux-amd64.tar.gz && \ - tar xf helm.tar.gz && \ - mv linux-amd64/helm /usr/local/bin/helm - -RUN curl -sL -o gh.tar.gz https://github.com/cli/cli/releases/download/v${GH_VER}/gh_${GH_VER}_linux_amd64.tar.gz && \ - tar xf gh.tar.gz && \ - mv gh_2.11.3_linux_amd64/bin/gh /usr/local/bin/gh - -FROM python:3.10.5-alpine3.16 -LABEL AUTHOR sungil(usnexp@gmail.com) - -ENV PATH /root/helm2yaml:$PATH - -USER root -RUN mkdir -p /root/.config/kustomize/plugin/openinfradev.github.com/v1/helmvaluestransformer -RUN apk add --no-cache bash git curl -COPY --from=gobuilder /root/.config/kustomize/plugin/openinfradev.github.com/v1/helmvaluestransformer/HelmValuesTransformer.so /root/.config/kustomize/plugin/openinfradev.github.com/v1/helmvaluestransformer/ -COPY --from=gobuilder /usr/local/bin/kustomize /usr/local/bin/kustomize - -COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm -COPY --from=builder /usr/local/bin/gh /usr/local/bin/gh - -WORKDIR /root -COPY . ./ -RUN pip install -r requirements.txt - -CMD [ "/root/helm2yaml/helm2yaml" ] -# sktcloud/decapod-render:v3.0.0 (23.8.14) -# - support oci protocol for a helm repository -# - generate CRDs using helm cli -# Build CLI: docker build --network host -t siim/helm2yaml:v3.0.0 . diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..61e2cf0 --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,8 @@ +set -ex + +VER=v3.0.0 +IMG=harbor.taco-cat.xyz/tks/decapod-render:${VER} + +docker build . -f package/Dockerfile --network host --tag ${IMG} +# docker build ./package --network host --tag $IMG +sudo docker push $IMG diff --git a/helm2yaml/applib/helm.py b/helm2yaml/applib/helm.py index 93f743d..b29521f 100755 --- a/helm2yaml/applib/helm.py +++ b/helm2yaml/applib/helm.py @@ -1,311 +1,250 @@ -import yaml,os +import os +import sys +from pickle import FALSE +from typing import List, Optional, Dict, Any +import yaml from .repo import Repo, RepoType class Helm: - def __init__(self, repo, name, namespace, override): + def __init__(self, repo: Repo, name: str, namespace: str, override: Dict[str, Any]): self.repo = repo self.name = name self.namespace = namespace self.override = override - def checkPrerequisitions(self): - # stream = os.popen('kubectl get ns {}' - # .format(self.namespace)) - # try: - # return stream.read().rsplit("STATUS:")[1].split()[0].strip() - # except IndexError as exc: - # return None - return True - - def toString(self): - return('[HELM: {}, {}, {}, {}]'.format( self.repo.toString(), - self.name, - self.namespace, - self.override)) - - def autoApplyPrerequisitions(self): - return True - - def install(self, verbose=False, kubeconfig='~/.kube/config'): - if self.getStatus() == None: - self.repo.install(self.name, self.namespace, self.override, verbose, kubeconfig) + def __str__(self) -> str: + return f'[HELM: {self.repo}, {self.name}, {self.namespace}, {self.override}]' + + def install(self, kubeconfig: str = '~/.kube/config', verbose: bool = False, target_namespace: str = '') -> str: + self._dump_override() + self.namespace = target_namespace or self.namespace + print( + f'[install {self.repo.chart()} from {self.repo.repository()} as {self.name} in {self.namespace} using {kubeconfig}]') + + if self.repo.repotype == RepoType.HELMREPO: + self._install_from_helm_repo(kubeconfig) + elif self.repo.repotype == RepoType.GIT: + self._install_git_repo(kubeconfig, verbose) else: - print("{} in the namespace-{} already installed".format(self.name, self.namespace)) + print(f'(WARN) Unsupported repo type: {self.repo.repotype}') - def uninstall(self, verbose=False): - print('helm delete -n {} {} | grep status' - .format(self.namespace, self.name, verbose)) + os.system(f'rm {self.name}.vo') - def getStatus(self): - stream = os.popen('helm status -n {} {}' - .format(self.namespace, self.name)) - try: - return stream.read().rsplit("STATUS:")[1].split()[0].strip() - except IndexError as exc: - return None + return f'{self.name}.plain.yaml' - def getStatusfull(self): - os.system('helm status -n {} {}' - .format(self.namespace, self.name)) + def _dump_override(self): + with open(f'{self.name}.vo', 'w') as f: + yaml.dump(self.override, f, default_flow_style=False) - def template(self): - self.repo.template(self.name, self.namespace, self.override) + def _install_from_helm_repo(self, kubeconfig: str): + cmd = f'helm upgrade --install -n {self.namespace} {self.name} --repo {self.repo.repository()} {self.repo.chart()} --version {self.repo.version()} -f {self.name}.vo --kubeconfig {kubeconfig} --create-namespace' + if os.system(cmd): + print(f'FAILED!! {self.name} cannot be installed due to the errors above.') + print(f'- overrides: {self.override}') + sys.exit(-1) - def __imageName(self, fullname): - if fullname.find('/') < 0: - return fullname + def _install_git_repo(self, kubeconfig: str, verbose: bool): + self._clone_repo_and_dependency_update(verbose) + cmd = f'helm upgrade --install -n {self.namespace} {self.name} .temporary-clone/{self.repo.chartOrPath} -f {self.name}.vo --kubeconfig {kubeconfig}' + if verbose > 0: + print(f'(DEBUG) install helm:{cmd}') + os.system(cmd) + os.system('rm -rf .temporary-clone') + + def _clone_repo_and_dependency_update(self, verbose: bool): + cmd = f'git clone -b {self.repo.versionOrReference} {self.repo} .temporary-clone' + if verbose: + print(f'(DEBUG) {cmd}') + os.system(cmd) + else: + os.system(f'{cmd} t; cat t | grep fatal; rm t') + os.system(f'helm dependency update .temporary-clone/{self.repo.chartOrPath}') - return fullname.rsplit('/', 1)[1] + def uninstall(self, verbose: bool = False): + cmd = f'helm delete -n {self.namespace} {self.name} | grep status' + if verbose: + print(cmd) + os.system(cmd) - def __replaceImages(self, parsed, local_repository, verbose=0): - if parsed is not None and 'spec' in parsed: - spec = parsed['spec'] - if 'template' in spec: - template = spec['template'] - if 'spec' in template: - spec = template['spec'] - if 'containers' in spec: - for container in spec['containers']: - if verbose > 0: - print("Case 1 - container: {}".format(container['image'])) - container['image']=local_repository+'/'+self.__imageName(container['image']) - if verbose > 0: - print('changed '+container['image']) - if 'initContainers' in spec: - for initcontainer in spec['initContainers']: - if verbose > 0: - print("Case 2 - Init container: {}".format(initcontainer['image'])) - initcontainer['image']=local_repository+'/'+self.__imageName(initcontainer['image']) - if verbose > 0: - print('changed '+initcontainer['image']) - if 'containers' in spec: - for container in spec['containers']: - if verbose > 0: - print("Case 3 - spec container: {}".format(container['image'])) - container['image']=local_repository+'/'+self.__imageName(container['image']) - if verbose > 0: - print('changed '+container['image']) - if 'image' in spec: - if verbose > 0: - print("Case 4 - spec image: {}".format(spec['image'])) - spec['image']=local_repository+'/'+self.__imageName(spec['image']) - if verbose > 0: - print('changed '+spec['image']) + def get_status(self, kubeconfig: str = '~/.kube/config') -> Optional[str]: + stream = os.popen(f'helm status -n {self.namespace} {self.name} --kubeconfig {kubeconfig}') + try: + return stream.read().rsplit("STATUS:")[1].split()[0].strip() + except IndexError: + return None - def toSeperatedResources(self, targetdir='/output', verbose=False, local_repository=None): - target = '{}/{}/'.format(targetdir, self.name) - os.system('mkdir -p {}'.format(target)) + def get_status_full(self): + os.system(f'helm status -n {self.namespace} {self.name}') - genfile=self.genTemplateFile(verbose) + # local_repository 가 정의되면 생성되는 자원에서 이미지 주소의 repository를 주어진 것으로 무조건 변경함 + def to_separated_resources(self, target_base_dir: str = '/output', verbose: bool = False, + local_repository: Optional[str] = None): + target = f'{target_base_dir}/{self.name}/' + os.makedirs(target, exist_ok=True, mode=0o755) - if verbose > 0: - print('(DEBUG) seperate all-in-one yaml into each resource yaml') - os.system('mv {} {}'.format(genfile, target)) - - # Split into each k8s resource yaml - splitcmd = "awk '{f=\""+target+"/_\" NR; print $0 > f}' RS='\n---\n' "+target+genfile - os.system(splitcmd) - os.system('rm {0}{1}'.format(target,genfile)) - - isCrd=self.name.endswith('-crds') - if isCrd: - for entry in os.scandir(target): - if entry.is_file(): - splitcmd = "awk '{f=\""+target+"/"+entry.name+"-\" NR; print $0 > f}' RS='' "+target+"/"+entry.name - os.system(splitcmd) - os.system('rm {0}{1}'.format(target,entry.name)) - - # Rename yaml to "KIND_RESOURCENAME.yaml" - if verbose > 0: - print('(DEBUG) rename resource yaml files') + if self.name.endswith('-crds'): # very conner case....... + self._handle_crd_only_connercase_solver(target_base_dir, verbose) + else: + self._handle_general_app(target, verbose, local_repository) + + def _handle_crd_only_connercase_solver(self, target_base_dir: str, verbose: bool): + # helm template을 통해 생성되는 자원에서 crd가 포함되지 않으므로 수동으로 해주는 프로세스가 필요. + print(f'[Copy CRD yamls for {self.name} from {self.repo.repository()}/{self.repo.chart()}]') + + # self._pull_helm_chart(verbose) + cmd = f'helm pull --repo {self.repo.repository()} {self.repo.chart()} --version {self.repo.version()} | grep -i error' + if verbose: + print(f'(DEBUG) Pull helm chart: {cmd}') + os.system(cmd) + + # self._extract_helm_chart(verbose) + cmd = f'tar xf {self.repo.chart()}-{self.repo.version()}.tgz' + if verbose: + print(f'(DEBUG) Extract helm chart: {cmd}') + os.system(cmd) + + os.system(f'cp {self.repo.chart()}/crds/* {target_base_dir}/{self.name}/') + os.system(f'rm -rf ./{self.repo.chart()}') + + def _handle_general_app(self, target: str, verbose: bool, local_repository: Optional[str]): + genfile = self.gen_template_file(verbose) + if genfile == None: + print(f'!!!!! Error to generate template using {self}') + cmd = f'mv {genfile} {target}{genfile}' + os.system(cmd) + # os.rename(genfile, f'{target}{genfile}') + if verbose: + print(f'(DEBUG) splite {genfile}') + self._split_yaml(target, f'{target}{genfile}') + if verbose: + print(f'(DEBUG) rename {genfile}') + self._rename_yaml_files(target, verbose, local_repository) + + def _split_yaml(self, target: str, genfile: str): + cmd = f"awk '{{f=\"{target}_\" NR; print $0 > f}}' RS='\\n---\\n' {genfile}" + os.system(cmd) + os.remove(f'{genfile}') + + def _rename_yaml_files(self, target: str, verbose: bool, local_repository: Optional[str]): for entry in os.scandir(target): - refinedname ='' + self._process_yaml_file(entry, target, verbose, local_repository) + + # rename upon metadata + def _process_yaml_file(self, entry, target: str, verbose: bool, local_repository: Optional[str]): + try: with open(entry, 'r') as stream: - try: - parsed = yaml.safe_load(stream) - refinedname = '{}_{}.yaml'.format(parsed['kind'],parsed['metadata']['name']) - - if(local_repository!=None): - self.__replaceImages(parsed, local_repository) - except yaml.constructor.ConstructorError as exc: - # Very very tricky logic to avoid a exception on some crds (alertmanagerconfigs.monitoring.coreos.com) - lines = open(entry, 'r').readlines() - rkind=rname='' - for line in lines: - if 'kind: ' in line: - rkind=line.split('kind: ')[-1].strip() - if 'name: ' in line: - rname=line.split('name: ')[-1].strip() - os.rename(entry, target+'/'+ '{}_{}.yaml'.format(rkind, rname)) - break - continue - except yaml.YAMLError as exc: - print('(WARN)',exc,":::", parsed) - except TypeError as exc: - if os.path.getsize(entry)>80: - print('(WARN)',exc,":::", parsed) - if verbose > 0: - print("(DEBUG) Contents in the file :", entry.name) - print(stream.readlines()) - if (refinedname!=''): - with open(target+refinedname, 'w') as file: - documents = yaml.dump(parsed, file) - if os.path.exists(target+refinedname): + meta = self._read_yaml_until_spec(stream) + parsed = next(yaml.safe_load_all(meta), None) + stream.close() + if parsed is None: os.remove(entry) - else: - os.rename(entry, target+'/'+refinedname) - elif os.path.exists(entry): + return + refined_name = f'{parsed["kind"]}_{parsed["metadata"]["name"]}.yaml' + cmd=f'mv {entry.path} {target}{refined_name}' + os.system(cmd) + # if local_repository: + # self._replace_images(parsed, local_repository, verbose) + # with open(f'{target}{refined_name}', 'w') as file: + # yaml.dump(parsed, file) + # os.remove(entry) + except (yaml.YAMLError, TypeError, KeyError) as exc: + if os.path.getsize(entry) > 80: + print(f'(WARN) {exc} ::: {parsed}') + if verbose: + print(f'(DEBUG) Contents in the file : {entry.name}') + with open(entry, 'r') as stream: + print(stream.read()) + else: os.remove(entry) - def get_image_list(self, verbose=False): + def _read_yaml_until_spec(self, file): + buffer = [] + for line in file: + if line.strip() == 'spec:': + break + buffer.append(line) + return ''.join(buffer) + + def _replace_images(self, parsed: Dict[str, Any], local_repository: str, verbose: int = 0): + spec = parsed.get('spec', {}) + template = spec.get('template', {}) + containers = template.get('spec', {}).get('containers', []) + template.get('spec', {}).get('initContainers', + []) + spec.get('containers', []) + + for container in containers: + if 'image' in container: + old_image = container['image'] + container['image'] = f"{local_repository}/{self._image_name(old_image)}" + if verbose > 0: + print(f'changed {old_image} to {container["image"]}') + + if 'image' in spec: + old_image = spec['image'] + spec['image'] = f"{local_repository}/{self._image_name(old_image)}" + if verbose > 0: + print(f'changed {old_image} to {spec["image"]}') - # For crd-only argoCD app, just copy crd files into output directory + @staticmethod + def _image_name(fullname: str) -> str: + return fullname.rsplit('/', 1)[-1] if '/' in fullname else fullname + + def get_image_list(self, verbose: bool = False) -> List[str]: if self.name.endswith('-crds'): print(f'[do nothing for {self.name}]') return [] - fd=open(self.genTemplateFile(verbose)) - image_list=[] - - for parsed in list(yaml.load_all(fd, Loader=yaml.loader.SafeLoader)): - if parsed is not None and 'spec' in parsed: - spec = parsed['spec'] - if 'template' in spec: - template = spec['template'] - if 'spec' in template: - spec = template['spec'] - if 'containers' in spec: - for container in spec['containers']: - if verbose > 0: - print("Case 1 - container: {}".format(container['image'])) - image_list.append(container['image']) - if 'initContainers' in spec and spec.get('initContainers',False): - for initcontainer in spec['initContainers']: - if verbose > 0: - print("Case 2 - Init container: {}".format(initcontainer['image'])) - image_list.append(initcontainer['image']) - if 'containers' in spec and spec.get('containers',False): - for container in spec['containers']: - if verbose > 0: - print("Case 3 - spec container: {}".format(container['image'])) - image_list.append(container['image']) - if 'image' in spec: - if verbose > 0: - print("Case 4 - spec image: {}".format(spec['image'])) - image_list.append(spec['image']) - + image_list = [] + with open(self.gen_template_file(verbose)) as fd: + for parsed in yaml.safe_load_all(fd): + image_list.extend(self._extract_images(parsed, verbose)) return image_list - def genTemplateFile(self, verbose=0): - # For crd-only argoCD app, using other cmd 'helm show crds' - isCrd=self.name.endswith('-crds') - - yaml.dump(self.override, open('vo', 'w') , default_flow_style=False) - print('[Generate resource yamls for {} from {}/{} in {}]'. - format(self.name, self.repo.repository(), self.repo.chart(), self.namespace)) - - if self.repo.repotype == RepoType.HELMREPO: - if isCrd: - helmcmd='helm show crds --repo {2} {3} --version {4} > {1}.plain.yaml'.format(self.namespace, self.name, self.repo.repository(), self.repo.chart(), self.repo.version()) - else: - helmcmd='helm template -n {0} {1} --repo {2} {3} --version {4} -f vo > {1}.plain.yaml'.format(self.namespace, self.name, self.repo.repository(), self.repo.chart(), self.repo.version()) - # Generate template file - if verbose > 0: - print('(DEBUG) gernerate template file') - print(self.toString()) - print(helmcmd) - - os.system(helmcmd) - - elif self.repo.repotype == RepoType.GIT: - # prepare repository - if verbose > 0: - print('(DEBUG) git clone -b {0} {1} .temporary-clone'.format(self.repo.reference(), self.repo.getUrl())) - os.system('git clone -b {0} {1} .temporary-clone' - .format(self.repo.reference(), self.repo.getUrl())) - if verbose > 2: - print('(DEBUG) Cloned dir :') - os.system('ls -al .temporary-clone') - else: - os.system('git clone -b {0} {1} .temporary-clone t; cat t | grep fatal; rm t' - .format(self.repo.reference(), self.repo.getUrl())) - - os.system('helm dependency update .temporary-clone/{}'.format(self.repo.path())) - # generate template file - if verbose > 0: - print('(DEBUG) gernerat a template file') - os.system('helm template -n {0} {1} .temporary-clone/{2} -f vo > {1}.plain.yaml (or show crds)' - .format(self.namespace, self.name, self.repo.path())) - - if isCrd: - helmcmd='helm show crds .temporary-clone/{2} > {1}.plain.yaml'.format(self.namespace, self.name, self.repo.path()) - else: - helmcmd='helm template -n {0} {1} .temporary-clone/{2} -f vo > {1}.plain.yaml'.format(self.namespace, self.name, self.repo.path()) - os.system(helmcmd) - - # clean reposiotry - os.system('rm -rf .temporary-clone') - # os.system('mv .temporary-clone '+name) - else: - print('(Error) Wrong repo type!') - print('(Error) Name: '+self.name) - return (self.name+'.plain.yaml') - - def dump(self, name, namespace, override, targetdir='/cd', verbose=False): - yaml.dump(override, open('vo', 'w') , default_flow_style=False) - print('[generate resource yamls {} from {} as {} in {}]'. - format(self.chart(), self.repository(), name, namespace)) - - if self.repotype == RepoType.HELMREPO: - os.system('mkdir -p {}/{}'.format(targetdir, name)) - - if verbose > 0: - print('(DEBUG) gernerate a template file') + def _extract_images(self, parsed: Dict[str, Any], verbose: bool) -> List[str]: + images = [] + if parsed and 'spec' in parsed: + spec = parsed['spec'] + template = spec.get('template', {}) + template_spec = template.get('spec', {}) - if name.endswith('-operator'): - os.system('helm template -n {0} {1} --repo {2} {3} --version {4} -f vo --include-crds > {5}/{1}.plain.yaml' - .format(namespace, name, self.repo.repository(), self.chart(), self.version(), targetdir)) - else: - os.system('helm template -n {0} {1} --repo {2} {3} --version {4} -f vo > {5}/{1}.plain.yaml' - .format(namespace, name, self.repo.repository(), self.chart(), self.version(), targetdir)) + for container in template_spec.get('containers', []) + template_spec.get('initContainers', []) + spec.get( + 'containers', []): + if 'image' in container: + images.append(container['image']) + if verbose: + print(f"Container image: {container['image']}") - if verbose > 0: - print('(DEBUG) seperate the template file') - target = '{}/{}'.format(targetdir, name) - splitcmd = "awk '{f=\""+target+"/_\" NR; print $0 > f}' RS='\n---\n' "+target+".plain.yaml" - os.system(splitcmd) + if 'image' in spec: + images.append(spec['image']) + if verbose: + print(f"Spec image: {spec['image']}") - if verbose > 0: - print('(DEBUG) rename resource yaml files') - for entry in os.scandir(target): - refinedname ='' - with open(entry, 'r') as stream: - try: - parsed = yaml.safe_load(stream) - refinedname = '{}_{}.yaml'.format(parsed['kind'],parsed['metadata']['name']) - except yaml.YAMLError as exc: - print('(WARN)',exc,":::", parsed) - except TypeError as exc: - if os.path.getsize(entry)>80: - print('(WARN)',exc,":::", parsed) - if verbose > 0: - print("(DEBUG) Contents in the file :", entry.name) - print(stream.readlines()) - if (refinedname!=''): - os.rename(entry, target+'/'+refinedname) - else: - os.remove(entry) + return images - # os.system("""awk '{f="tmp/{0}/_" NR; print $0 > f}' RS='---' tmp/{0}.plain.yaml""".format(name)) - os.system("rm {}/{}.plain.yaml".format(targetdir, name)) - elif self.repotype == RepoType.GIT: - if verbose > 0: - print('git clone -b {0} {1} temporary-clone'.format(self.versionOrReference, self.getUrl())) - os.system('git clone -b {0} {1} temporary-clone t; cat t | grep fatal; rm t' - .format(self.versionOrReference, self.getUrl())) + def gen_template_file(self, verbose: int = 0) -> str: + self._dump_override() + print( + f'[Generate resource yamls for {self.name} from {self.repo.repository()}/{self.repo.chart()} in {self.namespace}]') - os.system('rm -rf temporary-clone') + if self.repo.repotype == RepoType.HELMREPO: + return self._gen_helm_template(verbose) + elif self.repo.repotype == RepoType.GIT: + return self._gen_git_template(verbose) else: - print('(WARN) I CANNOT APPLY THIS. (email me - usnexp@gmail)') - print('(WARN) '+self.getUrl()) - print('(WARN) '+self.repotype) + raise ValueError(f'!!!!! Error: Wrong repo type for {self.name}') + + def _gen_helm_template(self, verbose: bool=False) -> str: + cmd = f'helm pull --repo {self.repo.repository()} {self.repo.chart()} --version {self.repo.version()}; helm template -n {self.namespace} {self.name} {self.repo.chart()}-{self.repo.version()}.tgz -f {self.name}.vo --include-crds > {self.name}.plain.yaml; rm {self.repo.chart()}-{self.repo.version()}.tgz' + # if verbose: + # print(f'(DEBUG) generate template file\n{self}\n{cmd}') + os.system(cmd) + + return f'{self.name}.plain.yaml' + + def _gen_git_template(self, verbose: bool=FALSE) -> str: + self._clone_repo_and_dependency_update(verbose) + cmd = f'helm template -n {self.namespace} {self.name} .temporary-clone/{self.repo.path()} -f {self.name}.vo --include-crds > {self.name}.plain.yaml' + # if verbose > 0: + # print(f'(DEBUG) generate template file\n{cmd}') + os.system(cmd) + os.system('rm -rf .temporary-clone') + return f'{self.name}.plain.yaml' diff --git a/helm2yaml/applib/helmdeploy.py b/helm2yaml/applib/helmdeploy.py new file mode 100644 index 0000000..57fea66 --- /dev/null +++ b/helm2yaml/applib/helmdeploy.py @@ -0,0 +1,179 @@ +from tokenize import String +from typing import Dict, List, Optional, Any +import sys +import yaml +import os +import time +import copy +from .helm import Helm +from .repo import Repo, RepoType +from .parameter_aware_yamlmerge import yaml_override, traverse_leaf + +class HelmDeployManager: + class Config: + DEFAULT_CHECK_INTERVAL = 10 + DEFAULT_KUBECONFIG = '~/.kube/config' + + def __init__(self, kubeconfig: str = Config.DEFAULT_KUBECONFIG, base: str = '', namespace: str = '', verbose: int = 0): + self.kubeconfig = kubeconfig + self.namespace = namespace + self.verbose = verbose + self.base: Dict[str, Helm] = self.__load_base(base) + self.manifests = None #Dict[str, Helm] + + def get_repo(self, parsed: Dict[str, Any]) -> Repo: + chart_spec = parsed['spec']['chart'] + if 'type' in chart_spec: + repo_type = RepoType.GIT if chart_spec['type'] == 'git' else RepoType.HELMREPO + return Repo(repo_type, chart_spec['repository'], chart_spec['name'], chart_spec['version']) + elif 'git' in chart_spec: + return Repo(RepoType.GIT, chart_spec['git'], chart_spec['path'], chart_spec['ref']) + elif 'repository' in chart_spec: + return Repo(RepoType.HELMREPO, chart_spec['repository'], chart_spec['name'], chart_spec['version']) + else: + raise ValueError(f'Wrong repo {parsed}') + + def __load_base(self, base: str) -> Dict[str, Helm]: + manifests: Dict[str, Helm] = {} + if os.path.isdir(base): + for item in os.listdir(base): + item_path = os.path.join(base, item) + if os.path.isdir(item_path) or item == 'resources.yaml': + manifests.update(self.__load_base(item_path)) + elif os.path.isfile(base): + with open(base, 'r') as f: + for parsed in yaml.safe_load_all(f): + if parsed is None or 'spec' not in parsed: + print(f'--- Warn: An invalid resource is given ---\n{parsed}\n--- Warn END ---') + continue + try: + name = parsed['metadata']['name'] + if name in manifests: + manifests[name].override = yaml_override(manifests[name].override, parsed['spec']['values']) + else: + manifests[name] = Helm( + self.get_repo(parsed), + parsed['spec']['releaseName'], + parsed['spec']['targetNamespace'], + parsed['spec']['values'] + ) + except (yaml.YAMLError, TypeError, KeyError) as exc: + print(f'Error processing yaml: {exc}') + else: + print(f"Error: {base} is neither a file nor a directory") + return manifests + + def template_yaml(self, gdir: str = 'cd') -> None: + for chart, helm in self.manifests.items(): + if self.verbose > 0: + print(f'(DEBUG) Generate resource yamls from {chart}\n{helm}') + helm.to_separated_resources(gdir, self.verbose) + + def install_and_check_done(self, install: List[str], config: Dict[str, Any], skip_exist: bool = False) -> None: + check_interval = config.get('metadata', {}).get('checkInterval', self.Config.DEFAULT_CHECK_INTERVAL) + pending = [chart for chart in install if not (skip_exist and manifests[chart].get_status(self.kubeconfig) == 'deployed')] + + for chart in pending: + print(f'{self.manifests[chart].override}') + self.manifests[chart].install(kubeconfig=self.kubeconfig, target_namespace=self.namespace, verbose=self.verbose) + + while pending: + pending = [chart for chart in pending if self.manifests[chart].get_status(self.kubeconfig) != 'deployed'] + if pending: + print(f"\nWaiting for finish({check_interval} sec): {pending}") + time.sleep(check_interval) + + def delete_and_check_done(self, delete: List[str], config: Dict[str, Any]) -> None: + for chart in delete: + if os.system(f'helm delete -n {self.namespace} {chart} --kubeconfig {self.kubeconfig}') != 0: + print(f'FAILED!! {chart} ({self.namespace}) cannot be deleted due to the errors above.') + sys.exit(1) + + + def load_site(self, site: str, prohibits: List[str]): + self.manifests: Dict[str, Helm] = {} + with open(site, 'r') as f: + for parsed in yaml.safe_load_all(f): + self.preprocess_site(parsed) + for chart in parsed['charts']: + chart_name = chart.get('name') + base_name = chart.get('base', chart_name) + self.manifests[chart_name] = copy.deepcopy(self.base[base_name]) + self.manifests[chart_name].name = chart_name + + if 'override' in chart and chart['override']: + self.manifests[chart_name].override = yaml_override(self.manifests[chart_name].override, chart['override']) + if self.verbose: + print(f'from ====> {self.manifests[chart_name].override}') + self.manifests[chart_name].override = traverse_leaf(self.manifests[chart_name].override, parsed.get('global',{}), prohibits) + if self.verbose: + print(f'to ====> {self.manifests[chart_name].override}') + + def load_manifest(self, fd: Any, local_repository: Optional[str] = None) -> Dict[str, Helm]: + manifests: Dict[str, Helm] = {} + for parsed in yaml.safe_load_all(fd): + if 'spec' not in parsed: + print(f'--- Warn: An invalid resource is given ---\n{parsed}\n--- Warn END ---') + continue + repo = self.get_repo() + if local_repository: + repo.repo = local_repository + + manifests[parsed['metadata']['name']] = Helm( + repo, + parsed['spec']['releaseName'], + parsed['spec']['targetNamespace'], + parsed['spec']['values'] + ) + return manifests + + def preprocess_site(self, site: Dict[str, Any]) -> None: + for chart in site['charts']: + if 'override' in chart and chart['override'] is not None: + overrides = {} + for k, v in chart['override'].items(): + v = self.__split_keys(k.split('.'), v) if '.' in k else {k: v} + overrides = yaml_override(overrides, v) + chart['override'] = overrides + + def __split_keys(self, sp: List[str], value: Any) -> Dict[str, Any]: + return {sp[0]: self.__split_keys(sp[1:], value)} if len(sp) > 1 else {sp[0]: value} + + def check_chart_repo(self, fd: Any, target_repo: str, except_list: List[str] = []) -> Dict[str, str]: + invalid_dic = {} + for parsed in yaml.safe_load_all(fd): + repo = parsed.get('spec', {}).get('chart', {}).get('repository') + if repo and not repo.startswith(target_repo) and repo not in except_list: + name = parsed.get('metadata', {}).get('name') + invalid_dic[name] = repo + if self.verbose >= 1: + print(f'{name} is defined with wrong repository: {repo}') + return invalid_dic + + def check_image_repo(self, fd: Any, target_repo: str, except_list: List[str] = []) -> Dict[str, List[str]]: + helm_dic = self.load_manifest(fd) + if self.verbose > 0: + print('(DEBUG) Loaded manifest:', helm_dic) + for key, value in helm_dic.items(): + print(key, value) + + invalid_dic = {} + for key, helm in helm_dic.items(): + invalid_images = [image_url for image_url in helm.get_image_list(self.verbose) + if not image_url.startswith(target_repo) and image_url not in except_list] + if invalid_images: + invalid_dic[key] = invalid_images + + return invalid_dic + + def __str__(self): + sl=[f'(DEBUG) Loaded base manifest: {self.base}'] + if self.verbose: + for key, value in self.base.items(): + sl.append(f'(DEBUG) key: {key}, {value}') + + if self.verbose and self.manifests: + sl.append(f'(DEBUG) Loaded base manifest: {self.manifests}') + for key, value in self.manifests.items(): + sl.append(f'(DEBUG) key: {key}, {value}') + return "\n".join(sl) \ No newline at end of file diff --git a/helm2yaml/applib/parameter_aware_yamlmerge.py b/helm2yaml/applib/parameter_aware_yamlmerge.py new file mode 100644 index 0000000..f742adc --- /dev/null +++ b/helm2yaml/applib/parameter_aware_yamlmerge.py @@ -0,0 +1,64 @@ +from typing import Any, Dict, List + +VAR_START = '$(' +VAR_END = ')' +VAR_START_OFFSET = len(VAR_START) + +def substitute_variables(v: str, var_dictionary: Dict[str, Any], prohibits: List[str]) -> Any: + # print(f'v:{v}, type:{type(v)},Dict:{var_dictionary}') + if not var_dictionary or VAR_START not in v: + return check_prohibits(v, prohibits) + + # print(f'substitute_variables: input={v} ', end=" ") + + try: + if v.startswith(VAR_START) and v.endswith(VAR_END) and v.count(VAR_START) == 1 and v.count(VAR_END) == 1: + v = var_dictionary[v[VAR_START_OFFSET:-1].strip()] + else: + while VAR_START in v: + sp = v.find(VAR_START) + ep = v.find(VAR_END, sp) + if ep < 0: + raise ValueError(f'The value "{v}" has wrong format. Fix it and try again.') + var = v[sp + VAR_START_OFFSET:ep].strip() + v = v.replace(f'{VAR_START}{var}{VAR_END}', str(var_dictionary[var])) + except KeyError: + print(f''' + ################################################################################## + - The "{v}" have any undefined variables. Fix it and try again. + - Dictionary : {var_dictionary} + ################################################################################## + ''') + raise ValueError(f'The "{v}" have any undefined variables. Fix it and try again.') + + return check_prohibits(v, prohibits) + +def check_prohibits(v: Any, prohibits: List[str]) -> Any: + # 금지어 포함여부 체크 및 에러발생 + if prohibits and isinstance(v, str) and len(v) > 2 and any(v in w for w in prohibits): + print(f''' + ################################################################################## + - You cannot use the string "{v}" as a value + - Prohibit List : {prohibits} + ################################################################################## + ''') + raise ValueError(f'You cannot use the string "{v}" as a value') + return v + +def traverse_leaf(values: Any, var_dictionary: Dict[str, Any], prohibits: List[str]) -> Any: + if isinstance(values, dict): + return {k: traverse_leaf(v, var_dictionary, prohibits) for k, v in values.items()} + elif isinstance(values, list): + return [traverse_leaf(v, var_dictionary, prohibits) for v in values] + elif isinstance(values, str): + return substitute_variables(values, var_dictionary, prohibits) + return values + +def yaml_override(target: Dict[str, Any], v: Any) -> Dict[str, Any]: + if not target: + return v + if isinstance(v, dict): + for k, val in v.items(): + target[k] = yaml_override(target.get(k, {}), val) + return target + return v diff --git a/helm2yaml/applib/repo.py b/helm2yaml/applib/repo.py index 1e98a33..3b88c3b 100755 --- a/helm2yaml/applib/repo.py +++ b/helm2yaml/applib/repo.py @@ -1,63 +1,42 @@ from enum import Enum, unique -import sys, yaml, os, time, getopt +from typing import Optional + @unique class RepoType(Enum): - HELMREPO=1 - GIT=2 - LOCAL=3 + HELMREPO = 1 + GIT = 2 + LOCAL = 3 class Repo: -# Will Make When it's needed - def __init__(self): - self.list=[] - - def __init__(self, repotype, repo, chartOrPath, versionOrReference): + def __init__(self, repotype: RepoType, repo: str, chart_or_path: str, version_or_reference: str): self.repotype = repotype self.repo = repo - self.chartOrPath = chartOrPath - self.versionOrReference = versionOrReference - - def toString(self): - return '[REPO: {}, {}, {}, {}]'.format( self.repotype, - self.repo, - self.chartOrPath, - self.versionOrReference ) - - def version(self): - if self.repotype == RepoType.HELMREPO: - return self.versionOrReference - else: - return None - - def reference(self): - if self.repotype == RepoType.GIT: - return self.versionOrReference - else: - return None - - def chart(self): - if self.repotype == RepoType.HELMREPO: - return self.chartOrPath - else: - return None - def path(self): - if self.repotype == RepoType.GIT: - return self.chartOrPath - else: - return None + self.chart_or_path = chart_or_path + self.version_or_reference = version_or_reference + + def __str__(self) -> str: + return f'[REPO: {self.repotype} {self.repo}, {self.chart_or_path}, {self.version_or_reference}]' + + def version(self) -> Optional[str]: + return self.version_or_reference if self.repotype == RepoType.HELMREPO else None + + def reference(self) -> Optional[str]: + return self.version_or_reference if self.repotype == RepoType.GIT else None + + def chart(self) -> Optional[str]: + return self.chart_or_path if self.repotype == RepoType.HELMREPO else None + + def path(self) -> Optional[str]: + return self.chart_or_path if self.repotype == RepoType.GIT else None - def repository(self): + def repository(self) -> str: return self.repo - - def getUrl(self): - if self.repotype == RepoType.GIT and self.repo.startswith('git@'): - if self.repo.endswith('.git'): - return self.repo.replace(':','/').replace('git@','https://') - else: - return self.repo.replace(':','/').replace('git@','https://')+'.git' + def get_url(self) -> Optional[str]: if self.repotype == RepoType.GIT: + if self.repo.startswith('git@'): + url = self.repo.replace(':', '/').replace('git@', 'https://') + return url if url.endswith('.git') else f"{url}.git" return self.repo - else: - return None \ No newline at end of file + return None \ No newline at end of file diff --git a/helm2yaml/check_repo.py b/helm2yaml/check_repo.py index 94485a7..4450dcc 100755 --- a/helm2yaml/check_repo.py +++ b/helm2yaml/check_repo.py @@ -1,8 +1,8 @@ #!/usr/local/bin/python3 from applib.helm import Helm from applib.repo import Repo, RepoType -import sys, os, time, getopt, yaml -from common import * +import sys, getopt +from monstar.installer.applib.helmdeploy import * TEMPDIR="tmp" diff --git a/helm2yaml/template b/helm2yaml/template new file mode 100755 index 0000000..dc5e550 --- /dev/null +++ b/helm2yaml/template @@ -0,0 +1,43 @@ +#!/usr/local/bin/python3 + +import sys +import argparse +from typing import Dict, Any +from applib.helmdeploy import HelmDeployManager + +class AppConfig: + DEFAULT_WORKFLOW = 'default' + DEFAULT_KUBECONFIG = '~/.kube/config' + DEFAULT_OUTPUT_DIR = 'cd' + DEFAULT_BASE = './base/ai-base.yaml' + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Process app manifest and generate YAML files") + parser.add_argument('-o', '--override', required=True, help="The manifest file with override values") + parser.add_argument('-b', '--base', default=AppConfig.DEFAULT_BASE, help="The base manifest file") + parser.add_argument('-v', '--verbose', action='count', default=0, help="Increase verbosity") + parser.add_argument('-k', '--kubeconfig', default=AppConfig.DEFAULT_KUBECONFIG, + help="Kubeconfig file to access your k8s cluster") + parser.add_argument('-n', '--namespace', help="Define namespace to install regardless of the manifest") + parser.add_argument('--output', default=AppConfig.DEFAULT_OUTPUT_DIR, help="Output directory for YAML files") + return parser.parse_args() + + +def print_debug_info(manifests: Dict[str, Any]) -> None: + print('(DEBUG) loaded manifest:', manifests) + for key, value in manifests.items(): + print(key, value) + + +def main() -> None: + args = parse_arguments() + + helm_manager = HelmDeployManager(kubeconfig=args.kubeconfig, base=args.base, namespace=args.namespace, verbose=args.verbose) + helm_manager.load_site(args.override, ['CHANGEME','FIXME','TODO']) + + helm_manager.template_yaml(args.output) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/package/Dockerfile b/package/Dockerfile new file mode 100755 index 0000000..b8845a0 --- /dev/null +++ b/package/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.12.4-alpine + +LABEL AUTHOR sungil(usnexp@gmail.com) + +ENV HELM_VER v3.9.0 +ENV GH_VER 2.11.3 + +RUN apk update && apk add curl +RUN curl -sL -o helm.tar.gz https://get.helm.sh/helm-${HELM_VER}-linux-amd64.tar.gz && \ + tar xf helm.tar.gz && \ + mv linux-amd64/helm /usr/local/bin/helm + +RUN curl -sL -o gh.tar.gz https://github.com/cli/cli/releases/download/v${GH_VER}/gh_${GH_VER}_linux_amd64.tar.gz && \ + tar xf gh.tar.gz && \ + mv gh_2.11.3_linux_amd64/bin/gh /usr/local/bin/gh + +COPY helm2yaml /helm2yaml + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +ENTRYPOINT [ "/helm2yaml/template" ] +# sktcloud/decapod-render:v3.0.0 (23.8.14) +# - support oci protocol for a helm repository +# - generate CRDs using helm cli +# Build CLI: docker build --network host -t siim/helm2yaml:v3.0.0 .