Skip to content

Commit

Permalink
Merge pull request #2275 from HubSpot/deploy-validation
Browse files Browse the repository at this point in the history
Prevent accidental large scale downs
  • Loading branch information
pschoenfelder authored Apr 1, 2022
2 parents 74279ca + bc6cf26 commit 3b120a9
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,25 @@ public SingularityRequestParent createDeployForSingularityRequest(
SingularityDeploy pendingDeploy,
Optional<Boolean> deployUnpause,
Optional<String> message,
Optional<SingularityRequest> updatedRequest
Optional<Boolean> largeScaleDownAcknowledged
) {
return createDeployForSingularityRequest(
requestId,
pendingDeploy,
deployUnpause,
message,
Optional.empty(),
largeScaleDownAcknowledged
);
}

public SingularityRequestParent createDeployForSingularityRequest(
String requestId,
SingularityDeploy pendingDeploy,
Optional<Boolean> deployUnpause,
Optional<String> message,
Optional<SingularityRequest> updatedRequest,
Optional<Boolean> largeScaleDownAcknowledged
) {
final Function<String, String> requestUri = (String host) ->
String.format(DEPLOYS_FORMAT, getApiBase(host));
Expand All @@ -1178,6 +1196,10 @@ public SingularityRequestParent createDeployForSingularityRequest(
message,
updatedRequest
)
),
Collections.singletonMap(
"largeScaleDownAcknowledged",
largeScaleDownAcknowledged.orElse(false)
)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ public class SingularityConfiguration extends Configuration {

private int maxUserIdSize = 100;

private int maxScaleDownWithoutAcknowledgement = 10;

private boolean storeAllMesosTaskInfoForDebugging = false;

@JsonProperty("historyPurging")
Expand Down Expand Up @@ -832,6 +834,10 @@ public int getMaxUserIdSize() {
return maxUserIdSize;
}

public int getMaxScaleDownWithoutAcknowledgement() {
return maxScaleDownWithoutAcknowledgement;
}

public int getMaxTasksPerOffer() {
return maxTasksPerOffer;
}
Expand Down Expand Up @@ -1245,6 +1251,12 @@ public void setMaxTasksPerOfferPerRequest(int maxTasksPerOfferPerRequest) {
this.maxTasksPerOfferPerRequest = maxTasksPerOfferPerRequest;
}

public void setMaxScaleDownWithoutAcknowledgement(
int maxScaleDownWithoutAcknowledgement
) {
this.maxScaleDownWithoutAcknowledgement = maxScaleDownWithoutAcknowledgement;
}

public void setMesosConfiguration(MesosConfiguration mesosConfiguration) {
this.mesosConfiguration = mesosConfiguration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1209,7 +1209,11 @@ private boolean isAllowBounceToSameHost(SingularityRequest request) {
}
}

public void checkScale(SingularityRequest request, Optional<Integer> previousScale) {
public void checkScale(
SingularityRequest request,
Optional<Integer> previousScale,
Optional<Boolean> largeScaleDownAcknowledged
) {
AgentPlacement placement = request.getAgentPlacement().orElse(defaultAgentPlacement);

if (placement != AgentPlacement.GREEDY && placement != AgentPlacement.OPTIMISTIC) {
Expand Down Expand Up @@ -1239,6 +1243,25 @@ public void checkScale(SingularityRequest request, Optional<Integer> previousSca
);
}
}

if (previousScale.isPresent() && !largeScaleDownAcknowledged.orElse(false)) {
int absMaxScaleDown = singularityConfiguration.getMaxScaleDownWithoutAcknowledgement();
boolean scaleDownExceedsAbsoluteMax =
previousScale.get() - request.getInstancesSafe() > absMaxScaleDown;
boolean scaleDownExceedsRelativeMax =
request.getInstancesSafe() < (previousScale.get() / 2);
checkBadRequest(
!scaleDownExceedsAbsoluteMax,
"Cannot scale down by more than %s instances at a time without explicit " +
"acknowledgement (set the largeScaleDownAcknowledged field in the request)",
absMaxScaleDown
);
checkBadRequest(
!(previousScale.get() > absMaxScaleDown && scaleDownExceedsRelativeMax),
"Cannot scale down by more than half of current instances without explicit " +
"acknowledgement (set the largeScaleDownAcknowledged field in the request)"
);
}
}

public void validateExpiringMachineStateChange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static com.hubspot.singularity.WebExceptions.checkNotNullBadRequest;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.hubspot.jackson.jaxrs.PropertyFiltering;
import com.hubspot.singularity.DeployState;
Expand Down Expand Up @@ -56,6 +57,7 @@
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.apache.curator.framework.recipes.leader.LeaderLatch;
Expand Down Expand Up @@ -131,19 +133,29 @@ public SingularityRequestParent deploy(
@RequestBody(
required = true,
description = "Deploy data"
) SingularityDeployRequest deployRequest
) SingularityDeployRequest deployRequest,
@QueryParam("largeScaleDownAcknowledged") Optional<Boolean> largeScaleDownAcknowledged
) {
return maybeProxyToLeader(
requestContext,
SingularityRequestParent.class,
deployRequest,
() -> deploy(deployRequest, user)
() -> deploy(deployRequest, user, largeScaleDownAcknowledged)
);
}

@VisibleForTesting
public SingularityRequestParent deploy(
SingularityDeployRequest deployRequest,
SingularityUser user
) {
return deploy(deployRequest, user, Optional.empty());
}

public SingularityRequestParent deploy(
SingularityDeployRequest deployRequest,
SingularityUser user,
Optional<Boolean> largeScaleDownAcknowledged
) {
validator.checkActionEnabled(SingularityAction.DEPLOY);
SingularityDeploy deploy = deployRequest.getDeploy();
Expand Down Expand Up @@ -197,7 +209,8 @@ public SingularityRequestParent deploy(

validator.checkScale(
request,
Optional.of(taskManager.getActiveTaskIdsForRequest(request.getId()).size())
Optional.of(taskManager.getActiveTaskIdsForRequest(request.getId()).size()),
largeScaleDownAcknowledged
);

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ private void submitRequest(
Optional<Boolean> skipHealthchecks,
Optional<String> message,
Optional<SingularityBounceRequest> maybeBounceRequest,
SingularityUser user
SingularityUser user,
Optional<Boolean> largeScaleDownAcknowledged
) {
checkNotNullBadRequest(request.getId(), "Request must have an id");
checkConflict(
Expand Down Expand Up @@ -224,11 +225,14 @@ private void submitRequest(
request.toBuilder().setInstances(Optional.of(currentActiveAgentCount)).build();
}

if (
!oldRequest.isPresent() ||
!(oldRequest.get().getInstancesSafe() == request.getInstancesSafe())
) {
validator.checkScale(request, Optional.empty());
if (!oldRequest.isPresent()) {
validator.checkScale(request, Optional.empty(), largeScaleDownAcknowledged);
} else if (!(oldRequest.get().getInstancesSafe() == request.getInstancesSafe())) {
validator.checkScale(
request,
Optional.of(oldRequest.get().getInstancesSafe()),
largeScaleDownAcknowledged
);
}

authorizationHelper.checkForAuthorization(
Expand Down Expand Up @@ -383,7 +387,8 @@ public SingularityRequestParent postRequest(
Optional.empty(),
Optional.empty(),
Optional.empty(),
user
user,
Optional.empty()
);
return fillEntireRequest(fetchRequestWithState(request.getId(), user));
}
Expand Down Expand Up @@ -463,7 +468,8 @@ private SingularityRequestParent updateAuthorizedGroups(
Optional.empty(),
updateGroupsRequest.getMessage(),
Optional.empty(),
user
user,
Optional.empty()
);
return fillEntireRequest(fetchRequestWithState(requestId, user));
}
Expand Down Expand Up @@ -1735,7 +1741,8 @@ public SingularityRequestParent setPriority(
Optional.of(false),
Optional.of(message),
Optional.empty(),
user
user,
Optional.empty()
);

if (priorityRequest.getDurationMillis().isPresent()) {
Expand Down Expand Up @@ -1775,20 +1782,30 @@ public SingularityRequestParent scale(
@RequestBody(
required = true,
description = "Object to hold number of instances to request"
) SingularityScaleRequest scaleRequest
) SingularityScaleRequest scaleRequest,
@QueryParam("largeScaleDownAcknowledged") Optional<Boolean> largeScaleDownAcknowledged
) {
return maybeProxyToLeader(
requestContext,
SingularityRequestParent.class,
scaleRequest,
() -> scale(requestId, scaleRequest, user)
() -> scale(requestId, scaleRequest, user, largeScaleDownAcknowledged)
);
}

public SingularityRequestParent scale(
String requestId,
SingularityScaleRequest scaleRequest,
SingularityUser user
) {
return scale(requestId, scaleRequest, user, Optional.empty());
}

public SingularityRequestParent scale(
String requestId,
SingularityScaleRequest scaleRequest,
SingularityUser user,
Optional<Boolean> largeScaleDownAcknowledged
) {
SingularityRequestWithState oldRequestWithState = fetchRequestWithState(
requestId,
Expand All @@ -1808,7 +1825,11 @@ public SingularityRequestParent scale(
.toBuilder()
.setInstances(scaleRequest.getInstances())
.build();
validator.checkScale(newRequest, Optional.<Integer>empty());
validator.checkScale(
newRequest,
oldRequest.getInstances(),
largeScaleDownAcknowledged
);

checkBadRequest(
oldRequest.getInstancesSafe() != newRequest.getInstancesSafe(),
Expand Down Expand Up @@ -1871,7 +1892,8 @@ public SingularityRequestParent scale(
scaleRequest.getSkipHealthchecks(),
Optional.of(scaleMessage),
Optional.of(bounceRequest),
user
user,
largeScaleDownAcknowledged
);
} else {
submitRequest(
Expand All @@ -1881,7 +1903,8 @@ public SingularityRequestParent scale(
scaleRequest.getSkipHealthchecks(),
Optional.of(scaleMessage),
Optional.empty(),
user
user,
largeScaleDownAcknowledged
);
}

Expand Down Expand Up @@ -2117,7 +2140,8 @@ public SingularityRequestParent skipHealthchecks(
Optional.empty(),
skipHealthchecksRequest.getMessage(),
Optional.empty(),
user
user,
Optional.empty()
);

if (skipHealthchecksRequest.getDurationMillis().isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2338,7 +2338,7 @@ public void testBounceReleasesLockWithAlternateCleanupType() {
@Test
public void testIncrementalBounce() {
initRequest();
resourceOffers(2); // set up agents so scale validate will pass
resourceOffers(3); // set up agents so scale validate will pass

SingularityRequest request = requestResource
.getRequest(requestId, singularityUser)
Expand Down
4 changes: 2 additions & 2 deletions SingularityUI/app/actions/api/requests.es6
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export const PersistSkipRequestHealthchecks = buildJsonApiAction(
export const ScaleRequest = buildJsonApiAction(
'SCALE_REQUEST',
'PUT',
(requestId, {instances, skipHealthchecks, durationMillis, message, actionId, bounce, incremental }) => ({
url: `/requests/request/${requestId}/scale`,
(requestId, {instances, skipHealthchecks, durationMillis, message, actionId, bounce, incremental, largeScaleDownAcknowledged }) => ({
url: `/requests/request/${requestId}/scale?largeScaleDownAcknowledged=${largeScaleDownAcknowledged}`,
body: { instances, skipHealthchecks, durationMillis, message, actionId, bounce, incremental }
})
);
Expand Down
9 changes: 7 additions & 2 deletions SingularityUI/app/components/common/modal/FormModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,17 @@ export default class FormModal extends React.Component {
}

static FormItem = (props) => {
if ((props.element.dependsOn && props.formState[props.element.dependsOn]) || !props.element.dependsOn) {
const noDepends = (!props.element.dependsOn && !props.element.dependsOnFormState);
const dependsOnOk = (props.element.dependsOn && props.formState[props.element.dependsOn]);
const dependsOnFormStateOk = (props.element.dependsOnFormState && props.element.dependsOnFormState(props.formState));
if (noDepends || dependsOnOk || dependsOnFormStateOk) {
return (
<div className={classNames(props.className, {'childItem': props.formState[props.element.dependsOn]})}>
{props.children}
</div>
);
}

return null;
};

Expand Down Expand Up @@ -502,6 +506,7 @@ FormModal.propTypes = {
values: React.PropTypes.array,
defaultValue: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool, React.PropTypes.number, React.PropTypes.array]),
validateField: React.PropTypes.func, // String -> String, return field validation error or falsey value if valid
dependsOn: React.PropTypes.string // Only show this item if the other item (referenced by name) has a truthy value
dependsOn: React.PropTypes.string, // Only show this item if the other item (referenced by name) has a truthy value
dependsOnFormState: React.PropTypes.func, // Only show this item if this function applied to form state returns a truthy value
})).isRequired
};
17 changes: 15 additions & 2 deletions SingularityUI/app/components/common/modalButtons/ScaleModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ class ScaleModal extends Component {
}

handleScale(data) {
const { instances, durationMillis, message, bounce, incremental } = data;
const { instances, durationMillis, message, bounce, incremental, largeScaleDownAcknowledged } = data;
const isIncremental = incremental === 'incremental';
this.props.scaleRequest(
{
instances,
durationMillis,
message,
bounce,
incremental: isIncremental
incremental: isIncremental,
largeScaleDownAcknowledged
}
);
}
Expand Down Expand Up @@ -95,6 +96,18 @@ class ScaleModal extends Component {
name: 'message',
type: FormModal.INPUT_TYPES.STRING,
label: 'Message: (optional)'
},
{
name: 'largeScaleDownAcknowledged',
type: FormModal.INPUT_TYPES.BOOLEAN,
label: 'Explciit acknowledgement of large scale down (less than -10 or 1/2 of previous)',
defaultValue: false,
dependsOnFormState: data => {
const scaleDownExceedsAbsoluteMax = (this.props.currentInstances - data.instances) > 10;
const scaleDownExceedsRelativeMax = (this.props.currentInstances > 10) && (data.instances < (this.props.currentInstances / 2));
// window.config flag in case the numbers change at some point
return scaleDownExceedsAbsoluteMax || scaleDownExceedsRelativeMax || window.config.largeScaleDownAcknowledged;
},
}
]}>
<p>Scaling request:</p>
Expand Down

0 comments on commit 3b120a9

Please sign in to comment.