def get_helm_chart(self): ''' Return a helm chart object ''' if self._helm_chart: return self._helm_chart # dependencies # [process_chart(x, is_dependency=True) for x in chart.dependencies] dependencies = [] for chart in self.chart.dependencies: LOG.info("Building dependency chart %s for release %s", chart.name, self.chart.release_name) dependencies.append(ChartBuilder(chart).get_helm_chart()) helm_chart = Chart( metadata=self.get_metadata(), templates=self.get_templates(), dependencies=dependencies, values=self.get_values(), files=self.get_files(), ) self._helm_chart = helm_chart return helm_chart
def get_templates(self): ''' Return all the chart templates ''' # process all files in templates/ as a template to attach to the chart # building a Template object templates = [] if not os.path.exists(os.path.join(self.source_directory, 'templates')): LOG.warn( "Chart %s has no templates directory," "no templates will be deployed", self.chart.name) for root, _, files in os.walk(os.path.join(self.source_directory, 'templates'), topdown=True): for tpl_file in files: tname = os.path.relpath( os.path.join(root, tpl_file), os.path.join(self.source_directory, 'templates')) templates.append( Template(name=tname, data=open(os.path.join(root, tpl_file), 'r').read())) return templates
def _pre_update_actions(self, actions, namespace): ''' :params actions - array of items actions :params namespace - name of pod for actions ''' try: for action in actions.get('delete', []): name = action.get("name") action_type = action.get("type") if "job" in action_type: LOG.info("Deleting %s in namespace: %s", name, namespace) self.k8s.delete_job_action(name, namespace) continue LOG.error("Unable to execute name: %s type: %s ", name, type) except Exception: LOG.debug("PRE: Could not delete anything, please check yaml") try: for action in actions.get('create', []): name = action.get("name") action_type = action.get("type") if "job" in action_type: LOG.info("Creating %s in namespace: %s", name, namespace) self.k8s.create_job_action(name, action_type) continue except Exception: LOG.debug("PRE: Could not create anything, please check yaml")
def _post_update_actions(self, actions, namespace): try: for action in actions.get('create', []): name = action.get("name") action_type = action.get("type") if "job" in action_type: LOG.info("Creating %s in namespace: %s", name, namespace) self.k8s.create_job_action(name, action_type) continue except Exception: LOG.debug("POST: Could not create anything, please check yaml")
def delete_job_action(self, name, namespace="default"): ''' :params name - name of the job :params namespace - name of pod that job ''' try: body = client.V1DeleteOptions() self.api_client.delete_namespaced_job(name=name, namespace=namespace, body=body) except ApiException as e: LOG.error("Exception when deleting a job: %s", e)
def show_diff(self, chart, installed_chart, installed_values, target_chart, target_values): ''' Produce a unified diff of the installed chart vs our intention TODO(alanmeadows): This needs to be rewritten to produce better unified diff output and avoid the use of print ''' chart_diff = list( difflib.unified_diff( installed_chart.SerializeToString().split('\n'), target_chart.split('\n'))) if len(chart_diff) > 0: LOG.info("Chart Unified Diff (%s)", chart.release_name) for line in chart_diff: LOG.debug(line) values_diff = list( difflib.unified_diff(installed_values.split('\n'), yaml.safe_dump(target_values).split('\n'))) if len(values_diff) > 0: LOG.info("Values Unified Diff (%s)", chart.release_name) for line in values_diff: LOG.debug(line) return (len(chart_diff) > 0) or (len(values_diff) > 0)
def get_values(self): ''' Return the chart (default) values ''' # create config object representing unmarshaled values.yaml if os.path.exists(os.path.join(self.source_directory, 'values.yaml')): raw_values = open( os.path.join(self.source_directory, 'values.yaml')).read() else: LOG.warn("No values.yaml in %s, using empty values", self.source_directory) raw_values = '' return Config(raw=raw_values)
def chart_cleanup(self, prefix, charts): ''' :params charts - list of yaml charts :params known_release - list of releases in tiller :result - will remove any chart that is not present in yaml ''' def release_prefix(prefix, chart): ''' how to attach prefix to chart ''' return "{}-{}".format(prefix, chart["chart"]["release_name"]) valid_charts = [release_prefix(prefix, chart) for chart in charts] actual_charts = [x.name for x in self.list_releases()] chart_diff = list(set(actual_charts) - set(valid_charts)) for chart in chart_diff: if chart.startswith(prefix): LOG.debug("Release: %s will be removed", chart) self.uninstall_release(chart)
def source_clone(self): ''' Clone the charts source We only support a git source type right now, which can also handle git:// local paths as well ''' if self.chart.source.type == 'git': tmpdir = tempfile.mkdtemp(prefix='armada', dir='/tmp') self._source_tmp_dir = tmpdir if self.parent: LOG.info("Cloning %s/%s as dependency for %s", self.chart.source.location, self.chart.source.subpath, self.parent) else: LOG.info("Cloning %s/%s for release %s", self.chart.source.location, self.chart.source.subpath, self.chart.release_name) pygit2.clone_repository(self.chart.source.location, tmpdir) return os.path.join(tmpdir, self.chart.source.subpath) else: LOG.exception("Unknown source type %s for chart %s", self.chart.name, self.chart.source.type)
def source_clone(self): ''' Clone the charts source We only support a git source type right now, which can also handle git:// local paths as well ''' if self.chart.source.type == 'git': if self.parent: LOG.info("Cloning %s/%s as dependency for %s", self.chart.source.location, self.chart.source.subpath, self.parent) else: LOG.info("Cloning %s/%s for release %s", self.chart.source.location, self.chart.source.subpath, self.chart.release_name) self._source_tmp_dir = git_clone(self.chart.source.location, self.chart.source.reference) return os.path.join(self._source_tmp_dir, self.chart.source.subpath) else: LOG.exception("Unknown source type %s for chart %s", self.chart.name, self.chart.source.type)
def create_job_action(self, name, namespace="default"): ''' :params name - name of the job :params namespace - name of pod that job ''' LOG.debug(" %s in namespace: %s", name, namespace)
def sync(self): ''' Syncronize Helm with the Armada Config(s) ''' def release_prefix(prefix, chart): ''' how to attach prefix to chart ''' return "{}-{}".format(prefix, chart) # extract known charts on tiller right now known_releases = self.tiller.list_charts() prefix = self.config.get('armada').get('release_prefix') for release in known_releases: LOG.debug("Release %s, Version %s found on tiller", release[0], release[1]) for entry in self.config['armada']['charts']: chart = dotify(entry['chart']) values = entry['chart']['values'] pre_actions = {} post_actions = {} if chart.release_name is None: continue # initialize helm chart and request a # protoc helm chart object which will # pull the sources down and walk the # dependencies chartbuilder = ChartBuilder(chart) protoc_chart = chartbuilder.get_helm_chart() # determine install or upgrade by examining known releases LOG.debug("RELEASE: %s", chart.release_name) if release_prefix(prefix, chart.release_name) in [ x[0] for x in known_releases ]: # indicate to the end user what path we are taking LOG.info("Upgrading release %s", chart.release_name) # extract the installed chart and installed values from the # latest release so we can compare to the intended state installed_chart, installed_values = self.find_release_chart( known_releases, release_prefix(prefix, chart.release_name)) if not self.disable_update_pre: pre_actions = getattr(chart.upgrade, 'pre', {}) if not self.disable_update_post: post_actions = getattr(chart.upgrade, 'post', {}) # show delta for both the chart templates and the chart values # TODO(alanmeadows) account for .files differences # once we support those upgrade_diff = self.show_diff(chart, installed_chart, installed_values, chartbuilder.dump(), values) if not upgrade_diff: LOG.info("There are no updates found in this chart") continue # do actual update self.tiller.update_release( protoc_chart, self.dry_run, chart.release_name, chart.namespace, prefix, pre_actions, post_actions, disable_hooks=chart.upgrade.no_hooks, values=yaml.safe_dump(values)) # process install else: LOG.info("Installing release %s", chart.release_name) self.tiller.install_release(protoc_chart, self.dry_run, chart.release_name, chart.namespace, prefix, values=yaml.safe_dump(values)) LOG.debug("Cleaning up chart source in %s", chartbuilder.source_directory) chartbuilder.source_cleanup() if self.enable_chart_cleanup: self.tiller.chart_cleanup(prefix, self.config['armada']['charts'])
def sync(self): ''' Syncronize Helm with the Armada Config(s) ''' # extract known charts on tiller right now known_releases = self.tiller.list_charts() for release in known_releases: LOG.debug("Release %s, Version %s found on tiller", release[0], release[1]) for entry in self.config['armada']['charts']: chart = dotify(entry['chart']) values = entry['chart']['values'] if chart.release_name is None: continue # initialize helm chart and request a # protoc helm chart object which will # pull the sources down and walk the # dependencies chartbuilder = ChartBuilder(chart) protoc_chart = chartbuilder.get_helm_chart() # determine install or upgrade by examining known releases if chart.release_name in [x[0] for x in known_releases]: # indicate to the end user what path we are taking LOG.info("Upgrading release %s", chart.release_name) # extract the installed chart and installed values from the # latest release so we can compare to the intended state installed_chart, installed_values = self.find_release_chart( known_releases, chart.release_name) # show delta for both the chart templates and the chart values # TODO(alanmeadows) account for .files differences # once we support those self.show_diff(chart, installed_chart, installed_values, chartbuilder.dump(), values) # do actual update self.tiller.update_release( protoc_chart, self.args.dry_run, chart.release_name, disable_hooks=chart.upgrade.no_hooks, values=yaml.safe_dump(values)) # process install else: LOG.info("Installing release %s", chart.release_name) self.tiller.install_release(protoc_chart, self.args.dry_run, chart.release_name, chart.namespace, values=yaml.safe_dump(values)) LOG.debug("Cleaning up chart source in %s", chartbuilder.source_directory) chartbuilder.source_cleanup()