Skip to content

Commit 86d36ae

Browse files
mjudeikisclaude
andauthored
feat(app-studio): per-workspace tenant access via the GraphQL gateway (#359)
* revert(app-studio): restore /clusters/<path> tenant addressing PR #357's bare-path change routed tenant traffic to the user's DefaultCluster, which is NOT the selected workspace when the user is in any non-default workspace — silently reading/writing the wrong workspace's projects instead of erroring. Restore the original /clusters/<tenantPath> addressing (an honest 403 from the hub proxy) until the GraphQL migration lands proper per-workspace access. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub): inject X-Kedge-Cluster (tenant logical-cluster ID) for providers The backend proxy already injects X-Kedge-Tenant (the workspace path), but the GraphQL gateway at /graphql/clusters/{id} keys its per-cluster schema on the logical-cluster ID, not the path. Add an optional cluster resolver that maps the resolved tenant path to its kcp logical-cluster ID (read from the workspace's LogicalCluster object, cached with a TTL) and inject it as X-Kedge-Cluster. Best-effort: a resolve failure drops only this header. Foundation for moving app-studio's tenant access onto the GraphQL gateway, which (unlike the kcp user-proxy) is not DefaultCluster-gated. Also bumps the gateway fork replace to the commit adding the update<Kind>Status mutation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump gateway fork to add <Plural>Yaml list query Adds the list analogue of <Kind>Yaml so app-studio can List/Get against the GraphQL gateway by exchanging serialized objects, without enumerating typed field-selection sets over unstructured resources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-studio): add GraphQL tenant client (gateway-backed) App Studio's per-workspace data path: talk to the hub's embedded GraphQL gateway at <hubBase>/graphql/<clusterID> as the caller, instead of direct kcp access (which the hub user-proxy gates to the caller's DefaultCluster, so it fails in non-default workspaces). Every op exchanges whole serialized objects via the gateway's <Kind>Yaml / <Plural>Yaml queries and applyYaml / applyStatusYaml mutations, so the client needs no typed field-selection set over unstructured resources. GraphQL errors are mapped onto k8s apierrors so callers' IsNotFound/IsConflict checks keep working. Not yet wired into the call sites — follow-up commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(app-studio): back asclient.Client with a ResourceClient interface Introduce ResourceClient (the per-resource subset App Studio uses) and a GraphQL-backed gqlResource implementing it. asclient.Client now resolves resources through Resource(), backed by either the GraphQL tenant Scope (production) or a dynamic client (tests) — both satisfy ResourceClient. Also handles the core ("") API group in the GraphQL query nesting (version sits directly on the root, no group wrapper) for Secret access. No behavior change yet: production still constructs the dynamic path; wiring the GraphQL path + migrating call sites follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-studio): route tenant access through the GraphQL gateway Activate the GraphQL path: clientFor now builds a tenant.Scope from the X-Kedge-Cluster header (the hub-injected workspace cluster ID) and the caller token, so App Studio reaches its workspace through the hub's gateway — which serves any workspace the caller has access to, unlike the DefaultCluster-gated kcp user-proxy. - http.go reads X-Kedge-Cluster into identity.clusterID - server.go/main.go construct the GraphQL client from KEDGE_HUB_URL - all ~18 c.Dynamic().Resource(gvr) call sites move to c.Resource(descriptor), backed by the GraphQL Scope (prod) or a dynamic client (tests) - tenant_resources.go holds the GraphQL descriptors (Kind/Plural) for Project, Secret, SandboxRunner, and code-provider Connection/Repository - isProjectAPIInitializingError recognises the gateway "schema not ready" shapes (no schema for cluster / Project type missing) for the 503 path Module builds, vets, and the full test suite passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: pin gateway fork to d10b61a (adds applyStatusYaml) The embedded gateway must expose applyStatusYaml, which app-studio calls at runtime for status writes. Bump the replace from a2eab00 to d10b61a so the hub's gateway has all three primitives (update<Kind>Status, <Plural>Yaml, applyStatusYaml). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7eca6f3 commit 86d36ae

17 files changed

Lines changed: 718 additions & 46 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,6 @@ require (
327327
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
328328
)
329329

330-
replace github.com/platform-mesh/kubernetes-graphql-gateway => github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260612154300-f9c2b298c4cd
330+
replace github.com/platform-mesh/kubernetes-graphql-gateway => github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626182922-d10b61a76a93
331331

332332
replace github.com/kcp-dev/multicluster-provider/client => github.com/kcp-dev/multicluster-provider/client v0.8.0

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2
144144
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
145145
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260612154300-f9c2b298c4cd h1:c/SnMtn9/6CXoB/+EATPqP87gOt5ENSFLq6xzGWqNOM=
146146
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260612154300-f9c2b298c4cd/go.mod h1:FTy6tfcfD1XoN081U3WaG+4t1GpgWNRYPYy+AcDYRKU=
147+
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626173633-9c3bfe012c22 h1:MGjoHiISmJLY+lMbfaeA8qIUG6PhfKqOCotXyZt1ygY=
148+
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626173633-9c3bfe012c22/go.mod h1:FTy6tfcfD1XoN081U3WaG+4t1GpgWNRYPYy+AcDYRKU=
149+
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626181934-a2eab00748ed h1:3s49oo4zW5ElcRDgxcWsxKWieqMhlNx+wVde/0BOP1Q=
150+
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626181934-a2eab00748ed/go.mod h1:FTy6tfcfD1XoN081U3WaG+4t1GpgWNRYPYy+AcDYRKU=
151+
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626182922-d10b61a76a93 h1:Ex8FjE9zP4KBWU5wVjKVs9YX57D/ZpGCO0vsG5m6AiA=
152+
github.com/faroshq/kubernetes-graphql-gateway v0.0.0-20260626182922-d10b61a76a93/go.mod h1:FTy6tfcfD1XoN081U3WaG+4t1GpgWNRYPYy+AcDYRKU=
147153
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
148154
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
149155
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright 2026 The Faros Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hub
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"sync"
23+
"time"
24+
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime/schema"
27+
"k8s.io/client-go/dynamic"
28+
"k8s.io/client-go/rest"
29+
30+
"github.com/faroshq/faros-kedge/pkg/apiurl"
31+
)
32+
33+
// logicalClusterGVR addresses the well-known per-workspace LogicalCluster
34+
// object named "cluster", whose kcp.io/cluster annotation is the workspace's
35+
// logical-cluster ID.
36+
var logicalClusterGVR = schema.GroupVersionResource{
37+
Group: "core.kcp.io", Version: "v1alpha1", Resource: "logicalclusters",
38+
}
39+
40+
// clusterIDResolverTTL bounds how long a (tenantPath → clusterID) mapping is
41+
// cached. The mapping is effectively stable for a workspace's lifetime, so a
42+
// generous TTL keeps the warm path free of kcp round-trips while still letting
43+
// a deleted-and-recreated workspace's stale ID age out.
44+
const clusterIDResolverTTL = 10 * time.Minute
45+
46+
// newClusterIDResolver returns a function mapping a tenant workspace path
47+
// (root:kedge:tenants:<org>[:<ws>]) to its kcp logical-cluster ID, for the
48+
// backend proxy to inject as X-Kedge-Cluster. It reads the workspace's
49+
// LogicalCluster "cluster" object via the hub's kcp admin config, scoping the
50+
// host to /clusters/<path> through the front-proxy (which resolves paths).
51+
//
52+
// Results are cached per path with a TTL; resolution failures are not cached so
53+
// a transient kcp hiccup self-heals on the next request.
54+
func newClusterIDResolver(kcpConfig *rest.Config) func(ctx context.Context, tenantPath string) (string, error) {
55+
type entry struct {
56+
id string
57+
expiresAt time.Time
58+
}
59+
var (
60+
mu sync.RWMutex
61+
hot = map[string]entry{}
62+
)
63+
64+
return func(ctx context.Context, tenantPath string) (string, error) {
65+
now := time.Now()
66+
67+
mu.RLock()
68+
e, ok := hot[tenantPath]
69+
mu.RUnlock()
70+
if ok && now.Before(e.expiresAt) {
71+
return e.id, nil
72+
}
73+
74+
cfg := rest.CopyConfig(kcpConfig)
75+
cfg.Host = apiurl.KCPClusterURL(cfg.Host, tenantPath)
76+
dyn, err := dynamic.NewForConfig(cfg)
77+
if err != nil {
78+
return "", fmt.Errorf("dynamic client for %q: %w", tenantPath, err)
79+
}
80+
lc, err := dyn.Resource(logicalClusterGVR).Get(ctx, "cluster", metav1.GetOptions{})
81+
if err != nil {
82+
return "", fmt.Errorf("getting LogicalCluster for %q: %w", tenantPath, err)
83+
}
84+
id := lc.GetAnnotations()["kcp.io/cluster"]
85+
if id == "" {
86+
return "", fmt.Errorf("LogicalCluster for %q has no kcp.io/cluster annotation", tenantPath)
87+
}
88+
89+
mu.Lock()
90+
hot[tenantPath] = entry{id: id, expiresAt: now.Add(clusterIDResolverTTL)}
91+
mu.Unlock()
92+
return id, nil
93+
}
94+
}

