Skip to content

Conversation

@Revolyssup
Copy link
Contributor

@Revolyssup Revolyssup commented Oct 30, 2025

Provide a validate API for standalone mode that only validate input like admin api but does not save input to memory.
Required for ingress controller to validate plugin configurations that convert from CRD.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Oct 30, 2025
@Revolyssup Revolyssup requested review from bzp2010, membphis and nic-6443 and removed request for bzp2010 October 30, 2025 14:52
})
end

local function validate(ctx)
Copy link
Member

Choose a reason for hiding this comment

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

can we reuse this new validate function in exist update function to reduce code duplicate between those two functions?

@nic-6443 nic-6443 requested a review from AlinsRan November 7, 2025 01:07
@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Nov 19, 2025
@AlinsRan AlinsRan force-pushed the revolyssup/standalone-validate branch from 8f48941 to b0be53b Compare November 19, 2025 01:37
@AlinsRan
Copy link
Contributor

The CI failure is not related to the current PR modification.

membphis
membphis previously approved these changes Nov 19, 2025
Copy link
Member

@membphis membphis left a comment

Choose a reason for hiding this comment

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

LGTM, only some minor code style issues

end
end

if collect_all_errors then
Copy link
Member

Choose a reason for hiding this comment

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

code style

    if collect_all_errors then
        return validation_results.valid, validation_results
    end
    
    return validation_results.valid

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed.


local valid, validation_results = validate_configuration(data, true)

if valid then
Copy link
Member

Choose a reason for hiding this comment

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

    if not valid then
        return core.response.exit(400, {
            error_msg = "Configuration validation failed",
            valid = false,
            errors = validation_results.errors
        })
    end
        
    return core.response.exit(200, {
          message = "Configuration is valid",
          valid = true
      })

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed.

@AlinsRan AlinsRan force-pushed the revolyssup/standalone-validate branch from 1849cb3 to 7355892 Compare November 20, 2025 05:45
Comment on lines +262 to +271
error_msg = "Configuration validation failed",
valid = false,
errors = validation_results.errors
})
end

return core.response.exit(200, {
message = "Configuration is valid",
valid = true
})
Copy link
Contributor

Choose a reason for hiding this comment

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

When the check passes, neither the message nor the valid field carries any meaning; an HTTP 200 response is sufficient to imply this outcome. In practice, neither of these return fields will be utilised anywhere. Remove them.

When the check fails, the valid field similarly holds no significance. An HTTP 400 response can indicate this error. Remove it. The error_msg should maintain a consistent style, using lowercase initial letters.

Comment on lines +197 to +275
it('validate config (success case with json)', async () => {
const resp = await client.post(`${ENDPOINT}/validate`, config1);
expect(resp.status).toEqual(200);
expect(resp.data).toEqual({
message: 'Configuration is valid',
valid: true,
});
});

it('validate config (success case with yaml)', async () => {
const resp = await client.post(`${ENDPOINT}/validate`, YAML.stringify(config1), {
headers: { 'Content-Type': 'application/yaml' },
});
expect(resp.status).toEqual(200);
expect(resp.data).toEqual({
message: 'Configuration is valid',
valid: true,
});
});

it('validate config (success case with multiple resources)', async () => {
const multiResourceConfig = {
routes: [
{
id: 'r1',
uri: '/r1',
upstream: {
nodes: { '127.0.0.1:1980': 1 },
type: 'roundrobin',
},
},
{
id: 'r2',
uri: '/r2',
upstream: {
nodes: { '127.0.0.1:1980': 1 },
type: 'roundrobin',
},
},
],
services: [
{
id: 's1',
upstream: {
nodes: { '127.0.0.1:1980': 1 },
type: 'roundrobin',
},
},
],
routes_conf_version: 1,
services_conf_version: 1,
};

const resp = await client.post(`${ENDPOINT}/validate`, multiResourceConfig);
expect(resp.status).toEqual(200);
expect(resp.data).toEqual({
message: 'Configuration is valid',
valid: true,
});
});

it('validate config with consumer credentials', async () => {
const resp = await client.post(`${ENDPOINT}/validate`, credential1);
expect(resp.status).toEqual(200);
expect(resp.data).toEqual({
message: 'Configuration is valid',
valid: true,
});
});

it('validate config does not persist changes', async () => {
// First validate a configuration
const validateResp = await client.post(`${ENDPOINT}/validate`, config1);
expect(validateResp.status).toEqual(200);

// Then check that the configuration was not persisted
const getResp = await client.get(ENDPOINT);
expect(getResp.data.routes).toBeUndefined();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend moving these validate API tests to a separate describe block, which will help achieve a clearer separation.

const resp = await client.post(`${ENDPOINT}/validate`, credential1);
expect(resp.status).toEqual(200);
expect(resp.data).toEqual({
message: 'Configuration is valid',
Copy link
Contributor

Choose a reason for hiding this comment

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

This message does not need assertion, as checking such constant strings is meaningless. You may switch to the toMatchObject API to assert fields other than message. Only fields expected to exist in the configuration will be checked.

If no further assertion of the response is required, then only assert the response status code.

'invalid routes at index 0, err: invalid configuration: failed to match pattern "^((uri|server_name|server_addr|request_uri|remote_port|remote_addr|query_string|host|hostname|mqtt_client_id)|arg_[0-9a-zA-z_-]+)$" with "args_invalid"',
});
});
it('validate config (duplicate route id)', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the purpose of these newly added tests? Did we previously have duplicate ID checking functionality without test coverage?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants