File: //snap/google-cloud-cli/394/lib/surface/meta/daemon.py
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""The `gcloud meta daemon` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import http.server
import json
import logging
import os
import socketserver
import sys
import time
from googlecloudsdk import gcloud_main
from googlecloudsdk.calliope import base
from googlecloudsdk.core.util import files
# --- Configuration ---
HOST = '0.0.0.0'
PORT = 8080
PRECOMPUTE_DATA = os.environ.get(
'GCLOUD_DAEMON_PRECOMPUTE_DATA', 'CLI'
) # or SURFACES or COMMANDS
def perform_gcloud_execution(cli_obj, command_list):
"""Executes the gcloud command using the precomputed CLI object."""
start_time = time.time()
pid = os.getpid() # Current process PID
logging.info(
"PID: %s - Starting perform_gcloud_execution with input: '%s'",
pid,
command_list,
)
if cli_obj is None:
logging.error('PID: %s - Precomputed CLI object is None!', pid)
return {'error': 'Internal Server Error: CLI object not available'}
try:
request = cli_obj.Execute(command_list)
response_data = {
'uri': request.uri,
'method': request.method,
'headers': {
k.decode('utf-8'): v.decode('utf-8')
for k, v in request.headers.items()
},
'body': (
request.body.decode('utf-8')
if isinstance(request.body, bytes)
else request.body
),
}
logging.info('PID: %s - Execution successful.', pid)
return response_data
except SystemExit as exc:
logging.exception('PID: %s - SystemExit processing request: %s', pid, exc)
exit_code = exc.code if isinstance(exc.code, int) else 1
return {
'error': 'Command failed with SystemExit: %s' % exc,
'exit_code': exit_code,
}
except Exception as e: # pylint: disable=broad-exception-caught
logging.error('PID: %s - Exception during command execution:', pid)
return {'error': 'Exception during command execution: %s' % str(e)}
finally:
end_time = time.time()
computation_time = end_time - start_time
logging.info(
'PID: %s - Completed perform_gcloud_execution in %.4f seconds',
pid,
computation_time,
)
@base.Hidden
@base.DefaultUniverseOnly
class Daemon(base.Command):
"""(DEBUG MODE) Precomputes gcloud CLI and runs a single-request server."""
detailed_help = {
'DESCRIPTION': """
(DEBUG MODE) Initializes the gcloud CLI environment based on PRECOMPUTE_DATA,
starts an HTTP server in the FOREGROUND, serves exactly one POST
request to the root path ('/'), executes the requested gcloud command
using the precomputed environment, returns the result, and then exits.
The command will BLOCK until the single request is received and processed.
""",
'EXAMPLES': """
To run the foreground daemon precomputing the basic CLI:
$ gcloud meta daemon
(The command will now wait here)
Open another terminal and send a request (e.g., using curl):
$ curl -X POST -H "Content-Type: application/json" -d '{"command_list": ["projects", "list", "--limit=1", "--format=json"]}' http://localhost:8080/
(The original 'gcloud meta daemon' command will process this, print logs, and then exit)
""",
}
def Run(self, args):
# Configure logging for the main foreground process
# pid = os.getpid()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - PID: %(process)d - %(levelname)s - %(message)s',
)
# --- Precompute CLI in this main process ---
logging.info('Starting precomputation...')
precomputation_start_time = time.time()
try:
# Set SDK properties needed for precomputation/execution
os.environ['CLOUDSDK_CORE_DRY_RUN'] = '1'
os.environ['CLOUDSDK_AUTH_DISABLE_CREDENTIALS'] = '1'
os.environ['CLOUDSDK_CORE_DISABLE_PROMPTS'] = '1'
os.environ['CLOUDSDK_CORE_DISABLE_USAGE_REPORTING'] = '1'
os.environ['CLOUDSDK_COMPONENT_MANAGER_DISABLE_UPDATE_CHECK'] = '1'
os.environ['CLOUDSDK_CORE_DISABLE_FILE_LOGGING'] = '1'
os.environ['CLOUDSDK_CORE_SHOW_STRUCTURED_LOGS'] = 'always'
os.environ['CLOUDSDK_CORE_VERBOSITY'] = 'error'
os.environ['CLOUDSDK_METRICS_ENVIRONMENT'] = 'gaas'
os.environ['CLOUDSDK_CORE_USER_OUTPUT_ENABLED'] = '0'
precomputed_cli = gcloud_main.CreateCLI([])
logging.info('Precomputing basic CLI...')
logging.info('Loading frequent commands')
try:
compute_instances_list_command = [
'compute',
'instances',
'list',
'--project=fake-project',
]
precomputed_cli.Execute(compute_instances_list_command)
except Exception: # pylint: disable=broad-exception-caught
pass
precomputation_end_time = time.time()
logging.info(
'Precomputation complete in %.4f seconds.',
precomputation_end_time - precomputation_start_time,
)
except Exception as e: # pylint: disable=broad-exception-caught
logging.exception('Failed during CLI precomputation:')
sys.exit('Error during CLI precomputation: %s' % e)
# --- Define Handler and Start Server ---
class SingleRequestHandler(http.server.BaseHTTPRequestHandler):
"""Handles ONE incoming POST request and then signals shutdown."""
# pylint: disable=invalid-name
def do_POST(self):
"""Handles the single POST request."""
pid = os.getpid() # Log current process PID
logging.info('PID: %s - Request received at path: %s', pid, self.path)
if self.path == '/':
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
try:
request_data = json.loads(post_data.decode('utf-8'))
command_list = request_data.get('command_list')
if not command_list or not isinstance(command_list, list):
logging.error(
"PID: %s - Missing or invalid 'command_list' (must be a list)"
' in request',
pid,
)
self.send_error(
400,
"Missing or invalid 'command_list' (must be a list) in"
' request',
)
return
except json.JSONDecodeError:
logging.error('PID: %s - Invalid JSON request', pid, exc_info=True)
self.send_error(400, 'Invalid JSON request')
return
logging.info(
'PID: %s - Received compute request with command: %s',
pid,
command_list,
)
# Execute the command using the precomputed CLI
response_data = perform_gcloud_execution(
precomputed_cli, command_list
)
# Send response
status_code = 200 # sandbox always returns 200 for IPC
self.send_response(status_code)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
logging.info('PID: %s - Response sent to client.', pid)
else:
logging.warning(
'PID: %s - Received request for unknown path: %s', pid, self.path
)
self.send_error(404) # Not Found
# Signal the server's main thread to shut down
logging.info(
'PID: %s - Request handled. Signaling server shutdown.', pid
)
# pylint: disable=redefined-builtin
def log_message(self, format, *args):
"""Log messages using the standard logger."""
# Adjust message slightly for foreground clarity
logging.info('PID: %s - HTTP Server: %s', os.getpid(), format % args)
# --- Start HTTP server directly in this process ---
logging.info('Starting foreground HTTP server...')
try:
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer((HOST, PORT), SingleRequestHandler) as httpd:
# Touch a file to signal readiness
gcloud_daemon_ready_file = '/tmp/gcloud_daemon.ready'
with files.FileWriter(gcloud_daemon_ready_file) as f:
f.write('ready')
logging.info('Created %s', gcloud_daemon_ready_file)
logging.info(
'Server started on http://%s:%s (PRECOMPUTE_DATA=%s)',
HOST,
PORT,
PRECOMPUTE_DATA,
)
# Process one request (blocks until one arrives) and then exits the
# 'with' block, which handles shutdown.
httpd.handle_request()
logging.info('Request handled. Server shutting down.')
httpd.server_close()
except Exception as e: # pylint: disable=broad-exception-caught
# Log any exception during server setup or the loop itself
logging.exception('Unhandled exception occurred in server process:')
sys.exit('Server error: %s' % e)
finally:
logging.info('Server process function exiting.')
logging.shutdown() # Flush and close handlers
logging.info('Daemon command finished normally after serving request.')
sys.exit(0) # Ensure clean exit