Skip to content

Conversation

LiteSun
Copy link
Contributor

@LiteSun LiteSun commented Oct 9, 2025

Description

Separate inline upstream in apisix backend as discussed in #319

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

@CLAassistant
Copy link

CLAassistant commented Oct 9, 2025

CLA assistant check
All committers have signed the CLA.

@LiteSun LiteSun changed the title fix: handle service creation with upstreams fix(apisix): handle inline upstream update and delete Oct 9, 2025
@LiteSun LiteSun changed the title fix(apisix): handle inline upstream update and delete feat(apisix): separate inline upstream Oct 10, 2025
@LiteSun LiteSun marked this pull request as ready for review October 10, 2025 07:06
@LiteSun LiteSun requested a review from bzp2010 as a code owner October 10, 2025 07:06
@LiteSun LiteSun force-pushed the sy/inline-upstream branch from d36ec42 to b7d3d36 Compare October 10, 2025 14:49
this.subject = opts.eventSubject;
}

private operate(event: ADCSDK.Event) {
Copy link
Collaborator

@bzp2010 bzp2010 Oct 11, 2025

Choose a reason for hiding this comment

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

Given that service is a special case, creating an operateService function for it is a better choice. Let other resources continue using the simple operate function, while services with specific upstream expansion logic use a separate function.

See the next comment actually only one function is enough.

private operate(event: ADCSDK.Event) {
   // keep original codes
}

// handle only event for service
private operateService(event: ADCSDK.Event) {
  const operateWithRetry = (op: () => Promise<AxiosResponse>) =>
    defer(op).pipe(retry({ count: 3, delay: 100 /* or fn */ }));
  const paths = ['/apisix/admin/upstreams/xxx', '/apisix/admin/services/xxx'];
  const isUpdate = event.type !== ADCSDK.EventType.DELETE;

  return from(isUpdate ? paths : paths.reverse()).pipe(
    map(
      (url) => () =>
        this.client.request({
          method: 'DELETE',
          url,
          ...(isUpdate && {
            method: 'PUT',
            data: this.fromADC(event, this.opts.version),
          }),
        }),
    ),
    concatMap(operateWithRetry),
  );
}

Copy link
Collaborator

@bzp2010 bzp2010 Oct 11, 2025

Choose a reason for hiding this comment

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

Subsequently, it can be further simplified to a single operate function.

private operate(event: ADCSDK.Event) {
  const { type, resourceType, resourceId, parentId } = event;
  const isUpdate = type !== ADCSDK.EventType.DELETE;
  const PATH_PREFIX = '/apisix/admin';
  const paths = [
    `${PATH_PREFIX}/${
      resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL
        ? `consumers/${parentId}/credentials/${resourceId}`
        : `${resourceTypeToAPIName(resourceType)}/${resourceId}`
    }`,
  ];

  if (event.resourceType === ADCSDK.ResourceType.SERVICE) {
    const path = `${PATH_PREFIX}/upstreams/${event.resourceId}`;
    if (event.type === ADCSDK.EventType.DELETE)
      paths.push(path); // services will be deleted before upstreams
    else paths.unshift(path); // services will be created/updated after upstreams
  }

  const operateWithRetry = (op: () => Promise<AxiosResponse>) =>
    defer(op).pipe(retry({ count: 3, delay: 100 /* or fn => timeout * 2 ** count */ }));
  return from(paths).pipe(
    map(
      (path) => () =>
        this.client.request({
          method: 'DELETE',
          url: path,
          ...(isUpdate && {
            method: 'PUT',
            data: this.fromADC(event, this.opts.version),
          }),
        }),
    ),
    concatMap(operateWithRetry),
  );
}

Copy link
Contributor Author

@LiteSun LiteSun Oct 12, 2025

Choose a reason for hiding this comment

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

In fact, if a service is updated from having an inlined upstream to having no upstream, the EventType for the service is update, while for the upstream it is delete. I prefer to abstract the operations on the service separately.

Copy link
Collaborator

@bzp2010 bzp2010 Oct 12, 2025

Choose a reason for hiding this comment

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

This is not allowed. The schema rejects such actions, and even if you disable configuration checks with --no-lint, subsequent processes will not function correctly. All code operates under the assumption that upstream exists inline.

If the inline upstream does not exist (null or undefined), I suspect the ADC will crash. We do not guarantee that such destructive behavior will be ineffective. If you choose not to check, you must verify it yourself or bear the consequences of any failure.

Therefore, inline upstream changes caused by the service will follow the service itself. If the service is deleted, it will also be deleted; if the service remains in existence (created or updated), it will also remain.

What you need to do:

  1. If a service is deleted, cascade-delete its connected upstream resources.
  2. If a service is created, create an upstream and connect it.
  3. If a service is updated, check whether its inline upstreams have been updated. If so, update the split upstream resources. (You can find examples of how to derive this result from diffs in previous PRs.)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants