def GetConfigManagement(membership, cluster_type):
    """Get ConfigManagement to check if multi-repo is enabled.

  Args:
    membership: The membership name or cluster name of the current cluster.
    cluster_type: The type of the current cluster. It is either a Fleet-cluster
      or a Config-controller cluster.

  Raises:
    Error: errors that happen when getting the object from the cluster.
  """
    config_management = None
    err = None
    timed_out = True
    with Timeout(5):
        config_management, err = RunKubectl([
            'get',
            'configmanagements.configmanagement.gke.io/config-management',
            '-o', 'json'
        ])
        timed_out = False
    if timed_out and cluster_type != 'Config Controller':
        raise exceptions.ConfigSyncError(
            'Timed out getting ConfigManagement object. ' +
            'Make sure you have setup Connect Gateway for ' + membership +
            ' following the instruction from ' +
            'https://cloud.google.com/anthos/multicluster-management/gateway/setup'
        )
    if timed_out:
        raise exceptions.ConfigSyncError(
            'Timed out getting ConfigManagement object from ' + membership)

    if err:
        raise exceptions.ConfigSyncError(
            'Error getting ConfigManagement object from {}: {}\n'.format(
                membership, err))
    config_management_obj = json.loads(config_management)
    if 'enableMultiRepo' not in config_management_obj['spec'] or (
            not config_management_obj['spec']['enableMultiRepo']):
        raise exceptions.ConfigSyncError(
            'Legacy mode is used in {}. '.format(membership) +
            'Please enable the multi-repo feature to use this command.')
    if 'status' not in config_management_obj:
        log.status.Print(
            'The ConfigManagement object is not reconciled in {}. '.format(
                membership) +
            'Please check if the Config Management is running on it.')
    errors = config_management_obj.get('status', {}).get('errors')
    if errors:
        log.status.Print(
            'The ConfigManagement object contains errors in{}:\n{}'.format(
                membership, errors))
def ListMemberships(project):
    """List hte memberships from a given project.

  Args:
    project: project that the memberships are in.

  Returns:
    The memberships registered to the fleet hosted by the given project.

  Raises:
    Error: The error occured when it failed to list memberships.
  """
    # TODO(b/202418506) Check if there is any library
    # function to list the memberships.
    args = [
        'container', 'fleet', 'memberships', 'list', '--format', 'json(name)',
        '--project', project
    ]
    output, err = _RunGcloud(args)
    if err:
        raise exceptions.ConfigSyncError(
            'Error listing memberships: {}'.format(err))
    json_output = json.loads(output)
    memberships = [m['name'] for m in json_output]
    return memberships
def ListConfigControllerClusters(project):
    """Runs a gcloud command to list the clusters that host Config Controller.

  Currently the Config Controller only works in select regions.
  Refer to the Config Controller doc:
  https://cloud.google.com/anthos-config-management/docs/how-to/config-controller-setup

  Args:
    project: project that the Config Controller is in.

  Returns:
    The list of (cluster, region) for Config Controllers.

  Raises:
    Error: The error occured when it failed to list clusters.
  """
    # TODO(b/202418506) Check if there is any library
    # function to list the clusters.
    args = [
        'container', 'clusters', 'list', '--project', project, '--filter',
        'name:krmapihost', '--format', 'json(name,location)'
    ]
    output, err = _RunGcloud(args)
    if err:
        raise exceptions.ConfigSyncError(
            'Error listing clusters: {}'.format(err))

    output_json = json.loads(output)
    clusters = [(c['name'], c['location']) for c in output_json]
    return clusters
def KubeconfigForCluster(project, region, cluster):
    """Get the kubeconfig of a GKE cluster.

  If the kubeconfig for the GKE cluster already exists locally, use it;
  Otherwise run a gcloud command to get the credential for it.

  Args:
    project: The project ID of the cluster.
    region: The region of the cluster.
    cluster: The name of the cluster.

  Returns:
    None

  Raises:
    Error: The error occured when it failed to get credential for the cluster.
  """
    context = 'gke_{project}_{region}_{cluster}'.format(project=project,
                                                        region=region,
                                                        cluster=cluster)
    command = ['config', 'use-context', context]
    _, err = RunKubectl(command)
    if err is None:
        return None
    # kubeconfig for the cluster doesn't exit locally
    # run a gcloud command to get the credential of the given
    # cluster
    args = [
        'container', 'clusters', 'get-credentials', cluster, '--region',
        region, '--project', project
    ]
    _, err = _RunGcloud(args)
    if err:
        raise exceptions.ConfigSyncError(
            'Error getting credential for cluster {}: {}'.format(cluster, err))
