Merge pull request #54 from apache/0.9.0

0.9.0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1eb7cc8..ab71904 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -22,7 +22,7 @@
     tags:
       - "v*"
     branches-ignore:
-      - "*"
+      - "**"
 
 permissions:
   contents: write
diff --git a/README.md b/README.md
index 8380751..1c21e60 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,8 @@
 
 Check [the installation instructions](https://openserverless.apache.org/docs/installation/install-cli/)
 
+For `ops config sso`, see [SSO command ownership and 0.9.0 prerequisites](docs/SSO.md).
+
 
 **NOTE** in windows there are some features like the ide that requires you use a devcontainer
 with `ops ide devcontainer`
@@ -205,7 +207,7 @@
 - `OPS_TMP` is a temporary folder where you can store temp files - defaults to `~/.ops/tmp`
 - `OPS_APIHOST` is the host for `ops -login`. It is used in place of the first argument of `ops -login`. If empty, the
   command will expect the first argument to be the apihost.
-- `OPS_USER` is set the username for `ops -login`. The default is `nuvolaris`. It can be overriden by passing the
+- `OPS_USER` is set the username for `ops -login`. The default is `openserverless`. It can be overriden by passing the
   username as an argument to `ops -login` or by setting the environment variable.
 - `OPS_PASSWORD`: set the password for `ops -login`. If not set, `ops -login` will prompt for the password. It is useful
   for tests and non-interactive environments.
diff --git a/auth/login.go b/auth/login.go
index 3c61a09..7798879 100644
--- a/auth/login.go
+++ b/auth/login.go
@@ -86,7 +86,7 @@
 const usage = `Usage:
 ops -login [options] <apihost> [<user>]
 
-Login to an OpenServerless instance. If no user is specified, the default user "nuvolaris" is used.
+Login to an OpenServerless instance. If no user is specified, the default user "openserverless" is used.
 You can set the environment variables OPS_APIHOST and OPS_USER to avoid specifying them on the command line.
 You can set OPS_PASSWORD to avoid entering the password interactively.
 When SSO is enabled, set OPS_SSO_LOGIN_FLOW=password or pass --sso-flow password to use
@@ -105,8 +105,8 @@
 const oidcDeviceStartPath = "/system/api/v1/auth/oidc/device/start"
 const oidcDevicePollPath = "/system/api/v1/auth/oidc/device/poll"
 const oidcPasswordPath = "/system/api/v1/auth/oidc/password"
-const defaultUser = "nuvolaris"
-const opsSecretServiceName = "nuvolaris"
+const defaultUser = "openserverless"
+const opsSecretServiceName = "openserverless"
 
 func LoginCmd() (*LoginResult, error) {
 
diff --git a/auth/login_test.go b/auth/login_test.go
index 11bb2cd..0c6815f 100644
--- a/auth/login_test.go
+++ b/auth/login_test.go
@@ -84,7 +84,7 @@
 	})
 
 	t.Run("with only apihost, add received credentials", func(t *testing.T) {
-		mockServer := setupMockServer(t, "nuvolaris", "a password", "{\"AUTH\": \"test\"}")
+		mockServer := setupMockServer(t, "openserverless", "a password", "{\"AUTH\": \"test\"}")
 		defer mockServer.Close()
 
 		oldPwdReader := pwdReader
diff --git a/config/sso_tool.go b/config/sso_tool.go
index 9e533ea..83c87d0 100644
--- a/config/sso_tool.go
+++ b/config/sso_tool.go
@@ -29,11 +29,11 @@
 )
 
 const (
-	defaultSSONamespace = "nuvolaris"
+	defaultSSONamespace = "openserverless"
 	defaultSSOConfigMap = "openserverless-sso-config"
 	defaultSSOSecret    = "openserverless-sso-secret"
-	defaultSSOWorkload  = "nuvolaris-system-api"
-	defaultSSOContainer = "nuvolaris-system-api"
+	defaultSSOWorkload  = "openserverless-system-api"
+	defaultSSOContainer = "openserverless-system-api"
 )
 
 type commandRunner func(name string, args []string, stdin []byte) ([]byte, error)
@@ -142,11 +142,11 @@
   --groups-claim CLAIM     OIDC groups claim. Default: groups
   --client-id CLIENT_ID    OIDC client id. Defaults to --audience when omitted
   --client-secret SECRET   OIDC confidential client secret stored only in Kubernetes Secret
-  --namespace NS           Kubernetes namespace. Default: nuvolaris
+  --namespace NS           Kubernetes namespace. Default: openserverless
   --configmap NAME         Kubernetes ConfigMap name. Default: openserverless-sso-config
   --secret NAME            Kubernetes Secret name. Default: openserverless-sso-secret
-  --statefulset NAME       admin-api StatefulSet name. Default: nuvolaris-system-api
-  --container NAME         admin-api container name. Default: nuvolaris-system-api
+  --statefulset NAME       admin-api StatefulSet name. Default: openserverless-system-api
+  --container NAME         admin-api container name. Default: openserverless-system-api
   --no-rollout             Do not restart or wait for admin-api rollout
 `)
 }
