def compile_plan( config: Config, k8sconfig: K8sConfig, local: ServerManifests, server: ServerManifests) -> Tuple[DeploymentPlan, bool]: """Return the `DeploymentPlan` to transition K8s to the `local` state. The deployment plan is a named tuple. It specifies which resources to create, patch and delete to ensure that the state of K8s matches that specified in `local`. Inputs: config: Square configuration. k8sconfig: K8sConfig local: ServerManifests Should be output from `load_manifest` or `load`. server: ServerManifests Should be output from `manio.download`. Returns: DeploymentPlan """ err_resp = (DeploymentPlan(tuple(), tuple(), tuple()), True) # Abort unless all local manifests reference valid K8s resource kinds. if any([k8s.resource(k8sconfig, meta)[1] for meta in local]): return err_resp # Replace the server resources fetched from the K8s preferred endpoint with # those fetched from the endpoint used by the local manifest. server, err = match_api_version(k8sconfig, local, server) assert not err # Apply the filters to all local and server manifests before we compute patches. server = { meta: manio.strip(k8sconfig, man, config.filters) for meta, man in server.items() } local = { meta: manio.strip(k8sconfig, man, config.filters) for meta, man in local.items() } # Abort if any of the manifests could not be stripped. err_srv = {_[2] for _ in server.values()} err_loc = {_[2] for _ in local.values()} if True in err_srv or True in err_loc: logit.error("Could not strip all manifests.") return err_resp # Unpack the stripped manifests (first element in the tuple returned from # `manio.strip`). server = {k: dotdict.undo(v[0]) for k, v in server.items()} local = {k: dotdict.undo(v[0]) for k, v in local.items()} # Partition the set of meta manifests into create/delete/patch groups. plan, err = partition_manifests(local, server) if err: logit.error("Could not partition the manifests for the plan.") return err_resp # Sanity check: the resources to patch *must* exist in both local and # server manifests. This is a bug if not. assert set(plan.patch).issubset(set(local.keys())) assert set(plan.patch).issubset(set(server.keys())) # For later: every DELETE request will have to pass along a `DeleteOptions` # manifest (see below). del_opts = { "apiVersion": "v1", "kind": "DeleteOptions", "gracePeriodSeconds": 0, "orphanDependents": False, } # Compile the Deltas to create the missing resources. create = [] for delta in plan.create: # We only need the resource and namespace, not its name, because that # is how the POST request to create a resource works in K8s. # Ignore the error flag because the `strip` function we used above # already ensured the resource exists. resource, err = k8s.resource(k8sconfig, delta._replace(name="")) assert not err # Compile the Delta and add it to the list. create.append(DeltaCreate(delta, resource.url, local[delta])) # Compile the Deltas to delete the excess resources. delete = [] for meta in plan.delete: # Resource URL. Ignore the error flag because the `strip` function # above already called `k8s.resource` and would have aborted on error. resource, err = k8s.resource(k8sconfig, meta) assert not err # Compile the Delta and add it to the list. delete.append(DeltaDelete(meta, resource.url, del_opts.copy())) # Iterate over each manifest that needs patching and determine the # necessary JSON Patch to transition K8s into the state specified in the # local manifests. patches = [] for meta in plan.patch: # Compute textual diff (only useful for the user to study the diff). diff_str, err = manio.diff(config, k8sconfig, local[meta], server[meta]) if err or diff_str is None: logit.error(f"Could not compute the diff for <{meta}>.") return err_resp # Compute the JSON patch that will change the K8s state to match the # one in the local files. patch, err = make_patch(config, k8sconfig, local[meta], server[meta]) if err or patch is None: logit.error(f"Could not compute the patch for <{meta}>") return err_resp # Append the patch to the list of patches, unless it is empty. if len(patch.ops): patches.append(DeltaPatch(meta, diff_str, patch)) # Assemble and return the deployment plan. return (DeploymentPlan(create, patches, delete), False)
def sync(local_manifests: LocalManifestLists, server_manifests: ServerManifests, selectors: Selectors, groupby: GroupBy) -> Tuple[LocalManifestLists, bool]: """Update the local manifests with the server values and return the result. Inputs: local_manifests: Dict[Filepath, Tuple[MetaManifest, dict]] server_manifests: Dict[MetaManifest, dict] selectors: Selectors Only operate on resources that match the selectors. groupby: GroupBy Specify relationship between new manifests and file names. Returns: Dict[Filepath, Tuple[MetaManifest, dict]] """ # Avoid side effects. server_manifests = copy.deepcopy(server_manifests) # Only retain server manifests with correct `kinds` and `namespaces`. server_manifests = { meta: manifest for meta, manifest in server_manifests.items() if select(manifest, selectors) } # Add all local manifests outside the specified `kinds` and `namespaces` # to the server list. This will *not* propagate to the server in any way, # but allows us to make the rest of the function oblivious to the fact that # we only care about a subset of namespaces and resources by pretending # that local and server manifests are already in sync. for fname, manifests in local_manifests.items(): for meta, manifest in manifests: if select(manifest, selectors): continue server_manifests[meta] = manifest # Create map for MetaManifest -> (File, doc-idx). The doc-idx denotes the # index of the manifest inside the YAML files (it may contain multiple # manifests). We will need that information later to find out which # manifest in which file we need to update. meta_to_fname = {} for fname in local_manifests: for idx, (meta, _) in enumerate(local_manifests[fname]): meta_to_fname[meta] = (fname, idx) del meta del fname # Make a copy of the local manifests to avoid side effects for the caller. # Also put it into a default dict for convenience. out_add_mod: DefaultDict[Filepath, List[Tuple[MetaManifest, dict]]] out_add_mod = collections.defaultdict(list) out_add_mod.update(copy.deepcopy(local_manifests)) # type: ignore del local_manifests # If the server's meta manifest exists locally then update the local one, # otherwise add it to the catchall YAML file. for meta, manifest in server_manifests.items(): try: # Find the file that defined `meta` and its position inside that file. fname, idx = meta_to_fname[meta] except KeyError: fname, err = filename_for_manifest(meta, manifest, groupby) if err: return ({}, True) out_add_mod[fname].append((meta, manifest)) else: # Update the correct YAML document in the correct file. out_add_mod[fname][idx] = (meta, manifest) # Iterate over all manifests in all files and drop the resources that do # not exist on the server. This will, in effect, delete those resources in # the local files if the caller chose to save them. out_add_mod_del: LocalManifestLists = {} for fname, manifests in out_add_mod.items(): pruned = [(meta, man) for (meta, man) in manifests if meta in server_manifests] out_add_mod_del[fname] = pruned return (out_add_mod_del, False)