Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions paasta_tools/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ def make_app(global_config=None):
"service.instance.set_state",
"/v1/services/{service}/{instance}/state/{desired_state}",
)
config.add_route(
"service.instance.replica.restart",
"/v1/services/{service}/{instance}/replicas/{replica_name}/restart",
)
config.add_route(
"service.instance.delay", "/v1/services/{service}/{instance}/delay"
)
Expand Down
54 changes: 54 additions & 0 deletions paasta_tools/api/api_docs/oapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ components:
InstanceSpecCassandraCluster:
description: Cassandra instance spec
type: object
InstanceReplicaRestartOutcome:
properties:
message:
type: string
service:
type: string
instance:
type: string
replica_name:
type: string
type: object
InstanceStatus:
properties:
adhoc:
Expand Down Expand Up @@ -1923,6 +1934,49 @@ paths:
summary: Get mesos task of service_name.instance_name by task_id
tags:
- service
/services/{service}/{instance}/replicas/{replica_name}/restart:
post:
operationId: instance_replica_restart
parameters:
- description: Service name
in: path
name: service
required: true
schema:
type: string
- description: Instance name
in: path
name: instance
required: true
schema:
type: string
- description: Replica name to restart
in: path
name: replica_name
required: true
schema:
type: string
- description: Force immediate deletion (grace_period_seconds=0) instead of graceful termination
in: query
name: force
required: false
schema:
type: boolean
default: false
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/InstanceReplicaRestartOutcome'
description: Replica restart initiated successfully
"404":
description: Replica not found or service/instance not found
"500":
description: Replica restart failed or instance type not supported
summary: Restart a specific replica for service_name.instance_name
tags:
- service
/version:
get:
operationId: showVersion
Expand Down
71 changes: 71 additions & 0 deletions paasta_tools/api/api_docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,60 @@
}
]
}
},
"/services/{service}/{instance}/replicas/{replica_name}/restart": {
"post": {
"responses": {
"200": {
"description": "Replica restart initiated successfully",
"schema": {
"$ref": "#/definitions/InstanceReplicaRestartOutcome"
}
},
"404": {
"description": "Replica not found or service/instance not found"
},
"500": {
"description": "Replica restart failed or instance type not supported"
}
},
"summary": "Restart a specific replica for service_name.instance_name",
"operationId": "instance_replica_restart",
"tags": [
"service"
],
"parameters": [
{
"in": "path",
"description": "Service name",
"name": "service",
"required": true,
"type": "string"
},
{
"in": "path",
"description": "Instance name",
"name": "instance",
"required": true,
"type": "string"
},
{
"in": "path",
"description": "Replica name to restart",
"name": "replica_name",
"required": true,
"type": "string"
},
{
"in": "query",
"description": "Force immediate deletion (grace_period_seconds=0) instead of graceful termination",
"name": "force",
"required": false,
"type": "boolean",
"default": false
}
]
}
}
},
"definitions": {
Expand Down Expand Up @@ -1196,6 +1250,23 @@
}
}
},
"InstanceReplicaRestartOutcome": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"service": {
"type": "string"
},
"instance": {
"type": "string"
},
"replica_name": {
"type": "string"
}
}
},
"InstanceStatus": {
"type": "object",
"properties": {
Expand Down
60 changes: 60 additions & 0 deletions paasta_tools/api/views/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,63 @@ def instance_mesh_status(
raise ApiFailure(error_message, 500)

return instance_mesh


@view_config(
route_name="service.instance.replica.restart",
request_method="POST",
renderer="json",
)
def instance_replica_restart(
request: Request,
) -> dict[str, Any]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: i wonder if a typeddict or using the generated model would maybe be nicer here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, at the very least having a named model for the response would be great... i don't really see us importing the models into api/views/ files or instance/kubernetes.py at all, and instead only used on the client side, so I suppose I could do both--typeddict on the api side, reference the generated model on the client side?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, i'm not sure if there's a reason for why we haven't tried using the generated named models or if we just never bothered since a lot of these endpoints predated our typing enthusiasm :p

service = request.swagger_data.get("service")
instance = request.swagger_data.get("instance")
replica_name = request.swagger_data.get("replica_name")
force = request.swagger_data.get("force", False)

try:
instance_type = validate_service_instance(
service, instance, settings.cluster, settings.soa_dir
)
except NoConfigurationForServiceError:
error_message = no_configuration_for_service_message(
settings.cluster,
service,
instance,
)
raise ApiFailure(error_message, 404)
except Exception:
error_message = traceback.format_exc()
raise ApiFailure(error_message, 500)

if pik.can_restart_replica(instance_type):
try:
replica_restarted = pik.restart_replica_by_name(
service=service,
instance=instance,
instance_type=instance_type,
replica_name=replica_name,
settings=settings,
force=force,
)
if replica_restarted:
return {
"message": f"Initiated replica restart of {replica_name} successfully",
"service": service,
"instance": instance,
"replica_name": replica_name,
}
else:
raise ApiFailure(
f"Replica {replica_name} not found in service {service}.{instance}",
404,
)
except RuntimeError as e:
raise ApiFailure(str(e), 500)
else:
error_message = (
f"instance_type {instance_type} of {service}.{instance} doesn't "
"support replica restart"
)
raise ApiFailure(error_message, 500)
14 changes: 1 addition & 13 deletions paasta_tools/cli/cmds/remote_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import json
import os
import shutil
import subprocess
Expand All @@ -24,6 +23,7 @@
from paasta_tools.cli.utils import get_paasta_oapi_api_clustername
from paasta_tools.cli.utils import get_paasta_oapi_client_with_auth
from paasta_tools.cli.utils import lazy_choices_completer
from paasta_tools.cli.utils import parse_error
from paasta_tools.cli.utils import run_interactive_cli
from paasta_tools.kubernetes.remote_run import format_remote_run_job_name
from paasta_tools.kubernetes.remote_run import load_eks_or_adhoc_deployment_config
Expand Down Expand Up @@ -75,18 +75,6 @@ def _get_kubectl_wrapper(cluster: str) -> str:
return kubectl_wrapper


def parse_error(body: str) -> str:
try:
body_object = json.loads(body)
except json.decoder.JSONDecodeError:
return body
return (
body_object.get("reason")
or body_object.get("message")
or json.dumps(body_object, indent=4)
)


def paasta_remote_run_copy(
args: argparse.Namespace,
system_paasta_config: SystemPaastaConfig,
Expand Down
Loading