Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ mlflow.db
openshell-arch/
plan_file_ga.md
plans/
specs/
docs/modernization/
quay-pull-secret.yaml
secrets.yaml
strategy-dashboard.html
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@ module github.com/stackrox/harness-openshell
go 1.25.0

require (
github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260818204536-8d67250a5d17
github.com/spf13/cobra v1.10.2
gopkg.in/yaml.v3 v3.0.1
)

require (
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
Expand Down
50 changes: 50 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260818204536-8d67250a5d17 h1:K/97EB/6IogQrPZx66aGNM+5HyHBfffnyVUPOhRw7pY=
github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260818204536-8d67250a5d17/go.mod h1:PPbbhH5tmSfoWzVcb5CXurnMkuJOYpesZmUB1itrlFY=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
30 changes: 30 additions & 0 deletions internal/openshell/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Package openshell is the harness-owned firewall over the OpenShell Go SDK.
//
// It defines the vocabulary the rest of harness-openshell uses to talk to a
// gateway — Client, Target, Health, Provider, and the sentinel errors — without
// leaking any SDK type. This package MUST NOT import the OpenShell SDK; the only
// package permitted to translate between these types and the SDK is
// internal/openshell/sdkclient (production) and internal/testutil (tests).
package openshell

import "context"

// Client is the harness-owned view of a single (gateway, workspace) target.
//
// The workspace is bound at construction, so methods never take one — a Client
// speaks for exactly one workspace on one gateway.
type Client interface {
// Health reports whether the gateway is reachable and healthy.
Health(ctx context.Context) (Health, error)
// Providers lists the providers registered in the bound workspace.
Providers(ctx context.Context) ([]Provider, error)
// Close releases any resources held by the client.
Close() error
}

// Factory constructs a Client for a Target.
//
// This is the only construction seam commands are allowed to depend on:
// production wires sdkclient.New, tests wire testutil.FakeFactory. No command
// calls into the SDK — or into sdkclient — directly.
type Factory func(ctx context.Context, t Target) (Client, error)
25 changes: 25 additions & 0 deletions internal/openshell/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package openshell

import "errors"

// Sentinel errors are the harness-owned meanings of gateway failures. Callers
// branch on these via errors.Is; they never inspect SDK error types. The single
// owner of the SDK-error → sentinel mapping is sdkclient.translate.
var (
// ErrNotFound is returned when a requested resource does not exist.
ErrNotFound = errors.New("openshell: not found")
// ErrUnavailable is returned when the gateway cannot be reached.
ErrUnavailable = errors.New("openshell: gateway unavailable")
// ErrUnauthenticated is returned when the caller's credentials are missing
// or rejected.
ErrUnauthenticated = errors.New("openshell: unauthenticated")
// ErrPermission is returned when the caller is authenticated but not
// authorized.
ErrPermission = errors.New("openshell: permission denied")
// ErrUnsupported is returned when the gateway does not implement the
// requested RPC (gRPC Unimplemented).
ErrUnsupported = errors.New("openshell: not supported by gateway")
// ErrConfig is returned when the gateway config cannot be loaded, parsed, or
// its auth mode cannot be satisfied.
ErrConfig = errors.New("openshell: gateway config error")
)
39 changes: 39 additions & 0 deletions internal/openshell/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package openshell

import (
"errors"
"fmt"
"testing"
)

// The sentinels must be distinct from one another and usable with errors.Is
// after wrapping, since sdkclient.translate wraps them with %w.
func TestSentinelsDistinctAndWrappable(t *testing.T) {
sentinels := map[string]error{
"ErrNotFound": ErrNotFound,
"ErrUnavailable": ErrUnavailable,
"ErrUnauthenticated": ErrUnauthenticated,
"ErrPermission": ErrPermission,
"ErrUnsupported": ErrUnsupported,
"ErrConfig": ErrConfig,
}

// Each sentinel is matched only by itself.
for nameA, a := range sentinels {
for nameB, b := range sentinels {
match := errors.Is(a, b)
want := nameA == nameB
if match != want {
t.Errorf("errors.Is(%s, %s) = %v, want %v", nameA, nameB, match, want)
}
}
}

// Wrapping preserves identity through errors.Is (the translate contract).
for name, s := range sentinels {
wrapped := fmt.Errorf("%w: underlying detail", s)
if !errors.Is(wrapped, s) {
t.Errorf("errors.Is(wrapped, %s) = false, want true", name)
}
}
}
95 changes: 95 additions & 0 deletions internal/openshell/sdkclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Package sdkclient is the ONLY production package permitted to import the
// OpenShell Go SDK. It translates between the harness-owned internal/openshell
// vocabulary and the SDK, keeping every SDK type behind the firewall.
//
// Slice S1 scope: construct a client for an mTLS gateway (the auth mode all our
// managed gateways use) and report Health. The full auth-mode resolver
// (planConnection) lands in S2; provider mapping and error translation in S3.
package sdkclient

import (
"context"
"fmt"
"path/filepath"

v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/gateway"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"

"github.com/stackrox/harness-openshell/internal/openshell"
)

// defaultWorkspace is the workspace assumed when a Target leaves it empty. This
// package is the single owner of that default.
const defaultWorkspace = "default"

// Compile-time guarantees that New satisfies the Factory seam and *client
// satisfies the Client interface.
var (
_ openshell.Factory = New
_ openshell.Client = (*client)(nil)
)

// client wraps the SDK client interface, binding it to one workspace. It holds
// the interface (not *v1.Client) so tests can inject the SDK fake.
type client struct {
raw v1.ClientInterface
workspace string
}

// New constructs an openshell.Client for the given target.
//
// S1 handles only mTLS gateways: it loads the CLI-managed gateway config, points
// TLS at the CLI-managed client certificate under <cfg.Dir>/mtls, and dials via
// the gateway.NewClient escape hatch (an explicit WithAuth skips the SDK's
// "mtls not supported" resolver; WithTLS supplies the client cert). Other auth
// modes return ErrConfig until S2 generalizes construction.
func New(ctx context.Context, t openshell.Target) (openshell.Client, error) {
cfg, err := gateway.LoadConfig(t.Gateway)
if err != nil {
return nil, fmt.Errorf("%w: load gateway %q: %v", openshell.ErrConfig, t.Gateway, err)
}

ws := t.Workspace
if ws == "" {
ws = defaultWorkspace
}

if cfg.AuthMode != gateway.AuthModeMTLS {
return nil, fmt.Errorf("%w: auth mode %q not yet supported (S1 handles mtls only)", openshell.ErrConfig, cfg.AuthMode)
}

mtlsDir := filepath.Join(cfg.Dir, "mtls")
tls := &types.TLSConfig{
CertFile: filepath.Join(mtlsDir, "tls.crt"),
KeyFile: filepath.Join(mtlsDir, "tls.key"),
CAFile: filepath.Join(mtlsDir, "ca.crt"),
}

raw, err := gateway.NewClient(t.Gateway, gateway.WithAuth(v1.NoAuth()), gateway.WithTLS(tls))
if err != nil {
return nil, fmt.Errorf("%w: dial gateway %q: %v", openshell.ErrConfig, t.Gateway, err)
}

return &client{raw: raw, workspace: ws}, nil
}

// Health reports gateway health. Error translation to openshell sentinels lands
// in S3; S1 surfaces the raw SDK error.
func (c *client) Health(ctx context.Context) (openshell.Health, error) {
h, err := c.raw.Health().Check(ctx)
if err != nil {
return openshell.Health{}, err
}
return openshell.Health{Healthy: h.Healthy, Version: h.Version}, nil
}

// Providers is not implemented until S3.
func (c *client) Providers(ctx context.Context) ([]openshell.Provider, error) {
return nil, fmt.Errorf("providers: not implemented until slice S3")
}

// Close releases the underlying SDK client.
func (c *client) Close() error {
return c.raw.Close()
}
47 changes: 47 additions & 0 deletions internal/openshell/sdkclient/health_e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package sdkclient_test

import (
"context"
"os"
"testing"
"time"

"github.com/stackrox/harness-openshell/internal/openshell"
"github.com/stackrox/harness-openshell/internal/openshell/sdkclient"
)

// TestHealthE2E is the S1 kill-gate: it proves an mTLS Health().Check succeeds
// from within the harness module (not a /tmp spike) against a real gateway.
//
// It is skipped unless HARNESS_E2E_GATEWAY names a registered openshell gateway
// (the local mTLS dev gateway is "openshell"). Optional HARNESS_E2E_WORKSPACE
// overrides the workspace.
//
// HARNESS_E2E_GATEWAY=openshell go test ./internal/openshell/sdkclient/ -run HealthE2E -v
func TestHealthE2E(t *testing.T) {
gw := os.Getenv("HARNESS_E2E_GATEWAY")
if gw == "" {
t.Skip("set HARNESS_E2E_GATEWAY to a registered mTLS gateway to run the S1 kill-gate")
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

c, err := sdkclient.New(ctx, openshell.Target{
Gateway: gw,
Workspace: os.Getenv("HARNESS_E2E_WORKSPACE"),
})
if err != nil {
t.Fatalf("sdkclient.New(%q): %v", gw, err)
}
defer c.Close()

h, err := c.Health(ctx)
if err != nil {
t.Fatalf("Health().Check: %v", err)
}
if !h.Healthy {
t.Fatalf("gateway reported unhealthy: %+v", h)
}
t.Logf("gateway %q healthy: version=%s", gw, h.Version)
}
28 changes: 28 additions & 0 deletions internal/openshell/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package openshell

// Target identifies what to connect to.
//
// Gateway is the OPENSHELL REGISTRATION name — the directory under
// ~/.config/openshell/gateways/<name> managed by the openshell CLI. It is NOT a
// harness gateway profile (e.g. "openshift", "local-container"); those name
// deployment recipes, not registered gateways. Never pass an agent.AgentConfig
// gateway profile here.
type Target struct {
Gateway string // required; openshell registration name
Workspace string // "" defaults to "default" (defaulting owned by sdkclient)
}

// Health is the harness view of a gateway health check.
type Health struct {
Healthy bool
Version string
}

// Provider is the minimal harness view of a registered provider.
//
// Deliberately minimal; later PRs widen it only as consumers need more fields
// (least-exposure firewall).
type Provider struct {
Name string
Type string
}
Loading