Skip to content

Commit

Permalink
Test user config objects before creating them.
Browse files Browse the repository at this point in the history
Provide detailed error message before proceeding with creation.
  • Loading branch information
jmchilton committed May 17, 2024
1 parent 9262bcb commit cc3e41b
Show file tree
Hide file tree
Showing 20 changed files with 626 additions and 115 deletions.
3 changes: 3 additions & 0 deletions client/src/api/configTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export type VariableValueType = (string | boolean | number) | undefined;
export type VariableData = { [key: string]: VariableValueType };
export type SecretData = { [key: string]: string };

export type PluginAspectStatus = components["schemas"]["PluginAspectStatus"];
export type PluginStatus = components["schemas"]["PluginStatus"];

export interface TemplateSummary {
description: string | null;
hidden?: boolean;
Expand Down
80 changes: 80 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ export interface paths {
/** Create a user-bound file source. */
post: operations["file_sources__create_instance"];
};
"/api/file_source_instances/test": {
/** Test payload for creating user-bound file source. */
post: operations["file_sources__test_new_instance_configuration"];
};
"/api/file_source_instances/{user_file_source_id}": {
/** Get a list of persisted file source instances defined by the requesting user. */
get: operations["file_sources__instances_get"];
Expand Down Expand Up @@ -1269,6 +1273,10 @@ export interface paths {
/** Create a user-bound object store. */
post: operations["object_stores__create_instance"];
};
"/api/object_store_instances/test": {
/** Test payload for creating user-bound object store. */
post: operations["object_stores__test_new_instance_configuration"];
};
"/api/object_store_instances/{user_object_store_id}": {
/** Get a persisted object store instances owned by the requesting user. */
get: operations["object_stores__instances_get"];
Expand Down Expand Up @@ -10446,12 +10454,28 @@ export interface components {
* @enum {string}
*/
PersonalNotificationCategory: "message" | "new_shared_item";
/** PluginAspectStatus */
PluginAspectStatus: {
/** Message */
message: string;
/**
* State
* @enum {string}
*/
state: "ok" | "not_ok" | "unknown";
};
/**
* PluginKind
* @description Enum to distinguish between different kinds or categories of plugins.
* @enum {string}
*/
PluginKind: "rfs" | "drs" | "rdm" | "stock";
/** PluginStatus */
PluginStatus: {
connection?: components["schemas"]["PluginAspectStatus"] | null;
template_definition: components["schemas"]["PluginAspectStatus"];
template_settings?: components["schemas"]["PluginAspectStatus"] | null;
};
/** Position */
Position: {
/** Left */
Expand Down Expand Up @@ -14994,6 +15018,34 @@ export interface operations {
};
};
};
file_sources__test_new_instance_configuration: {
/** Test payload for creating user-bound file source. */
parameters?: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateInstancePayload"];
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PluginStatus"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
file_sources__instances_get: {
/** Get a list of persisted file source instances defined by the requesting user. */
parameters: {
Expand Down Expand Up @@ -20871,6 +20923,34 @@ export interface operations {
};
};
};
object_stores__test_new_instance_configuration: {
/** Test payload for creating user-bound object store. */
parameters?: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateInstancePayload"];
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PluginStatus"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
object_stores__instances_get: {
/** Get a persisted object store instances owned by the requesting user. */
parameters: {
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/ConfigTemplates/formUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
Instance,
PluginStatus,
SecretData,
TemplateSecret,
TemplateSummary,
Expand Down Expand Up @@ -243,3 +244,14 @@ export function upgradeFormDataToPayload(template: TemplateSummary, formData: an
};
return payload;
}

export function pluginStatusToErrorMessage(pluginStatus: PluginStatus): string | null {
if (pluginStatus.template_definition.state == "not_ok") {
return pluginStatus.template_definition.message;
} else if (pluginStatus.template_settings?.state == "not_ok") {
return pluginStatus.template_settings.message;
} else if (pluginStatus.connection?.state == "not_ok") {
return pluginStatus.connection.message;
}
return null;
}
14 changes: 12 additions & 2 deletions client/src/components/FileSources/Instances/CreateForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { BAlert } from "bootstrap-vue";
import { computed, ref } from "vue";
import type { FileSourceTemplateSummary, UserFileSourceModel } from "@/api/fileSources";
import { createFormDataToPayload, createTemplateForm } from "@/components/ConfigTemplates/formUtil";
import {
createFormDataToPayload,
createTemplateForm,
pluginStatusToErrorMessage,
} from "@/components/ConfigTemplates/formUtil";
import { errorMessageAsString } from "@/utils/simple-error";
import { create } from "./services";
import { create, test } from "./services";
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
Expand All @@ -25,6 +29,12 @@ const inputs = computed(() => {
async function onSubmit(formData: any) {
const payload = createFormDataToPayload(props.template, formData);
const { data: pluginStatus } = await test(payload);
const testError = pluginStatusToErrorMessage(pluginStatus);
if (testError) {
error.value = testError;
return;
}
try {
const { data: fileSource } = await create(payload);
emit("created", fileSource);
Expand Down
1 change: 1 addition & 0 deletions client/src/components/FileSources/Instances/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { UserFileSourceModel } from "@/api/fileSources";
import { fetcher } from "@/api/schema/fetcher";

export const create = fetcher.path("/api/file_source_instances").method("post").create();
export const test = fetcher.path("/api/file_source_instances/test").method("post").create();
export const update = fetcher.path("/api/file_source_instances/{user_file_source_id}").method("put").create();

export async function hide(instance: UserFileSourceModel) {
Expand Down
18 changes: 18 additions & 0 deletions client/src/components/ObjectStore/Instances/CreateForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { mockFetcher } from "@/api/schema/__mocks__";
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";

import CreateForm from "./CreateForm.vue";
import { PluginStatus } from "@/api/configTemplates"

jest.mock("@/api/schema");

Expand Down Expand Up @@ -57,6 +58,21 @@ const SIMPLE_TEMPLATE: ObjectStoreTemplateSummary = {
badges: [],
};

const FAKE_PLUGIN_STATUS: PluginStatus = {
template_definition: {
state: 'ok',
message: 'ok',
},
template_settings: {
state: 'ok',
message: 'ok',
},
connection: {
state: 'ok',
message: 'ok',
}
}

describe("CreateForm", () => {
it("should render a form with admin markdown converted to HTML in help", async () => {
const wrapper = mount(CreateForm, {
Expand All @@ -83,6 +99,7 @@ describe("CreateForm", () => {
},
localVue,
});
mockFetcher.path("/api/object_store_instances/test").method("post").mock({ data: FAKE_PLUGIN_STATUS });
mockFetcher.path("/api/object_store_instances").method("post").mock({ data: FAKE_OBJECT_STORE });
await flushPromises();
const nameForElement = wrapper.find("#form-element-_meta_name");
Expand All @@ -102,6 +119,7 @@ describe("CreateForm", () => {
},
localVue,
});
mockFetcher.path("/api/object_store_instances/test").method("post").mock({ data: FAKE_PLUGIN_STATUS });
mockFetcher
.path("/api/object_store_instances")
.method("post")
Expand Down
15 changes: 13 additions & 2 deletions client/src/components/ObjectStore/Instances/CreateForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
import { BAlert } from "bootstrap-vue";
import { computed, ref } from "vue";
import { createFormDataToPayload, createTemplateForm, type FormEntry } from "@/components/ConfigTemplates/formUtil";
import {
createFormDataToPayload,
createTemplateForm,
type FormEntry,
pluginStatusToErrorMessage,
} from "@/components/ConfigTemplates/formUtil";
import type { UserConcreteObjectStore } from "@/components/ObjectStore/Instances/types";
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";
import { errorMessageAsString } from "@/utils/simple-error";
import { create } from "./services";
import { create, test } from "./services";
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
Expand All @@ -26,6 +31,12 @@ const inputs = computed<Array<FormEntry>>(() => {
async function onSubmit(formData: any) {
const payload = createFormDataToPayload(props.template, formData);
const { data: pluginStatus } = await test(payload);
const testError = pluginStatusToErrorMessage(pluginStatus);
if (testError) {
error.value = testError;
return;
}
try {
const { data: objectStore } = await create(payload);
emit("created", objectStore);
Expand Down
1 change: 1 addition & 0 deletions client/src/components/ObjectStore/Instances/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fetcher } from "@/api/schema/fetcher";
import type { UserConcreteObjectStore } from "./types";

export const create = fetcher.path("/api/object_store_instances").method("post").create();
export const test = fetcher.path("/api/object_store_instances/test").method("post").create();
export const update = fetcher.path("/api/object_store_instances/{user_object_store_id}").method("put").create();

export async function hide(instance: UserConcreteObjectStore) {
Expand Down
9 changes: 8 additions & 1 deletion lib/galaxy/managers/_config_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
secrets_as_dict,
SecretsDict,
Template,
TemplateEnvironmentEntry,
TemplateEnvironmentSecret,
TemplateEnvironmentVariable,
TemplateVariableValueType,
Expand Down Expand Up @@ -102,8 +103,14 @@ def recover_secrets(
def prepare_environment(
configuration_template: HasConfigEnvironment, vault: Vault, app_config: UsesTemplatesAppConfig
) -> EnvironmentDict:
return prepare_environment_from_root(configuration_template.template_environment.root, vault, app_config)


def prepare_environment_from_root(
root: Optional[List[TemplateEnvironmentEntry]], vault: Vault, app_config: UsesTemplatesAppConfig
):
environment: EnvironmentDict = {}
for environment_entry in configuration_template.template_environment.root:
for environment_entry in root or []:
e_type = environment_entry.type
e_name = environment_entry.name
if e_type == "secret":
Expand Down
Loading

0 comments on commit cc3e41b

Please sign in to comment.