Skip to content
Open
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
37 changes: 37 additions & 0 deletions internal/adc/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,43 @@ func (c *Client) DeleteConfig(ctx context.Context, args Task) error {
return err
}

func (c *Client) Validate(ctx context.Context, task Task) error {
if len(task.Configs) == 0 || task.Resources == nil {
return nil
}

fileIOStart := time.Now()
syncFilePath, cleanup, err := prepareSyncFile(task.Resources)
if err != nil {
pkgmetrics.RecordFileIODuration("prepare_sync_file", "failure", time.Since(fileIOStart).Seconds())
return err
}
pkgmetrics.RecordFileIODuration("prepare_sync_file", adctypes.StatusSuccess, time.Since(fileIOStart).Seconds())
defer cleanup()

args2 := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes)

var errs types.ADCValidationErrors
for _, config := range task.Configs {
if config.BackendType == "" {
config.BackendType = c.defaultMode
}
if err := c.executor.Validate(ctx, config, args2); err != nil {
var validationErr types.ADCValidationError
if errors.As(err, &validationErr) {
errs.Errors = append(errs.Errors, validationErr)
continue
}
return err
}
}

if len(errs.Errors) > 0 {
return errs
}
return nil
}

func (c *Client) Sync(ctx context.Context) (map[string]types.ADCExecutionErrors, error) {
c.syncMu.Lock()
defer c.syncMu.Unlock()
Expand Down
143 changes: 139 additions & 4 deletions internal/adc/client/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (

type ADCExecutor interface {
Execute(ctx context.Context, config adctypes.Config, args []string) error
Validate(ctx context.Context, config adctypes.Config, args []string) error
}

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

type ADCValidateResult struct {
Success *bool `json:"success,omitempty"`
ErrorMessage string `json:"message,omitempty"`
Errors []types.ADCValidationDetail `json:"errors,omitempty"`
}

// HTTPADCExecutor implements ADCExecutor interface using HTTP calls to ADC Server
type HTTPADCExecutor struct {
httpClient *http.Client
Expand Down Expand Up @@ -123,6 +130,10 @@ func (e *HTTPADCExecutor) Execute(ctx context.Context, config adctypes.Config, a
return e.runHTTPSync(ctx, config, args)
}

func (e *HTTPADCExecutor) Validate(ctx context.Context, config adctypes.Config, args []string) error {
return e.runHTTPValidate(ctx, config, args)
}

// runHTTPSync performs HTTP sync to ADC Server for each server address
func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config adctypes.Config, args []string) error {
var execErrs = types.ADCExecutionError{
Expand Down Expand Up @@ -157,6 +168,38 @@ func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config adctypes.Confi
return nil
}

func (e *HTTPADCExecutor) runHTTPValidate(ctx context.Context, config adctypes.Config, args []string) error {
var validationErr = types.ADCValidationError{
Name: config.Name,
}
var infraErrs []error

serverAddrs := func() []string {
return config.ServerAddrs
}()
e.log.V(1).Info("running http validate", "serverAddrs", serverAddrs)

for _, addr := range serverAddrs {
if err := e.runHTTPValidateForSingleServer(ctx, addr, config, args); err != nil {
e.log.Error(err, "failed to run http validate for server", "server", addr)
var validationServerErr types.ADCValidationServerAddrError
if errors.As(err, &validationServerErr) {
validationErr.FailedErrors = append(validationErr.FailedErrors, validationServerErr)
continue
}
infraErrs = append(infraErrs, err)
}
}

if len(validationErr.FailedErrors) > 0 {
return validationErr
}
if len(infraErrs) > 0 {
return errors.Join(infraErrs...)
}
return nil
}

// runHTTPSyncForSingleServer performs HTTP sync to a single ADC Server
func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, serverAddr string, config adctypes.Config, args []string) error {
ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
Expand All @@ -175,7 +218,7 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server
}

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

func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, serverAddr string, config adctypes.Config, args []string) error {
ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
defer cancel()

labels, types, filePath, err := e.parseArgs(args)
if err != nil {
return fmt.Errorf("failed to parse args: %w", err)
}

resources, err := e.loadResourcesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to load resources from file %s: %w", filePath, err)
}

req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, "/validate")
if err != nil {
return fmt.Errorf("failed to build validate request: %w", err)
}

resp, err := e.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send HTTP request: %w", err)
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
e.log.Error(closeErr, "failed to close response body")
}
}()

return e.handleHTTPValidateResponse(resp, serverAddr)
}

