Files
OpenShell/sdk/go/openshell/v1/profile_client.go
Shiju e585b49173 feat(providers): support stable credential placeholders (#3339)
* feat(providers): support stable credential placeholders

Add credentials[].stable_placeholder so a running client keeps using an externally rotated static credential through the reference it already holds. Squashed replay of the earlier four commits onto upstream main 71c3cd957; re-derives the frozen schema fingerprints and adapts the provider environment revision test to the added capability argument.

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(e2e): apply stable-placeholder supervisor image override

Use ManagedGateway's per-process supervisor image override so the fixture
CA reaches the supervisor when runtime image environment variables take
precedence over gateway TOML. Restore the wrapper's original image after
each test, following the provider-readiness fixture.

Signed-off-by: Shiju <shiju@nvidia.com>

* test(server): update public RPC schema fingerprint after merging main

Signed-off-by: Shiju <shiju@nvidia.com>

---------

Signed-off-by: Shiju <shiju@nvidia.com>
2026-10-12 16:39:31 +00:00

196 lines
6.1 KiB
Go

// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package v1
import (
"context"
"fmt"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/converter"
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
"google.golang.org/grpc"
)
type profileClient struct {
client pb.OpenShellClient
}
func newProfileClient(conn grpc.ClientConnInterface) *profileClient {
return &profileClient{client: pb.NewOpenShellClient(conn)}
}
func (p *profileClient) List(workspace string, opts ...ListOptions) (*Pager[*ProviderProfile], error) {
pageSize, err := listPageSize(opts)
if err != nil {
return nil, err
}
var pageToken string
if len(opts) > 0 {
pageToken = opts[0].PageToken
}
return newPager(pageToken, func(ctx context.Context, pageToken string) (*Page[*ProviderProfile], error) {
req := &pb.ListProviderProfilesRequest{WorkspaceScope: profileWorkspaceScope(workspace), PageSize: pageSize, PageToken: pageToken}
resp, err := p.client.ListProviderProfiles(ctx, req)
if err != nil {
return nil, converter.FromGRPCError(err)
}
profiles := make([]*ProviderProfile, 0, len(resp.GetProfiles()))
for _, profile := range resp.GetProfiles() {
profiles = append(profiles, converter.ProviderProfileFromProto(profile))
}
return &Page[*ProviderProfile]{Items: profiles, NextPageToken: resp.GetNextPageToken()}, nil
}), nil
}
func (p *profileClient) ListAll(ctx context.Context, workspace string, opts ...ListOptions) ([]*ProviderProfile, error) {
pager, err := p.List(workspace, opts...)
if err != nil {
return nil, err
}
return pager.All(ctx)
}
func (p *profileClient) Get(ctx context.Context, workspace, id string) (*ProviderProfile, error) {
resp, err := p.client.GetProviderProfile(ctx, &pb.GetProviderProfileRequest{
Id: id,
WorkspaceScope: profileWorkspaceScope(workspace),
})
if err != nil {
return nil, converter.FromGRPCError(err)
}
return converter.ProviderProfileFromProto(resp.GetProfile()), nil
}
func (p *profileClient) Import(ctx context.Context, workspace string, items []ProfileImportItem) (*ImportResult, error) {
pbItems := make([]*pb.ProviderProfileImportItem, len(items))
for i := range items {
pbItems[i] = converter.ProfileImportItemToProto(&items[i])
}
if profilesRequireStablePlaceholders(pbItems) {
if _, err := p.lint(ctx, workspace, pbItems); err != nil {
return nil, err
}
}
resp, err := p.client.ImportProviderProfiles(ctx, &pb.ImportProviderProfilesRequest{
Profiles: pbItems,
WorkspaceScope: profileWorkspaceScope(workspace),
})
if err != nil {
return nil, converter.FromGRPCError(err)
}
result := &ImportResult{
Imported: resp.GetImported(),
}
for _, d := range resp.GetDiagnostics() {
if diag := converter.ProfileDiagnosticFromProto(d); diag != nil {
result.Diagnostics = append(result.Diagnostics, *diag)
}
}
for _, pp := range resp.GetProfiles() {
if profile := converter.ProviderProfileFromProto(pp); profile != nil {
result.Profiles = append(result.Profiles, *profile)
}
}
return result, nil
}
func (p *profileClient) Update(ctx context.Context, workspace, id string, expectedResourceVersion uint64, item ProfileImportItem) (*UpdateResult, error) {
pbItem := converter.ProfileImportItemToProto(&item)
if items := []*pb.ProviderProfileImportItem{pbItem}; profilesRequireStablePlaceholders(items) {
if _, err := p.lint(ctx, workspace, items); err != nil {
return nil, err
}
}
resp, err := p.client.UpdateProviderProfiles(ctx, &pb.UpdateProviderProfilesRequest{
Id: id,
Profile: pbItem,
ExpectedResourceVersion: expectedResourceVersion,
WorkspaceScope: profileWorkspaceScope(workspace),
})
if err != nil {
return nil, converter.FromGRPCError(err)
}
result := &UpdateResult{
Updated: resp.GetUpdated(),
Profile: converter.ProviderProfileFromProto(resp.GetProfile()),
}
for _, d := range resp.GetDiagnostics() {
if diag := converter.ProfileDiagnosticFromProto(d); diag != nil {
result.Diagnostics = append(result.Diagnostics, *diag)
}
}
return result, nil
}
func (p *profileClient) Lint(ctx context.Context, workspace string, items []ProfileImportItem) (*LintResult, error) {
pbItems := make([]*pb.ProviderProfileImportItem, len(items))
for i := range items {
pbItems[i] = converter.ProfileImportItemToProto(&items[i])
}
resp, err := p.lint(ctx, workspace, pbItems)
if err != nil {
return nil, err
}
result := &LintResult{
Valid: resp.GetValid(),
}
for _, d := range resp.GetDiagnostics() {
if diag := converter.ProfileDiagnosticFromProto(d); diag != nil {
result.Diagnostics = append(result.Diagnostics, *diag)
}
}
return result, nil
}
func profilesRequireStablePlaceholders(items []*pb.ProviderProfileImportItem) bool {
for _, item := range items {
for _, credential := range item.GetProfile().GetCredentials() {
if credential.GetStablePlaceholder() {
return true
}
}
}
return false
}
// Lint acknowledges feature support before an opt-in is persisted. Legacy
// gateways can otherwise silently discard unknown credential fields.
func (p *profileClient) lint(ctx context.Context, workspace string, items []*pb.ProviderProfileImportItem) (*pb.LintProviderProfilesResponse, error) {
resp, err := p.client.LintProviderProfiles(ctx, &pb.LintProviderProfilesRequest{
Profiles: items,
WorkspaceScope: profileWorkspaceScope(workspace),
})
if err != nil {
return nil, converter.FromGRPCError(err)
}
if profilesRequireStablePlaceholders(items) && !resp.GetSupportsStablePlaceholder() {
return nil, fmt.Errorf("gateway does not support stable_placeholder; upgrade the gateway before enabling it")
}
return resp, nil
}
func (p *profileClient) Delete(ctx context.Context, workspace, id string, opts ...DeleteOptions) (*DeletionResult, error) {
resp, err := p.client.DeleteProviderProfile(ctx, &pb.DeleteProviderProfileRequest{
AllowMissing: allowMissing(opts),
Id: id,
WorkspaceScope: profileWorkspaceScope(workspace),
})
if err != nil {
return nil, converter.FromGRPCError(err)
}
return &DeletionResult{Outcome: DeletionOutcome(resp.GetOutcome())}, nil
}