@@ -16,11 +16,6 @@ import (
1616 "github.com/spf13/cobra"
1717)
1818
19- var (
20- sccPrivilegedSAs = []string {"openshell" , "openshell-sandbox" , "default" }
21- secretNames = []string {"openshell-gws" , "openshell-atlassian" }
22- )
23-
2419func NewDeployCmd (harnessDir , cli string ) * cobra.Command {
2520 var (
2621 local bool
@@ -29,31 +24,113 @@ func NewDeployCmd(harnessDir, cli string) *cobra.Command {
2924 )
3025
3126 cmd := & cobra.Command {
32- Use : "deploy [--local|--remote ]" ,
27+ Use : "deploy [gateway ]" ,
3328 Short : "Deploy or verify the gateway" ,
29+ Long : "Deploy a gateway by name (e.g., local, ocp, kind). Reads configuration from gateways/<name>/gateway.toml." ,
30+ Args : cobra .MaximumNArgs (1 ),
3431 RunE : func (cmd * cobra.Command , args []string ) error {
35- if remote {
36- gw := gateway .New (cli )
37- kc := k8s .New (kubeconfig , k8s .DefaultNamespace ())
38- clusterRunner := k8s .New (kubeconfig , "" )
39- return deployRemote (harnessDir , gw , kc , clusterRunner )
32+ gatewayName , err := resolveGatewayName (args , local , remote )
33+ if err != nil {
34+ return err
4035 }
41- if local {
42- gw := gateway .New (cli )
36+
37+ gwDir := filepath .Join (harnessDir , "gateways" , gatewayName )
38+ gwCfg , err := gateway .LoadConfig (gwDir )
39+ if err != nil {
40+ return fmt .Errorf ("loading gateway config %q: %w" , gatewayName , err )
41+ }
42+
43+ gw := gateway .New (cli )
44+
45+ if gwCfg .IsLocal () {
4346 return deployLocal (gw )
4447 }
45- return fmt .Errorf ("specify --local or --remote" )
48+
49+ kc := k8s .New (kubeconfig , k8s .DefaultNamespace ())
50+ clusterRunner := k8s .New (kubeconfig , "" )
51+ return deployFromConfig (harnessDir , gwCfg , gw , kc , clusterRunner )
4652 },
4753 }
4854
49- cmd .Flags ().BoolVar (& local , "local" , false , "Verify local podman gateway " )
50- cmd .Flags ().BoolVar (& remote , "remote" , false , "Deploy to OpenShift cluster " )
55+ cmd .Flags ().BoolVar (& local , "local" , false , "Alias for 'harness deploy local' " )
56+ cmd .Flags ().BoolVar (& remote , "remote" , false , "Alias for 'harness deploy ocp' " )
5157 cmd .Flags ().StringVar (& kubeconfig , "kubeconfig" , "" , "Path to kubeconfig (remote only)" )
58+ cmd .Flags ().MarkHidden ("local" )
59+ cmd .Flags ().MarkHidden ("remote" )
5260
5361 return cmd
5462}
5563
56- func deployRemote (harnessDir string , gw gateway.Gateway , kc , clusterRunner k8s.Runner ) (retErr error ) {
64+ func resolveGatewayName (args []string , local , remote bool ) (string , error ) {
65+ if len (args ) > 0 {
66+ return args [0 ], nil
67+ }
68+ if local {
69+ return "local" , nil
70+ }
71+ if remote {
72+ return "ocp" , nil
73+ }
74+ return "" , fmt .Errorf ("specify a gateway: harness deploy <local|ocp|kind>" )
75+ }
76+
77+ func deployLocal (gw gateway.Gateway ) error {
78+ cliPath := gw .CLIPath ()
79+ if cliPath == "" {
80+ return fmt .Errorf ("openshell CLI not found. Install it first:\n curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" )
81+ }
82+
83+ status .Section ("Container Runtime" )
84+ if _ , err := exec .LookPath ("podman" ); err != nil {
85+ status .Fail ("Podman not found" )
86+ return fmt .Errorf ("podman is required" )
87+ }
88+ out , _ := exec .Command ("podman" , "--version" ).Output ()
89+ status .OKf ("Podman: %s" , strings .TrimSpace (string (out )))
90+
91+ status .Section ("Gateway" )
92+ gateways , err := gw .GatewayList ()
93+ if err != nil {
94+ return fmt .Errorf ("listing gateways: %w" , err )
95+ }
96+
97+ var localGW string
98+ for _ , g := range gateways {
99+ if strings .Contains (g .Endpoint , "127.0.0.1" ) {
100+ localGW = g .Name
101+ break
102+ }
103+ }
104+
105+ if localGW == "" {
106+ status .Fail ("No local gateway found" )
107+ fmt .Println ()
108+ fmt .Println (" Install OpenShell (auto-registers the gateway):" )
109+ fmt .Println (" curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" )
110+ return fmt .Errorf ("no local gateway" )
111+ }
112+
113+ if err := gw .GatewaySelect (localGW ); err != nil {
114+ return fmt .Errorf ("selecting gateway %s: %w" , localGW , err )
115+ }
116+
117+ if gw .InferenceGet () == nil {
118+ status .OKf ("%s (active, reachable)" , localGW )
119+ } else {
120+ status .Failf ("%s (not responding)" , localGW )
121+ fmt .Println ()
122+ fmt .Println (" Start the gateway:" )
123+ fmt .Println (" macOS: brew services start openshell" )
124+ fmt .Println (" Linux: systemctl --user start openshell" )
125+ return fmt .Errorf ("gateway not responding" )
126+ }
127+
128+ fmt .Println ()
129+ status .Done ("Done." )
130+ return nil
131+ }
132+
133+ func deployFromConfig (harnessDir string , gwCfg * gateway.GatewayConfig , gw gateway.Gateway , kc , clusterRunner k8s.Runner ) (retErr error ) {
57134 defer func () {
58135 if retErr != nil {
59136 fmt .Fprintf (os .Stderr , "\n Deploy failed. Clean up with: harness teardown --k8s\n " )
@@ -65,22 +142,21 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
65142 chartVersion := os .Getenv ("OPENSHELL_CHART_VERSION" )
66143 if chartVersion == "" {
67144 cfg , _ := preflight .LoadConfig (filepath .Join (harnessDir , "openshell.toml" ))
68- if cfg != nil {
145+ if cfg != nil && cfg . Upstream . ChartVersion != "" {
69146 chartVersion = cfg .Upstream .ChartVersion
70147 }
71148 }
72149 if chartVersion == "" {
73- chartVersion = "0.0.55"
150+ chartVersion = gwCfg . Chart . Version
74151 }
75- chartOCI := "oci://ghcr.io/nvidia/openshell/helm-chart"
76152
77153 fmt .Printf ("OpenShell chart: %s\n " , chartVersion )
78154 if kbcfg := os .Getenv ("KUBECONFIG" ); kbcfg != "" {
79155 fmt .Printf ("KUBECONFIG: %s\n " , kbcfg )
80156 }
81157 fmt .Println ()
82158
83- // Step 1: Namespace (idempotent — ignore AlreadyExists)
159+ // Step 1: Namespace
84160 status .Step (1 , "Creating namespace" )
85161 clusterRunner .RunKubectl (ctx , "create" , "ns" , namespace )
86162 if _ , err := clusterRunner .RunKubectl (ctx , "label" , "ns" , namespace ,
@@ -92,42 +168,61 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
92168
93169 // Step 2: Sandbox CRD
94170 status .Step (2 , "Installing Sandbox CRD" )
95- if err := clusterRunner .RunKubectlPassthrough (ctx , "apply" , "-f" ,
96- "https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml" ); err != nil {
171+ if err := clusterRunner .RunKubectlPassthrough (ctx , "apply" , "-f" , gwCfg .Chart .CRD .URL ); err != nil {
97172 return fmt .Errorf ("installing sandbox CRD: %w" , err )
98173 }
99174
100- // Step 3: OpenShift SCCs (best-effort — oc may not exist on non-OpenShift)
101- status .Step (3 , "Granting OpenShift SCCs" )
102- for _ , sa := range sccPrivilegedSAs {
103- kc .RunOC (ctx , "adm" , "policy" , "add-scc-to-user" , "privileged" , "-z" , sa , "-n" , namespace )
175+ // Step 3: Platform-specific setup
176+ if gwCfg .IsOCP () {
177+ status .Step (3 , "Granting OpenShift SCCs" )
178+ for _ , sa := range gwCfg .OCP .SCCPrivileged {
179+ kc .RunOC (ctx , "adm" , "policy" , "add-scc-to-user" , "privileged" , "-z" , sa , "-n" , namespace )
180+ }
181+ for _ , sa := range gwCfg .OCP .SCCAnyuid {
182+ kc .RunOC (ctx , "adm" , "policy" , "add-scc-to-user" , "anyuid" , "-z" , sa , "-n" , namespace )
183+ }
184+ clusterRunner .RunKubectl (ctx , "create" , "clusterrolebinding" , "agent-sandbox-admin" ,
185+ "--clusterrole=cluster-admin" ,
186+ "--serviceaccount=agent-sandbox-system:agent-sandbox-controller" )
104187 }
105- kc .RunOC (ctx , "adm" , "policy" , "add-scc-to-user" , "anyuid" , "-z" , "openshell" , "-n" , namespace )
106- clusterRunner .RunKubectl (ctx , "create" , "clusterrolebinding" , "agent-sandbox-admin" ,
107- "--clusterrole=cluster-admin" ,
108- "--serviceaccount=agent-sandbox-system:agent-sandbox-controller" )
109188
110- // RBAC for launcher
111- if err := kc .RunKubectlPassthrough (ctx , "apply" , "-f" , filepath .Join (harnessDir , "gateways" , "ocp" , "addons" , "rbac.yaml" )); err != nil {
112- return fmt .Errorf ("applying launcher RBAC: %w" , err )
189+ // Addon manifests (RBAC, etc.)
190+ for _ , manifestPath := range gwCfg .ManifestPaths () {
191+ if err := kc .RunKubectlPassthrough (ctx , "apply" , "-f" , manifestPath ); err != nil {
192+ return fmt .Errorf ("applying %s: %w" , filepath .Base (manifestPath ), err )
193+ }
113194 }
114195
115196 // Step 4: Helm install
116197 status .Step (4 , "Deploying gateway via Helm" )
117- sandboxImage := envOr ("SANDBOX_IMAGE" , "ghcr.io/robbycochran/harness-openshell:sandbox" )
118198
119- appsDomain , err := clusterRunner .GetJSONPath (ctx , "ingresses.config.openshift.io/cluster" , "{.spec.domain}" )
120- if err != nil || appsDomain == "" {
121- return fmt .Errorf ("could not determine OpenShift apps domain — is this an OpenShift cluster? (kubectl get ingresses.config.openshift.io cluster)" )
199+ var gatewayURL string
200+ var routeHost string
201+
202+ switch gwCfg .Gateway .Service {
203+ case "route" :
204+ appsDomain , err := clusterRunner .GetJSONPath (ctx , "ingresses.config.openshift.io/cluster" , "{.spec.domain}" )
205+ if err != nil || appsDomain == "" {
206+ return fmt .Errorf ("could not determine OpenShift apps domain — is this an OpenShift cluster? (kubectl get ingresses.config.openshift.io cluster)" )
207+ }
208+ routeHost = fmt .Sprintf ("gateway-openshell.%s" , appsDomain )
209+ gatewayURL = fmt .Sprintf ("https://%s:443" , routeHost )
210+ case "nodeport" :
211+ gatewayURL = ""
212+ case "loadbalancer" :
213+ gatewayURL = ""
122214 }
123- routeHost := fmt .Sprintf ("gateway-openshell.%s" , appsDomain )
124215
125216 helmArgs := []string {
126- "upgrade" , "--install" , "openshell" , chartOCI ,
217+ "upgrade" , "--install" , "openshell" , gwCfg . Chart . OCI ,
127218 "--version" , chartVersion ,
128- "--values" , filepath .Join (harnessDir , "gateways" , "ocp" , "helm" , "values.yaml" ),
129- "--set" , "server.sandboxImage=" + sandboxImage ,
130- "--set" , "pkiInitJob.serverDnsNames[0]=" + routeHost ,
219+ }
220+ if valuesPath := gwCfg .HelmValuesPath (); valuesPath != "" {
221+ helmArgs = append (helmArgs , "--values" , valuesPath )
222+ }
223+ helmArgs = append (helmArgs , "--set" , "server.sandboxImage=" + gwCfg .Images .Sandbox )
224+ if routeHost != "" {
225+ helmArgs = append (helmArgs , "--set" , "pkiInitJob.serverDnsNames[0]=" + routeHost )
131226 }
132227 if ps := os .Getenv ("PULL_SECRET" ); ps != "" {
133228 helmArgs = append (helmArgs , "--set" , "imagePullSecrets[0].name=" + ps )
@@ -139,28 +234,22 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
139234 return fmt .Errorf ("helm install failed: %w" , err )
140235 }
141236
142- // Wait for gateway
143237 status .Section ("Waiting for gateway" )
144238 if err := kc .RunKubectlPassthrough (ctx , "rollout" , "status" , "statefulset/openshell" , "--timeout=300s" ); err != nil {
145239 return fmt .Errorf ("gateway rollout failed: %w" , err )
146240 }
147241
148- // Step 5: Route
149- status .Step (5 , "Creating OpenShift route" )
150- if err := kc .RunKubectlQuiet (ctx , "get" , "route" , "gateway" ); err != nil {
151- kc .RunKubectlPassthrough (ctx , "apply" , "-f" , filepath .Join (harnessDir , "gateways" , "ocp" , "addons" , "route.yaml" ))
152- }
153- fmt .Printf (" Route: %s\n " , routeHost )
242+ // Step 5: CLI gateway config
243+ status .Step (5 , "Configuring CLI gateway" )
244+ gatewayName := gwCfg .Gateway .Name
154245
155- // Step 6: CLI gateway config
156- status .Step (6 , "Configuring CLI gateway" )
157- gatewayName := envOr ("GATEWAY_NAME" , "openshell-remote-ocp" )
158- gatewayURL := fmt .Sprintf ("https://%s:443" , routeHost )
246+ if gatewayURL == "" {
247+ return fmt .Errorf ("service type %q endpoint resolution not yet implemented" , gwCfg .Gateway .Service )
248+ }
159249
160- // Remove existing gateways for this host
161250 existing , _ := gw .GatewayList ()
162251 for _ , g := range existing {
163- if strings .Contains (g .Endpoint , routeHost ) {
252+ if routeHost != "" && strings .Contains (g .Endpoint , routeHost ) {
164253 gw .GatewayRemove (g .Name )
165254 }
166255 }
@@ -169,7 +258,6 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
169258 return fmt .Errorf ("registering gateway %s: %w" , gatewayName , err )
170259 }
171260
172- // Extract mTLS certs
173261 home , err := os .UserHomeDir ()
174262 if err != nil {
175263 return fmt .Errorf ("determining home directory: %w" , err )
@@ -179,9 +267,9 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
179267 return fmt .Errorf ("creating mtls directory: %w" , err )
180268 }
181269 for _ , field := range []string {"ca.crt" , "tls.crt" , "tls.key" } {
182- data , err := kc .GetSecretField (ctx , "openshell-client-tls" , field )
270+ data , err := kc .GetSecretField (ctx , gwCfg . Secrets . MTLS , field )
183271 if err != nil {
184- return fmt .Errorf ("extracting %s from openshell-client-tls : %w" , field , err )
272+ return fmt .Errorf ("extracting %s from %s : %w" , field , gwCfg . Secrets . MTLS , err )
185273 }
186274 if err := os .WriteFile (filepath .Join (mtlsDir , field ), data , 0o600 ); err != nil {
187275 return fmt .Errorf ("writing %s: %w" , field , err )
@@ -193,7 +281,6 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
193281 }
194282 status .OKf ("%s registered (certs from cluster)" , gatewayName )
195283
196- // Wait for gateway to be reachable
197284 fmt .Print (" Waiting for gateway..." )
198285 var gwReachable bool
199286 for range 30 {
@@ -214,59 +301,3 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R
214301 status .Done ("Done." )
215302 return nil
216303}
217-
218- func deployLocal (gw gateway.Gateway ) error {
219- cliPath := gw .CLIPath ()
220- if cliPath == "" {
221- return fmt .Errorf ("openshell CLI not found. Install it first:\n curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" )
222- }
223-
224- status .Section ("Container Runtime" )
225- if _ , err := exec .LookPath ("podman" ); err != nil {
226- status .Fail ("Podman not found" )
227- return fmt .Errorf ("podman is required" )
228- }
229- out , _ := exec .Command ("podman" , "--version" ).Output ()
230- status .OKf ("Podman: %s" , strings .TrimSpace (string (out )))
231-
232- status .Section ("Gateway" )
233- gateways , err := gw .GatewayList ()
234- if err != nil {
235- return fmt .Errorf ("listing gateways: %w" , err )
236- }
237-
238- var localGW string
239- for _ , g := range gateways {
240- if strings .Contains (g .Endpoint , "127.0.0.1" ) {
241- localGW = g .Name
242- break
243- }
244- }
245-
246- if localGW == "" {
247- status .Fail ("No local gateway found" )
248- fmt .Println ()
249- fmt .Println (" Install OpenShell (auto-registers the gateway):" )
250- fmt .Println (" curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh" )
251- return fmt .Errorf ("no local gateway" )
252- }
253-
254- if err := gw .GatewaySelect (localGW ); err != nil {
255- return fmt .Errorf ("selecting gateway %s: %w" , localGW , err )
256- }
257-
258- if gw .InferenceGet () == nil {
259- status .OKf ("%s (active, reachable)" , localGW )
260- } else {
261- status .Failf ("%s (not responding)" , localGW )
262- fmt .Println ()
263- fmt .Println (" Start the gateway:" )
264- fmt .Println (" macOS: brew services start openshell" )
265- fmt .Println (" Linux: systemctl --user start openshell" )
266- return fmt .Errorf ("gateway not responding" )
267- }
268-
269- fmt .Println ()
270- status .Done ("Done." )
271- return nil
272- }
0 commit comments