// parseArgs parses the command line arguments to extract labels, types, and file path
func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, []string, string, error) {
labels := make(map[string]string)
Expand Down Expand Up @@ -248,7 +323,7 @@ func (e *HTTPADCExecutor) loadResourcesFromFile(filePath string) (*adctypes.Reso
}

// buildHTTPRequest builds the HTTP request for ADC Server
func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, types []string, resources *adctypes.Resources) (*http.Request, error) {
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) {
// Prepare request body
tlsVerify := config.TlsVerify
reqBody := ADCServerRequest{
Expand All @@ -274,7 +349,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin
}

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

// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "PUT", e.serverURL+"/sync", bytes.NewBuffer(jsonData))
req, err := http.NewRequestWithContext(ctx, method, e.serverURL+path, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
Expand Down Expand Up @@ -357,3 +432,63 @@ func (e *HTTPADCExecutor) handleHTTPResponse(resp *http.Response, serverAddr str
e.log.V(1).Info("ADC Server sync success", "result", result)
return nil
}

func (e *HTTPADCExecutor) handleHTTPValidateResponse(resp *http.Response, serverAddr string) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}

e.log.V(1).Info("received HTTP validate response from ADC Server",
"server", serverAddr,
"status", resp.StatusCode,
"response", string(body),
)

parseValidationResult := func() *ADCValidateResult {
if len(body) == 0 {
return nil
}
var result ADCValidateResult
if err := json.Unmarshal(body, &result); err != nil {
return nil
}
return &result
}

if resp.StatusCode == http.StatusBadRequest {
result := parseValidationResult()
errMsg := string(body)
if result != nil && result.ErrorMessage != "" {
errMsg = result.ErrorMessage
}
return types.ADCValidationServerAddrError{
ServerAddr: serverAddr,
Err: errMsg,
ValidationErrors: func() []types.ADCValidationDetail {
if result == nil {
return nil
}
return result.Errors
}(),
}
}

if resp.StatusCode/100 != 2 {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}

if result := parseValidationResult(); result != nil && result.Success != nil && !*result.Success {
errMsg := result.ErrorMessage
if errMsg == "" {
errMsg = "ADC validation failed"
}
return types.ADCValidationServerAddrError{
ServerAddr: serverAddr,
Err: errMsg,
ValidationErrors: result.Errors,
}
}

return nil
}
113 changes: 113 additions & 0 deletions internal/controller/webhook_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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.

package controller

import (
"context"

"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"

v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/utils"
)

func PrepareApisixRouteForValidation(ctx context.Context, c client.Client, log logr.Logger, route *apiv2.ApisixRoute) (*provider.TranslateContext, error) {
tctx := provider.NewDefaultTranslateContext(ctx)

ingressClass, err := FindMatchingIngressClass(tctx, c, log, route)
if err != nil {
return nil, err
}
if err := ProcessIngressClassParameters(tctx, c, log, route, ingressClass); err != nil {
return nil, err
}

reconciler := &ApisixRouteReconciler{
Client: c,
Log: log,
}
if err := reconciler.processApisixRoute(tctx, route); err != nil {
return nil, err
}
return tctx, nil
}

func PrepareApisixConsumerForValidation(ctx context.Context, c client.Client, log logr.Logger, consumer *apiv2.ApisixConsumer) (*provider.TranslateContext, error) {
tctx := provider.NewDefaultTranslateContext(ctx)

ingressClass, err := FindMatchingIngressClass(tctx, c, log, consumer)
if err != nil {
return nil, err
}
if err := ProcessIngressClassParameters(tctx, c, log, consumer, ingressClass); err != nil {
return nil, err
}

reconciler := &ApisixConsumerReconciler{
Client: c,
Log: log,
}
if err := reconciler.processSpec(ctx, tctx, consumer); err != nil {
return nil, err
}
return tctx, nil
}

func PrepareConsumerForValidation(ctx context.Context, c client.Client, log logr.Logger, consumer *v1alpha1.Consumer) (*provider.TranslateContext, error) {
tctx := provider.NewDefaultTranslateContext(ctx)

reconciler := &ConsumerReconciler{
Client: c,
Log: log,
}
gateway, err := reconciler.getGateway(ctx, consumer)
if err != nil {
return nil, err
}
if err := ProcessGatewayProxy(c, log, tctx, gateway, utils.NamespacedNameKind(consumer)); err != nil {
return nil, err
}
if err := reconciler.processSpec(ctx, tctx, consumer); err != nil {
return nil, err
}
return tctx, nil
}

func PrepareApisixTlsForValidation(ctx context.Context, c client.Client, log logr.Logger, tls *apiv2.ApisixTls) (*provider.TranslateContext, error) {
tctx := provider.NewDefaultTranslateContext(ctx)

ingressClass, err := FindMatchingIngressClass(tctx, c, log, tls)
if err != nil {
return nil, err
}
if err := ProcessIngressClassParameters(tctx, c, log, tls, ingressClass); err != nil {
return nil, err
}

reconciler := &ApisixTlsReconciler{
Client: c,
Log: log,
}
if err := reconciler.processApisixTls(ctx, tctx, tls); err != nil {
return nil, err
}
return tctx, nil
}
Loading
Loading