def ListResources(project, name, namespace, repo_cluster, membership):
  """List managed resources.

  Args:
    project: The project id the repo is from.
    name: The name of the corresponding ResourceGroup CR.
    namespace: The namespace of the corresponding ResourceGroup CR.
    repo_cluster: The cluster that the repo is synced to.
    membership: membership name that the repo should be from.

  Returns:
    List of raw ResourceGroup dicts

  """
  if membership and repo_cluster:
    raise exceptions.ConfigSyncError(
        'only one of --membership and --cluster may be specified.')

  resource_groups = []
  # Get ResourceGroups from the Config Controller cluster
  if not membership:  # exclude CC clusters if membership option is provided
    cc_rg = _GetResourceGroupsFromConfigController(
        project, name, namespace, repo_cluster)
    resource_groups.extend(cc_rg)

  # Get ResourceGroups from memberships
  member_rg = _GetResourceGroupsFromMemberships(
      project, name, namespace, repo_cluster, membership)
  resource_groups.extend(member_rg)

  # Parse ResourceGroups to structured output
  return ParseResultFromRawResourceGroups(resource_groups)
def _AppendReposFromCluster(membership, repos_cross_clusters, cluster_type,
                            namespaces, selector):
    """List all the RepoSync and RootSync CRs from the given cluster.

  Args:
    membership: The membership name or cluster name of the current cluster.
    repos_cross_clusters: The repos across multiple clusters.
    cluster_type: The type of the current cluster. It is either a Fleet-cluster
      or a Config-controller cluster.
    namespaces: The namespaces that the list should get RepoSync|RootSync from.
    selector: The label selector that the RepoSync|RootSync should match.

  Returns:
    None

  Raises:
    Error: errors that happen when listing the CRs from the cluster.
  """
    utils.GetConfigManagement(membership, cluster_type)

    params = []
    if not namespaces or '*' in namespaces:
        params = [['--all-namespaces']]
    else:

        params = [['-n', ns] for ns in namespaces.split(',')]
    all_repos = []
    errors = []
    for p in params:
        repos, err = utils.RunKubectl(
            ['get', 'rootsync,reposync', '-o', 'json'] + p)
        if err:
            errors.append(err)
            continue
        if repos:
            obj = json.loads(repos)
            if 'items' in obj:
                if namespaces and '*' in namespaces:
                    for item in obj['items']:
                        ns = _GetPathValue(item, ['metadata', 'namespace'], '')
                        if fnmatch.fnmatch(ns, namespaces):
                            all_repos.append(item)
                else:
                    all_repos += obj['items']
    if errors:
        raise exceptions.ConfigSyncError(
            'Error getting RootSync and RepoSync custom resources: {}'.format(
                errors))

    count = 0
    for repo in all_repos:
        if not _LabelMatched(repo, selector):
            continue
        repos_cross_clusters.AddRepo(membership, repo, None, cluster_type)
        count += 1
    if count > 0:
        log.status.Print('getting {} RepoSync and RootSync from {}'.format(
            count, membership))
def _GetResourceGroups(cluster_name, cluster_type, name, namespace):
  """List all the ResourceGroup CRs from the given cluster.

  Args:
    cluster_name: The membership name or cluster name of the current cluster.
    cluster_type: The type of the current cluster. It is either a Fleet-cluster
      or a Config-controller cluster.
    name: The name of the desired ResourceGroup.
    namespace: The namespace of the desired ResourceGroup.

  Returns:
    List of raw ResourceGroup dicts

  Raises:
    Error: errors that happen when listing the CRs from the cluster.
  """
  utils.GetConfigManagement(cluster_name, cluster_type)
  if not namespace:
    params = ['--all-namespaces']
  else:
    params = ['-n', namespace]
  repos, err = utils.RunKubectl(
      ['get', 'resourcegroup.kpt.dev', '-o', 'json'] + params)
  if err:
    raise exceptions.ConfigSyncError(
        'Error getting ResourceGroup custom resources for cluster {}: {}'
        .format(cluster_name, err))

  if not repos:
    return []
  obj = json.loads(repos)
  if 'items' not in obj or not obj['items']:
    return []

  resource_groups = []
  for item in obj['items']:
    _, nm = utils.GetObjectKey(item)
    if name and nm != name:
      continue
    resource_groups.append(RawResourceGroup(cluster_name, item))

  return resource_groups
def ListRepos(project_id, status, namespace, membership, selector, targets):
    """List repos across clusters.

  Args:
    project_id: project id that the command should list the repo from.
    status: status of the repo that the list result should contain
    namespace: namespace of the repo that the command should list.
    membership: membership name that the repo should be from.
    selector: label selectors for repo. It applies to the RootSync|RepoSync CRs.
    targets: The targets from which to list the repos. The value should be one
      of "all", "fleet-clusters" and "config-controller".

  Returns:
    A list of RepoStatus.

  """
    if targets and targets not in [
            'all', 'fleet-clusters', 'config-controller'
    ]:
        raise exceptions.ConfigSyncError(
            '--targets must be one of "all", "fleet-clusters" and "config-controller"'
        )
    if targets != 'fleet-clusters' and membership:
        raise exceptions.ConfigSyncError(
            '--membership should only be specified when --targets=fleet-clusters'
        )
    if status not in ['all', 'synced', 'error', 'pending', 'stalled']:
        raise exceptions.ConfigSyncError(
            '--status must be one of "all", "synced", "pending", "error", "stalled"'
        )
    selector_map, err = _ParseSelector(selector)
    if err:
        raise exceptions.ConfigSyncError(err)

    repo_cross_clusters = RawRepos()

    if targets == 'all' or targets == 'config-controller':
        # list the repos from Config Controller cluster
        clusters = []
        try:
            clusters = utils.ListConfigControllerClusters(project_id)
        except exceptions.ConfigSyncError as err:
            log.error(err)
        if clusters:
            for cluster in clusters:
                try:
                    utils.KubeconfigForCluster(project_id, cluster[1],
                                               cluster[0])
                    _AppendReposFromCluster(cluster[0], repo_cross_clusters,
                                            'Config Controller', namespace,
                                            selector_map)
                except exceptions.ConfigSyncError as err:
                    log.error(err)

    if targets == 'all' or targets == 'fleet-clusters':
        # list the repos from membership clusters
        try:
            memberships = utils.ListMemberships(project_id)
        except exceptions.ConfigSyncError as err:
            raise err

        for member in memberships:
            if not utils.MembershipMatched(member, membership):
                continue
            try:
                utils.KubeconfigForMembership(project_id, member)
                _AppendReposFromCluster(member, repo_cross_clusters,
                                        'Membership', namespace, selector_map)
            except exceptions.ConfigSyncError as err:
                log.error(err)

    # aggregate all the repos
    return _AggregateRepoStatus(repo_cross_clusters, status)
def DescribeRepo(project, name, namespace, source, repo_cluster,
                 managed_resources):
    """Describe a repo for the detailed status and managed resources.

  Args:
    project: The project id the repo is from.
    name: The name of the correspoinding RepoSync|RootSync CR.
    namespace: The namespace of the correspoinding RepoSync|RootSync CR.
    source: The source of the repo.
    repo_cluster: The cluster that the repo is synced to.
    managed_resources: The status to filter the managed resources for the
      output.

  Returns:
    It returns an instance of DescribeResult

  """
    if name and source or namespace and source:
        raise exceptions.ConfigSyncError(
            '--sync-name and --sync-namespace cannot be specified together with '
            '--source.')
    if name and not namespace or namespace and not name:
        raise exceptions.ConfigSyncError(
            '--sync-name and --sync-namespace must be specified together.')
    if managed_resources not in [
            'all', 'current', 'inprogress', 'notfound', 'failed', 'unknown'
    ]:
        raise exceptions.ConfigSyncError(
            '--managed-resources must be one of all, current, inprogress, notfound, failed or unknown'
        )

    repo_cross_clusters = RawRepos()
    # Get repos from the Config Controller cluster
    clusters = []
    try:
        clusters = utils.ListConfigControllerClusters(project)
    except exceptions.ConfigSyncError as err:
        log.error(err)
    if clusters:
        for cluster in clusters:
            if repo_cluster and repo_cluster != cluster[0]:
                continue
            try:
                utils.KubeconfigForCluster(project, cluster[1], cluster[0])
                _AppendReposAndResourceGroups(cluster[0], repo_cross_clusters,
                                              'Config Controller', name,
                                              namespace, source)
            except exceptions.ConfigSyncError as err:
                log.error(err)

    # Get repos from memberships
    try:
        memberships = utils.ListMemberships(project)
    except exceptions.ConfigSyncError as err:
        raise err
    for membership in memberships:
        if repo_cluster and repo_cluster != membership:
            continue
        try:
            utils.KubeconfigForMembership(project, membership)
            _AppendReposAndResourceGroups(membership, repo_cross_clusters,
                                          'Membership', name, namespace,
                                          source)
        except exceptions.ConfigSyncError as err:
            log.error(err)
    # Describe the repo
    repo = _Describe(managed_resources, repo_cross_clusters)
    return repo
