Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)