Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,56 @@ Run it
kn operator -h
```

## Remote Component Installs With ClusterProfile

You can install Serving or Eventing to a remote target cluster by creating the
hub `KnativeServing` or `KnativeEventing` CR with `spec.clusterProfileRef`:

```sh
kn operator install -c serving \
--namespace knative-serving \
--cluster-profile spoke \
--cluster-profile-namespace fleet-system

kn operator install -c eventing \
--namespace knative-eventing \
--cluster-profile spoke \
--cluster-profile-namespace fleet-system
```

By default, the hub component CR name matches the existing local install names:
`knative-serving` for Serving and `knative-eventing` for Eventing. Remote
installs can use `--cr-name` to manage multiple component CRs in the same hub
namespace, each pointing at a different ClusterProfile:

```sh
kn operator install -c serving \
--namespace knative-serving \
--cr-name spoke-a-serving \
--cluster-profile spoke-a \
--cluster-profile-namespace fleet-system

kn operator install -c serving \
--namespace knative-serving \
--cr-name spoke-b-serving \
--cluster-profile spoke-b \
--cluster-profile-namespace fleet-system
```

`--cr-name` identifies the hub `KnativeServing` or `KnativeEventing` custom
resource. It is not the ClusterProfile name and it is not the spoke namespace.
Use the same `--cr-name` with `configure`, `remove`, `enable`, and component
`uninstall` commands when managing a named remote CR. Local component installs
continue to use the fixed default CR names.

For remote installs, `--kubeconfig` must point to the hub cluster. The Knative
Operator must already be installed on the hub and configured with
`--clusterprofile-provider-file`; this plugin does not create provider
configuration.

`spec.clusterProfileRef` is immutable per component CR. Moving a named
component CR to another ClusterProfile requires deleting and recreating that
named CR.

You can use the built binary to run the commands. You can also use the bash scripts directly to run your commands.
All the bash scripts are available under the directory [scripts](scripts/).
68 changes: 68 additions & 0 deletions core/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2026 The Knative Authors

Licensed 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.
*/

package core

import (
"testing"

"knative.dev/kn-plugin-operator/pkg/command/common"
)

func TestCRNameFlagRegistration(t *testing.T) {
root := NewOperationCommand()
commands := [][]string{
{"install"},
{"uninstall"},
{"configure", "annotations"},
{"configure", "configmaps"},
{"configure", "envvars"},
{"configure", "images"},
{"configure", "labels"},
{"configure", "manifests"},
{"configure", "nodeSelectors"},
{"configure", "replicas"},
{"configure", "resources"},
{"configure", "selectors"},
{"configure", "tolerations"},
{"remove", "annotations"},
{"remove", "configmaps"},
{"remove", "envvars"},
{"remove", "images"},
{"remove", "labels"},
{"remove", "nodeSelectors"},
{"remove", "replicas"},
{"remove", "resources"},
{"remove", "selectors"},
{"remove", "tolerations"},
{"enable", "ingress"},
{"enable", "eventing-source"},
}

for _, path := range commands {
cmd, _, err := root.Find(path)
if err != nil {
t.Fatalf("failed to find command %v: %v", path, err)
}
flag := cmd.Flags().Lookup(common.CRNameFlag)
if flag == nil {
t.Fatalf("expected --%s on command %v", common.CRNameFlag, path)
}
if flag.Shorthand != "" {
t.Fatalf("expected --%s on command %v to have no shorthand, got %q", common.CRNameFlag, path, flag.Shorthand)
}
}
}
109 changes: 109 additions & 0 deletions pkg/command/common/component_ref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2026 The Knative Authors

Licensed 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.
*/

package common

import (
"fmt"
"strings"

"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/validation"
)

const CRNameFlag = "cr-name"

type ComponentRef struct {
Component string
Namespace string
Name string
}

func (r ComponentRef) String() string {
return fmt.Sprintf("%s %s/%s", ComponentKind(r.Component), r.Namespace, r.Name)
}

func ComponentKind(component string) string {
if strings.EqualFold(component, ServingComponent) {
return "KnativeServing"
}
if strings.EqualFold(component, EventingComponent) {
return "KnativeEventing"
}
return "Knative component"
}

func DefaultComponentName(component string) string {
if strings.EqualFold(component, ServingComponent) {
return KnativeServingName
}
if strings.EqualFold(component, EventingComponent) {
return KnativeEventingName
}
return ""
}

func NormalizeComponentName(component, name string) (string, error) {
normalized := strings.TrimSpace(name)
if normalized == "" {
normalized = DefaultComponentName(component)
}
if normalized == "" {
return "", fmt.Errorf("--%s requires --component serving or --component eventing", CRNameFlag)
}
if err := ValidateComponentName(normalized); err != nil {
return "", err
}
return normalized, nil
}

func NormalizeExplicitComponentName(component, name string) (string, error) {
normalized := strings.TrimSpace(name)
if normalized == "" {
return "", fmt.Errorf("--%s must be non-empty after trimming whitespace", CRNameFlag)
}
if DefaultComponentName(component) == "" {
return "", fmt.Errorf("--%s requires --component serving or --component eventing", CRNameFlag)
}
if err := ValidateComponentName(normalized); err != nil {
return "", err
}
return normalized, nil
}

func ValidateComponentName(name string) error {
if errs := validation.IsDNS1123Subdomain(name); len(errs) > 0 {
return fmt.Errorf("--%s must be a valid Kubernetes DNS subdomain: %s", CRNameFlag, strings.Join(errs, "; "))
}
return nil
}

func SetComponentNameFromFlag(flags *pflag.FlagSet, component string, name *string) error {
if flags.Changed(CRNameFlag) {
normalized, err := NormalizeExplicitComponentName(component, *name)
if err != nil {
return err
}
*name = normalized
return nil
}
normalized, err := NormalizeComponentName(component, *name)
if err != nil {
return err
}
*name = normalized
return nil
}
38 changes: 38 additions & 0 deletions pkg/command/common/component_ref_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2026 The Knative Authors

Licensed 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.
*/

package common

import (
"strings"
"testing"

"knative.dev/kn-plugin-operator/pkg/command/testingUtil"
)

func TestNormalizeComponentName(t *testing.T) {
name, err := NormalizeComponentName(ServingComponent, "")
testingUtil.AssertEqual(t, err, nil)
testingUtil.AssertEqual(t, name, KnativeServingName)

name, err = NormalizeExplicitComponentName(EventingComponent, " spoke-a.example ")
testingUtil.AssertEqual(t, err, nil)
testingUtil.AssertEqual(t, name, "spoke-a.example")

if err := ValidateComponentName("Invalid_Name"); err == nil || !strings.Contains(err.Error(), "valid Kubernetes DNS subdomain") {
t.Fatalf("expected invalid DNS subdomain error, got %v", err)
}
}
2 changes: 2 additions & 0 deletions pkg/command/common/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ type CMsFlags struct {
Component string
Namespace string
CMName string
CRName string
}

type KeyValueFlags struct {
Value string
Key string
Component string
Namespace string
CRName string
DeployName string
ServiceName string
Selector bool
Expand Down
Loading
Loading