def _AppendReposAndResourceGroups(membership, repos_cross_clusters,
                                  cluster_type, name, namespace, source):
    """List all the RepoSync,RootSync CRs and ResourceGroup CRs from the given cluster.

  Args:
    membership: The membership name or cluster name of the current cluster.
    repos_cross_clusters: The repos across multiple clusters.
    cluster_type: The type of the current cluster. It is either a Fleet-cluster
      or a Config-controller cluster.
    name: The name of the desired repo.
    namespace: The namespace of the desired repo.
    source: The source of the repo. It should be copied from the output of the
      list command.

  Returns:
    None

  Raises:
    Error: errors that happen when listing the CRs from the cluster.
  """
    utils.GetConfigManagement(membership, cluster_type)
    params = []
    if not namespace:
        params = ['--all-namespaces']
    else:
        params = ['-n', namespace]
    repos, err = utils.RunKubectl(
        ['get', 'rootsync,reposync,resourcegroup', '-o', 'json'] + params)
    if err:
        raise exceptions.ConfigSyncError(
            'Error getting RootSync,RepoSync,Resourcegroup custom resources: {}'
            .format(err))

    if not repos:
        return
    obj = json.loads(repos)
    if 'items' not in obj or not obj['items']:
        return

    repos = {}
    resourcegroups = {}
    for item in obj['items']:
        ns, nm = utils.GetObjectKey(item)
        if name and nm != name:
            continue
        key = ns + '/' + nm
        kind = item['kind']
        if kind == 'ResourceGroup':
            resourcegroups[key] = item
        else:
            repos[key] = item

    count = 0
    for key, repo in repos.items():
        repo_source = _GetGitKey(repo)
        if source and repo_source != source:
            continue
        rg = None
        if key in resourcegroups:
            rg = resourcegroups[key]
        repos_cross_clusters.AddRepo(membership, repo, rg, cluster_type)
        count += 1
    if count > 0:
        log.status.Print('getting {} RepoSync and RootSync from {}'.format(
            count, membership))
def KubeconfigForMembership(project, membership):
    """Get the kubeconfig of a membership.

  If the kubeconfig for the membership already exists locally, use it;
  Otherwise run a gcloud command to get the credential for it.

  Args:
    project: The project ID of the membership.
    membership: The name of the membership.

  Returns:
    None

  Raises:
      Error: The error occured when it failed to get credential for the
      membership.
  """
    context = 'connectgateway_{project}_{membership}'.format(
        project=project, membership=membership)
    command = ['config', 'use-context', context]
    _, err = RunKubectl(command)
    if err is None:
        return

    # kubeconfig for the membership doesn't exit locally
    # run a gcloud command to get the credential of the given
    # membership

    # Check if the membership is for a GKE cluster.
    # If it is, use the kubeconfig for the GKE cluster.
    args = [
        'container', 'fleet', 'memberships', 'describe', membership,
        '--project', project, '--format', 'json'
    ]
    output, err = _RunGcloud(args)
    if err:
        raise exceptions.ConfigSyncError(
            'Error describing the membership {}: {}'.format(membership, err))
    if output:
        description = json.loads(output)
        cluster_link = description.get('endpoint',
                                       {}).get('gkeCluster',
                                               {}).get('resourceLink', '')
        if cluster_link:
            m = re.compile('.*/projects/(.*)/locations/(.*)/clusters/(.*)'
                           ).match(cluster_link)
            project = ''
            location = ''
            cluster = ''
            try:
                project = m.group(1)
                location = m.group(2)
                cluster = m.group(3)
            except IndexError:
                pass
            if project and location and cluster:
                KubeconfigForCluster(project, location, cluster)
                return

    args = [
        'container', 'fleet', 'memberships', 'get-credentials', membership,
        '--project', project
    ]
    _, err = _RunGcloud(args)
    if err:
        raise exceptions.ConfigSyncError(
            'Error getting credential for membership {}: {}'.format(
                membership, err))