Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/adc-rust/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

FROM ubuntu:24.04

RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY adc /usr/local/bin/adc
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /usr/local/bin/adc /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
24 changes: 24 additions & 0 deletions .github/adc-rust/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

set -e
if [ "$1" = "server" ]; then
shift
set -- ingress-server "$@"
fi
exec /usr/local/bin/adc "$@"
26 changes: 16 additions & 10 deletions .github/workflows/apisix-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ concurrency:
cancel-in-progress: true

env:
ADC_VERSION: dev
ADC_RUST_VERSION: "0.31.0"

jobs:
e2e-test:
Expand Down Expand Up @@ -99,16 +99,22 @@ jobs:
run: |
make install

- name: Extract adc binary
if: ${{ env.ADC_VERSION == 'dev' }}
- name: Setup adc (rust)
run: |
docker create --name adc-temp ghcr.io/api7/adc:dev
# main.cjs lives in the image's WORKDIR, not at the container root,
# and docker cp resolves a relative SRC_PATH against the root.
docker cp "adc-temp:$(docker inspect -f '{{.Config.WorkingDir}}' adc-temp)/main.cjs" adc.js
docker rm adc-temp
node $(pwd)/adc.js -v
echo "ADC_BIN=node $(pwd)/adc.js" >> $GITHUB_ENV
curl -fsSL -o adc.tar.gz "https://github.com/api7/adc/releases/download/v${{ env.ADC_RUST_VERSION }}/adc_rust_${{ env.ADC_RUST_VERSION }}_linux_amd64.tar.gz"
tar -xzf adc.tar.gz adc
chmod +x adc
./adc --version

# The pod manifest points ADCImage at a real image, unlike ADC_BIN
# (a bare host path the test scaffold execs directly) — the Rust
# binary needs to be wrapped in one to stand in as the sidecar.
cp .github/adc-rust/Dockerfile .github/adc-rust/entrypoint.sh ./
docker build -t adc-rust:dev -f Dockerfile .
kind load docker-image adc-rust:dev --name apisix-ingress-cluster

echo "ADC_BIN=$(pwd)/adc" >> $GITHUB_ENV
echo "ADC_IMAGE=adc-rust:dev" >> $GITHUB_ENV

- name: Start OpenLDAP server
run: make e2e-ldap
Expand Down
26 changes: 16 additions & 10 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ concurrency:
cancel-in-progress: true

env:
ADC_VERSION: dev
ADC_RUST_VERSION: "0.31.0"

jobs:
e2e-test:
Expand Down Expand Up @@ -101,16 +101,22 @@ jobs:
run: |
make kind-load-images

- name: Extract adc binary
if: ${{ env.ADC_VERSION == 'dev' }}
- name: Setup adc (rust)
run: |
docker create --name adc-temp ghcr.io/api7/adc:dev
# main.cjs lives in the image's WORKDIR, not at the container root,
# and docker cp resolves a relative SRC_PATH against the root.
docker cp "adc-temp:$(docker inspect -f '{{.Config.WorkingDir}}' adc-temp)/main.cjs" adc.js
docker rm adc-temp
node $(pwd)/adc.js -v
echo "ADC_BIN=node $(pwd)/adc.js" >> $GITHUB_ENV
curl -fsSL -o adc.tar.gz "https://github.com/api7/adc/releases/download/v${{ env.ADC_RUST_VERSION }}/adc_rust_${{ env.ADC_RUST_VERSION }}_linux_amd64.tar.gz"
tar -xzf adc.tar.gz adc
chmod +x adc
./adc --version

# The pod manifest points ADCImage at a real image, unlike ADC_BIN
# (a bare host path the test scaffold execs directly) — the Rust
# binary needs to be wrapped in one to stand in as the sidecar.
cp .github/adc-rust/Dockerfile .github/adc-rust/entrypoint.sh ./
docker build -t adc-rust:dev -f Dockerfile .
kind load docker-image adc-rust:dev --name apisix-ingress-cluster

echo "ADC_BIN=$(pwd)/adc" >> $GITHUB_ENV
echo "ADC_IMAGE=adc-rust:dev" >> $GITHUB_ENV

- name: Start OpenLDAP server
run: make e2e-ldap
Expand Down
37 changes: 25 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -202,34 +202,47 @@ kind-down:
&& kind delete cluster --name $(KIND_NAME) \
|| echo "kind cluster does not exist"

