Fleet UI: Update error messages for config variables (#45467)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Added managed app configuration for iOS and iPadOS apps (VPP and in-house), configurable via UI, REST API, and GitOps, with `$FLEET_VAR_*` substitution.
|
||||
+106
@@ -97,6 +97,112 @@ describe("getErrorMessage", () => {
|
||||
"Couldn't update configuration. Please try again."
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 'doesn't exist' for an unknown $FLEET_VAR_ variable", () => {
|
||||
const err = {
|
||||
response: {
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
name: "configuration",
|
||||
reason: "unsupported variable $FLEET_VAR_BLA_BLA",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(err, true)).toBe(
|
||||
`Couldn't edit. Variable "$FLEET_VAR_BLA_BLA" doesn't exist.`
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["NDES_SCEP_CHALLENGE"],
|
||||
["NDES_SCEP_PROXY_URL"],
|
||||
["DIGICERT_DATA_myCA"],
|
||||
["DIGICERT_PASSWORD_myCA"],
|
||||
["SCEP_WINDOWS_CERTIFICATE_ID"],
|
||||
["SMALLSTEP_SCEP_CHALLENGE_myCA"],
|
||||
["SMALLSTEP_SCEP_PROXY_URL_myCA"],
|
||||
["CUSTOM_SCEP_CHALLENGE_myCA"],
|
||||
["CUSTOM_SCEP_PROXY_URL_myCA"],
|
||||
["SCEP_RENEWAL_ID"],
|
||||
])(
|
||||
"returns 'isn't supported in managed configuration' for %s",
|
||||
(varSuffix) => {
|
||||
const err = {
|
||||
response: {
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
name: "configuration",
|
||||
reason: `unsupported variable $FLEET_VAR_${varSuffix}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(err, true)).toBe(
|
||||
`Couldn't edit. Variable "$FLEET_VAR_${varSuffix}" isn't supported in managed configuration. It can only be used in configuration profiles.`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it("returns 'doesn't exist' for a typo like $FLEET_VAR_NDES_SCEP_FOO (not a real profile variable)", () => {
|
||||
const err = {
|
||||
response: {
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
name: "configuration",
|
||||
reason: "unsupported variable $FLEET_VAR_NDES_SCEP_FOO",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(err, true)).toBe(
|
||||
`Couldn't edit. Variable "$FLEET_VAR_NDES_SCEP_FOO" doesn't exist.`
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 'doesn't exist' for a missing $FLEET_SECRET_ variable", () => {
|
||||
const err = {
|
||||
response: {
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
name: "configuration",
|
||||
reason:
|
||||
'Couldn\'t add. Secret variable "$FLEET_SECRET_BLA_BLA" missing from database',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(err, true)).toBe(
|
||||
`Couldn't edit. Variable "$FLEET_SECRET_BLA_BLA" doesn't exist.`
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 'doesn't exist' listing all variables for multiple missing $FLEET_SECRET_ variables", () => {
|
||||
const err = {
|
||||
response: {
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
name: "configuration",
|
||||
reason:
|
||||
'Couldn\'t add. Secret variables "$FLEET_SECRET_A", "$FLEET_SECRET_B" missing from database',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(err, true)).toBe(
|
||||
`Couldn't edit. Variables "$FLEET_SECRET_A", "$FLEET_SECRET_B" don't exist.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateJson", () => {
|
||||
|
||||
+68
@@ -5,6 +5,54 @@ import { getErrorReason } from "interfaces/errors";
|
||||
const DEFAULT_ERROR_MESSAGE =
|
||||
"Couldn't update configuration. Please try again.";
|
||||
|
||||
/** Variables that are valid in configuration profiles but NOT in managed configuration. */
|
||||
const PROFILE_ONLY_VARIABLE_PATTERNS = [
|
||||
/^NDES_SCEP_(CHALLENGE|PROXY_URL)$/,
|
||||
/^CUSTOM_SCEP_(CHALLENGE|PROXY_URL)_/,
|
||||
/^SCEP_RENEWAL_ID$/,
|
||||
/^DIGICERT_(DATA|PASSWORD)_/,
|
||||
/^SCEP_WINDOWS_CERTIFICATE_ID$/,
|
||||
/^SMALLSTEP_SCEP_(CHALLENGE|PROXY_URL)_/,
|
||||
];
|
||||
|
||||
const isProfileOnlyVariable = (varNameWithoutPrefix: string): boolean => {
|
||||
return PROFILE_ONLY_VARIABLE_PATTERNS.some((pattern) =>
|
||||
pattern.test(varNameWithoutPrefix)
|
||||
);
|
||||
};
|
||||
|
||||
const generateUnsupportedVariableErrMsg = (errMsg: string) => {
|
||||
const match = errMsg.match(/\$FLEET_VAR_(\w+)/);
|
||||
if (!match) {
|
||||
return DEFAULT_ERROR_MESSAGE;
|
||||
}
|
||||
const fullVarName = match[0];
|
||||
const varNameWithoutPrefix = match[1];
|
||||
|
||||
if (isProfileOnlyVariable(varNameWithoutPrefix)) {
|
||||
return `Couldn't edit. Variable "${fullVarName}" isn't supported in managed configuration. It can only be used in configuration profiles.`;
|
||||
}
|
||||
|
||||
return `Couldn't edit. Variable "${fullVarName}" doesn't exist.`;
|
||||
};
|
||||
|
||||
const generateMissingSecretErrMsg = (errMsg: string) => {
|
||||
const regex = /"\$FLEET_SECRET_\w+"/g;
|
||||
const varNames: string[] = [];
|
||||
let m = regex.exec(errMsg);
|
||||
while (m) {
|
||||
varNames.push(m[0].replace(/"/g, ""));
|
||||
m = regex.exec(errMsg);
|
||||
}
|
||||
if (varNames.length === 0) {
|
||||
return DEFAULT_ERROR_MESSAGE;
|
||||
}
|
||||
const plural = varNames.length > 1 ? "s" : "";
|
||||
const verb = varNames.length > 1 ? "don't" : "doesn't";
|
||||
const quoted = varNames.map((v) => `"${v}"`).join(", ");
|
||||
return `Couldn't edit. Variable${plural} ${quoted} ${verb} exist.`;
|
||||
};
|
||||
|
||||
export const getErrorMessage = (err: unknown, isApplePlatform: boolean) => {
|
||||
const reason = getErrorReason(err);
|
||||
|
||||
@@ -23,6 +71,26 @@ export const getErrorMessage = (err: unknown, isApplePlatform: boolean) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Fleet variable ($FLEET_VAR_) unsupported in managed configuration.
|
||||
// Note: the backend validates $FLEET_VAR_ variables one at a time and
|
||||
// returns on the first unsupported one it finds, so only one variable is
|
||||
// surfaced per request even if the configuration contains multiple invalid
|
||||
// variables. $FLEET_SECRET_ errors can contain multiple variables.
|
||||
if (
|
||||
reason.includes("unsupported variable") &&
|
||||
reason.includes("$FLEET_VAR_")
|
||||
) {
|
||||
return generateUnsupportedVariableErrMsg(reason);
|
||||
}
|
||||
|
||||
// Secret variable missing from database
|
||||
if (
|
||||
reason.includes("missing from database") &&
|
||||
reason.includes("$FLEET_SECRET_")
|
||||
) {
|
||||
return generateMissingSecretErrMsg(reason);
|
||||
}
|
||||
|
||||
return reason || DEFAULT_ERROR_MESSAGE;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { getInstallErrorMessage } from "./helpers";
|
||||
|
||||
const makeErr = (reason: string) => ({
|
||||
response: {
|
||||
data: {
|
||||
errors: [{ name: "base", reason }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("getInstallErrorMessage", () => {
|
||||
it("returns fleetd-specific message", () => {
|
||||
const result = getInstallErrorMessage(
|
||||
makeErr("host has fleetd installed from another source")
|
||||
);
|
||||
expect(result).toContain("fleetd installed");
|
||||
});
|
||||
|
||||
it("returns macOS-only install message", () => {
|
||||
const result = getInstallErrorMessage(
|
||||
makeErr("software can be installed only on darwin")
|
||||
);
|
||||
expect(result).toBe(
|
||||
"Couldn't install. Software can be installed only on macOS."
|
||||
);
|
||||
});
|
||||
|
||||
it("returns MDM turned off message as-is", () => {
|
||||
const result = getInstallErrorMessage(makeErr("MDM is turned off."));
|
||||
expect(result).toBe("MDM is turned off");
|
||||
});
|
||||
|
||||
it("returns no available licenses message as-is", () => {
|
||||
const result = getInstallErrorMessage(makeErr("No available licenses."));
|
||||
expect(result).toBe("No available licenses");
|
||||
});
|
||||
|
||||
it("returns unresolvable Fleet variable message", () => {
|
||||
const result = getInstallErrorMessage(
|
||||
makeErr(
|
||||
"apple_mdm: unresolvable Fleet variable in managed app configuration"
|
||||
)
|
||||
);
|
||||
expect(result).toBe(
|
||||
"Couldn't install. Couldn't resolve a Fleet variable in the managed app configuration for this host."
|
||||
);
|
||||
});
|
||||
|
||||
it("returns default message for unknown errors", () => {
|
||||
const result = getInstallErrorMessage(makeErr("something unexpected"));
|
||||
expect(result).toBe("Couldn't install. Please try again.");
|
||||
});
|
||||
|
||||
it("returns default message when no reason can be extracted", () => {
|
||||
expect(getInstallErrorMessage({})).toBe(
|
||||
"Couldn't install. Please try again."
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,10 @@ export const getInstallErrorMessage = (e: unknown) => {
|
||||
return createOnlyInstallableOnMacOSMessage(reason);
|
||||
} else if (reason.includes("VPP token expired")) {
|
||||
return createVPPTokenExpiredMessage();
|
||||
} else if (
|
||||
reason.includes("unresolvable Fleet variable in managed app configuration")
|
||||
) {
|
||||
return `${INSTALL_SOFTWARE_ERROR_PREFIX} Couldn't resolve a Fleet variable in the managed app configuration for this host.`;
|
||||
} else if (showAPIMessage(reason)) {
|
||||
return reason;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user