Skip to content

Commit 5c236d0

Browse files
authored
feat: add ADC-backed admission validation for APISIX CRDs (#2758)
1 parent e89f0f2 commit 5c236d0

19 files changed

Lines changed: 1704 additions & 82 deletions

internal/adc/client/client.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,43 @@ func (c *Client) DeleteConfig(ctx context.Context, args Task) error {
171171
return err
172172
}
173173

174+
func (c *Client) Validate(ctx context.Context, task Task) error {
175+
if len(task.Configs) == 0 || task.Resources == nil {
176+
return nil
177+
}
178+
179+
fileIOStart := time.Now()
180+
syncFilePath, cleanup, err := prepareSyncFile(task.Resources)
181+
if err != nil {
182+
pkgmetrics.RecordFileIODuration("prepare_sync_file", "failure", time.Since(fileIOStart).Seconds())
183+
return err
184+
}
185+
pkgmetrics.RecordFileIODuration("prepare_sync_file", adctypes.StatusSuccess, time.Since(fileIOStart).Seconds())
186+
defer cleanup()
187+
188+
args2 := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes)
189+
190+
var errs types.ADCValidationErrors
191+
for _, config := range task.Configs {
192+
if config.BackendType == "" {
193+
config.BackendType = c.defaultMode
194+
}
195+
if err := c.executor.Validate(ctx, config, args2); err != nil {
196+
var validationErr types.ADCValidationError
197+
if errors.As(err, &validationErr) {
198+
errs.Errors = append(errs.Errors, validationErr)
199+
continue
200+
}
201+
return err
202+
}
203+
}
204+
205+
if len(errs.Errors) > 0 {
206+
return errs
207+
}
208+
return nil
209+
}
210+
174211
func (c *Client) Sync(ctx context.Context) (map[string]types.ADCExecutionErrors, error) {
175212
c.syncMu.Lock()
176213
defer c.syncMu.Unlock()

internal/adc/client/executor.go

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343

4444
type ADCExecutor interface {
4545
Execute(ctx context.Context, config adctypes.Config, args []string) error
46+
Validate(ctx context.Context, config adctypes.Config, args []string) error
4647
}
4748

4849
func BuildADCExecuteArgs(filePath string, labels map[string]string, types []string) []string {
@@ -81,6 +82,12 @@ type ADCServerOpts struct {
8182
CacheKey string `json:"cacheKey"`
8283
}
8384

85+
type ADCValidateResult struct {
86+
Success *bool `json:"success,omitempty"`
87+
ErrorMessage string `json:"message,omitempty"`
88+
Errors []types.ADCValidationDetail `json:"errors,omitempty"`
89+
}
90+
8491
// HTTPADCExecutor implements ADCExecutor interface using HTTP calls to ADC Server
8592
type HTTPADCExecutor struct {
8693
httpClient *http.Client
@@ -123,6 +130,10 @@ func (e *HTTPADCExecutor) Execute(ctx context.Context, config adctypes.Config, a
123130
return e.runHTTPSync(ctx, config, args)
124131
}
125132

133+
func (e *HTTPADCExecutor) Validate(ctx context.Context, config adctypes.Config, args []string) error {
134+
return e.runHTTPValidate(ctx, config, args)
135+
}
136+
126137
// runHTTPSync performs HTTP sync to ADC Server for each server address
127138
func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config adctypes.Config, args []string) error {
128139
var execErrs = types.ADCExecutionError{
@@ -157,6 +168,38 @@ func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config adctypes.Confi
157168
return nil
158169
}
159170

171+
func (e *HTTPADCExecutor) runHTTPValidate(ctx context.Context, config adctypes.Config, args []string) error {
172+
var validationErr = types.ADCValidationError{
173+
Name: config.Name,
174+
}
175+
var infraErrs []error
176+
177+
serverAddrs := func() []string {
178+
return config.ServerAddrs
179+
}()
180+
e.log.V(1).Info("running http validate", "serverAddrs", serverAddrs)
181+
182+
for _, addr := range serverAddrs {
183+
if err := e.runHTTPValidateForSingleServer(ctx, addr, config, args); err != nil {
184+
e.log.Error(err, "failed to run http validate for server", "server", addr)
185+
var validationServerErr types.ADCValidationServerAddrError
186+
if errors.As(err, &validationServerErr) {
187+
validationErr.FailedErrors = append(validationErr.FailedErrors, validationServerErr)
188+
continue
189+
}
190+
infraErrs = append(infraErrs, err)
191+
}
192+
}
193+
194+
if len(validationErr.FailedErrors) > 0 {
195+
return validationErr
196+
}
197+
if len(infraErrs) > 0 {
198+
return errors.Join(infraErrs...)
199+
}
200+
return nil
201+
}
202+
160203
// runHTTPSyncForSingleServer performs HTTP sync to a single ADC Server
161204
func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, serverAddr string, config adctypes.Config, args []string) error {
162205
ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
@@ -175,7 +218,7 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server
175218
}
176219

177220
// Build HTTP request
178-
req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources)
221+
req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, "/sync")
179222
if err != nil {
180223
return fmt.Errorf("failed to build HTTP request: %w", err)
181224
}
@@ -195,6 +238,38 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server
195238
return e.handleHTTPResponse(resp, serverAddr)
196239
}
197240