pkg/hub/providers/proxy.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package providers
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"crypto/sha256"
2223
"encoding/hex"
2324
"errors"
@@ -114,6 +115,7 @@ func NewBackendProxy(reg *Registry, log logr.Logger) *ProviderProxy {
114115
p.setHeaders = func(req *http.Request, name, _ string) {
115116
req.Header.Del("X-Kedge-User")
116117
req.Header.Del("X-Kedge-Tenant")
118+
req.Header.Del("X-Kedge-Cluster")
117119
if p.tenantResolver == nil {
118120
// V(2) so tests / non-bootstrapper hubs don't spam, but
119121
// devs can flip on verbosity to see the dropped path.
@@ -150,6 +152,17 @@ func NewBackendProxy(reg *Registry, log logr.Logger) *ProviderProxy {
150152
} else {
151153
p.log.Info("tenant resolved but tenantPath empty — forwarding without X-Kedge-Tenant", "provider", name, "user", user, "hint", "user may not have a personal Organization workspace bootstrapped yet")
152154
}
155+
// Resolve the workspace path to its kcp logical-cluster ID and inject
156+
// X-Kedge-Cluster. Best-effort: a resolve failure drops only this
157+
// header (the provider still has X-Kedge-Tenant), so a provider that
158+
// doesn't need the ID is unaffected.
159+
if tenantPath != "" && p.clusterResolver != nil {
160+
if clusterID, err := p.clusterResolver(req.Context(), tenantPath); err != nil {
161+
p.log.Info("cluster-id resolve failed — forwarding without X-Kedge-Cluster", "provider", name, "tenant", tenantPath, "err", err.Error())
162+
} else if clusterID != "" {
163+
req.Header.Set("X-Kedge-Cluster", clusterID)
164+
}
165+
}
153166
}
154167
return p
155168
}
@@ -163,6 +176,14 @@ func (p *ProviderProxy) SetTenantResolver(r TenantResolver) {
163176
p.tenantResolver = r
164177
}
165178

179+
// SetClusterResolver installs an optional resolver mapping a tenant workspace
180+
// path to its kcp logical-cluster ID, injected as X-Kedge-Cluster on
181+
// backend-proxied requests. Wire alongside SetTenantResolver; without it the
182+
// header is simply omitted (and any inbound value is still stripped).
183+
func (p *ProviderProxy) SetClusterResolver(f func(ctx context.Context, tenantPath string) (string, error)) {
184+
p.clusterResolver = f
185+
}
186+
166187
// ProviderProxy is the shared implementation backing both proxies. Exported
167188
// so the server can call SetFallback on the UI proxy after the portal SPA
168189
// handler is built (the two are constructed at different points in Server.Run).
@@ -188,6 +209,14 @@ type ProviderProxy struct {
188209
// UI proxy serves static assets and has no use for caller identity.
189210
// See SetTenantResolver.
190211
tenantResolver TenantResolver
212+
213+
// clusterResolver, when set, maps the resolved tenant workspace path to
214+
// its kcp logical-cluster ID, injected as X-Kedge-Cluster. Providers need
215+
// the ID (not the path) to address per-workspace surfaces that key on it —
216+
// notably the hub's GraphQL gateway at /graphql/clusters/{id}, whose
217+
// per-cluster schema lookup only matches a cluster ID. See
218+
// SetClusterResolver.
219+
clusterResolver func(ctx context.Context, tenantPath string) (string, error)
191220
}
192221

193222
// SetFallback installs the portal SPA handler invoked for non-asset paths

pkg/hub/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,10 @@ func (s *Server) Run(ctx context.Context) error {
533533
// resolver (lives here to avoid a providers→proxy→kcp→providers
534534
// import cycle).
535535
backendProxy.SetTenantResolver(newKCPTenantResolver(kcpProxy, userClient))
536+
// Inject X-Kedge-Cluster (the resolved tenant's logical-cluster
537+
// ID) so providers can address per-workspace surfaces that key on
538+
// the ID — notably the GraphQL gateway at /graphql/clusters/{id}.
539+
backendProxy.SetClusterResolver(newClusterIDResolver(kcpConfig))
536540

537541
// Step 10: Org / Workspace / Membership / User REST
538542
apiMgr := restapi.NewManager(userClient, bootstrapper)

providers/app-studio/api/code_repository.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func (s *Server) prepareProjectRepository(ctx context.Context, c *asclient.Clien
101101

102102
func selectCodeConnection(ctx context.Context, c *asclient.Client, requested string) (string, error) {
103103
if requested != "" {
104-
conn, err := c.Dynamic().Resource(codeConnectionsGVR).Get(ctx, requested, metav1.GetOptions{})
104+
conn, err := c.Resource(codeConnectionResource, "").Get(ctx, requested, metav1.GetOptions{})
105105
if err != nil {
106106
return "", codeProviderRequestError("get Code connection", err)
107107
}
@@ -111,7 +111,7 @@ func selectCodeConnection(ctx context.Context, c *asclient.Client, requested str
111111
return requested, nil
112112
}
113113

114-
list, err := c.Dynamic().Resource(codeConnectionsGVR).List(ctx, metav1.ListOptions{})
114+
list, err := c.Resource(codeConnectionResource, "").List(ctx, metav1.ListOptions{})
115115
if err != nil {
116116
return "", codeProviderRequestError("list Code connections", err)
117117
}
@@ -142,7 +142,7 @@ func repositoryName(ctx context.Context, c *asclient.Client, requested, displayN
142142
if i > 0 {
143143
name = dns1123LabelWithSuffix(base, uuid.NewString()[:6])
144144
}
145-
if _, err := c.Dynamic().Resource(codeRepositoriesGVR).Get(ctx, name, metav1.GetOptions{}); apierrors.IsNotFound(err) {
145+
if _, err := c.Resource(codeRepositoryResource, "").Get(ctx, name, metav1.GetOptions{}); apierrors.IsNotFound(err) {
146146
return name, nil
147147
} else if err != nil {
148148
return "", codeProviderRequestError("get Code repository", err)
@@ -169,7 +169,7 @@ func (s *Server) createProjectRepository(ctx context.Context, c *asclient.Client
169169
"description": plan.Description,
170170
"autoInit": true,
171171
}
172-
if _, err := c.Dynamic().Resource(codeRepositoriesGVR).Create(ctx, repo, metav1.CreateOptions{}); err != nil {
172+
if _, err := c.Resource(codeRepositoryResource, "").Create(ctx, repo, metav1.CreateOptions{}); err != nil {
173173
if apierrors.IsAlreadyExists(err) {
174174
return newValidationError(fmt.Sprintf("Code repository %q already exists", plan.Ref))
175175
}
@@ -183,10 +183,10 @@ func projectRepositoryView(ctx context.Context, c *asclient.Client, p *aiv1alpha
183183
var list codeResourceLister
184184
if c != nil {
185185
get = func(ctx context.Context, gvr schema.GroupVersionResource, name string) (*unstructured.Unstructured, error) {
186-
return c.Dynamic().Resource(gvr).Get(ctx, name, metav1.GetOptions{})
186+
return c.Resource(codeResourceFor(gvr), "").Get(ctx, name, metav1.GetOptions{})
187187
}
188188
list = func(ctx context.Context, gvr schema.GroupVersionResource, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
189-
return c.Dynamic().Resource(gvr).List(ctx, opts)
189+
return c.Resource(codeResourceFor(gvr), "").List(ctx, opts)
190190
}
191191
}
192192
return projectRepositoryViewFromResources(ctx, p, get, list)

providers/app-studio/api/development_runtime.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (s *Server) runtimeTargetForProject(ctx context.Context, c *asclient.Client
6868
if strings.TrimSpace(name) == "" {
6969
return runtimeTarget{}, nil, fmt.Errorf("sandbox runner name is empty")
7070
}
71-
obj, err := c.Dynamic().Resource(sandboxRunnerGVR).Get(ctx, name, metav1.GetOptions{})
71+
obj, err := c.Resource(sandboxRunnerResource, "").Get(ctx, name, metav1.GetOptions{})
7272
if err != nil {
7373
return runtimeTarget{}, nil, err
7474
}
@@ -332,14 +332,14 @@ func patchLastSync(ctx context.Context, c *asclient.Client, name string, t metav
332332
if c == nil {
333333
return nil
334334
}
335-
obj, err := c.Dynamic().Resource(sandboxRunnerGVR).Get(ctx, name, metav1.GetOptions{})
335+
obj, err := c.Resource(sandboxRunnerResource, "").Get(ctx, name, metav1.GetOptions{})
336336
if err != nil {
337337
return err
338338
}
339339
if err := unstructured.SetNestedField(obj.Object, t.Format("2006-01-02T15:04:05Z07:00"), "status", "lastSyncTime"); err != nil {
340340
return err
341341
}
342-
_, err = c.Dynamic().Resource(sandboxRunnerGVR).UpdateStatus(ctx, obj, metav1.UpdateOptions{})
342+
_, err = c.Resource(sandboxRunnerResource, "").UpdateStatus(ctx, obj, metav1.UpdateOptions{})
343343
if apierrors.IsNotFound(err) || apierrors.IsForbidden(err) {
344344
return nil
345345
}

providers/app-studio/api/development_sync.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,8 +457,8 @@ func (s *Server) scheduleDevelopmentSyncAfterMutation(id identity, p *aiv1alpha1
457457
}
458458

459459
func (s *Server) syncDevelopmentAfterMutation(id identity, p *aiv1alpha1.Project, name string) {
460-
if s.clients == nil {
461-
klog.V(2).Infof("development sandbox sync after %s skipped for project %s: tenant client factory is not configured", projectToolBaseName(name), p.Name)
460+
if s.gql == nil {
461+
klog.V(2).Infof("development sandbox sync after %s skipped for project %s: tenant GraphQL client is not configured", projectToolBaseName(name), p.Name)
462462
return
463463
}
464464
c, err := s.clientFor(id)

providers/app-studio/api/http.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
// headers (defense in depth — a spoofed header cannot mis-scope storage).
3232
type identity struct {
3333
tenantPath string // X-Kedge-Tenant, e.g. root:kedge:orgs:<org>:<ws>
34+
clusterID string // X-Kedge-Cluster, the workspace's kcp logical-cluster ID
3435
orgUUID string // parsed from tenantPath
3536
workspaceUUID string // parsed from tenantPath ("" when the path is org-only)
3637
user string // X-Kedge-User
@@ -50,6 +51,7 @@ func identityFromRequest(w http.ResponseWriter, r *http.Request) (identity, bool
5051
org, ws := parseTenantPath(tenantPath)
5152
return identity{
5253
tenantPath: tenantPath,
54+
clusterID: strings.TrimSpace(r.Header.Get("X-Kedge-Cluster")),
5355
orgUUID: org,
5456
workspaceUUID: ws,
5557
user: strings.TrimSpace(r.Header.Get("X-Kedge-User")),

providers/app-studio/api/llm.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,7 @@ func firstSSELine(body []byte) (json.RawMessage, bool) {
12791279

12801280
func readProjectLLMSettings(ctx context.Context, c *asclient.Client) (projectLLMSettings, error) {
12811281
settings := defaultProjectLLMSettings()
1282-
secret, err := c.Dynamic().Resource(secretGVR).Namespace(projectLLMSecretNamespace).Get(ctx, projectLLMSecretName, metav1.GetOptions{})
1282+
secret, err := c.Resource(secretResource, projectLLMSecretNamespace).Get(ctx, projectLLMSecretName, metav1.GetOptions{})
12831283
if apierrors.IsNotFound(err) {
12841284
return settings, nil
12851285
}
@@ -1301,16 +1301,16 @@ func readProjectLLMSettings(ctx context.Context, c *asclient.Client) (projectLLM
13011301

13021302
func writeProjectLLMSettings(ctx context.Context, c *asclient.Client, settings projectLLMSettings) error {
13031303
secret := projectLLMSettingsSecret(settings)
1304-
existing, err := c.Dynamic().Resource(secretGVR).Namespace(projectLLMSecretNamespace).Get(ctx, projectLLMSecretName, metav1.GetOptions{})
1304+
existing, err := c.Resource(secretResource, projectLLMSecretNamespace).Get(ctx, projectLLMSecretName, metav1.GetOptions{})
13051305
if apierrors.IsNotFound(err) {
1306-
_, err = c.Dynamic().Resource(secretGVR).Namespace(projectLLMSecretNamespace).Create(ctx, secret, metav1.CreateOptions{})
1306+
_, err = c.Resource(secretResource, projectLLMSecretNamespace).Create(ctx, secret, metav1.CreateOptions{})
13071307
return err
13081308
}
13091309
if err != nil {
13101310
return err
13111311
}
13121312
secret.SetResourceVersion(existing.GetResourceVersion())
1313-
_, err = c.Dynamic().Resource(secretGVR).Namespace(projectLLMSecretNamespace).Update(ctx, secret, metav1.UpdateOptions{})
1313+
_, err = c.Resource(secretResource, projectLLMSecretNamespace).Update(ctx, secret, metav1.UpdateOptions{})
13141314
return err
13151315
}
13161316

0 commit comments

Comments
 (0)