diff --git a/config/sso_tool_test.go b/config/sso_tool_test.go
index 9f10229..4176813 100644
--- a/config/sso_tool_test.go
+++ b/config/sso_tool_test.go
@@ -168,8 +168,8 @@
 
 	require.Len(t, commands, 3)
 	require.Equal(t, []string{"apply", "-f", "-"}, commands[0].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[1].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[2].args[7]}, commands[2].args)
+	require.Equal(t, []string{"-n", "openserverless", "get", "statefulset", "openserverless-system-api", "-o", "json"}, commands[1].args)
+	require.Equal(t, []string{"-n", "openserverless", "patch", "statefulset", "openserverless-system-api", "--type=json", "-p", commands[2].args[7]}, commands[2].args)
 
 	var cmObj map[string]interface{}
 	require.NoError(t, json.Unmarshal([]byte(commands[0].stdin), &cmObj))
@@ -217,10 +217,10 @@
 
 	require.Len(t, commands, 5)
 	require.Equal(t, []string{"apply", "-f", "-"}, commands[0].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[1].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[2].args[7]}, commands[2].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "restart", "statefulset/nuvolaris-system-api"}, commands[3].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "status", "statefulset/nuvolaris-system-api", "--timeout=180s"}, commands[4].args)
+	require.Equal(t, []string{"-n", "openserverless", "get", "statefulset", "openserverless-system-api", "-o", "json"}, commands[1].args)
+	require.Equal(t, []string{"-n", "openserverless", "patch", "statefulset", "openserverless-system-api", "--type=json", "-p", commands[2].args[7]}, commands[2].args)
+	require.Equal(t, []string{"-n", "openserverless", "rollout", "restart", "statefulset/openserverless-system-api"}, commands[3].args)
+	require.Equal(t, []string{"-n", "openserverless", "rollout", "status", "statefulset/openserverless-system-api", "--timeout=180s"}, commands[4].args)
 }
 
 func TestConfigSSOToolKeycloakWithClientSecret(t *testing.T) {
@@ -281,8 +281,8 @@
 	secretData := secretObj["stringData"].(map[string]interface{})
 	require.Equal(t, "super-secret", secretData["OIDC_CLIENT_SECRET"])
 
-	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[2].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[3].args[7]}, commands[3].args)
+	require.Equal(t, []string{"-n", "openserverless", "get", "statefulset", "openserverless-system-api", "-o", "json"}, commands[2].args)
+	require.Equal(t, []string{"-n", "openserverless", "patch", "statefulset", "openserverless-system-api", "--type=json", "-p", commands[3].args[7]}, commands[3].args)
 	require.Contains(t, commands[3].args[7], "custom-sso-secret")
 }
 
@@ -336,10 +336,10 @@
 		},
 	}, gotConfig)
 	require.Len(t, commands, 4)