241+
func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, serverAddr string, config adctypes.Config, args []string) error {
242+
ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
243+
defer cancel()
244+
245+
labels, types, filePath, err := e.parseArgs(args)
246+
if err != nil {
247+
return fmt.Errorf("failed to parse args: %w", err)
248+
}
249+
250+
resources, err := e.loadResourcesFromFile(filePath)
251+
if err != nil {
252+
return fmt.Errorf("failed to load resources from file %s: %w", filePath, err)
253+
}
254+
255+
req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, "/validate")
256+
if err != nil {
257+
return fmt.Errorf("failed to build validate request: %w", err)
258+
}
259+
260+
resp, err := e.httpClient.Do(req)
261+
if err != nil {
262+
return fmt.Errorf("failed to send HTTP request: %w", err)
263+
}
264+
defer func() {
265+
if closeErr := resp.Body.Close(); closeErr != nil {
266+
e.log.Error(closeErr, "failed to close response body")
267+
}
268+
}()
269+
270+
return e.handleHTTPValidateResponse(resp, serverAddr)
271+
}
272+
198273
// parseArgs parses the command line arguments to extract labels, types, and file path
199274
func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, []string, string, error) {
200275
labels := make(map[string]string)
@@ -248,7 +323,7 @@ func (e *HTTPADCExecutor) loadResourcesFromFile(filePath string) (*adctypes.Reso
248323
}
249324

250325
// buildHTTPRequest builds the HTTP request for ADC Server
251-
func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, types []string, resources *adctypes.Resources) (*http.Request, error) {
326+
func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, types []string, resources *adctypes.Resources, method string, path string) (*http.Request, error) {
252327
// Prepare request body
253328
tlsVerify := config.TlsVerify
254329
reqBody := ADCServerRequest{
@@ -274,7 +349,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin
274349
}
275350

276351
e.log.V(1).Info("sending HTTP request to ADC Server",
277-
"url", e.serverURL+"/sync",
352+
"url", e.serverURL+path,
278353
"server", serverAddr,
279354
"mode", config.BackendType,
280355
"cacheKey", config.Name,
@@ -284,7 +359,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin
284359
)
285360

286361
// Create HTTP request
287-
req, err := http.NewRequestWithContext(ctx, "PUT", e.serverURL+"/sync", bytes.NewBuffer(jsonData))
362+
req, err := http.NewRequestWithContext(ctx, method, e.serverURL+path, bytes.NewBuffer(jsonData))
288363
if err != nil {
289364
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
290365
}
@@ -357,3 +432,63 @@ func (e *HTTPADCExecutor) handleHTTPResponse(resp *http.Response, serverAddr str
357432
e.log.V(1).Info("ADC Server sync success", "result", result)
358433
return nil
359434
}
435+
436+
func (e *HTTPADCExecutor) handleHTTPValidateResponse(resp *http.Response, serverAddr string) error {
437+
body, err := io.ReadAll(resp.Body)
438+
if err != nil {
439+
return fmt.Errorf("failed to read response body: %w", err)
440+
}
441+
442+
e.log.V(1).Info("received HTTP validate response from ADC Server",
443+
"server", serverAddr,
444+
"status", resp.StatusCode,
445+
"response", string(body),
446+
)
447+
448+
parseValidationResult := func() *ADCValidateResult {
449+
if len(body) == 0 {
450+
return nil
451+
}
452+
var result ADCValidateResult
453+
if err := json.Unmarshal(body, &result); err != nil {
454+
return nil
455+
}
456+
return &result
457+
}
458+
459+
if resp.StatusCode == http.StatusBadRequest {
460+
result := parseValidationResult()
461+
errMsg := string(body)
462+
if result != nil && result.ErrorMessage != "" {
463+
errMsg = result.ErrorMessage
464+
}
465+
return types.ADCValidationServerAddrError{
466+
ServerAddr: serverAddr,
467+
Err: errMsg,
468+
ValidationErrors: func() []types.ADCValidationDetail {
469+
if result == nil {
470+
return nil
471+
}
472+
return result.Errors
473+
}(),
474+
}
475+
}
476+
477+
if resp.StatusCode/100 != 2 {
478+
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
479+
}
480+
481+
if result := parseValidationResult(); result != nil && result.Success != nil && !*result.Success {
482+
errMsg := result.ErrorMessage
483+
if errMsg == "" {
484+
errMsg = "ADC validation failed"
485+
}
486+
return types.ADCValidationServerAddrError{
487+
ServerAddr: serverAddr,
488+
Err: errMsg,
489+
ValidationErrors: result.Errors,
490+
}
491+
}
492+
493+
return nil
494+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package controller
19+
20+
import (
21+
"context"
22+
23+
"github.com/go-logr/logr"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
26+
v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
27+
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
28+
"github.com/apache/apisix-ingress-controller/internal/provider"
29+
"github.com/apache/apisix-ingress-controller/internal/utils"
30+
)
31+
32+
func PrepareApisixRouteForValidation(ctx context.Context, c client.Client, log logr.Logger, route *apiv2.ApisixRoute) (*provider.TranslateContext, error) {
33+
tctx := provider.NewDefaultTranslateContext(ctx)
34+
35+
ingressClass, err := FindMatchingIngressClass(tctx, c, log, route)
36+
if err != nil {
37+
return nil, err
38+
}
39+
if err := ProcessIngressClassParameters(tctx, c, log, route, ingressClass); err != nil {
40+
return nil, err
41+
}
42+
43+
reconciler := &ApisixRouteReconciler{
44+
Client: c,
45+
Log: log,
46+
}
47+
if err := reconciler.processApisixRoute(tctx, route); err != nil {
48+
return nil, err
49+
}
50+
return tctx, nil
51+
}
52+
53+
func PrepareApisixConsumerForValidation(ctx context.Context, c client.Client, log logr.Logger, consumer *apiv2.ApisixConsumer) (*provider.TranslateContext, error) {
54+
tctx := provider.NewDefaultTranslateContext(ctx)
55+
56+
ingressClass, err := FindMatchingIngressClass(tctx, c, log, consumer)
57+
if err != nil {
58+
return nil, err
59+
}
60+
if err := ProcessIngressClassParameters(tctx, c, log, consumer, ingressClass); err != nil {
61+
return nil, err
62+
}
63+
64+
reconciler := &ApisixConsumerReconciler{
65+
Client: c,
66+
Log: log,
67+
}
68+
if err := reconciler.processSpec(ctx, tctx, consumer); err != nil {
69+
return nil, err
70+
}
71+
return tctx, nil
72+
}
73+
74+
func PrepareConsumerForValidation(ctx context.Context, c client.Client, log logr.Logger, consumer *v1alpha1.Consumer) (*provider.TranslateContext, error) {
75+
tctx := provider.NewDefaultTranslateContext(ctx)
76+
77+
reconciler := &ConsumerReconciler{
78+
Client: c,
79+
Log: log,
80+
}
81+
gateway, err := reconciler.getGateway(ctx, consumer)
82+
if err != nil {
83+
return nil, err
84+
}
85+
if err := ProcessGatewayProxy(c, log, tctx, gateway, utils.NamespacedNameKind(consumer)); err != nil {
86+
return nil, err
87+
}
88+
if err := reconciler.processSpec(ctx, tctx, consumer); err != nil {
89+
return nil, err
90+
}
91+
return tctx, nil
92+
}
93+
94+
func PrepareApisixTlsForValidation(ctx context.Context, c client.Client, log logr.Logger, tls *apiv2.ApisixTls) (*provider.TranslateContext, error) {
95+
tctx := provider.NewDefaultTranslateContext(ctx)
96+
97+
ingressClass, err := FindMatchingIngressClass(tctx, c, log, tls)
98+
if err != nil {
99+
return nil, err
100+
}
101+
if err := ProcessIngressClassParameters(tctx, c, log, tls, ingressClass); err != nil {
102+
return nil, err
103+
}
104+
105+
reconciler := &ApisixTlsReconciler{
106+
Client: c,
107+
Log: log,
108+
}
109+
if err := reconciler.processApisixTls(ctx, tctx, tls); err != nil {
110+
return nil, err
111+
}
112+
return tctx, nil
113+
}

0 commit comments

Comments
 (0)