# kind imports images with `ctr import --all-platforms`. With Docker's containerd
# image store, `docker save` exports every platform of a pulled multi-platform image
# although only one was pulled, so the import fails; save only the node platform.
define kind-load-image
@if docker info -f '{{.DriverStatus}}' | grep -q io.containerd.snapshotter; then \
archive=$$(mktemp) && trap 'rm -f "$$archive"' EXIT && \
docker save --platform "$$(docker version -f '{{.Server.Os}}/{{.Server.Arch}}')" -o "$$archive" $(1) && \
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
kind load image-archive "$$archive" --name $(KIND_NAME); \
else \
kind load docker-image $(1) --name $(KIND_NAME); \
fi
endef

.PHONY: kind-load-images
kind-load-images: pull-infra-images kind-load-ingress-image kind-load-adc-image
@kind load docker-image ghcr.io/api7/api7-ee-3-gateway:dev --name $(KIND_NAME)
@kind load docker-image ghcr.io/api7/api7-ee-dp-manager:$(DASHBOARD_VERSION) --name $(KIND_NAME)
@kind load docker-image ghcr.io/api7/api7-ee-3-integrated:$(DASHBOARD_VERSION) --name $(KIND_NAME)
@kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME)
@kind load docker-image jmalloc/echo-server:latest --name $(KIND_NAME)
@kind load docker-image apache/apisix:dev --name $(KIND_NAME)
@kind load docker-image openresty/openresty:1.27.1.2-4-bullseye-fat --name $(KIND_NAME)
$(call kind-load-image,ghcr.io/api7/api7-ee-3-gateway:dev)
$(call kind-load-image,ghcr.io/api7/api7-ee-dp-manager:$(DASHBOARD_VERSION))
$(call kind-load-image,ghcr.io/api7/api7-ee-3-integrated:$(DASHBOARD_VERSION))
$(call kind-load-image,kennethreitz/httpbin:latest)
$(call kind-load-image,jmalloc/echo-server:latest)
$(call kind-load-image,apache/apisix:dev)
$(call kind-load-image,openresty/openresty:1.27.1.2-4-bullseye-fat)

.PHONY: kind-load-gateway-image
kind-load-gateway-image:
@kind load docker-image ghcr.io/api7/api7-ee-3-gateway:dev --name $(KIND_NAME)
$(call kind-load-image,ghcr.io/api7/api7-ee-3-gateway:dev)

.PHONY: kind-load-dashboard-images
kind-load-dashboard-images:
@kind load docker-image ghcr.io/api7/api7-ee-dp-manager:$(DASHBOARD_VERSION) --name $(KIND_NAME)
@kind load docker-image ghcr.io/api7/api7-ee-3-integrated:$(DASHBOARD_VERSION) --name $(KIND_NAME)
$(call kind-load-image,ghcr.io/api7/api7-ee-dp-manager:$(DASHBOARD_VERSION))
$(call kind-load-image,ghcr.io/api7/api7-ee-3-integrated:$(DASHBOARD_VERSION))

.PHONY: kind-load-ingress-image
kind-load-ingress-image:
@kind load docker-image $(IMG) --name $(KIND_NAME)
$(call kind-load-image,$(IMG))

.PHONY: kind-load-adc-image
kind-load-adc-image:
@docker pull ghcr.io/api7/adc:$(ADC_VERSION)
@docker tag ghcr.io/api7/adc:$(ADC_VERSION) ghcr.io/api7/adc:dev
@kind load docker-image ghcr.io/api7/adc:dev --name $(KIND_NAME)
$(call kind-load-image,ghcr.io/api7/adc:dev)