-	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[0].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[1].args[7]}, commands[1].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[2].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[3].args)
+	require.Equal(t, []string{"-n", "openserverless", "get", "statefulset", "openserverless-system-api", "-o", "json"}, commands[0].args)
+	require.Equal(t, []string{"-n", "openserverless", "patch", "statefulset", "openserverless-system-api", "--type=json", "-p", commands[1].args[7]}, commands[1].args)
+	require.Equal(t, []string{"-n", "openserverless", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[2].args)
+	require.Equal(t, []string{"-n", "openserverless", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[3].args)
 }
 
 func TestConfigSSOToolDisableWaitsForAdminAPIRolloutByDefault(t *testing.T) {
@@ -363,11 +363,11 @@
 	require.NoError(t, err)
 
 	require.Len(t, commands, 5)
-	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[0].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[1].args[7]}, commands[1].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[2].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[3].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "status", "statefulset/nuvolaris-system-api", "--timeout=180s"}, commands[4].args)
+	require.Equal(t, []string{"-n", "openserverless", "get", "statefulset", "openserverless-system-api", "-o", "json"}, commands[0].args)
+	require.Equal(t, []string{"-n", "openserverless", "patch", "statefulset", "openserverless-system-api", "--type=json", "-p", commands[1].args[7]}, commands[1].args)
+	require.Equal(t, []string{"-n", "openserverless", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[2].args)
+	require.Equal(t, []string{"-n", "openserverless", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[3].args)
+	require.Equal(t, []string{"-n", "openserverless", "rollout", "status", "statefulset/openserverless-system-api", "--timeout=180s"}, commands[4].args)
 }
 
 func TestConfigSSOToolDisableAlreadyAbsentDoesNotPatchOrRollOut(t *testing.T) {
@@ -392,9 +392,9 @@
 
 	require.NoError(t, ConfigSSOTool(cm, []string{"disable"}))
 	require.Len(t, commands, 3)
-	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[0].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[1].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[2].args)
+	require.Equal(t, []string{"-n", "openserverless", "get", "statefulset", "openserverless-system-api", "-o", "json"}, commands[0].args)
+	require.Equal(t, []string{"-n", "openserverless", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[1].args)
+	require.Equal(t, []string{"-n", "openserverless", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[2].args)
 }
 
 func TestConfigSSOToolPreservesForeignWorkloadFieldsAcrossEnableDisableEnable(t *testing.T) {
diff --git a/docs/SSO.md b/docs/SSO.md
index d6a51e0..7a93a1a 100644
--- a/docs/SSO.md
+++ b/docs/SSO.md
@@ -17,13 +17,108 @@
   ~ under the License.
   -->
 
-# Legacy embedded SSO configuration ownership
+# SSO command ownership and 0.9.0 prerequisites
 
-The public `ops config sso` command is supplied by the task repository. The
-embedded `ops -config sso` form remains temporarily available for compatibility
-with already published scripts and installations. It manages the following
-configuration. Resource names can be changed with `--configmap`, `--secret`,
-`--statefulset`, and `--container`.
+The public `ops config sso` command requires SSO tasks from
+`apache/openserverless-task` and an OIDC-capable `apache/openserverless-admin-api`.
+At the branch snapshots below, those components are missing from `0.9.0`, even
+though the CLI already contains the SSO configuration and login code from
+`0.9.1`. Updating the CLI alone does not enable SSO on a `0.9.0` installation.
+
+## Command ownership
+
+| Command | Implementation | Responsibility |
+| --- | --- | --- |
+| `ops config sso ...` | Task repository: `config/sso/docopts.md`, `config/sso/opsfile.yml`, `config/sso/sso.ts` | Public configuration command, executed with Bun and kubectl. |
+| `ops -config sso ...` | CLI: [config/config_tool.go](../config/config_tool.go), [config/sso_tool.go](../config/sso_tool.go) | Legacy embedded configuration tool. |
+| `ops -login ...` | CLI: [auth/login.go](../auth/login.go) | SSO device and password login through admin-api, alongside legacy login. |
+| `ops ide login ...` | Task repository: `ide/opsfile.yml` | Workspace setup and delegation to the embedded login tool. |
+
+The CLI routes `ops config sso` through its normal task lookup. It does not
+fall back to the embedded configuration tool when the SSO task is missing.
+The embedded form remains temporarily available for compatibility with
+already published scripts and installations; it still requires an
+OIDC-capable admin-api for SSO login to work.
+
+## Branch comparison
+
+Checked on 2026-09-10. The links pin the reviewed commits because branch heads
+can change independently across repositories.
+
+| Repository | `0.9.0` snapshot | `0.9.1` snapshot | SSO status in `0.9.0` |
+| --- | --- | --- | --- |
+| `apache/openserverless-cli` | [83075cbc](https://github.com/apache/openserverless-cli/tree/83075cbc890dd9692782e27340fd2eee6b498a59) | [a0dd56cf](https://github.com/apache/openserverless-cli/tree/a0dd56cf5667acb092cfbac14de531f5de139af0) | `config/` and `auth/`, including their tests, are identical to `0.9.1`. No SSO code backport is needed in the CLI. |
+| `apache/openserverless-task` | [eeac57e4](https://github.com/apache/openserverless-task/tree/eeac57e4179381ee18fba290ab79d6cec1dafb09) | [71bd01f6](https://github.com/apache/openserverless-task/tree/71bd01f649d4f6b551240b7a3fa1db47954f4c6b) | Missing `config/sso/`, SSO help discovery, `admin/sso/`, and SSO changes to IDE login and user lifecycle tasks. |
+| `apache/openserverless-admin-api` | [161c479f](https://github.com/apache/openserverless-admin-api/tree/161c479f318f932e58eced08acd0830e7e75b288) | [35f9a1bc](https://github.com/apache/openserverless-admin-api/tree/35f9a1bcac5f75aab6dfac07d7efa6ac1bd0ae56) | Missing the OIDC bridge, device/password endpoints, SSO namespace mapping and login-triggered provisioning. |
+| `apache/openserverless-testing` | [be7b3338](https://github.com/apache/openserverless-testing/tree/be7b33382e02c6543d3e8813724b0177fae1c18d) | [e017ff5f](https://github.com/apache/openserverless-testing/tree/e017ff5fb5b1147cdb772aa584223d6e0d6c83da) | Missing `tests/11-sso-mock.sh` and `tests/mock-oidc-provider.py`. |
+
+The external components to review for a separate backport are:
+
+- **Configuration tasks:** `config/sso/` (including `sso.test.ts`) and the SSO
+  entries in `config/docopts.md`. The task uses Bun and kubectl from the task
+  prerequisites.
+- **Login and lifecycle tasks:** SSO handling in `ide/opsfile.yml`,
+  `ide/docopts.md`, `admin/opsfile.yml`, `admin/docopts.md`, `admin/sso/`, and
+  SSO display columns in `setup/kubernetes/crds/whisk-user-crd.yaml`.
+- **Admin-api:** `openserverless/common/oidc_validator.py`,
+  `openserverless/common/sso_namespace.py`,
+  `openserverless/impl/auth/oidc_device_flow_service.py`, the SSO additions in
+  `openserverless/impl/auth/auth_service.py` and `openserverless/rest/auth.py`,
+  their dependencies, configuration documentation and tests. These supply
+  `/auth/oidc`, `/auth/oidc/device/start`,
+  `/auth/oidc/device/poll`, and `/auth/oidc/password`, all under
+  `/system/api/v1`.
+- **Integration tests:** the mock provider and SSO smoke script in the testing
+  repository. They complement the CLI unit tests with a deployed admin-api
+  and Kubernetes resources.
+
+Setting local `SSO_*` values or creating a ConfigMap cannot add the missing
+server endpoints. A coordinated task/admin-api backport and integration
+validation are required before the public SSO flow can be considered usable
+with `0.9.0`.
+
+With the `0.9.0` task snapshot above, `ops config sso --help` and
+`ops config sso show` exit with `no command named sso found`.
+`ops -config sso --help` still succeeds because that command is embedded in
+the CLI. The same CLI can resolve `ops config sso --help` with the `0.9.1`
+tasks; this verifies command discovery, not server-side SSO compatibility.
+
+## Kubernetes compatibility
+
+The `0.9.0` deployment manifests and the SSO defaults use different names:
+
+| Resource | `0.9.0` deployment | Embedded SSO / `0.9.1` deployment |
+| --- | --- | --- |
+| Namespace | `openserverless` | `openserverless` |
+| Admin-api StatefulSet and container | `openserverless-system-api` | `openserverless-system-api` |
+| `WhiskUser` API version | `openserverless.org/v1` | `openserverless.org/v1` in the `0.9.1` admin-api/operator |
+
+See the task repository's
+[0.9.0 admin-api template](https://github.com/apache/openserverless-task/blob/eeac57e4179381ee18fba290ab79d6cec1dafb09/setup/openserverless/system-api/api-template.yaml)
+and the operator's
+[0.9.0 WhiskUser CRD](https://github.com/apache/openserverless-operator/blob/5df76feda569b374276f4e2c72a46d3a980a4b77/deploy/openserverless-permissions/whisk-user-crd.yaml).
+The corresponding
+[0.9.1 CRD](https://github.com/apache/openserverless-operator/blob/fdcbbdf2332ae5aa8e22afba0704b37fdb1697bd/deploy/openserverless-permissions/whisk-user-crd.yaml)
+uses `openserverless.org`.
+
+`--namespace`, `--statefulset`, and `--container` select the resources patched
+by the configuration command. They do not change the namespace or API group
+used internally by the
+[0.9.1 admin-api provisioning code](https://github.com/apache/openserverless-admin-api/blob/35f9a1bcac5f75aab6dfac07d7efa6ac1bd0ae56/openserverless/impl/auth/auth_service.py)
+and its
+[Kubernetes client](https://github.com/apache/openserverless-admin-api/blob/35f9a1bcac5f75aab6dfac07d7efa6ac1bd0ae56/openserverless/common/kube_api_client.py).
+A future backport must align those values with the deployed operator and CRD;
+copying the `0.9.1` image or tasks without adaptation is insufficient.
+
+The external-IdP flow uses the operator's existing `WhiskUser` reconciliation.
+Configuring SSO does not install Keycloak or add a Keycloak reconciler to the
+operator. An external Keycloak/IdP must already be available.
+
+## Legacy embedded configuration ownership
+
+`ops -config sso` manages the configuration described below. Resource names
+can be changed with `--namespace`, `--configmap`, `--secret`, `--statefulset`,
+and `--container`.
 
 ## Kubernetes resources
 
@@ -56,7 +151,7 @@
 
 ## Disable behavior
 
-`ops config sso disable` removes only the exact `envFrom` references described
+`ops -config sso disable` removes only the exact `envFrom` references described
 above and deletes the two dedicated resources with Kubernetes
 `--ignore-not-found`. Other `envFrom` entries, all direct `env` entries,
 volumes, volume mounts, and existing annotations remain unchanged.
diff --git a/tests/awk.bats b/tests/awk.bats
index ab4752a..e0274f6 100644
--- a/tests/awk.bats
+++ b/tests/awk.bats
@@ -44,8 +44,8 @@
 }
 
 @test "-awk replace" {
-    run echo "$(echo "Hello World" | ops -awk '{$2="Nuvolaris"; print $0}')"
-    assert_line "Hello Nuvolaris"
+    run echo "$(echo "Hello World" | ops -awk '{$2="OpenServerless"; print $0}')"
+    assert_line "Hello OpenServerless"
     assert_success
 }
 
diff --git a/tests/login.bats b/tests/login.bats
index f1f8557..3fc8f77 100644
--- a/tests/login.bats
+++ b/tests/login.bats
@@ -34,8 +34,8 @@
 
 @test "ops -login with OPS_PASSWORD env does not prompt for password" {
     export OPS_PASSWORD=1234
-    run ops -login nuvolaris.dev
-    assert_line --partial "Logging in https://nuvolaris.dev"
+    run ops -login openserverless.dev
+    assert_line --partial "Logging in https://openserverless.dev"
     refute_line "Enter Password:"
 }
 
@@ -61,7 +61,7 @@
     unset OPS_USER
     run ops -login
     assert_failure
-    assert_line "Logging in http://localhost as nuvolaris"
+    assert_line "Logging in http://localhost as openserverless"
 }
 
 @test "ops -login with OPS_APIHOST and OPS_USER env" {
diff --git a/tests/test_helper/fail_then_succeed.sh b/tests/test_helper/fail_then_succeed.sh
index f1de160..737d5fb 100755
--- a/tests/test_helper/fail_then_succeed.sh
+++ b/tests/test_helper/fail_then_succeed.sh
@@ -1,4 +1,21 @@
 #!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+
 
 file_path=".test_fail_then_succeed"