diff --git a/gateway/gateway-controller/pkg/immutable/loader.go b/gateway/gateway-controller/pkg/immutable/loader.go index 3085e27dd5..092874c1c9 100644 --- a/gateway/gateway-controller/pkg/immutable/loader.go +++ b/gateway/gateway-controller/pkg/immutable/loader.go @@ -21,7 +21,6 @@ package immutable import ( "errors" "fmt" - "io/fs" "log/slog" "net/http" "os" @@ -37,6 +36,13 @@ import ( "github.com/wso2/api-platform/gateway/gateway-controller/pkg/utils" ) +// artifactContentTypes maps supported file extensions to their content types. +var artifactContentTypes = map[string]string{ + ".yaml": "application/x-yaml", + ".yml": "application/x-yaml", + ".json": "application/json", +} + // ImmutableGW manages immutable gateway mode: artifact loading on startup and // read-only enforcement on the management API at runtime. // When cfg.Enabled is false, all methods are no-ops. @@ -91,28 +97,23 @@ func (g *ImmutableGW) LoadArtifacts(log *slog.Logger) error { // pass3: RestApi, WebSubApi, LlmProxy, Mcp — LlmProxy depends on LlmProvider var pass1, pass2, pass3 []artifact - if err := filepath.WalkDir(g.cfg.ArtifactsDir, func(path string, d fs.DirEntry, walkErr error) error { - if walkErr != nil { - return fmt.Errorf("error accessing path %s: %w", path, walkErr) - } - if d.IsDir() { - return nil - } + filePaths, err := collectArtifacts(g.cfg.ArtifactsDir) + if err != nil { + return err + } + + for _, path := range filePaths { ext := strings.ToLower(filepath.Ext(path)) - if ext != ".yaml" && ext != ".yml" && ext != ".json" { - return nil + contentType, ok := artifactContentTypes[ext] + if !ok { + log.Warn("Skipping file with unsupported extension", slog.String("path", path)) + continue } - data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read artifact %s: %w", path, err) } - contentType := "application/x-yaml" - if ext == ".json" { - contentType = "application/json" - } - var envelope struct { Kind string `yaml:"kind" json:"kind"` } @@ -134,9 +135,6 @@ func (g *ImmutableGW) LoadArtifacts(log *slog.Logger) error { default: return fmt.Errorf("artifact %s has unsupported kind %q", path, envelope.Kind) } - return nil - }); err != nil { - return err } total := len(pass1) + len(pass2) + len(pass3) @@ -210,6 +208,35 @@ func (g *ImmutableGW) applyArtifact(path, kind, contentType string, data []byte, return nil } +// collectArtifacts lists files in dir, skipping dot-prefixed entries. +func collectArtifacts(dir string) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("reading artifacts dir %s: %w", dir, err) + } + var paths []string + for _, e := range entries { + if strings.HasPrefix(e.Name(), ".") { + continue + } + path := filepath.Join(dir, e.Name()) + fi, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("stat %s: %w", path, err) + } + if fi.IsDir() { + nested, err := collectArtifacts(path) + if err != nil { + return nil, err + } + paths = append(paths, nested...) + continue + } + paths = append(paths, path) + } + return paths, nil +} + // Middleware returns a handler that rejects POST, PUT, and DELETE with 405 // when immutable mode is enabled. When disabled, it returns a passthrough handler. func (g *ImmutableGW) Middleware() func(http.Handler) http.Handler { diff --git a/gateway/gateway-controller/pkg/immutable/loader_test.go b/gateway/gateway-controller/pkg/immutable/loader_test.go new file mode 100644 index 0000000000..e78fb317a2 --- /dev/null +++ b/gateway/gateway-controller/pkg/immutable/loader_test.go @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. 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 immutable + +import ( + "os" + "path/filepath" + "testing" +) + +const petstoreArtifact = `apiVersion: gateway.api-platform.wso2.com/v1 +kind: RestApi +metadata: + name: petstore-v1 +spec: + displayName: Petstore + version: v1.0 + context: /petstore/$version + upstream: + main: + url: https://petstore3.swagger.io/api/v3 + operations: + - method: GET + path: /pet/{petId} +` + +func TestCollectArtifacts_ConfigMapMountYieldsFileOnce(t *testing.T) { + // Kubernetes ConfigMap mount layout: + //