.PHONY: pull-infra-images
pull-infra-images:
Expand Down
17 changes: 14 additions & 3 deletions api/adc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,11 +761,22 @@ type SyncResult struct {
// This is only used in apisix-standalone mode where endpoint-level status is reported
// instead of resource-level status.
type EndpointStatus struct {
Server string `json:"server"`
Success bool `json:"success"`
Reason string `json:"reason,omitempty"`
Server string `json:"server"`
Success bool `json:"success"`
Confirmation Confirmation `json:"confirmation,omitempty"`
Reason string `json:"reason,omitempty"`
}

type Confirmation string

const (
// ConfirmationApplied means configuration has been accepted and applied.
ConfirmationApplied Confirmation = "applied"

// ConfirmationAccepted means the configuration has been accepted but not yet applied.
ConfirmationAccepted Confirmation = "accepted"
)

type SyncStatus struct {
Event StatusEvent `json:"event"`
FailedAt time.Time `json:"failed_at,omitempty"`
Expand Down
89 changes: 56 additions & 33 deletions internal/adc/client/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin
return req, nil
}

// distinctReasons joins every distinct, non-empty reason in failed, in the order first
// seen -- several resources failing for the exact same reason (a rejected conf_version,
// say) still reports it once.
func distinctReasons(failed []adctypes.SyncStatus) string {
seen := make(map[string]bool, len(failed))
reasons := make([]string, 0, len(failed))
for _, f := range failed {
if f.Reason != "" && !seen[f.Reason] {
seen[f.Reason] = true
reasons = append(reasons, f.Reason)
}
}
return strings.Join(reasons, "; ")
}

// handleHTTPResponse handles the HTTP response from ADC Server
func (e *HTTPADCExecutor) handleHTTPResponse(resp *http.Response, serverAddr string) error {
body, err := io.ReadAll(resp.Body)
Expand All @@ -417,51 +432,59 @@ func (e *HTTPADCExecutor) handleHTTPResponse(resp *http.Response, serverAddr str
"response", string(body),
)

// not only 200, HTTP 202 is also accepted
if resp.StatusCode/100 != 2 {
// ADC Server's /sync status codes:
// - 200: apisix-standalone, every server took and confirmed the write.
// - 202: apisix/api7ee always (no confirm concept); apisix-standalone when every
// server took the write but not every one confirmed it, or one server rejected
// it while others accepted.
// - 400: malformed request, unrelated to any backend's content rejection (see 422).
// - 413: request body over the fixed 100 MB limit.
// - 422: apisix-standalone, every server ended up success:false on the write itself
// (rejected the content, unreachable, or a mix -- concurrent writes aren't
// cancelled on the first failure).
// - 500: ADC Server failed before attempting the write at all (e.g. can't reach any
// server to fetch the current remote state) -- never a verdict on the write.
// Only 200/202/422 carry a SyncResult body -- the other three shouldn't be parsed as
// one: most fields are optional, so an unrelated shape can unmarshal as an empty,
// unremarkable "success".
switch resp.StatusCode {
case http.StatusOK, http.StatusAccepted, http.StatusUnprocessableEntity:
default:
return types.ADCExecutionServerAddrError{
ServerAddr: serverAddr,
Err: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(body)),
}
}

// Parse response body
var result adctypes.SyncResult
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("failed to unmarshal response body: %s, err: %w", string(body), err)
}

// Check for sync failures
// For apisix-standalone mode: Failed is always empty, check EndpointStatus instead
if result.FailedCount > 0 {
if len(result.Failed) > 0 {
reason := result.Failed[0].Reason
e.log.Error(fmt.Errorf("ADC Server sync failed: %s", reason), "ADC Server sync failed", "result", result)
return types.ADCExecutionServerAddrError{
ServerAddr: serverAddr,
Err: reason,
FailedStatuses: result.Failed,
}
if len(result.Failed) > 0 {
reason := distinctReasons(result.Failed)
e.log.Error(fmt.Errorf("ADC Server sync failed: %s", reason), "ADC Server sync failed", "result", result)
return types.ADCExecutionServerAddrError{
ServerAddr: serverAddr,
Err: reason,
FailedStatuses: result.Failed,
EndpointStatuses: result.EndpointStatus,
}
if len(result.EndpointStatus) > 0 {
// apisix-standalone mode: use EndpointStatus
var failedEndpoints []string
for _, ep := range result.EndpointStatus {
if !ep.Success {
failedEndpoints = append(failedEndpoints, fmt.Sprintf("%s: %s", ep.Server, ep.Reason))
}
}
if len(failedEndpoints) > 0 {
reason := strings.Join(failedEndpoints, "; ")
e.log.Error(fmt.Errorf("ADC Server sync failed (standalone mode): %s", reason), "ADC Server sync failed", "result", result)
return types.ADCExecutionServerAddrError{
ServerAddr: serverAddr,
Err: reason,
FailedStatuses: []adctypes.SyncStatus{
{Reason: reason},
},
}
}
}

var failedEndpoints []string
for _, ep := range result.EndpointStatus {
if !ep.Success {
failedEndpoints = append(failedEndpoints, fmt.Sprintf("%s: %s", ep.Server, ep.Reason))
}
}
if len(failedEndpoints) > 0 {
reason := strings.Join(failedEndpoints, "; ")
e.log.Error(fmt.Errorf("ADC Server sync failed (standalone mode): %s", reason), "ADC Server sync failed", "result", result)
return types.ADCExecutionServerAddrError{
ServerAddr: serverAddr,
Err: reason,
EndpointStatuses: result.EndpointStatus,
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

Expand Down
Loading
Loading