# -*- coding: utf-8 -*- # # Copyright 2023 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Provides a parser for --container arguments.""" from __future__ import annotations import collections from collections.abc import Sequence import re from typing import Any from googlecloudsdk.calliope import base as calliope_base from googlecloudsdk.calliope import cli from googlecloudsdk.calliope import parser_arguments from googlecloudsdk.calliope import parser_errors from googlecloudsdk.calliope import parser_extensions from googlecloudsdk.command_lib.run import flags def AddContainerFlags( parser: parser_arguments.ArgumentInterceptor, container_arg_group: calliope_base.ArgumentGroup, release_track=calliope_base.ReleaseTrack.GA, ): """AddContainerFlags updates parser to add --container arg parsing. Args: parser: The parser to patch. container_arg_group: Arguments that can be specified per-container. release_track: The release track of the command. """ flags.ContainerFlag().AddToParser(parser) container_arg_group.AddToParser(parser) container_parser = ContainerParser( parser.parser, container_arg_group, release_track ) parser.parser.parse_known_args = container_parser.ParseKnownArgs class ContainerParser(object): """ContainerParser adds custom container parsing behavior to ArgumentParser.""" _CONTAINER_FLAG_NAME = '--container' _PRESET_FLAG_NAME = '--preset' _FLAG_PATTERN = r'^(--[^:=]+)[:=]?.*' def __init__( self, parser: parser_extensions.ArgumentParser, container_arg_group: calliope_base.ArgumentGroup, release_track: calliope_base.ReleaseTrack, ): """ContainerParser constructor. Args: parser: The original command's parser. Used to parse non-container args. container_arg_group: Arguments to add to per-container parsers. release_track: The release track of the command. """ self._parse_known_args = parser.parse_known_args self._prog = parser.prog self._calliope_command = parser._calliope_command self._container_arg_group = container_arg_group self._release_track = release_track def _GetContainerFlags(self) -> frozenset[str]: """_GetContainerFlags returns the configured set of per-container flags.""" args = [self._container_arg_group] flag_names = [] while args: arg = args.pop() if isinstance(arg, calliope_base.ArgumentGroup): args.extend(arg.arguments) else: flag_names.append(arg.name) return frozenset(flag_names) def _NewContainerParser(self) -> parser_extensions.ArgumentParser: """_NewContainerParser creates a new parser for parsing container args.""" parser = parser_extensions.ArgumentParser( add_help=False, prog=self._prog, calliope_command=self._calliope_command, ) ai = parser_arguments.ArgumentInterceptor( parser=parser, is_global=False, cli_generator=None, allow_positional=True, ) self._container_arg_group.AddToParser(ai) cli.FLAG_INTERNAL_FLAG_FILE_LINE.AddToParser(ai) return parser def _CheckForContainerFlags(self, namespace: parser_extensions.Namespace): """_CheckForContainerFlags checks that no container flags were specified. Args: namespace: The namespace to check. """ container_flags = self._GetContainerFlags().intersection( namespace.GetSpecifiedArgNames() ) if container_flags: raise parser_errors.ArgumentError( 'When --container is specified {flags} must be specified after' ' --container.', flags=', '.join(container_flags), ) def _IsFlagArg(self, arg: Any): return isinstance(arg, str) and arg.startswith('--') def _ExtractFlag(self, arg: str): flag = re.match(self._FLAG_PATTERN, arg) if flag: return flag.group(1) return None def ParseKnownArgs( self, args: Sequence[Any], namespace: parser_extensions.Namespace, ) -> tuple[parser_extensions.Namespace, Sequence[Any]]: """Performs custom --container arg parsing. Groups arguments after each --container flag to be parsed into that container's namespace. For each container a new parser is used to parse that container's flags into fresh namespace and those namespaces are stored as a dict in namespace.containers. Remaining args are parsed by the orignal parser's parse_known_args method. Args: args: The arguments to parse. namespace: The namespace to store parsed args in. Returns: A tuple containing the updated namespace and a list of unknown args. """ remaining = [] containers = collections.defaultdict(list) current = remaining i = 0 while i < len(args): value = args[i] i += 1 if value == self._CONTAINER_FLAG_NAME: if i >= len(args): remaining.append(value) else: current = containers[args[i]] i += 1 elif isinstance(value, str) and value.startswith( self._CONTAINER_FLAG_NAME + '=' ): current = containers[value.split(sep='=', maxsplit=1)[1]] # Add container for preset to container dict to capture container specific # flags for the placeholder container. # TODO(b/436350694): Change this to use preset metadata and rework this # branch to use regex similar to how the base parsers work. # For case "--preset=ollama" elif ( isinstance(value, str) and value.startswith(self._PRESET_FLAG_NAME + '=') ): preset_arg = value.split(sep='=', maxsplit=1)[1] preset_name = preset_arg.split(':')[0] if preset_name in flags.INGRESS_CONTAINER_PRESETS: current = containers[preset_name] remaining.append(value) # For case "--preset ollama" elif ( isinstance(value, str) and value.startswith(self._PRESET_FLAG_NAME) and i < len(args) ): preset_arg = args[i] preset_name = preset_arg.split(':')[0] if preset_name in flags.INGRESS_CONTAINER_PRESETS: current = containers[args[i]] remaining.append(value) remaining.append(args[i]) i += 1 elif value == '--': remaining.append(value) remaining.extend(args[i:]) break # For any flags not in the container flag list that come after a container # flag, add the flag and all arguments to 'remaining' list until the next # flag is encountered. elif ( self._release_track in [calliope_base.ReleaseTrack.BETA, calliope_base.ReleaseTrack.ALPHA] and containers and self._IsFlagArg(value) and self._ExtractFlag(value) not in self._GetContainerFlags() ): remaining.append(value) while i < len(args) and not self._IsFlagArg(args[i]): remaining.append(args[i]) i += 1 else: current.append(value) if not containers: return self._parse_known_args(args=remaining, namespace=namespace) namespace.containers = {} # pylint: disable=protected-access namespace._specified_args['containers'] = self._CONTAINER_FLAG_NAME for container_name, container_args in containers.items(): container_namespace = parser_extensions.Namespace() container_namespace = self._NewContainerParser().parse_args( args=container_args, namespace=container_namespace ) namespace.containers[container_name] = container_namespace namespace, unknown_args = self._parse_known_args( args=remaining, namespace=namespace ) self._CheckForContainerFlags(namespace) return namespace, unknown_args