diff --git a/cmd/cli/analyze/analyze.go b/cmd/cli/analyze/analyze.go deleted file mode 100644 index 38c0c5cec..000000000 --- a/cmd/cli/analyze/analyze.go +++ /dev/null @@ -1,345 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 analyze - -import ( - "fmt" - "os" - "slices" - "sort" - "strings" - - "github.com/spf13/cobra" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/genericiooptions" - "k8s.io/cli-runtime/pkg/resource" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/extension" - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/gatewayeffectivepolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/notfoundrefvalidator" - "github.com/flomesh-io/fsm/pkg/cli/extension/refgrantvalidator" - extensionutils "github.com/flomesh-io/fsm/pkg/cli/extension/utils" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -func NewCmd(factory common.Factory, iostreams genericiooptions.IOStreams) *cobra.Command { - flags := &analyzeFlags{ - fileNameFlags: genericclioptions.NewResourceBuilderFlags().FileNameFlags, - } - - cmd := &cobra.Command{ - Use: "analyze -f FILENAME|DIRECTORY", - Short: "Analyze resources by file names or stdin", - Run: func(_ *cobra.Command, args []string) { - o, err := flags.ToOptions(args, factory, iostreams) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - - err = o.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - }, - } - - flags.fileNameFlags.AddFlags(cmd.Flags()) - return cmd -} - -// analyzeFlags contains the flags used with analyze command. -type analyzeFlags struct { - fileNameFlags *genericclioptions.FileNameFlags -} - -func (f *analyzeFlags) ToOptions(_ []string, factory common.Factory, iostreams genericiooptions.IOStreams) (*analyzeOptions, error) { - namespace, _, _ := factory.KubeConfigNamespace() - - return &analyzeOptions{ - fileNameOptions: f.fileNameFlags.ToOptions(), - factory: factory, - namespace: namespace, - IOStreams: iostreams, - }, nil -} - -type analyzeOptions struct { - fileNameOptions resource.FilenameOptions - factory common.Factory - namespace string - - genericclioptions.IOStreams -} - -func (o *analyzeOptions) Run() error { - fmt.Fprintf(o.IOStreams.Out, "\n") - fmt.Fprintf(o.IOStreams.Out, "Analyzing %v...\n", strings.Join(o.fileNameOptions.Filenames, ",")) - fmt.Fprintf(o.IOStreams.Out, "\n") - - // Step 1: Parse the files and extract the objects from the files. - infos, err := o.factory.NewBuilder(). - Unstructured(). - FilenameParam(false, &o.fileNameOptions). - Flatten(). - NamespaceParam(o.namespace).DefaultNamespace(). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return err - } - - // Step 2: Classify whether the object already exists, or not. If it already - // exists, cache the version which already exists. - existingObjects := map[*resource.Info]*unstructured.Unstructured{} - for _, info := range infos { - helper := resource.NewHelper(info.Client, info.Mapping) - obj, err := helper.Get(info.Namespace, info.Name) //nolint:govet - if err != nil { - if !apierrors.IsNotFound(err) { - return err - } - existingObjects[info] = nil // Object does not exist. - continue - } - // Object does exist, cache it. - o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) - if err != nil { - return err - } - u := &unstructured.Unstructured{Object: o} - existingObjects[info] = u - } - - // Step 3: Build graph using the provided objects in the files as the - // source. - sources := []*unstructured.Unstructured{} - for _, info := range infos { - o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) //nolint:govet - if err != nil { - return err - } - u := &unstructured.Unstructured{Object: o} - sources = append(sources, u) - } - graph, err := topology.NewBuilder(common.NewDefaultGroupKindFetcher(o.factory, common.WithAdditionalResources(sources))). - StartFrom(sources). - UseRelationships(topologygw.AllRelations). - WithMaxDepth(4). - Build() - if err != nil { - return err - } - - policyManager := policymanager.New(common.NewDefaultGroupKindFetcher(o.factory, common.WithAdditionalResources(sources))) - if err := policyManager.Init(); err != nil { //nolint:govet - return err - } - // Execute extensions. - err = extension.ExecuteAll(graph, - directlyattachedpolicy.NewExtension(policyManager), - gatewayeffectivepolicy.NewExtension(), - refgrantvalidator.NewExtension( - refgrantvalidator.NewDefaultReferenceGrantFetcher(o.factory, refgrantvalidator.WithAdditionalResources(sources)), - ), - notfoundrefvalidator.NewExtension(), - ) - if err != nil { - return err - } - - // Step 4: Collect errors from the graph. These are the collective set of - // errors which will be observed after the new changes are applied. - errorsAfterChanges, err := collectErrors(graph) - if err != nil { - return err - } - - // Step 5: Remove nodes from the graph which are going to be newly created, - // or revert them to their state before creation. The resulting graph should - // represent a state which currently exists in the server (before applying - // the newer changes.) - for info, existingObject := range existingObjects { - gknn := common.GKNN{ - Group: info.Mapping.GroupVersionKind.Group, - Kind: info.Mapping.GroupVersionKind.Kind, - Namespace: info.Namespace, - Name: info.Name, - } - if existingObject == nil { - // This means the object would have been newly created, and thus we - // need to delete it to revert the graph back to it's original - // state. - graph.DeleteNodeUsingGKNN(gknn) - } else if !graph.HasNode(gknn) { - node := graph.Nodes[gknn.GroupKind()][gknn.NamespacedName()] - node.Object = existingObject // Revert object back to it's original state which exists in the server. - } - } - - // Step 6: Build new graph by running extensions - policyManager = policymanager.New(common.NewDefaultGroupKindFetcher(o.factory)) - if err := policyManager.Init(); err != nil { //nolint:govet - return err - } - // Execute extensions. - err = extension.ExecuteAll(graph, - directlyattachedpolicy.NewExtension(policyManager), - gatewayeffectivepolicy.NewExtension(), - refgrantvalidator.NewExtension( - refgrantvalidator.NewDefaultReferenceGrantFetcher(o.factory), - ), - notfoundrefvalidator.NewExtension(), - ) - if err != nil { - return err - } - - // Step 6: Collect errors from the graph. These are the collective set of - // errors which will be observed in the server before the new changes are - // applied. - errorsBeforeChanges, err := collectErrors(graph) - if err != nil { - return err - } - - // Step 7: Report analysis - - fmt.Fprintf(o.IOStreams.Out, "Summary:\n") - fmt.Fprintf(o.IOStreams.Out, "\n") - created, updated := generateSummary(existingObjects) - for _, info := range created { - fmt.Fprintf(o.IOStreams.Out, "\t- Created %v", info.ObjectName()) - if info.Namespaced() { - fmt.Fprintf(o.IOStreams.Out, " in namespace %v", info.Namespace) - } - fmt.Fprintf(o.IOStreams.Out, "\n") - } - for _, info := range updated { - fmt.Fprintf(o.IOStreams.Out, "\t- Updated %v", info.ObjectName()) - if info.Namespaced() { - fmt.Fprintf(o.IOStreams.Out, " in namespace %v", info.Namespace) - } - fmt.Fprintf(o.IOStreams.Out, "\n") - } - fmt.Fprintf(o.IOStreams.Out, "\n") - - newIssues, fixedIssues, unchangedIssues := classifyErrors(errorsBeforeChanges, errorsAfterChanges) - - fmt.Fprintf(o.IOStreams.Out, "Potential Issues Introduced\n") - fmt.Fprintf(o.IOStreams.Out, "(These issues will arise after applying the changes in the analyzed file.):\n") - fmt.Fprintf(o.IOStreams.Out, "\n") - for _, s := range newIssues { - fmt.Fprintf(o.IOStreams.Out, "\t- %v:\n", s) - } - if len(newIssues) == 0 { - fmt.Fprintf(o.IOStreams.Out, "\tNone.\n") - } - fmt.Fprintf(o.IOStreams.Out, "\n") - - fmt.Fprintf(o.IOStreams.Out, "Existing Issues Fixed\n") - fmt.Fprintf(o.IOStreams.Out, "(These issues were present before the changes but will be resolved after applying them.):\n") - fmt.Fprintf(o.IOStreams.Out, "\n") - for _, s := range fixedIssues { - fmt.Fprintf(o.IOStreams.Out, "\t- %v:\n", s) - } - if len(fixedIssues) == 0 { - fmt.Fprintf(o.IOStreams.Out, "\tNone\n") - } - fmt.Fprintf(o.IOStreams.Out, "\n") - - fmt.Fprintf(o.IOStreams.Out, "Existing Issues Unchanged\n") - fmt.Fprintf(o.IOStreams.Out, "(These issues were present before the changes and will remain even after applying them.):\n") - fmt.Fprintf(o.IOStreams.Out, "\n") - for _, s := range unchangedIssues { - fmt.Fprintf(o.IOStreams.Out, "\t- %v:\n", s) - } - if len(unchangedIssues) == 0 { - fmt.Fprintf(o.IOStreams.Out, "\tNone\n") - } - fmt.Fprintf(o.IOStreams.Out, "\n") - - return nil -} - -func collectErrors(graph *topology.Graph) (map[string]bool, error) { - errors := map[string]bool{} - for i := range graph.Nodes { - for j := range graph.Nodes[i] { - node := graph.Nodes[i][j] - aggregateAnalysisErrors, err := extensionutils.AggregateAnalysisErrors(node) - if err != nil { - return nil, err - } - for _, err := range aggregateAnalysisErrors { - s := fmt.Sprintf("%v: %v", node.GKNN(), err) - errors[s] = true - } - } - } - return errors, nil -} - -func generateSummary(objects map[*resource.Info]*unstructured.Unstructured) (created, updated []*resource.Info) { - for info, existingObject := range objects { - if existingObject == nil { - created = append(created, info) - } else { - updated = append(updated, info) - } - } - infoComparer := func(a, b *resource.Info) bool { - p := fmt.Sprintf("%v/%v/%v", a.Object.GetObjectKind().GroupVersionKind().GroupKind(), a.Namespace, a.Name) - q := fmt.Sprintf("%v/%v/%v", b.Object.GetObjectKind().GroupVersionKind().GroupKind(), b.Namespace, b.Name) - return p < q - } - sort.Slice(created, func(i, j int) bool { return infoComparer(created[i], created[j]) }) - sort.Slice(updated, func(i, j int) bool { return infoComparer(updated[i], updated[j]) }) - return created, updated -} - -func classifyErrors(errorsBeforeChanges, errorsAfterChanges map[string]bool) (newIssues, fixedIssues, unchangedIssues []string) { - for s := range errorsAfterChanges { - existsBefore := errorsBeforeChanges[s] - if !existsBefore { - newIssues = append(newIssues, s) - } else { - unchangedIssues = append(unchangedIssues, s) - } - } - for s := range errorsBeforeChanges { - existsAfter := errorsAfterChanges[s] - if !existsAfter { - fixedIssues = append(fixedIssues, s) - } else { - unchangedIssues = append(unchangedIssues, s) - } - } - slices.Sort(newIssues) - slices.Sort(fixedIssues) - slices.Sort(unchangedIssues) - return newIssues, fixedIssues, unchangedIssues -} diff --git a/cmd/cli/apply/apply.go b/cmd/cli/apply/apply.go deleted file mode 100644 index 54ce2045d..000000000 --- a/cmd/cli/apply/apply.go +++ /dev/null @@ -1,139 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 apply - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/genericiooptions" - "k8s.io/cli-runtime/pkg/printers" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/utils/ptr" - - "github.com/flomesh-io/fsm/pkg/cli/common" -) - -const ( - fieldManager = "gwctl-server-side-apply" -) - -func NewCmd(factory common.Factory, iostreams genericiooptions.IOStreams) *cobra.Command { - fileNameFlags := genericclioptions.NewResourceBuilderFlags().FileNameFlags - fileNameFlags.Usage = "The files that contain the configurations to apply." - fileNameFlags.Recursive = ptr.To(false) - fileNameFlags.Kustomize = ptr.To("") - - flags := &applyFlags{ - fileNameFlags: fileNameFlags, - } - - cmd := &cobra.Command{ - Use: "apply -f FILENAME|DIRECTORY", - Short: "Apply the provided resources from file or stdin to the cluster.", - Args: cobra.ExactArgs(0), - Run: func(_ *cobra.Command, args []string) { - o, err := flags.ToOptions(args, factory, iostreams) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - - err = o.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - }, - } - - flags.fileNameFlags.AddFlags(cmd.Flags()) - return cmd -} - -// applyFlags contains the flags used with apply command. -type applyFlags struct { - fileNameFlags *genericclioptions.FileNameFlags -} - -func (f *applyFlags) ToOptions(_ []string, factory common.Factory, iostreams genericiooptions.IOStreams) (*applyOptions, error) { - namespace, _, _ := factory.KubeConfigNamespace() - - return &applyOptions{ - fileNameOptions: f.fileNameFlags.ToOptions(), - factory: factory, - namespace: namespace, - IOStreams: iostreams, - }, nil -} - -type applyOptions struct { - fileNameOptions resource.FilenameOptions - factory common.Factory - namespace string - - genericclioptions.IOStreams -} - -func (o *applyOptions) Run() error { - infos, err := o.factory.NewBuilder(). - Unstructured(). - FilenameParam(false, &o.fileNameOptions). - Flatten(). - NamespaceParam(o.namespace).DefaultNamespace(). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return err - } - - printer := printers.NamePrinter{Operation: "configured"} - - // Loop over all objects from the file(s) or stdin. - for _, info := range infos { - helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(fieldManager) - - data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object) - if err != nil { - return fmt.Errorf("%v: %v", info.Source, err) - } - - obj, err := helper.Patch( - info.Namespace, - info.Name, - types.ApplyPatchType, - data, - nil, - ) - if err != nil { - return err - } - - err = printer.PrintObj(obj, o.Out) - if err != nil { - return err - } - } - - return nil -} diff --git a/cmd/cli/delete/delete.go b/cmd/cli/delete/delete.go deleted file mode 100644 index 7677f8eda..000000000 --- a/cmd/cli/delete/delete.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 delete - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/genericiooptions" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/utils/ptr" - - "github.com/flomesh-io/fsm/pkg/cli/common" -) - -func NewCmd(factory common.Factory, iostreams genericiooptions.IOStreams) *cobra.Command { - fileNameFlags := genericclioptions.NewResourceBuilderFlags().FileNameFlags - fileNameFlags.Usage = "The files that contain the configurations to apply." - fileNameFlags.Recursive = ptr.To(false) - fileNameFlags.Kustomize = ptr.To("") - - flags := &deleteFlags{ - fileNameFlags: fileNameFlags, - } - - cmd := &cobra.Command{ - Use: "delete", - Short: "Delete resources by file names, stdin, resources and names, or by resources and label selector.", - Run: func(_ *cobra.Command, args []string) { - o, err := flags.ToOptions(args, factory, iostreams) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - - err = o.Run(args) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - }, - } - - flags.fileNameFlags.AddFlags(cmd.Flags()) - return cmd -} - -// deleteFlags contains the flags used with delete command. -type deleteFlags struct { - fileNameFlags *genericclioptions.FileNameFlags -} - -func (f *deleteFlags) ToOptions(_ []string, factory common.Factory, iostreams genericiooptions.IOStreams) (*deleteOptions, error) { - namespace, _, _ := factory.KubeConfigNamespace() - - return &deleteOptions{ - fileNameOptions: f.fileNameFlags.ToOptions(), - factory: factory, - namespace: namespace, - IOStreams: iostreams, - }, nil -} - -type deleteOptions struct { - fileNameOptions resource.FilenameOptions - factory common.Factory - namespace string - - genericclioptions.IOStreams -} - -func (o *deleteOptions) Run(args []string) error { - infos, err := o.factory.NewBuilder(). - Unstructured(). - FilenameParam(false, &o.fileNameOptions). - ResourceTypeOrNameArgs(false, args...).RequireObject(false). - Flatten(). - NamespaceParam(o.namespace).DefaultNamespace(). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return err - } - - // Loop over all objects from the file(s)/stdin/args. - for _, info := range infos { - helper := resource.NewHelper(info.Client, info.Mapping) - _, err := helper.Delete(info.Namespace, info.Name) - if err != nil { - if !apierrors.IsNotFound(err) { - return err - } - fmt.Fprintf(o.IOStreams.Out, "Error when deleting %v: %v\n", info.ObjectName(), err) - } else { - fmt.Fprintf(o.IOStreams.Out, "%v deleted\n", info.ObjectName()) - } - } - - return nil -} diff --git a/cmd/cli/fsm.go b/cmd/cli/fsm.go index 2b30a1ce7..087a23c0a 100644 --- a/cmd/cli/fsm.go +++ b/cmd/cli/fsm.go @@ -5,10 +5,10 @@ import ( "fmt" "os" - cmdanalyze "github.com/flomesh-io/fsm/cmd/cli/analyze" - cmdapply "github.com/flomesh-io/fsm/cmd/cli/apply" - cmddelete "github.com/flomesh-io/fsm/cmd/cli/delete" - cmdget "github.com/flomesh-io/fsm/cmd/cli/get" + cmdanalyze "sigs.k8s.io/gwctl/cmd/analyze" + cmdapply "sigs.k8s.io/gwctl/cmd/apply" + cmddelete "sigs.k8s.io/gwctl/cmd/delete" + cmdget "sigs.k8s.io/gwctl/cmd/get" "github.com/spf13/pflag" diff --git a/cmd/cli/get/get.go b/cmd/cli/get/get.go deleted file mode 100644 index 69a74965e..000000000 --- a/cmd/cli/get/get.go +++ /dev/null @@ -1,308 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 get - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/genericiooptions" - "k8s.io/utils/clock" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/extension" - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/gatewayeffectivepolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/notfoundrefvalidator" - "github.com/flomesh-io/fsm/pkg/cli/extension/refgrantvalidator" - gwctlflags "github.com/flomesh-io/fsm/pkg/cli/flags" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/printer" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -func NewCmd(factory common.Factory, iostreams genericiooptions.IOStreams, isDescribe bool) *cobra.Command { - flags := newGetFlags() - - cmdName := "get" - if isDescribe { - cmdName = "describe" - } - - cmd := &cobra.Command{ - Use: fmt.Sprintf("%v TYPE [RESOURCE_NAME]", cmdName), - Short: "Display one or many resources", - Args: cobra.RangeArgs(1, 2), - Run: func(_ *cobra.Command, args []string) { - o, err := flags.ToOptions(args, factory, iostreams, isDescribe) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - - err = o.Run(args) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - os.Exit(1) - } - }, - } - - flags.resourceBuilderFlags.AddFlags(cmd.Flags()) - - if !isDescribe { - printableAllowedFormats := strings.Join(printer.AllowedOutputFormatsForHelp(), ",") - cmd.Flags().StringVarP(&flags.outputFormat, "output", "o", "", fmt.Sprintf("Output format. Must be one of: %v", printableAllowedFormats)) - - flags.forFlag.AddFlag(cmd.Flags()) - } - - return cmd -} - -// getFlags contains the flags used with get command. -type getFlags struct { - resourceBuilderFlags *genericclioptions.ResourceBuilderFlags - outputFormat string - forFlag gwctlflags.ForFlag -} - -func newGetFlags() *getFlags { - resourceBuilderFlags := genericclioptions.NewResourceBuilderFlags(). - WithAllNamespaces(false). - WithLabelSelector("") - resourceBuilderFlags.FileNameFlags = nil - - return &getFlags{ - resourceBuilderFlags: resourceBuilderFlags, - } -} - -func (f *getFlags) ToOptions(args []string, factory common.Factory, iostreams genericiooptions.IOStreams, isDescribe bool) (*getOptions, error) { - o := &getOptions{ - isDescribe: isDescribe, - factory: factory, - IOStreams: iostreams, - allNamespaces: *f.resourceBuilderFlags.AllNamespaces, - labelSelector: *f.resourceBuilderFlags.LabelSelector, - } - - var err error - o.isPolicy, o.isPolicyCRD, err = parseResourceTypeOrNameArgs(args) - if err != nil { - return nil, err - } - - o.namespace, _, err = factory.KubeConfigNamespace() - if err != nil { - return nil, err - } - - // Parse outputFormat - o.output, err = printer.ValidateAndReturnOutputFormat(f.outputFormat) - if err != nil { - return nil, err - } - - return o, nil -} - -type getOptions struct { - isDescribe bool - - factory common.Factory - - allNamespaces bool - namespace string - labelSelector string - output printer.OutputFormat - - isPolicy bool - isPolicyCRD bool - - genericclioptions.IOStreams -} - -func (o *getOptions) Run(args []string) error { - if o.isPolicy || o.isPolicyCRD { - return o.handlePolicy(args) - } - infos, err := o.factory.NewBuilder(). - Unstructured(). - Flatten(). - NamespaceParam(o.namespace).DefaultNamespace().AllNamespaces(o.allNamespaces). - ResourceTypeOrNameArgs(true, args...). - LabelSelectorParam(o.labelSelector). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return err - } - - sources := []*unstructured.Unstructured{} - for _, info := range infos { - o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) //nolint:govet - if err != nil { - return err - } - u := &unstructured.Unstructured{Object: o} - sources = append(sources, u) - } - - var graph *topology.Graph - if o.isDescribe || o.output == printer.OutputFormatWide || o.output == printer.OutputFormatGraph { - graph, err = topology.NewBuilder(common.NewDefaultGroupKindFetcher(o.factory)). - StartFrom(sources). - UseRelationships(topologygw.AllRelations). - Build() - if err != nil { - return err - } - - policyManager := policymanager.New(common.NewDefaultGroupKindFetcher(o.factory)) - if err := policyManager.Init(); err != nil { //nolint:govet - return err - } - - err := extension.ExecuteAll(graph, //nolint:govet - directlyattachedpolicy.NewExtension(policyManager), - gatewayeffectivepolicy.NewExtension(), - refgrantvalidator.NewExtension(refgrantvalidator.NewDefaultReferenceGrantFetcher(o.factory)), - notfoundrefvalidator.NewExtension(), - ) - if err != nil { - return err - } - } else { - graph, err = topology.NewBuilder(common.NewDefaultGroupKindFetcher(o.factory)). - StartFrom(sources). - Build() - if err != nil { - return err - } - } - - if o.output == printer.OutputFormatGraph { - toDotGraph, err := topologygw.ToDot(graph) - if err != nil { - return err - } - fmt.Fprintf(o.IOStreams.Out, "%v\n", string(toDotGraph)) - - return nil - } - - return o.printNodes(graph.Sources) -} - -func (o *getOptions) handlePolicy(args []string) error { - policyManager := policymanager.New(common.NewDefaultGroupKindFetcher(o.factory)) - if err := policyManager.Init(); err != nil { - return err - } - - nodes := []*topology.Node{} - if o.isPolicy { - for _, policy := range policyManager.GetPolicies() { - shouldSkip := (!o.allNamespaces && o.namespace != policy.GKNN().Namespace) || - (len(args) == 2 && args[1] != policy.GKNN().Name) - if shouldSkip { - continue - } - nodes = append(nodes, encodePolicyAsNode(policy)) - } - } else { - for _, policyCRD := range policyManager.GetCRDs() { - shouldSkip := len(args) == 2 && (args[1] != policyCRD.CRD.GetName()) - if shouldSkip { - continue - } - node, err := encodePolicyCRDAsNode(policyCRD) - if err != nil { - return err - } - nodes = append(nodes, node) - } - } - - return o.printNodes(nodes) -} - -func (o *getOptions) printNodes(nodes []*topology.Node) error { - printerOptions := printer.PrinterOptions{ - OutputFormat: o.output, - Clock: clock.RealClock{}, - Description: o.isDescribe, - EventFetcher: printer.NewDefaultEventFetcher(o.factory), - } - p := printer.NewPrinter(printerOptions) - defer p.Flush(o.IOStreams.Out) - for _, node := range topology.SortedNodes(nodes) { - err := p.PrintNode(node, o.IOStreams.Out) - if err != nil { - return err - } - } - return nil -} - -func parseResourceTypeOrNameArgs(args []string) (isPolicy, isPolicyCRD bool, err error) { - if strings.Contains(args[0], ",") { - return false, false, fmt.Errorf("cannot specify more than one type, received types: %v", strings.Split(args[0], ",")) - } - - switch args[0] { - case "policy", "policies": - isPolicy = true - - case "policycrd", "policycrds": - isPolicyCRD = true - } - - return isPolicy, isPolicyCRD, nil -} - -func encodePolicyAsNode(policy *policymanager.Policy) *topology.Node { - return &topology.Node{ - Object: policy.Unstructured, - Metadata: map[string]any{ - common.PolicyGK.String(): policy, - }, - } -} - -func encodePolicyCRDAsNode(policyCRD *policymanager.PolicyCRD) (*topology.Node, error) { - o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(policyCRD.CRD) - if err != nil { - return nil, err - } - u := &unstructured.Unstructured{Object: o} - - return &topology.Node{ - Object: u, - Metadata: map[string]any{ - common.PolicyCRDGK.String(): policyCRD, - }, - }, nil -} diff --git a/cmd/cli/proxy.go b/cmd/cli/proxy.go index b6f856c0c..3499f7f61 100644 --- a/cmd/cli/proxy.go +++ b/cmd/cli/proxy.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v3/pkg/action" - "github.com/flomesh-io/fsm/pkg/cli/common" + "sigs.k8s.io/gwctl/pkg/common" ) const proxyCmdDescription = ` diff --git a/cmd/cli/proxy_get.go b/cmd/cli/proxy_get.go index 876532192..37cfd4140 100644 --- a/cmd/cli/proxy_get.go +++ b/cmd/cli/proxy_get.go @@ -11,8 +11,9 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "sigs.k8s.io/gwctl/pkg/common" + "github.com/flomesh-io/fsm/pkg/cli" - "github.com/flomesh-io/fsm/pkg/cli/common" "github.com/flomesh-io/fsm/pkg/constants" ) diff --git a/go.mod b/go.mod index 57b96f169..ff24b4369 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.19.1 sigs.k8s.io/gateway-api v1.2.0 + sigs.k8s.io/gwctl v0.1.0 sigs.k8s.io/kind v0.24.0 ) @@ -82,7 +83,6 @@ require ( github.com/cilium/ebpf v0.10.0 github.com/containernetworking/cni v1.1.2 github.com/deckarep/golang-set v1.8.0 - github.com/evanphx/json-patch v5.9.0+incompatible github.com/florianl/go-tc v0.4.2 github.com/fsnotify/fsnotify v1.7.0 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 @@ -92,7 +92,6 @@ require ( github.com/go-logr/zerologr v1.2.3 github.com/go-resty/resty/v2 v2.14.0 github.com/gobwas/glob v0.2.3 - github.com/goccy/go-graphviz v0.2.9 github.com/hashicorp/consul/api v1.29.1 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hudl/fargo v1.4.0 @@ -214,6 +213,7 @@ require ( github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect + github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -255,6 +255,7 @@ require ( github.com/go-toolsmith/strparse v1.0.0 // indirect github.com/go-toolsmith/typep v1.0.2 // indirect github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect + github.com/goccy/go-graphviz v0.2.9 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect diff --git a/go.sum b/go.sum index 51c037df3..652758242 100644 --- a/go.sum +++ b/go.sum @@ -2703,6 +2703,8 @@ sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/gateway-api v1.2.0 h1:LrToiFwtqKTKZcZtoQPTuo3FxhrrhTgzQG0Te+YGSo8= sigs.k8s.io/gateway-api v1.2.0/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0= +sigs.k8s.io/gwctl v0.1.0 h1:7DcEdT+OJYiiF6L6qXIbgpYeo9a1YzP8hf46OHyUpo4= +sigs.k8s.io/gwctl v0.1.0/go.mod h1:44D3F2jTz76WTXsEkfXgjlQOJqJojFr9GjFbLaZ0AS4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kind v0.24.0 h1:g4y4eu0qa+SCeKESLpESgMmVFBebL0BDa6f777OIWrg= diff --git a/pkg/cli/common/clients.go b/pkg/cli/common/clients.go deleted file mode 100644 index d95a6fff4..000000000 --- a/pkg/cli/common/clients.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -type GroupKindFetcher interface { - Fetch(gk schema.GroupKind) ([]*unstructured.Unstructured, error) -} - -var _ GroupKindFetcher = (*defaultGroupKindFetcher)(nil) - -type defaultGroupKindFetcher struct { - factory Factory - additionalResourcesByGK map[schema.GroupKind][]*unstructured.Unstructured -} - -type groupKindFetcherOption func(*defaultGroupKindFetcher) - -func WithAdditionalResources(resources []*unstructured.Unstructured) groupKindFetcherOption { //nolint:revive - return func(f *defaultGroupKindFetcher) { - for _, resource := range resources { - gk := resource.GetObjectKind().GroupVersionKind().GroupKind() - f.additionalResourcesByGK[gk] = append(f.additionalResourcesByGK[gk], resource) - } - } -} - -func NewDefaultGroupKindFetcher(factory Factory, options ...groupKindFetcherOption) *defaultGroupKindFetcher { //nolint:revive - d := &defaultGroupKindFetcher{ - factory: factory, - additionalResourcesByGK: make(map[schema.GroupKind][]*unstructured.Unstructured), - } - for _, option := range options { - option(d) - } - return d -} - -func (d defaultGroupKindFetcher) Fetch(gk schema.GroupKind) ([]*unstructured.Unstructured, error) { - infos, err := d.factory.NewBuilder(). - Unstructured(). - Flatten(). - AllNamespaces(true). - ResourceTypeOrNameArgs(true, []string{fmt.Sprintf("%v.%v", gk.Kind, gk.Group)}...). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return nil, err - } - - var result []*unstructured.Unstructured - for _, info := range infos { - o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) - if err != nil { - return nil, err - } - result = append(result, &unstructured.Unstructured{Object: o}) - } - - // Return any additional Resources if they have been provided. - //for _, u := range d.additionalResourcesByGK[gk] { - result = append(result, d.additionalResourcesByGK[gk]...) - //} - - return result, nil -} diff --git a/pkg/cli/common/errors.go b/pkg/cli/common/errors.go deleted file mode 100644 index a98600a9e..000000000 --- a/pkg/cli/common/errors.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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" -) - -type ReferenceToNonExistentResourceError struct { - ReferenceFromTo -} - -func (r ReferenceToNonExistentResourceError) Error() string { - return fmt.Sprintf("%v %q references a non-existent %v %q", - r.referringObjectKind(), r.referringObjectName(), - r.referredObjectKind(), r.referredObjectName()) -} - -type ReferenceNotPermittedError struct { - ReferenceFromTo -} - -func (r ReferenceNotPermittedError) Error() string { - return fmt.Sprintf("%v %q is not permitted to reference %v %q", - r.referringObjectKind(), r.referringObjectName(), - r.referredObjectKind(), r.referredObjectName()) -} - -type ReferenceFromTo struct { - // ReferringObject is the "from" object which is referring "to" some other - // object. - ReferringObject GKNN - // ReferredObject is the actual object which is being referenced by another - // object. - ReferredObject GKNN -} - -// referringObjectKind returns a human readable Kind. -func (r ReferenceFromTo) referringObjectKind() string { - if r.ReferringObject.Group != "" { - return fmt.Sprintf("%v(.%v)", r.ReferringObject.Kind, r.ReferringObject.Group) - } - return r.ReferringObject.Kind -} - -// referredObjectKind returns a human readable Kind. -func (r ReferenceFromTo) referredObjectKind() string { - if r.ReferredObject.Group != "" { - return fmt.Sprintf("%v(.%v)", r.ReferredObject.Kind, r.ReferredObject.Group) - } - return r.ReferredObject.Kind -} - -// referringObjectName returns a human readable Name. -func (r ReferenceFromTo) referringObjectName() string { - if r.ReferringObject.Namespace != "" { - return fmt.Sprintf("%v/%v", r.ReferringObject.Namespace, r.ReferringObject.Name) - } - return r.ReferringObject.Name -} - -// referredObjectName returns a human readable Name. -func (r ReferenceFromTo) referredObjectName() string { - if r.ReferredObject.Namespace != "" { - return fmt.Sprintf("%v/%v", r.ReferredObject.Namespace, r.ReferredObject.Name) - } - return r.ReferredObject.Name -} diff --git a/pkg/cli/common/factory.go b/pkg/cli/common/factory.go deleted file mode 100644 index 99629afa1..000000000 --- a/pkg/cli/common/factory.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 ( - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" -) - -type Factory interface { - NewBuilder() *resource.Builder - KubeConfigNamespace() (string, bool, error) -} - -type factoryImpl struct { - clientGetter genericclioptions.RESTClientGetter -} - -func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory { - return &factoryImpl{clientGetter: clientGetter} -} - -func (f *factoryImpl) NewBuilder() *resource.Builder { - return resource.NewBuilder(f.clientGetter) -} - -func (f *factoryImpl) KubeConfigNamespace() (string, bool, error) { - return f.clientGetter.ToRawKubeConfigLoader().Namespace() -} diff --git a/pkg/cli/common/testhelpers.go b/pkg/cli/common/testhelpers.go deleted file mode 100644 index 51a3af56e..000000000 --- a/pkg/cli/common/testhelpers.go +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 ( - "encoding/json" - "fmt" - "strings" - - "github.com/google/go-cmp/cmp" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// MultiLine defines a custom type for wrapping texts spanning multilpe lines. -// It makes use of MultiLineTransformer to generate slightly better diffing -// output from cmp.Diff() for multi-line texts. -type MultiLine string - -// MultiLineTransformer transforms a MultiLine into a slice of strings by -// splitting on each new line. This allows the diffing function (used in tests) -// to compare each line independently. The result is that the diff output marks -// each line where a diff was observed. -var MultiLineTransformer = cmp.Transformer("MultiLine", func(m MultiLine) []string { - return strings.Split(string(m), "\n") -}) - -const ( - beginMarker = "#################################### BEGIN #####################################" - endMarker = "##################################### END ######################################" -) - -func (m MultiLine) String() string { - return fmt.Sprintf("%v\n%v%v", beginMarker, string(m), endMarker) -} - -type JSONString string - -func (src JSONString) CmpDiff(tgt JSONString) (diff string, err error) { - var srcMap, targetMap map[string]interface{} - err = json.Unmarshal([]byte(src), &srcMap) - if err != nil { - err = fmt.Errorf("failed to unmarshal the source json: %w", err) - return - } - err = json.Unmarshal([]byte(tgt), &targetMap) - if err != nil { - err = fmt.Errorf("failed to unmarshal the target json: %w", err) - return - } - - return cmp.Diff(srcMap, targetMap), nil -} - -func NamespaceForTest(name string) *corev1.Namespace { - return &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Status: corev1.NamespaceStatus{ - Phase: corev1.NamespaceActive, - }, - } -} - -func MustPrettyPrint(data any) { - b, err := json.MarshalIndent(data, "", " ") - if err != nil { - panic(err) - } - fmt.Println(string(b)) -} diff --git a/pkg/cli/common/types.go b/pkg/cli/common/types.go deleted file mode 100644 index a8267ece8..000000000 --- a/pkg/cli/common/types.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" -) - -const ( - gwctlPolicyGroup = "gwctl.gateway.networking.k8s.io" -) - -var ( - GatewayClassGK schema.GroupKind = schema.GroupKind{Group: gatewayv1.GroupName, Kind: "GatewayClass"} - GatewayGK schema.GroupKind = schema.GroupKind{Group: gatewayv1.GroupName, Kind: "Gateway"} - HTTPRouteGK schema.GroupKind = schema.GroupKind{Group: gatewayv1.GroupName, Kind: "HTTPRoute"} - NamespaceGK schema.GroupKind = schema.GroupKind{Group: corev1.GroupName, Kind: "Namespace"} - ServiceGK schema.GroupKind = schema.GroupKind{Group: corev1.GroupName, Kind: "Service"} - ReferenceGrantGK schema.GroupKind = schema.GroupKind{Group: gatewayv1beta1.GroupName, Kind: "ReferenceGrant"} - PolicyGK schema.GroupKind = schema.GroupKind{Group: gwctlPolicyGroup, Kind: "Policy"} - PolicyCRDGK schema.GroupKind = schema.GroupKind{Group: gwctlPolicyGroup, Kind: "PolicyCRD"} -) - -type GKNN struct { - Group string `json:",omitempty"` - Kind string `json:",omitempty"` - Namespace string `json:",omitempty"` - Name string `json:",omitempty"` -} - -func (g GKNN) GroupKind() schema.GroupKind { - return schema.GroupKind{ - Group: g.Group, - Kind: g.Kind, - } -} - -func (g GKNN) NamespacedName() types.NamespacedName { - return types.NamespacedName{ - Namespace: g.Namespace, - Name: g.Name, - } -} - -func (g GKNN) String() string { - gk := g.Kind - if g.Group != "" { - gk = fmt.Sprintf("%v.%v", g.Kind, g.Group) - } - name := g.Name - if g.Namespace != "" { - name = fmt.Sprintf("%v/%v", g.Namespace, g.Name) - } - return gk + "/" + name -} - -func (g GKNN) MarshalText() ([]byte, error) { - return []byte(g.String()), nil -} - -func GKNNFromUnstructured(u *unstructured.Unstructured) GKNN { - return GKNN{ - Group: u.GetObjectKind().GroupVersionKind().Group, - Kind: u.GetObjectKind().GroupVersionKind().Kind, - Namespace: u.GetNamespace(), - Name: u.GetName(), - } -} diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 06d9c1060..67bc36cbd 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -54,7 +54,7 @@ import ( "gopkg.in/yaml.v2" "k8s.io/cli-runtime/pkg/genericclioptions" - "github.com/flomesh-io/fsm/pkg/cli/common" + "sigs.k8s.io/gwctl/pkg/common" ) const ( diff --git a/pkg/cli/extension/directlyattachedpolicy/directlyattachedpolicy.go b/pkg/cli/extension/directlyattachedpolicy/directlyattachedpolicy.go deleted file mode 100644 index b4d8ce391..000000000 --- a/pkg/cli/extension/directlyattachedpolicy/directlyattachedpolicy.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 directlyattachedpolicy - -import ( - "fmt" - - "k8s.io/klog/v2" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -const ( - extensionName = "DirectlyAttachedPolicy" -) - -type Extension struct { - policyManager *policymanager.PolicyManager -} - -func NewExtension(policyManager *policymanager.PolicyManager) *Extension { - return &Extension{policyManager: policyManager} -} - -func (a *Extension) Execute(graph *topology.Graph) error { - graph.RemoveMetadata(extensionName) - for _, policy := range a.policyManager.GetPolicies() { - gk := policy.TargetRef.GroupKind() - nn := policy.TargetRef.NamespacedName() - - if graph.Nodes[gk] == nil || graph.Nodes[gk][nn] == nil { - // This target doesn't exist in the graph, so skip the policy. - continue - } - - node := graph.Nodes[gk][nn] - if node.Metadata == nil { - node.Metadata = map[string]any{} - } - if node.Metadata[extensionName] == nil { - node.Metadata[extensionName] = map[common.GKNN]*policymanager.Policy{} - } - - data, err := Access(node) - if err != nil { - return err - } - data[policy.GKNN()] = policy - } - return nil -} - -func Access(node *topology.Node) (map[common.GKNN]*policymanager.Policy, error) { - rawData, ok := node.Metadata[extensionName] - if !ok || rawData == nil { - klog.V(3).InfoS(fmt.Sprintf("no data found in node for %v", extensionName), "node", node.GKNN()) - return nil, nil - } - data, ok := rawData.(map[common.GKNN]*policymanager.Policy) - if !ok { - return nil, fmt.Errorf("unable to perform type assertion for %v in node %v", extensionName, node.GKNN()) - } - return data, nil -} diff --git a/pkg/cli/extension/extensions.go b/pkg/cli/extension/extensions.go deleted file mode 100644 index 8dfdd1c59..000000000 --- a/pkg/cli/extension/extensions.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 extension - -import "github.com/flomesh-io/fsm/pkg/cli/topology" - -type Extension interface { - Execute(*topology.Graph) error -} - -// TODO: Scope of improvement in the future involves: -// - Making executions parallel, when there are blocking operations. -// - Defining dependent extensions to determine their relative order. -func ExecuteAll(graph *topology.Graph, extensions ...Extension) error { - for _, nodes := range graph.Nodes { - for _, node := range nodes { - if node.Metadata == nil { - node.Metadata = make(map[string]any) - } - } - } - - for _, extension := range extensions { - err := extension.Execute(graph) - if err != nil { - return err - } - } - return nil -} diff --git a/pkg/cli/extension/gatewayeffectivepolicy/gatewayeffectivepolicy.go b/pkg/cli/extension/gatewayeffectivepolicy/gatewayeffectivepolicy.go deleted file mode 100644 index 089f059af..000000000 --- a/pkg/cli/extension/gatewayeffectivepolicy/gatewayeffectivepolicy.go +++ /dev/null @@ -1,472 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 gatewayeffectivepolicy - -import ( - "fmt" - "maps" - - "k8s.io/klog/v2" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -const ( - extensionName = "InheritedPolicy" -) - -type Extension struct{} - -func NewExtension() *Extension { - return &Extension{} -} - -// Extension calculates the effective policies for all Gateways, HTTPRoutes, and -// Backends in the Graph. -func (a *Extension) Execute(graph *topology.Graph) error { - graph.RemoveMetadata(extensionName) - if err := a.calculateInheritedPolicies(graph); err != nil { - return err - } - return a.calculateEffectivePolicies(graph) -} - -// calculateInheritedPolicies calculates the inherited polices for all Gateways, -// HTTRoutes, and Backends in the Graph. -func (a *Extension) calculateInheritedPolicies(graph *topology.Graph) error { - if err := a.calculateInheritedPoliciesForGateways(graph); err != nil { - return err - } - if err := a.calculateInheritedPoliciesForHTTPRoutes(graph); err != nil { - return err - } - if err := a.calculateInheritedPoliciesForBackends(graph); err != nil { - return err - } - return nil -} - -// calculateInheritedPoliciesForGateways calculates the inherited policies for -// all Gateways present in the Graph. -func (a *Extension) calculateInheritedPoliciesForGateways(graph *topology.Graph) error { - for _, gatewayNode := range graph.Nodes[common.GatewayGK] { - result := make(map[common.GKNN]*policymanager.Policy) - - // Policies inherited from Gateway's namespace. - namespaceNode := topologygw.GatewayNode(gatewayNode).Namespace() - if namespaceNode != nil { - namespacePoliciesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - maps.Copy(result, filterInheritablePolicies(namespacePoliciesMap)) - } - - // Policies inherited from GatewayClass. - gatewayClassNode := topologygw.GatewayNode(gatewayNode).GatewayClass() - if gatewayClassNode != nil { - gatewayClassPoliciesMap, err := directlyattachedpolicy.Access(gatewayClassNode) - if err != nil { - return err - } - maps.Copy(result, filterInheritablePolicies(gatewayClassPoliciesMap)) - } - - gatewayNode.Metadata[extensionName] = &NodeMetadata{GatewayInheritedPolicies: result} - } - return nil -} - -// calculateInheritedPoliciesForHTTPRoutes calculates the inherited policies for -// all HTTPRoutes present in the Graph. -func (a *Extension) calculateInheritedPoliciesForHTTPRoutes(graph *topology.Graph) error { - for _, httpRouteNode := range graph.Nodes[common.HTTPRouteGK] { - result := make(map[common.GKNN]*policymanager.Policy) - - // Policies inherited from HTTPRoute's namespace. - namespaceNode := topologygw.HTTPRouteNode(httpRouteNode).Namespace() - if namespaceNode != nil { - namespacePoliciesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - maps.Copy(result, filterInheritablePolicies(namespacePoliciesMap)) - } - - // Policies inherited from Gateways. - gatewayNodes := topologygw.HTTPRouteNode(httpRouteNode).Gateways() - //if gatewayNodes != nil { - for _, gatewayNode := range gatewayNodes { - // Add policies inherited by GatewayNode. - effPolicyMetadata, err := Access(gatewayNode) - if err != nil { - return err - } - if effPolicyMetadata != nil { - maps.Copy(result, effPolicyMetadata.GatewayInheritedPolicies) - } - - // Add inheritable policies directly applied to GatewayNode. - gatewayPoliciesMap, err := directlyattachedpolicy.Access(gatewayNode) - if err != nil { - return err - } - maps.Copy(result, filterInheritablePolicies(gatewayPoliciesMap)) - } - //} - - httpRouteNode.Metadata[extensionName] = &NodeMetadata{HTTPRouteInheritedPolicies: result} - } - return nil -} - -// calculateInheritedPoliciesForBackends calculates the inherited policies for -// all Backends present in ResourceModel. -func (a *Extension) calculateInheritedPoliciesForBackends(graph *topology.Graph) error { - for _, backendNode := range graph.Nodes[common.ServiceGK] { - result := make(map[common.GKNN]*policymanager.Policy) - - // Policies inherited from Backend's namespace. - namespaceNode := topologygw.BackendNode(backendNode).Namespace() - if namespaceNode != nil { - namespacePoliciesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - maps.Copy(result, filterInheritablePolicies(namespacePoliciesMap)) - } - - // Policies inherited from HTTPRoutes. - httpRouteNodes := topologygw.BackendNode(backendNode).HTTPRoutes() - //if httpRouteNodes != nil { - for _, httpRouteNode := range httpRouteNodes { - // Add policies inherited by HTTPRouteNode. - effPolicyMetadata, err := Access(httpRouteNode) - if err != nil { - return err - } - if effPolicyMetadata != nil { - maps.Copy(result, effPolicyMetadata.HTTPRouteInheritedPolicies) - } - - // Add inheritable policies directly applied to HTTPRouteNode. - httpRoutePoliciesMap, err := directlyattachedpolicy.Access(httpRouteNode) - if err != nil { - return err - } - maps.Copy(result, filterInheritablePolicies(httpRoutePoliciesMap)) - } - //} - - backendNode.Metadata[extensionName] = &NodeMetadata{BackendInheritedPolicies: result} - } - return nil -} - -// filterInheritablePolicies filters and returns policies which can be inherited. -func filterInheritablePolicies(policies map[common.GKNN]*policymanager.Policy) map[common.GKNN]*policymanager.Policy { - inheritablePolicies := make(map[common.GKNN]*policymanager.Policy) - - for gknn, policy := range policies { - if policy.IsInheritable() { - inheritablePolicies[gknn] = policy - } - } - - return inheritablePolicies -} - -func (a *Extension) calculateEffectivePolicies(graph *topology.Graph) error { - if err := a.calculateEffectivePoliciesForGateways(graph); err != nil { - return err - } - if err := a.calculateEffectivePoliciesForHTTPRoutes(graph); err != nil { - return err - } - if err := a.calculateEffectivePoliciesForBackends(graph); err != nil { - return err - } - return nil -} - -// calculateEffectivePoliciesForGateways calculates the effective policies for -// each Gateway by merging policies from different hierarchies (GatewayClass, -// Namespace, and Gateway). -func (a *Extension) calculateEffectivePoliciesForGateways(graph *topology.Graph) error { - for _, gatewayNode := range graph.Nodes[common.GatewayGK] { - if gatewayNode.Depth > graph.MaxDepth { - continue - } - - gatewayClassNode := topologygw.GatewayNode(gatewayNode).GatewayClass() - if gatewayClassNode == nil { - klog.V(3).InfoS("No GatewayClass node found for Gateway, skipping effective policy calculation", "gateway", gatewayNode.GKNN()) - continue - } - namespaceNode := topologygw.GatewayNode(gatewayNode).Namespace() - if namespaceNode == nil { - klog.V(3).InfoS("No Namespace node found for Gateway, skipping effective policy calculation", "gateway", gatewayNode.GKNN()) - continue - } - - gatewayClassPoliciesMap, err := directlyattachedpolicy.Access(gatewayClassNode) - if err != nil { - return err - } - namespacePoliciesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - gatewayPoliciesMap, err := directlyattachedpolicy.Access(gatewayNode) - if err != nil { - return err - } - - // Do not calculate effective policy for the Gateway if the referenced - // GatewayClass does not exist. For now, we only calculate effective policy - // once the references are corrected. - if gatewayClassNode == nil { - continue - } - - // Fetch all policies. - gatewayClassPolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(gatewayClassPoliciesMap)) - gatewayNamespacePolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(namespacePoliciesMap)) - gatewayPolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(gatewayPoliciesMap)) - - // Merge policies by their kind. - gatewayClassPoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(gatewayClassPolicies) - if err != nil { - return err - } - gatewayNamespacePoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(gatewayNamespacePolicies) - if err != nil { - return err - } - gatewayPoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(gatewayPolicies) - if err != nil { - return err - } - - // Merge all hierarchial policies. - result, err := policymanager.MergePoliciesOfDifferentHierarchy(gatewayClassPoliciesByKind, gatewayNamespacePoliciesByKind) - if err != nil { - return err - } - - result, err = policymanager.MergePoliciesOfDifferentHierarchy(result, gatewayPoliciesByKind) - if err != nil { - return err - } - - gatewayNodeMetadata, err := Access(gatewayNode) - if err != nil { - return err - } - if gatewayNodeMetadata == nil { - gatewayNodeMetadata = &NodeMetadata{} - gatewayNode.Metadata[extensionName] = gatewayNodeMetadata - } - gatewayNodeMetadata.GatewayEffectivePolicies = result - } - return nil -} - -// calculateEffectivePoliciesForHTTPRoutes calculates the effective policies for -// each HTTPRoute, taking into account policies from different hierarchies -// (GatewayClass, Namespace, Gateway, and HTTPRoute). -func (a *Extension) calculateEffectivePoliciesForHTTPRoutes(graph *topology.Graph) error { - for _, httpRouteNode := range graph.Nodes[common.HTTPRouteGK] { - result := make(map[common.GKNN]map[policymanager.PolicyCrdID]*policymanager.Policy) - - namespaceNode := topologygw.HTTPRouteNode(httpRouteNode).Namespace() - if namespaceNode == nil { - klog.V(3).InfoS("No Namespace node found for HTTPRoute, skipping effective policy calculation", "httpRoute", httpRouteNode.GKNN()) - continue - } - - httpRoutePoliciesMap, err := directlyattachedpolicy.Access(httpRouteNode) - if err != nil { - return err - } - namespacePoliciesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - - // Step 1: Aggregate all policies of the HTTPRoute and the - // HTTPRoute-namespace. - httpRoutePolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(httpRoutePoliciesMap)) - httpRouteNamespacePolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(namespacePoliciesMap)) - - // Step 2: Merge HTTPRoute and HTTPRoute-namespace policies by their kind. - httpRoutePoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(httpRoutePolicies) - if err != nil { - return err - } - httpRouteNamespacePoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(httpRouteNamespacePolicies) - if err != nil { - return err - } - - // Step 3: Loop through all Gateways and merge policies for each Gateway. - // End result is we get policies partitioned by each Gateway. - for gatewayGKNN, gatewayNode := range topologygw.HTTPRouteNode(httpRouteNode).Gateways() { - gatewayNodeMetadata, err := Access(gatewayNode) //nolint:govet - if err != nil { - return err - } - gatewayPoliciesByKind := gatewayNodeMetadata.GatewayEffectivePolicies - - // Merge all hierarchial policies. - mergedPolicies, err := policymanager.MergePoliciesOfDifferentHierarchy(gatewayPoliciesByKind, httpRouteNamespacePoliciesByKind) - if err != nil { - return err - } - - mergedPolicies, err = policymanager.MergePoliciesOfDifferentHierarchy(mergedPolicies, httpRoutePoliciesByKind) - if err != nil { - return err - } - - result[gatewayGKNN] = mergedPolicies - } - - httpRouteNodeMetadata, err := Access(httpRouteNode) - if err != nil { - return err - } - if httpRouteNodeMetadata == nil { - httpRouteNodeMetadata = &NodeMetadata{} - httpRouteNode.Metadata[extensionName] = httpRouteNodeMetadata - } - httpRouteNodeMetadata.HTTPRouteEffectivePolicies = result - } - return nil -} - -// calculateEffectivePoliciesForBackends calculates the effective policies for -// each Backend, considering policies from different hierarchies (GatewayClass, -// Namespace, Gateway, HTTPRoute, and Backend). -func (a *Extension) calculateEffectivePoliciesForBackends(graph *topology.Graph) error { - for _, backendNode := range graph.Nodes[common.ServiceGK] { - result := make(map[common.GKNN]map[policymanager.PolicyCrdID]*policymanager.Policy) - - namespaceNode := topologygw.BackendNode(backendNode).Namespace() - if namespaceNode == nil { - klog.V(3).InfoS("No Namespace node found for Backend, skipping effective policy calculation", "backend", backendNode.GKNN()) - continue - } - - backendPoliciesMap, err := directlyattachedpolicy.Access(backendNode) - if err != nil { - return err - } - namespacePoliciesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - - // Step 1: Aggregate all policies of the Backend and the Backend-namespace. - backendPolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(backendPoliciesMap)) - backendNamespacePolicies := policymanager.ConvertPoliciesMapToSlice(filterInheritablePolicies(namespacePoliciesMap)) - - // Step 2: Merge Backend and Backend-namespace policies by their kind. - backendPoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(backendPolicies) - if err != nil { - return err - } - backendNamespacePoliciesByKind, err := policymanager.MergePoliciesOfSimilarKind(backendNamespacePolicies) - if err != nil { - return err - } - - // Step 3: Loop through all HTTPRoutes and get their effective policies. Merge - // effective policies such that we get policies partitioned by Gateway. - for _, httpRouteNode := range topologygw.BackendNode(backendNode).HTTPRoutes() { - httpRouteNodeMetadata, err := Access(httpRouteNode) //nolint:govet - if err != nil { - return err - } - httpRoutePoliciesByGateway := httpRouteNodeMetadata.HTTPRouteEffectivePolicies - - for gatewayID, policies := range httpRoutePoliciesByGateway { - result[gatewayID], err = policymanager.MergePoliciesOfSameHierarchy(result[gatewayID], policies) - if err != nil { - return err - } - } - } - - // Step 4: Loop through all Gateways and merge the Backend and - // Backend-namespace specific policies. Note that this needs to be done - // separately from Step 4 i.e. we can't have this loop within Step 4 itself. - // This is because we first want to merge all policies of the same-hierarchy - // together and then move to the next hierarchy of Backend and - // Backend-namespace. - for gatewayID := range result { - // Merge all hierarchial policies. - result[gatewayID], err = policymanager.MergePoliciesOfDifferentHierarchy(result[gatewayID], backendNamespacePoliciesByKind) - if err != nil { - return err - } - - result[gatewayID], err = policymanager.MergePoliciesOfDifferentHierarchy(result[gatewayID], backendPoliciesByKind) - if err != nil { - return err - } - } - - backendNodeMetadata, err := Access(backendNode) - if err != nil { - return err - } - if backendNodeMetadata == nil { - backendNodeMetadata = &NodeMetadata{} - backendNode.Metadata[extensionName] = backendNodeMetadata - } - backendNodeMetadata.BackendEffectivePolicies = result - } - return nil -} - -type NodeMetadata struct { - GatewayInheritedPolicies map[common.GKNN]*policymanager.Policy - HTTPRouteInheritedPolicies map[common.GKNN]*policymanager.Policy - BackendInheritedPolicies map[common.GKNN]*policymanager.Policy - - GatewayEffectivePolicies map[policymanager.PolicyCrdID]*policymanager.Policy - HTTPRouteEffectivePolicies map[common.GKNN]map[policymanager.PolicyCrdID]*policymanager.Policy - BackendEffectivePolicies map[common.GKNN]map[policymanager.PolicyCrdID]*policymanager.Policy -} - -func Access(node *topology.Node) (*NodeMetadata, error) { - rawData, ok := node.Metadata[extensionName] - if !ok || rawData == nil { - klog.V(3).InfoS(fmt.Sprintf("no data found in node for %v", extensionName), "node", node.GKNN()) - return nil, nil - } - data, ok := rawData.(*NodeMetadata) - if !ok { - return nil, fmt.Errorf("unable to perform type assertion for %v in node %v", extensionName, node.GKNN()) - } - return data, nil -} diff --git a/pkg/cli/extension/notfoundrefvalidator/notfoundrefvalidator.go b/pkg/cli/extension/notfoundrefvalidator/notfoundrefvalidator.go deleted file mode 100644 index 35877bbaf..000000000 --- a/pkg/cli/extension/notfoundrefvalidator/notfoundrefvalidator.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 notfoundrefvalidator - -import ( - "fmt" - - "k8s.io/klog/v2" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -const ( - extensionName = "NotFoundReferenceValidator" -) - -type Extension struct{} - -func NewExtension() *Extension { - return &Extension{} -} - -func (a *Extension) Execute(graph *topology.Graph) error { - graph.RemoveMetadata(extensionName) - for _, relation := range graph.Relations { - for _, fromNode := range graph.Nodes[relation.From] { - if fromNode.Depth > graph.MaxDepth { - klog.V(3).InfoS("Not validating resource since it's depth is greater than the max depth", - "extension", extensionName, "resource", fromNode.GKNN(), "depth", fromNode.Depth, "MaxDepth", graph.MaxDepth, - ) - } - - for _, toNodeGKNN := range relation.NeighborFunc(fromNode.Object) { - err := common.ReferenceToNonExistentResourceError{ReferenceFromTo: common.ReferenceFromTo{ - ReferringObject: fromNode.GKNN(), - ReferredObject: toNodeGKNN, - }} - - if _, ok := graph.Nodes[toNodeGKNN.GroupKind()]; !ok { - if err := a.puErrorInNode(fromNode, err); err != nil { - return err - } - klog.V(1).Info(err) - continue - } - toNode := graph.Nodes[toNodeGKNN.GroupKind()][toNodeGKNN.NamespacedName()] - if toNode == nil { - if err := a.puErrorInNode(fromNode, err); err != nil { - return err - } - klog.V(1).Info(err) - } - } - } - } - return nil -} - -func (a *Extension) puErrorInNode(node *topology.Node, notFoundErr error) error { - if node.Metadata == nil { - node.Metadata = map[string]any{} - } - if node.Metadata[extensionName] == nil { - node.Metadata[extensionName] = &NodeMetadata{ - Errors: make([]error, 0), - } - } - - data, err := Access(node) - if err != nil { - return err - } - data.Errors = append(data.Errors, notFoundErr) - return nil -} - -type NodeMetadata struct { - Errors []error -} - -func Access(node *topology.Node) (*NodeMetadata, error) { - rawData, ok := node.Metadata[extensionName] - if !ok || rawData == nil { - klog.V(3).InfoS(fmt.Sprintf("no data found in node for %v", extensionName), "node", node.GKNN()) - return nil, nil - } - data, ok := rawData.(*NodeMetadata) - if !ok { - return nil, fmt.Errorf("unable to perform type assertion for %v in node %v", extensionName, node.GKNN()) - } - return data, nil -} diff --git a/pkg/cli/extension/refgrantvalidator/refgrantvalidator.go b/pkg/cli/extension/refgrantvalidator/refgrantvalidator.go deleted file mode 100644 index 846f91371..000000000 --- a/pkg/cli/extension/refgrantvalidator/refgrantvalidator.go +++ /dev/null @@ -1,303 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 refgrantvalidator - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/klog/v2" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -const ( - extensionName = "ReferenceGrantsForBackend" -) - -type Extension struct { - fetcher referenceGrantFetcher -} - -func NewExtension(fetcher referenceGrantFetcher) *Extension { - return &Extension{fetcher: fetcher} -} - -// Extension calculates the effective policies for all Gateways, HTTPRoutes, and -// Backends in the Graph. -func (a *Extension) Execute(graph *topology.Graph) error { - graph.RemoveMetadata(extensionName) - if err := a.discoverReferenceGrantsForBackends(graph); err != nil { - return err - } - return a.validateHTTPRoutes(graph) -} - -func (a *Extension) discoverReferenceGrantsForBackends(graph *topology.Graph) error { - referenceGrantsByNamespace := make(map[string][]*gatewayv1beta1.ReferenceGrant) - for _, backendNode := range graph.Nodes[common.ServiceGK] { - backendNS := backendNode.Object.GetNamespace() - - referenceGrants, ok := referenceGrantsByNamespace[backendNS] - if !ok { - var err error - referenceGrants, err = a.fetcher.FetchReferenceGrantsForNamespace(backendNS) - if err != nil { - return err - } - referenceGrantsByNamespace[backendNS] = referenceGrants - } - - for _, referenceGrant := range referenceGrants { - backendRef := backendNode.GKNN() - if ReferenceGrantExposes(referenceGrant, backendRef) { - klog.V(1).InfoS("ReferenceGrant exposes Backend", - "referenceGrant", referenceGrant.GetNamespace()+"/"+referenceGrant.GetName(), - "backendRef", backendRef.Namespace+"/"+backendRef.Name, - ) - if err := a.putReferenceGrantInNode(backendNode, referenceGrant); err != nil { - return err - } - } - } - } - return nil -} - -func (a *Extension) validateHTTPRoutes(graph *topology.Graph) error { - for _, httpRouteNode := range graph.Nodes[common.HTTPRouteGK] { - if httpRouteNode.Depth > graph.MaxDepth { - klog.V(3).InfoS("Not validating HTTPRoute since it's depth is greater than the max depth", - "extension", extensionName, "httpRouteNode.Depth", httpRouteNode.Depth, "MaxDepth", graph.MaxDepth, - ) - continue - } - - for backendGKNN, backendNode := range topologygw.HTTPRouteNode(httpRouteNode).Backends() { - // Ensure that if this is a cross namespace reference, then it is accepted - // through some ReferenceGrant. - if httpRouteNode.GKNN().Namespace != backendGKNN.Namespace { - backendNodeMetadata, err := Access(backendNode) - if err != nil { - return err - } - - var referenceAccepted bool - if backendNodeMetadata != nil { - for _, referenceGrant := range backendNodeMetadata.ReferenceGrants { - if ReferenceGrantAccepts(referenceGrant, httpRouteNode.GKNN()) { - referenceAccepted = true - break - } - } - } - if !referenceAccepted { - err := common.ReferenceNotPermittedError{ReferenceFromTo: common.ReferenceFromTo{ - ReferringObject: httpRouteNode.GKNN(), - ReferredObject: backendGKNN, - }} - if err := a.putReferenceGrantErrorInNode(httpRouteNode, err); err != nil { - return err - } - klog.V(1).InfoS("Reference not permitted", "from", httpRouteNode.GKNN(), "to", backendGKNN) - continue - } - } - } - } - return nil -} - -func (a *Extension) putReferenceGrantInNode(node *topology.Node, referenceGrant *gatewayv1beta1.ReferenceGrant) error { - if node.Metadata == nil { - node.Metadata = map[string]any{} - } - if node.Metadata[extensionName] == nil { - node.Metadata[extensionName] = &NodeMetadata{ - ReferenceGrants: make(map[common.GKNN]*gatewayv1beta1.ReferenceGrant), - Errors: make([]error, 0), - } - } - - data, err := Access(node) - if err != nil { - return err - } - gknn := common.GKNN{ - Group: common.ReferenceGrantGK.Group, - Kind: common.ReferenceGrantGK.Kind, - Namespace: referenceGrant.GetNamespace(), - Name: referenceGrant.GetName(), - } - data.ReferenceGrants[gknn] = referenceGrant - return nil -} - -func (a *Extension) putReferenceGrantErrorInNode(node *topology.Node, refGrantErr error) error { - if node.Metadata == nil { - node.Metadata = map[string]any{} - } - if node.Metadata[extensionName] == nil { - node.Metadata[extensionName] = &NodeMetadata{ - ReferenceGrants: make(map[common.GKNN]*gatewayv1beta1.ReferenceGrant), - Errors: make([]error, 0), - } - } - - data, err := Access(node) - if err != nil { - return err - } - data.Errors = append(data.Errors, refGrantErr) - return nil -} - -type NodeMetadata struct { - ReferenceGrants map[common.GKNN]*gatewayv1beta1.ReferenceGrant - Errors []error -} - -func Access(node *topology.Node) (*NodeMetadata, error) { - rawData, ok := node.Metadata[extensionName] - if !ok || rawData == nil { - klog.V(3).InfoS(fmt.Sprintf("no data found in node for %v", extensionName), "node", node.GKNN()) - return nil, nil - } - data, ok := rawData.(*NodeMetadata) - if !ok { - return nil, fmt.Errorf("unable to perform type assertion for %v in node %v", extensionName, node.GKNN()) - } - return data, nil -} - -// ReferenceGrantExposes returns true if the provided reference grant "exposes" -// the given resource. "Exposes" means that the resource is part of the "To" -// fields within the ReferenceGrant. -func ReferenceGrantExposes(referenceGrant *gatewayv1beta1.ReferenceGrant, resource common.GKNN) bool { - if referenceGrant.GetNamespace() != resource.Namespace { - return false - } - for _, to := range referenceGrant.Spec.To { - if to.Group != gatewayv1.Group(resource.Group) { - continue - } - if to.Kind != gatewayv1.Kind(resource.Kind) { - continue - } - if to.Name == nil || len(*to.Name) == 0 || *to.Name == gatewayv1.ObjectName(resource.Name) { - return true - } - } - return false -} - -// ReferenceGrantAccepts returns true if the provided reference grant "accepts" -// references from the given resource. "Accepts" means that the resource is part -// of the "From" fields within the ReferenceGrant. -func ReferenceGrantAccepts(referenceGrant *gatewayv1beta1.ReferenceGrant, resource common.GKNN) bool { - resource.Name = "" - for _, from := range referenceGrant.Spec.From { - fromRef := common.GKNN{ - Group: string(from.Group), - Kind: string(from.Kind), - Namespace: string(from.Namespace), - } - if fromRef == resource { - return true - } - } - return false -} - -type referenceGrantFetcher interface { - FetchReferenceGrantsForNamespace(string) ([]*gatewayv1beta1.ReferenceGrant, error) -} - -var _ referenceGrantFetcher = (*defaultReferenceGrantFetcher)(nil) - -type defaultReferenceGrantFetcher struct { - factory common.Factory - additionalResourcesByNamespace map[string][]*unstructured.Unstructured -} - -type referenceGrantFetcherOption func(*defaultReferenceGrantFetcher) - -func WithAdditionalResources(resources []*unstructured.Unstructured) referenceGrantFetcherOption { //nolint:revive - return func(f *defaultReferenceGrantFetcher) { - for _, resource := range resources { - if resource.GroupVersionKind().GroupKind() == common.ReferenceGrantGK { - f.additionalResourcesByNamespace[resource.GetNamespace()] = append(f.additionalResourcesByNamespace[resource.GetNamespace()], resource) - } - } - } -} - -func NewDefaultReferenceGrantFetcher(factory common.Factory, options ...referenceGrantFetcherOption) *defaultReferenceGrantFetcher { //nolint:revive - f := &defaultReferenceGrantFetcher{ - factory: factory, - additionalResourcesByNamespace: make(map[string][]*unstructured.Unstructured), - } - for _, option := range options { - option(f) - } - return f -} - -func (f *defaultReferenceGrantFetcher) FetchReferenceGrantsForNamespace(namespace string) ([]*gatewayv1beta1.ReferenceGrant, error) { - infos, err := f.factory.NewBuilder(). - Unstructured(). - Flatten(). - NamespaceParam(namespace).RequireNamespace(). - AllNamespaces(false). - ResourceTypeOrNameArgs(true, []string{fmt.Sprintf("%v.%v", common.ReferenceGrantGK.Kind, common.ReferenceGrantGK.Group)}...). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return nil, err - } - - var result []*gatewayv1beta1.ReferenceGrant - for _, info := range infos { - u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) - if err != nil { - return nil, err - } - refGrant := &gatewayv1beta1.ReferenceGrant{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u, refGrant); err != nil { - return nil, err - } - result = append(result, refGrant) - } - - // Return any additional ReferenceGrants if they have been provided. - for _, u := range f.additionalResourcesByNamespace[namespace] { - refGrant := &gatewayv1beta1.ReferenceGrant{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), refGrant); err != nil { - return nil, fmt.Errorf("converting local ReferenceGrant from Unstructurued to typed: %v", err) - } - result = append(result, refGrant) - } - - return result, nil -} diff --git a/pkg/cli/extension/utils/utils.go b/pkg/cli/extension/utils/utils.go deleted file mode 100644 index 40c21e5d2..000000000 --- a/pkg/cli/extension/utils/utils.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 utils - -import ( - "github.com/flomesh-io/fsm/pkg/cli/extension/notfoundrefvalidator" - "github.com/flomesh-io/fsm/pkg/cli/extension/refgrantvalidator" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -func AggregateAnalysisErrors(node *topology.Node) ([]error, error) { - var analysisErrors []error - refGrantValidationMetadata, err := refgrantvalidator.Access(node) - if err != nil { - return nil, err - } - if refGrantValidationMetadata != nil && len(refGrantValidationMetadata.Errors) != 0 { - analysisErrors = append(analysisErrors, refGrantValidationMetadata.Errors...) - } - notFoundRefValidatorMetadata, err := notfoundrefvalidator.Access(node) - if err != nil { - return nil, err - } - if notFoundRefValidatorMetadata != nil && len(notFoundRefValidatorMetadata.Errors) != 0 { - analysisErrors = append(analysisErrors, notFoundRefValidatorMetadata.Errors...) - } - return analysisErrors, nil -} diff --git a/pkg/cli/flags/flags.go b/pkg/cli/flags/flags.go deleted file mode 100644 index 1355afbb4..000000000 --- a/pkg/cli/flags/flags.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 flags - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/pflag" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/flomesh-io/fsm/pkg/cli/common" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -type ForFlag string - -func NewForFlag() *ForFlag { - f := ForFlag("") - return &f -} - -func (f *ForFlag) AddFlag(flagSet *pflag.FlagSet) { - flagSet.StringVar((*string)(f), "for", "", `Filter results to only those related to the specified resource. Format: TYPE[/NAMESPACE]/NAME. Not specifying a NAMESPACE assumes the 'default' value. Examples: gateway/ns2/foo-gateway, httproute/bar-httproute, service/ns1/my-svc`) -} - -func (f *ForFlag) ToOption() (common.GKNN, error) { - objRef := common.GKNN{} - - if *f != "" { - parts := strings.Split(string(*f), "/") - if len(parts) < 2 || len(parts) > 3 { - fmt.Fprintf(os.Stderr, "invalid value used in --for flag; value must be in the format TYPE[/NAMESPACE]/NAME\n") - os.Exit(1) - } - if len(parts) == 2 { - objRef = common.GKNN{Kind: parts[0], Namespace: metav1.NamespaceDefault, Name: parts[1]} - } else { - objRef = common.GKNN{Kind: parts[0], Namespace: parts[1], Name: parts[2]} - } - switch strings.ToLower(objRef.Kind) { - case "gatewayclass", "gateawyclasses": - objRef.Group = gatewayv1.GroupVersion.Group - objRef.Kind = "GatewayClass" - objRef.Namespace = "" - case "gateway", "gateways": - objRef.Group = gatewayv1.GroupVersion.Group - objRef.Kind = "Gateway" - case "httproute", "httproutes": - objRef.Group = gatewayv1.GroupVersion.Group - objRef.Kind = "HTTPRoute" - case "service", "services": - objRef.Kind = "Service" - default: - fmt.Fprintf(os.Stderr, "invalid type provided in --for flag; type must be one of [gatewayclass, gateway, httproute, service]\n") - os.Exit(1) - } - } - - return objRef, nil -} diff --git a/pkg/cli/policymanager/helpers.go b/pkg/cli/policymanager/helpers.go deleted file mode 100644 index 5ff3d41ab..000000000 --- a/pkg/cli/policymanager/helpers.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 policymanager - -import "github.com/flomesh-io/fsm/pkg/cli/common" - -// ToPolicyRefs returns the Object references of all given policies. Note that -// these are not the value of targetRef within the Policies but rather the -// reference to the Policy object itself. -func ToPolicyRefs(policies []Policy) []common.GKNN { - var result []common.GKNN - for _, policy := range policies { - result = append(result, policy.GKNN()) - } - return result -} diff --git a/pkg/cli/policymanager/manager.go b/pkg/cli/policymanager/manager.go deleted file mode 100644 index 6c5d4abe5..000000000 --- a/pkg/cli/policymanager/manager.go +++ /dev/null @@ -1,324 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 policymanager - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - - corev1 "k8s.io/api/core/v1" - - "golang.org/x/exp/maps" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/flomesh-io/fsm/pkg/cli/common" - - gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" -) - -type PolicyManager struct { - Fetcher common.GroupKindFetcher - - // policyCRDs maps a CRD name to the CRD object. - policyCRDs map[PolicyCrdID]*PolicyCRD - // policies maps a policy name to the policy object. - policies map[common.GKNN]*Policy -} - -func New(fetcher common.GroupKindFetcher) *PolicyManager { - return &PolicyManager{ - Fetcher: fetcher, - policyCRDs: make(map[PolicyCrdID]*PolicyCRD), - policies: make(map[common.GKNN]*Policy), - } -} - -// Init will construct a local cache of all Policy CRDs and Policy Resources. -func (p *PolicyManager) Init() error { - err := p.initPolicyCRDs() - if err != nil { - return err - } - - return p.initPolicies() -} - -func (p *PolicyManager) initPolicyCRDs() error { - crdGK := schema.GroupKind{Group: apiextensionsv1.GroupName, Kind: "CustomResourceDefinition"} - - allUnstructuredCRDs, err := p.Fetcher.Fetch(crdGK) - if err != nil { - return err - } - for _, uCRD := range allUnstructuredCRDs { - crd := &apiextensionsv1.CustomResourceDefinition{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uCRD.UnstructuredContent(), crd); err != nil { - panic(fmt.Sprintf("failed to convert unstructured CustomResourceDefinition to structured: %v", err)) - } - policyCRD := &PolicyCRD{crd} - // Check if the CRD is a Gateway Policy CRD - if policyCRD.IsValid() { - p.policyCRDs[policyCRD.ID()] = policyCRD - } - } - - return nil -} - -func (p *PolicyManager) initPolicies() error { - for _, policyCRD := range p.policyCRDs { - gk := schema.GroupKind{Group: policyCRD.CRD.Spec.Group, Kind: policyCRD.CRD.Spec.Names.Kind} - policies, err := p.Fetcher.Fetch(gk) - if err != nil { - return err - } - - for _, unstrucutredPolicy := range policies { - policy, err := ConstructPolicy(unstrucutredPolicy, policyCRD.IsInheritable()) - if err != nil { - return err - } - p.policies[policy.GKNN()] = &policy - } - } - return nil -} - -func (p *PolicyManager) PoliciesAttachedTo(objRef common.GKNN) []*Policy { - var result []*Policy - for _, policy := range p.policies { - if policy.IsAttachedTo(objRef) { - result = append(result, policy) - } - } - return result -} - -func (p *PolicyManager) GetCRDs() []*PolicyCRD { - return maps.Values(p.policyCRDs) -} - -func (p *PolicyManager) GetCRD(name string) (*PolicyCRD, bool) { - for _, policyCrd := range p.policyCRDs { - if name == policyCrd.CRD.Name { - return policyCrd, true - } - } - - return nil, false -} - -func (p *PolicyManager) GetPolicies() []*Policy { - return maps.Values(p.policies) -} - -// PolicyCrdID has the structurued "." -type PolicyCrdID string - -type PolicyCRD struct { - CRD *apiextensionsv1.CustomResourceDefinition -} - -// ID returns a unique identifier for this PolicyCRD. -func (p PolicyCRD) ID() PolicyCrdID { - return PolicyCrdID(p.CRD.Spec.Names.Kind + "." + p.CRD.Spec.Group) -} - -// IsValid return true if the PolicyCRD satisfies requirements for qualifying as -// a Gateway Policy CRD. -func (p PolicyCRD) IsValid() bool { - return p.IsInheritable() || p.IsDirect() || p.CRD.GetLabels()[gatewayv1alpha2.PolicyLabelKey] == "true" -} - -func (p PolicyCRD) IsInheritable() bool { - return strings.ToLower(p.CRD.GetLabels()[gatewayv1alpha2.PolicyLabelKey]) == "inherited" -} - -func (p PolicyCRD) IsDirect() bool { - return strings.ToLower(p.CRD.GetLabels()[gatewayv1alpha2.PolicyLabelKey]) == "direct" -} - -// IsClusterScoped returns true if the CRD is cluster scoped. Such policies can -// be used to target a cluster scoped resource like GatewayClass. -func (p PolicyCRD) IsClusterScoped() bool { - return p.CRD.Spec.Scope == apiextensionsv1.ClusterScoped -} - -type Policy struct { - Unstructured *unstructured.Unstructured - // TargetRefs references the target objects this policy is attached to. This - // only makes sense in case of a directly-attached-policy, or an - // unmerged-inherited-policy. - TargetRef common.GKNN - // Indicates whether the policy is supposed to be "inherited" (as opposed to - // "direct"). - Inheritable bool -} - -func ConstructPolicy(u *unstructured.Unstructured, inherited bool) (Policy, error) { - result := Policy{Unstructured: u} - - // Identify targetRef of Policy. - type genericPolicy struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec struct { - TargetRef gatewayv1alpha2.NamespacedPolicyTargetReference - } - } - structuredPolicy := &genericPolicy{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), structuredPolicy); err != nil { - return Policy{}, fmt.Errorf("failed to convert unstructured policy resource to structured: %v", err) - } - result.TargetRef = common.GKNN{ - Group: string(structuredPolicy.Spec.TargetRef.Group), - Kind: string(structuredPolicy.Spec.TargetRef.Kind), - Namespace: structuredPolicy.GetNamespace(), - Name: string(structuredPolicy.Spec.TargetRef.Name), - } - if result.TargetRef.Namespace == "" { - result.TargetRef.Namespace = result.Unstructured.GetNamespace() - } - if structuredPolicy.Spec.TargetRef.Namespace != nil { - result.TargetRef.Namespace = string(*structuredPolicy.Spec.TargetRef.Namespace) - } - - result.Inheritable = inherited - - return result, nil -} - -func (p Policy) GKNN() common.GKNN { - return common.GKNN{ - Group: p.Unstructured.GroupVersionKind().Group, - Kind: p.Unstructured.GroupVersionKind().Kind, - Namespace: p.Unstructured.GetNamespace(), - Name: p.Unstructured.GetName(), - } -} - -// PolicyCrdID returns a unique identifier for the CRD of this policy. -func (p Policy) PolicyCrdID() PolicyCrdID { - return PolicyCrdID(p.Unstructured.GetObjectKind().GroupVersionKind().Kind + "." + p.Unstructured.GetObjectKind().GroupVersionKind().Group) -} - -func (p Policy) IsInheritable() bool { - return p.Inheritable -} - -func (p Policy) IsDirect() bool { - return !p.Inheritable -} - -func (p Policy) IsAttachedTo(objRef common.GKNN) bool { - if p.TargetRef.Kind == K8sNamespaceKind && p.TargetRef.Name == "" { - p.TargetRef.Name = "default" - } - if objRef.Kind == K8sNamespaceKind && objRef.Name == "" { - objRef.Name = "default" - } - if p.TargetRef.Kind != K8sNamespaceKind && p.TargetRef.Namespace == "" { - p.TargetRef.Namespace = corev1.NamespaceDefault - } - if objRef.Kind != K8sNamespaceKind && objRef.Namespace == "" { - objRef.Namespace = corev1.NamespaceDefault - } - return p.TargetRef == objRef -} - -func (p Policy) DeepCopy() *Policy { - clone := &Policy{ - Unstructured: p.Unstructured.DeepCopy(), - TargetRef: p.TargetRef, - Inheritable: p.Inheritable, - } - return clone -} - -func (p Policy) Spec() map[string]interface{} { - spec, ok, err := unstructured.NestedFieldCopy(p.Unstructured.UnstructuredContent(), "spec") - if err != nil || !ok { - return nil - } - - result, ok := spec.(map[string]interface{}) - if !ok { - return nil - } - return result -} - -func (p Policy) EffectiveSpec() (map[string]interface{}, error) { - if !p.IsInheritable() { - // No merging is required in case of Direct policies. - result := p.Spec() - delete(result, "targetRef") - return result, nil - } - - spec := p.Spec() - if spec == nil { - return nil, nil - } - - defaultSpec, ok := p.Spec()["default"] - if !ok { - defaultSpec = make(map[string]interface{}) - } - overrideSpec, ok := p.Spec()["override"] - if !ok { - overrideSpec = make(map[string]interface{}) - } - - // Check if both are non-scalar and merge them. - defaultSpecNonScalar, isDefaultSpecNonScalar := defaultSpec.(map[string]interface{}) - overrideSpecNonScalar, isOverrideSpecNonScalar := overrideSpec.(map[string]interface{}) - if !isDefaultSpecNonScalar || !isOverrideSpecNonScalar { - return nil, fmt.Errorf("spec.default and spec.override must be non-scalar") - } - - result, err := mergeUnstructured(defaultSpecNonScalar, overrideSpecNonScalar) - if err != nil { - return nil, err - } - - return result, nil -} - -func (p *Policy) MarshalJSON() ([]byte, error) { - effectiveSpec, err := p.EffectiveSpec() - if err != nil { - return nil, err - } - return json.Marshal(effectiveSpec) -} - -func ConvertPoliciesMapToSlice(policies map[common.GKNN]*Policy) []*Policy { - result := maps.Values(policies) - sort.Slice(result, func(i, j int) bool { - a := fmt.Sprintf("%v/%v/%v", result[i].PolicyCrdID(), result[i].Unstructured.GetNamespace(), result[i].Unstructured.GetName()) - b := fmt.Sprintf("%v/%v/%v", result[j].PolicyCrdID(), result[j].Unstructured.GetNamespace(), result[j].Unstructured.GetName()) - return a < b - }) - return result -} diff --git a/pkg/cli/policymanager/merger.go b/pkg/cli/policymanager/merger.go deleted file mode 100644 index fcd0304b5..000000000 --- a/pkg/cli/policymanager/merger.go +++ /dev/null @@ -1,204 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 policymanager - -import ( - "encoding/json" - "fmt" - - jsonpatch "github.com/evanphx/json-patch" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/flomesh-io/fsm/pkg/cli/common" -) - -// MergePoliciesOfSimilarKind will convert a slice a policies to a map of -// policies by merging policies of similar kind. The returned map will have the -// policy kind as the key. -func MergePoliciesOfSimilarKind(policies []*Policy) (map[PolicyCrdID]*Policy, error) { - result := make(map[PolicyCrdID]*Policy) - for _, policy := range policies { - policyCrdID := policy.PolicyCrdID() - - if _, ok := result[policyCrdID]; !ok { - // Policy of kind policyCrdID doesn't already exist so simply insert it - // into the resulting map. - result[policyCrdID] = policy - continue - } - - // At this point, we know that a policy of kind policyCrdID already exists - // so we need to merge the new policy with the existing one. - - // Merge existing policy with new policy. Reuse existing function to merge - // policies of similar hierarchy. - mergedPolicies, err := MergePoliciesOfSameHierarchy( - map[PolicyCrdID]*Policy{ - policyCrdID: result[policyCrdID], // Existing policy. - }, - map[PolicyCrdID]*Policy{ - policyCrdID: policy, // New policy. - }, - ) - if err != nil { - return nil, err - } - - result[policyCrdID] = mergedPolicies[policyCrdID] - } - return result, nil -} - -func MergePoliciesOfSameHierarchy(policies1, policies2 map[PolicyCrdID]*Policy) (map[PolicyCrdID]*Policy, error) { - return mergePolicies(policies1, policies2, orderPolicyByPrecedence) -} - -func MergePoliciesOfDifferentHierarchy(parentPolicies, childPolicies map[PolicyCrdID]*Policy) (map[PolicyCrdID]*Policy, error) { - return mergePolicies(parentPolicies, childPolicies, func(a, b *Policy) (*Policy, *Policy) { return a, b }) -} - -// mergePolicies will merge policies which are partitioned by their Kind. -// -// precedence function will order two policies such that the second policy -// returned will have a higher precedence. -func mergePolicies(policies1, policies2 map[PolicyCrdID]*Policy, precedence func(a, b *Policy) (*Policy, *Policy)) (map[PolicyCrdID]*Policy, error) { - result := make(map[PolicyCrdID]*Policy) - - // Copy policies1 into result. - for policyCrdID, policy := range policies1 { - result[policyCrdID] = policy - } - - // Merge policies2 with result. - for policyCrdID, policy := range policies2 { - existingPolicy, ok := result[policyCrdID] - if !ok { - // Policy of kind policyCrdID doesn't already exist so simply insert it - // into the resulting map. - result[policyCrdID] = policy - continue - } - - // Policy of kind policyCrdID already exists so merge them. - - lowerPolicy, higherPolicy := precedence(existingPolicy, policy) - - res, err := mergePolicy(lowerPolicy, higherPolicy) - if err != nil { - return nil, err - } - result[policyCrdID] = res - } - return result, nil -} - -// mergePolicy will merge two policies of similar kind. -// - overrides from parent will take precedence over the overrides from the -// child. -// - defaults from child will take precedence over the defaults from the -// parent. -func mergePolicy(parent, child *Policy) (*Policy, error) { - // Only policies of similar kind can be merged. - if parent.PolicyCrdID() != child.PolicyCrdID() { - return nil, fmt.Errorf("cannot merge policies of different kind; kind1=%v, kind2=%v", parent.PolicyCrdID(), child.PolicyCrdID()) - } - - resultUnstructured, err := mergeUnstructured(parent.Unstructured.UnstructuredContent(), child.Unstructured.UnstructuredContent()) - if err != nil { - return nil, err - } - - if parent.IsInheritable() { - // In case of an Inherited policy, the "spec.override" field of the parent - // should take precedence over the child. So we patch the override field - // from the parent into the result. - override, ok, err := unstructured.NestedFieldCopy(parent.Unstructured.UnstructuredContent(), "spec", "override") - if err != nil { - return nil, err - } - // If ok=false, it means "spec.override" field was missing, so we have - // nothing to do in that case. On the other hand, ok=true means - // "spec.override" field exists so we override the value of the parent. - if ok { - resultUnstructured, err = mergeUnstructured(resultUnstructured, map[string]interface{}{ - "spec": map[string]interface{}{ - "override": override, - }, - }) - if err != nil { - return nil, err - } - } - } - - result := child.DeepCopy() - result.Unstructured.SetUnstructuredContent(resultUnstructured) - // Merging two policies means the targetRef no longer makes any sense since - // since they can be conflicting. So we unset the targetRef. - result.TargetRef = common.GKNN{} - return result, nil -} - -func mergeUnstructured(parent, patch map[string]interface{}) (map[string]interface{}, error) { - currentJSON, err := json.Marshal(parent) - if err != nil { - return nil, err - } - - modifiedJSON, err := json.Marshal(patch) - if err != nil { - return nil, err - } - - resultJSON, err := jsonpatch.MergePatch(currentJSON, modifiedJSON) - if err != nil { - return nil, err - } - - result := make(map[string]interface{}) - if err := json.Unmarshal(resultJSON, &result); err != nil { - return nil, err - } - - return result, nil -} - -// orderPolicyByPrecedence will decide the precedence of two policies as per the -// [Gateway Specification]. The second policy returned will have a higher -// precedence. -// -// [Gateway Specification]: https://gateway-api.sigs.k8s.io/geps/gep-713/#conflict-resolution -func orderPolicyByPrecedence(a, b *Policy) (*Policy, *Policy) { - lowerPolicy := a.DeepCopy() // lowerPolicy will have lower precedence. - higherPolicy := b.DeepCopy() // higherPolicy will have higher precedence. - - if lowerPolicy.Unstructured.GetCreationTimestamp() == higherPolicy.Unstructured.GetCreationTimestamp() { - // Policies have the same creation time, so precedence is decided based - // on alphabetical ordering. - higherNN := fmt.Sprintf("%v/%v", higherPolicy.Unstructured.GetNamespace(), higherPolicy.Unstructured.GetName()) - lowerNN := fmt.Sprintf("%v/%v", lowerPolicy.Unstructured.GetNamespace(), lowerPolicy.Unstructured.GetName()) - if higherNN > lowerNN { - higherPolicy, lowerPolicy = lowerPolicy, higherPolicy - } - } else if higherPolicy.Unstructured.GetCreationTimestamp().Time.After(lowerPolicy.Unstructured.GetCreationTimestamp().Time) { - // Policies have difference creation time, so this will decide the precedence - higherPolicy, lowerPolicy = lowerPolicy, higherPolicy - } - - // At this point, higherPolicy will have precedence over lowerPolicy. - return lowerPolicy, higherPolicy -} diff --git a/pkg/cli/policymanager/merger_test.go b/pkg/cli/policymanager/merger_test.go deleted file mode 100644 index 11607f0b7..000000000 --- a/pkg/cli/policymanager/merger_test.go +++ /dev/null @@ -1,447 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 policymanager - -import ( - "reflect" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestMergePoliciesOfSimilarKind(t *testing.T) { - timeSmall := metav1.Time{Time: time.Now().Add(-1 * time.Hour)}.String() - timeLarge := metav1.Time{Time: time.Now()}.String() - policies := []*Policy{ - { - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-1", - "creationTimestamp": timeSmall, - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "a", - "key3": "b", - }, - "default": map[string]interface{}{ - "key2": "d", - "key4": "e", - "key5": "c", - }, - }, - }, - }, - Inheritable: true, - }, - { - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-2", - "creationTimestamp": timeLarge, - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "f", - }, - "default": map[string]interface{}{ - "key2": "i", - "key4": "j", - }, - }, - }, - }, - Inheritable: true, - }, - { - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-1", - }, - "spec": map[string]interface{}{ - "condition": "path=/def", - "seconds": float64(30), - "targetRef": map[string]interface{}{ - "kind": "Namespace", - "name": "default", - }, - }, - }, - }, - }, - { - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "condition": "path=/abc", - "seconds": float64(60), - "targetRef": map[string]interface{}{ - "kind": "Namespace", - "name": "default", - }, - }, - }, - }, - }, - } - - want := map[PolicyCrdID]*Policy{ - PolicyCrdID("HealthCheckPolicy.foo.com"): { - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-1", - "creationTimestamp": timeSmall, - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "f", - "key3": "b", - }, - "default": map[string]interface{}{ - "key2": "d", - "key4": "e", - "key5": "c", - }, - }, - }, - }, - Inheritable: true, - }, - PolicyCrdID("TimeoutPolicy.bar.com"): { - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-1", - }, - "spec": map[string]interface{}{ - "condition": "path=/def", - "seconds": float64(30), - "targetRef": map[string]interface{}{ - "kind": "Namespace", - "name": "default", - }, - }, - }, - }, - }, - } - - got, err := MergePoliciesOfSimilarKind(policies) - if err != nil { - t.Fatalf("MergePoliciesOfSimilarKind returned err=%v; want no error", err) - } - cmpopts := cmp.Exporter(func(t reflect.Type) bool { - return t == reflect.TypeOf(Policy{}) - }) - if diff := cmp.Diff(want, got, cmpopts); diff != "" { - t.Errorf("MergePoliciesOfSimilarKind returned unexpected diff (-want, +got):\n%v", diff) - } -} - -func TestMergePoliciesOfDifferentHierarchy(t *testing.T) { - testCases := []struct { - name string - parentPolicies []*Policy - childPolicies []*Policy - - wantMergedPolicies []*Policy - wantErr bool - }{ - { - name: "parent and child both have overrides and defaults", - parentPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-1", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "parentValue1", - "key2": "parentValue2", - }, - "default": map[string]interface{}{ - "key4": "parentValue4", - "key5": "parentValue5", - }, - }, - }, - }, - }}, - childPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "childValue1", - "key3": "childValue3", - }, - "default": map[string]interface{}{ - "key4": "childValue4", - "key6": "childValue6", - }, - }, - }, - }, - }}, - wantMergedPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "parentValue1", - "key2": "parentValue2", - "key3": "childValue3", - }, - "default": map[string]interface{}{ - "key4": "childValue4", - "key5": "parentValue5", - "key6": "childValue6", - }, - }, - }, - }, - }}, - }, - { - name: "parent has defaults, child has overrides", - parentPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-1", - }, - "spec": map[string]interface{}{ - "default": map[string]interface{}{ - "key4": "parentValue4", - "key5": "parentValue5", - }, - }, - }, - }, - }}, - childPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "childValue1", - "key3": "childValue3", - }, - }, - }, - }, - }}, - wantMergedPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "childValue1", - "key3": "childValue3", - }, - "default": map[string]interface{}{ - "key4": "parentValue4", - "key5": "parentValue5", - }, - }, - }, - }, - }}, - }, - { - name: "policies of different kind do not intersect with each other", - parentPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-1", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "a", - "key3": "b", - }, - "default": map[string]interface{}{ - "key2": "d", - "key4": "e", - "key5": "c", - }, - }, - }, - }, - }}, - childPolicies: []*Policy{{ - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "childValue1", - "key3": "childValue3", - }, - "default": map[string]interface{}{ - "key4": "childValue4", - "key6": "childValue6", - }, - }, - }, - }, - }}, - wantMergedPolicies: []*Policy{ - { - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-2", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "childValue1", - "key3": "childValue3", - }, - "default": map[string]interface{}{ - "key4": "childValue4", - "key6": "childValue6", - }, - }, - }, - }, - }, - { - Inheritable: true, - Unstructured: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-1", - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "a", - "key3": "b", - }, - "default": map[string]interface{}{ - "key2": "d", - "key4": "e", - "key5": "c", - }, - }, - }, - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - gotMergedPolicies, err := MergePoliciesOfDifferentHierarchy( - policySliceToMap(tc.parentPolicies), - policySliceToMap(tc.childPolicies), - ) - - if (err != nil) != tc.wantErr { - t.Fatalf("MergePoliciesOfDifferentHierarchy(...) returned err=%v; want err=%v", err, tc.wantErr) - } - - // Use a custom transformer to only compare specific fields of the Policy - // that we are interested in testing. - cmpopts := cmp.Transformer("PolicyTransformer", func(p Policy) map[string]interface{} { - return map[string]interface{}{ - "Object": p.Unstructured, - "Inherited": p.Inheritable, - } - }) - if diff := cmp.Diff(policySliceToMap(tc.wantMergedPolicies), gotMergedPolicies, cmpopts); diff != "" { - t.Errorf("MergePoliciesOfDifferentHierarchy returned unexpected diff (-want, +got):\n%v", diff) - } - }) - } -} - -func policySliceToMap(policies []*Policy) map[PolicyCrdID]*Policy { - res := make(map[PolicyCrdID]*Policy) - for _, policy := range policies { - res[policy.PolicyCrdID()] = policy - } - return res -} diff --git a/pkg/cli/policymanager/types.go b/pkg/cli/policymanager/types.go deleted file mode 100644 index 8b6a231d8..000000000 --- a/pkg/cli/policymanager/types.go +++ /dev/null @@ -1,5 +0,0 @@ -package policymanager - -const ( - K8sNamespaceKind string = "Namespace" -) diff --git a/pkg/cli/printer/backends.go b/pkg/cli/printer/backends.go deleted file mode 100644 index fae6735a5..000000000 --- a/pkg/cli/printer/backends.go +++ /dev/null @@ -1,193 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 printer - -import ( - "fmt" - "io" - "strings" - - "golang.org/x/exp/maps" - "k8s.io/apimachinery/pkg/util/duration" - - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/gatewayeffectivepolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/refgrantvalidator" - extensionutils "github.com/flomesh-io/fsm/pkg/cli/extension/utils" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -func (p *TablePrinter) printBackend(backendNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("Backend", w); err != nil { - return err - } - - if p.table == nil { - var columnNames []string - if p.OutputFormat == OutputFormatWide { - columnNames = []string{"NAMESPACE", "NAME", "TYPE", "AGE", "REFERRED BY ROUTES", "POLICIES"} - } else { - columnNames = []string{"NAMESPACE", "NAME", "TYPE", "AGE"} - } - p.table = &Table{ - ColumnNames: columnNames, - UseSeparator: false, - } - } - - backend := backendNode.Object - - namespace := backend.GetNamespace() - name := backend.GetName() - backendType := backend.GetKind() - - age := UnknownAge - creationTimestamp := backend.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - namespace, - name, - backendType, - age, - } - if p.OutputFormat == OutputFormatWide { - httpRouteNodes := maps.Values(topologygw.BackendNode(backendNode).HTTPRoutes()) - sortedHTTPRouteNodes := topology.SortedNodes(httpRouteNodes) - totalRoutes := len(sortedHTTPRouteNodes) - var referredByRoutes string - if totalRoutes == 0 { - referredByRoutes = "None" - } else { - var routes []string - for i, httpRouteNode := range sortedHTTPRouteNodes { - if i < 2 { - namespacedName := httpRouteNode.GKNN().NamespacedName().String() - routes = append(routes, namespacedName) - } else { - break - } - } - referredByRoutes = strings.Join(routes, ", ") - if totalRoutes > 2 { - referredByRoutes += fmt.Sprintf(" + %d more", totalRoutes-2) - } - } - policiesMap, err := directlyattachedpolicy.Access(backendNode) - if err != nil { - return err - } - policiesCount := fmt.Sprintf("%d", len(policiesMap)) - row = append(row, referredByRoutes, policiesCount) - } - p.table.Rows = append(p.table.Rows, row) - - return nil -} - -func (p *DescriptionPrinter) printBackend(backendNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - backend := backendNode.Object.DeepCopy() - backend.SetLabels(nil) - backend.SetAnnotations(nil) - - pairs := []*DescriberKV{ - {Key: "Name", Value: backendNode.Object.GetName()}, - {Key: "Namespace", Value: backendNode.Object.GetNamespace()}, - {Key: "Labels", Value: backendNode.Object.GetLabels()}, - {Key: "Annotations", Value: backendNode.Object.GetAnnotations()}, - {Key: "Backend", Value: backend}, - } - - // ReferencedByRoutes - routes := &Table{ - ColumnNames: []string{"Kind", "Name"}, - UseSeparator: true, - } - for _, httpRouteNode := range topologygw.BackendNode(backendNode).HTTPRoutes() { - row := []string{ - httpRouteNode.GKNN().Kind, // Kind - httpRouteNode.GKNN().NamespacedName().String(), // Name - } - routes.Rows = append(routes.Rows, row) - } - pairs = append(pairs, &DescriberKV{Key: "ReferencedByRoutes", Value: routes}) - - // DirectlyAttachedPolicies - policiesMap, err := directlyattachedpolicy.Access(backendNode) - if err != nil { - return err - } - policies := policymanager.ConvertPoliciesMapToSlice(policiesMap) - pairs = append(pairs, &DescriberKV{Key: "DirectlyAttachedPolicies", Value: convertPoliciesToRefsTable(policies, false)}) - - // InheritedPolicies - effectivePolicies, err := gatewayeffectivepolicy.Access(backendNode) - if err != nil { - return err - } - policies = policymanager.ConvertPoliciesMapToSlice(effectivePolicies.BackendInheritedPolicies) - pairs = append(pairs, &DescriberKV{Key: "InheritedPolicies", Value: convertPoliciesToRefsTable(policies, true)}) - - // EffectivePolicies - if err != nil { - return err - } - if len(effectivePolicies.BackendEffectivePolicies) != 0 { - pairs = append(pairs, &DescriberKV{Key: "EffectivePolicies", Value: effectivePolicies.BackendEffectivePolicies}) - } - - // ReferenceGrants - referenceGrantsMetadata, err := refgrantvalidator.Access(backendNode) - if err != nil { - return err - } - if referenceGrantsMetadata != nil && len(referenceGrantsMetadata.ReferenceGrants) != 0 { - var names []string - for _, refGrantNode := range referenceGrantsMetadata.ReferenceGrants { - names = append(names, refGrantNode.GetName()) - } - pairs = append(pairs, &DescriberKV{Key: "ReferenceGrants", Value: names}) - } - - // Analysis - analysisErrors, err := extensionutils.AggregateAnalysisErrors(backendNode) - if err != nil { - return err - } - if len(analysisErrors) != 0 { - pairs = append(pairs, &DescriberKV{Key: "Analysis", Value: convertErrorsToString(analysisErrors)}) - } - - // Events - events, err := p.EventFetcher.FetchEventsFor(backendNode.Object) - if err != nil { - return err - } - pairs = append(pairs, &DescriberKV{Key: "Events", Value: convertEventsSliceToTable(events, p.Clock)}) - - Describe(w, pairs) - return nil -} diff --git a/pkg/cli/printer/backends_test.go b/pkg/cli/printer/backends_test.go deleted file mode 100644 index 3a2805c06..000000000 --- a/pkg/cli/printer/backends_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestTablePrinter_printBackend(t *testing.T) { - options := PrinterOptions{} - p := &TablePrinter{PrinterOptions: options} - out := &bytes.Buffer{} - - for _, ns := range testData(t)[common.ServiceGK] { - p.printBackend(ns, out) - p.Flush(out) - } - - wantOut := ` -NAMESPACE NAME TYPE AGE -ns-1 svc-1 Service -` - - got := common.MultiLine(out.String()) - want := common.MultiLine(strings.TrimPrefix(wantOut, "\n")) - - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } -} diff --git a/pkg/cli/printer/gatewayclasses.go b/pkg/cli/printer/gatewayclasses.go deleted file mode 100644 index 3cabc553c..000000000 --- a/pkg/cli/printer/gatewayclasses.go +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 printer - -import ( - "fmt" - "io" - - "golang.org/x/exp/maps" - "k8s.io/apimachinery/pkg/util/duration" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -func (p *TablePrinter) printGatewayClass(gatewayClassNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("GatewayClass", w); err != nil { - return err - } - - if p.table == nil { - var columnNames []string - if p.OutputFormat == OutputFormatWide { - columnNames = []string{"NAME", "CONTROLLER", "ACCEPTED", "AGE", "GATEWAYS"} - } else { - columnNames = []string{"NAME", "CONTROLLER", "ACCEPTED", "AGE"} - } - p.table = &Table{ - ColumnNames: columnNames, - UseSeparator: false, - } - } - - gatewayClass := topology.MustAccessObject(gatewayClassNode, &gatewayv1.GatewayClass{}) - - accepted := "Unknown" - for _, condition := range gatewayClass.Status.Conditions { - if condition.Type == "Accepted" { - accepted = string(condition.Status) - } - } - - age := UnknownAge - creationTimestamp := gatewayClass.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - gatewayClass.GetName(), - string(gatewayClass.Spec.ControllerName), - accepted, - age, - } - if p.OutputFormat == OutputFormatWide { - gatewayCount := fmt.Sprintf("%d", len(topologygw.GatewayClassNode(gatewayClassNode).Gateways())) - row = append(row, gatewayCount) - } - p.table.Rows = append(p.table.Rows, row) - return nil -} - -func (p *DescriptionPrinter) printGatewayClass(gatewayClassNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - gatewayClass := topology.MustAccessObject(gatewayClassNode, &gatewayv1.GatewayClass{}) - - metadata := gatewayClass.ObjectMeta.DeepCopy() - metadata.Labels = nil - metadata.Annotations = nil - metadata.Name = "" - metadata.Namespace = "" - metadata.ManagedFields = nil - - pairs := []*DescriberKV{ - {Key: "Name", Value: gatewayClass.GetName()}, - {Key: "Labels", Value: gatewayClass.GetLabels()}, - {Key: "Annotations", Value: gatewayClass.GetAnnotations()}, - {Key: "APIVersion", Value: gatewayClass.APIVersion}, - {Key: "Kind", Value: gatewayClass.Kind}, - {Key: "Metadata", Value: metadata}, - {Key: "Spec", Value: &gatewayClass.Spec}, - {Key: "Status", Value: &gatewayClass.Status}, - } - - const ( - maxGateways = 10 - ) - - // AttachedGateways - attachedGateways := &Table{ - ColumnNames: []string{"Kind", "Name"}, - UseSeparator: true, - } - gatewaysCount := 0 - gatewayNodes := maps.Values(topologygw.GatewayClassNode(gatewayClassNode).Gateways()) - for _, gatewayNode := range topology.SortedNodes(gatewayNodes) { - gatewaysCount++ - if gatewaysCount > maxGateways { - attachedGateways.Rows = append(attachedGateways.Rows, []string{"(Truncated)"}) - break - } - row := []string{ - gatewayNode.GKNN().Kind, // Kind - gatewayNode.GKNN().NamespacedName().String(), // Name - } - attachedGateways.Rows = append(attachedGateways.Rows, row) - } - pairs = append(pairs, &DescriberKV{Key: "AttachedGateways", Value: attachedGateways}) - - // DirectlyAttachedPolicies - policiesMap, err := directlyattachedpolicy.Access(gatewayClassNode) - if err != nil { - return err - } - policies := policymanager.ConvertPoliciesMapToSlice(policiesMap) - pairs = append(pairs, &DescriberKV{Key: "DirectlyAttachedPolicies", Value: convertPoliciesToRefsTable(policies, false)}) - - // Events - events, err := p.EventFetcher.FetchEventsFor(gatewayClass) - if err != nil { - return err - } - pairs = append(pairs, &DescriberKV{Key: "Events", Value: convertEventsSliceToTable(events, p.Clock)}) - - Describe(w, pairs) - return nil -} diff --git a/pkg/cli/printer/gatewayclasses_test.go b/pkg/cli/printer/gatewayclasses_test.go deleted file mode 100644 index 0f1f8de3a..000000000 --- a/pkg/cli/printer/gatewayclasses_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestTablePrinter_printGatewayClass(t *testing.T) { - options := PrinterOptions{} - p := &TablePrinter{PrinterOptions: options} - out := &bytes.Buffer{} - - for _, ns := range testData(t)[common.GatewayClassGK] { - p.printGatewayClass(ns, out) - p.Flush(out) - } - - wantOut := ` -NAME CONTROLLER ACCEPTED AGE -gateway-class-1 foo.com/external-gateway-class True -` - - got := common.MultiLine(out.String()) - want := common.MultiLine(strings.TrimPrefix(wantOut, "\n")) - - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } -} diff --git a/pkg/cli/printer/gateways.go b/pkg/cli/printer/gateways.go deleted file mode 100644 index 98fff358b..000000000 --- a/pkg/cli/printer/gateways.go +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 printer - -import ( - "fmt" - "io" - "strings" - - "golang.org/x/exp/maps" - "k8s.io/apimachinery/pkg/util/duration" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/gatewayeffectivepolicy" - extensionutils "github.com/flomesh-io/fsm/pkg/cli/extension/utils" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" - topologygw "github.com/flomesh-io/fsm/pkg/cli/topology/gateway" -) - -func (p *TablePrinter) printGateway(gatewayNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("Gateway", w); err != nil { - return err - } - - if p.table == nil { - var columnNames []string - if p.OutputFormat == OutputFormatWide { - columnNames = []string{"NAMESPACE", "NAME", "CLASS", "ADDRESSES", "PORTS", "PROGRAMMED", "AGE", "POLICIES", "HTTPROUTES"} - } else { - columnNames = []string{"NAMESPACE", "NAME", "CLASS", "ADDRESSES", "PORTS", "PROGRAMMED", "AGE"} - } - p.table = &Table{ - ColumnNames: columnNames, - UseSeparator: false, - } - } - - gateway := topology.MustAccessObject(gatewayNode, &gatewayv1.Gateway{}) - - var addresses []string - for _, address := range gateway.Status.Addresses { - addresses = append(addresses, address.Value) - } - addressesOutput := strings.Join(addresses, ",") - if cnt := len(addresses); cnt > 2 { - addressesOutput = fmt.Sprintf("%v + %v more", strings.Join(addresses[:2], ","), cnt-2) - } - - var ports []string - for _, listener := range gateway.Spec.Listeners { - ports = append(ports, fmt.Sprintf("%d", int(listener.Port))) - } - portsOutput := strings.Join(ports, ",") - - programmedStatus := "Unknown" - for _, condition := range gateway.Status.Conditions { - if condition.Type == "Programmed" { - programmedStatus = string(condition.Status) - break - } - } - - age := UnknownAge - creationTimestamp := gateway.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - gateway.GetNamespace(), - gateway.GetName(), - string(gateway.Spec.GatewayClassName), - addressesOutput, - portsOutput, - programmedStatus, - age, - } - if p.OutputFormat == OutputFormatWide { - policiesMap, err := directlyattachedpolicy.Access(gatewayNode) - if err != nil { - return err - } - policiesCount := fmt.Sprintf("%d", len(policiesMap)) - - httpRoutesCount := fmt.Sprintf("%d", len(topologygw.GatewayNode(gatewayNode).HTTPRoutes())) - - row = append(row, policiesCount, httpRoutesCount) - } - p.table.Rows = append(p.table.Rows, row) - - return nil -} - -func (p *DescriptionPrinter) printGateway(gatewayNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - gateway := topology.MustAccessObject(gatewayNode, &gatewayv1.Gateway{}) - - metadata := gateway.ObjectMeta.DeepCopy() - metadata.Labels = nil - metadata.Annotations = nil - metadata.Name = "" - metadata.Namespace = "" - metadata.ManagedFields = nil - - pairs := []*DescriberKV{ - {Key: "Name", Value: gateway.GetName()}, - {Key: "Namespace", Value: gateway.GetNamespace()}, - {Key: "Labels", Value: gateway.Labels}, - {Key: "Annotations", Value: gateway.Annotations}, - {Key: "APIVersion", Value: gateway.APIVersion}, - {Key: "Kind", Value: gateway.Kind}, - {Key: "Metadata", Value: metadata}, - {Key: "Spec", Value: &gateway.Spec}, - {Key: "Status", Value: &gateway.Status}, - } - - const ( - maxHTTPRoutes = 10 - maxBackends = 10 - ) - - // AttachedRoutes - attachedRoutes := &Table{ - ColumnNames: []string{"Kind", "Name"}, - UseSeparator: true, - } - // Backends - backends := &Table{ - ColumnNames: []string{"Kind", "Name"}, - UseSeparator: true, - } - httpRouteCount, backendsCount := 0, 0 - httpRouteNodes := maps.Values(topologygw.GatewayNode(gatewayNode).HTTPRoutes()) - for _, httpRouteNode := range topology.SortedNodes(httpRouteNodes) { - httpRouteCount++ - if httpRouteCount > maxHTTPRoutes { - attachedRoutes.Rows = append(attachedRoutes.Rows, []string{"(Truncated)"}) - break - } - row := []string{ - httpRouteNode.GKNN().Kind, // Kind - httpRouteNode.GKNN().NamespacedName().String(), // Name - } - attachedRoutes.Rows = append(attachedRoutes.Rows, row) - - backendNodes := maps.Values(topologygw.HTTPRouteNode(httpRouteNode).Backends()) - for _, backendNode := range topology.SortedNodes(backendNodes) { - backendsCount++ - if backendsCount > maxBackends { - backends.Rows = append(backends.Rows, []string{"(Truncated)"}) - break - } - row := []string{ - backendNode.GKNN().Kind, // Kind - backendNode.GKNN().NamespacedName().String(), // Name - } - backends.Rows = append(backends.Rows, row) - } - } - pairs = append(pairs, &DescriberKV{Key: "AttachedRoutes", Value: attachedRoutes}) - pairs = append(pairs, &DescriberKV{Key: "Backends", Value: backends}) - - // DirectlyAttachedPolicies - policiesMap, err := directlyattachedpolicy.Access(gatewayNode) - if err != nil { - return err - } - policies := policymanager.ConvertPoliciesMapToSlice(policiesMap) - pairs = append(pairs, &DescriberKV{Key: "DirectlyAttachedPolicies", Value: convertPoliciesToRefsTable(policies, false)}) - - // InheritedPolicies - effectivePolicies, err := gatewayeffectivepolicy.Access(gatewayNode) - if err != nil { - return err - } - policies = policymanager.ConvertPoliciesMapToSlice(effectivePolicies.GatewayInheritedPolicies) - pairs = append(pairs, &DescriberKV{Key: "InheritedPolicies", Value: convertPoliciesToRefsTable(policies, true)}) - - // EffectivePolicies`` - if len(effectivePolicies.GatewayEffectivePolicies) != 0 { - pairs = append(pairs, &DescriberKV{Key: "EffectivePolicies", Value: effectivePolicies.GatewayEffectivePolicies}) - } - - // // Analysis - analysisErrors, err := extensionutils.AggregateAnalysisErrors(gatewayNode) - if err != nil { - return err - } - if len(analysisErrors) != 0 { - pairs = append(pairs, &DescriberKV{Key: "Analysis", Value: convertErrorsToString(analysisErrors)}) - } - - // Events - events, err := p.EventFetcher.FetchEventsFor(gateway) - if err != nil { - return err - } - pairs = append(pairs, &DescriberKV{Key: "Events", Value: convertEventsSliceToTable(events, p.Clock)}) - - Describe(w, pairs) - return nil -} diff --git a/pkg/cli/printer/gateways_test.go b/pkg/cli/printer/gateways_test.go deleted file mode 100644 index 3bfbeb53f..000000000 --- a/pkg/cli/printer/gateways_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestTablePrinter_printGateway(t *testing.T) { - options := PrinterOptions{} - p := &TablePrinter{PrinterOptions: options} - out := &bytes.Buffer{} - - for _, ns := range testData(t)[common.GatewayGK] { - p.printGateway(ns, out) - p.Flush(out) - } - - wantOut := ` -NAMESPACE NAME CLASS ADDRESSES PORTS PROGRAMMED AGE -ns-1 gateway-1 gateway-class-1 10.0.0.1,10.0.0.2 + 1 more 80 True -` - - got := common.MultiLine(out.String()) - want := common.MultiLine(strings.TrimPrefix(wantOut, "\n")) - - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } -} diff --git a/pkg/cli/printer/httproutes.go b/pkg/cli/printer/httproutes.go deleted file mode 100644 index 83f8cba33..000000000 --- a/pkg/cli/printer/httproutes.go +++ /dev/null @@ -1,165 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 printer - -import ( - "fmt" - "io" - "strings" - - "k8s.io/apimachinery/pkg/util/duration" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/extension/gatewayeffectivepolicy" - extensionutils "github.com/flomesh-io/fsm/pkg/cli/extension/utils" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -func (p *TablePrinter) printHTTPRoute(httpRouteNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("HTTPRoute", w); err != nil { - return err - } - - if p.table == nil { - var columnNames []string - if p.OutputFormat == OutputFormatWide { - columnNames = []string{"NAMESPACE", "NAME", "HOSTNAMES", "PARENT REFS", "AGE", "POLICIES"} - } else { - columnNames = []string{"NAMESPACE", "NAME", "HOSTNAMES", "PARENT REFS", "AGE"} - } - - p.table = &Table{ - ColumnNames: columnNames, - UseSeparator: false, - } - } - - httpRoute := topology.MustAccessObject(httpRouteNode, &gatewayv1.HTTPRoute{}) - - var hostNames []string - for _, hostName := range httpRoute.Spec.Hostnames { - hostNames = append(hostNames, string(hostName)) - } - hostNamesOutput := "None" - if hostNamesCount := len(hostNames); hostNamesCount > 0 { - if hostNamesCount > 2 { - hostNamesOutput = fmt.Sprintf("%v + %v more", strings.Join(hostNames[:2], ","), hostNamesCount-2) - } else { - hostNamesOutput = strings.Join(hostNames, ",") - } - } - - parentRefsCount := fmt.Sprintf("%d", len(httpRoute.Spec.ParentRefs)) - - age := UnknownAge - creationTimestamp := httpRoute.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - httpRoute.GetNamespace(), - httpRoute.GetName(), - hostNamesOutput, - parentRefsCount, - age, - } - if p.OutputFormat == OutputFormatWide { - policiesMap, err := directlyattachedpolicy.Access(httpRouteNode) - if err != nil { - return err - } - policiesCount := fmt.Sprintf("%d", len(policiesMap)) - row = append(row, policiesCount) - } - p.table.Rows = append(p.table.Rows, row) - return nil -} - -func (p *DescriptionPrinter) printHTTPRoute(httpRouteNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - httpRoute := topology.MustAccessObject(httpRouteNode, &gatewayv1.HTTPRoute{}) - - metadata := httpRoute.ObjectMeta.DeepCopy() - metadata.Labels = nil - metadata.Annotations = nil - metadata.Name = "" - metadata.Namespace = "" - metadata.ManagedFields = nil - - pairs := []*DescriberKV{ - {"Name", httpRoute.GetName()}, - {"Namespace", httpRoute.Namespace}, - {"Label", httpRoute.Labels}, - {"Annotations", httpRoute.Annotations}, - {"APIVersion", httpRoute.APIVersion}, - {"Kind", httpRoute.Kind}, - {"Metadata", metadata}, - {"Spec", httpRoute.Spec}, - {"Status", httpRoute.Status}, - } - - // DirectlyAttachedPolicies - policiesMap, err := directlyattachedpolicy.Access(httpRouteNode) - if err != nil { - return err - } - policies := policymanager.ConvertPoliciesMapToSlice(policiesMap) - pairs = append(pairs, &DescriberKV{Key: "DirectlyAttachedPolicies", Value: convertPoliciesToRefsTable(policies, false)}) - - // InheritedPolicies - effectivePolicies, err := gatewayeffectivepolicy.Access(httpRouteNode) - if err != nil { - return err - } - policies = policymanager.ConvertPoliciesMapToSlice(effectivePolicies.HTTPRouteInheritedPolicies) - pairs = append(pairs, &DescriberKV{Key: "InheritedPolicies", Value: convertPoliciesToRefsTable(policies, true)}) - - // EffectivePolicies - if err != nil { - return err - } - if len(effectivePolicies.HTTPRouteEffectivePolicies) != 0 { - pairs = append(pairs, &DescriberKV{Key: "EffectivePolicies", Value: effectivePolicies.HTTPRouteEffectivePolicies}) - } - - // Analysis - analysisErrors, err := extensionutils.AggregateAnalysisErrors(httpRouteNode) - if err != nil { - return err - } - if len(analysisErrors) != 0 { - pairs = append(pairs, &DescriberKV{Key: "Analysis", Value: convertErrorsToString(analysisErrors)}) - } - - // Events - events, err := p.EventFetcher.FetchEventsFor(httpRoute) - if err != nil { - return err - } - pairs = append(pairs, &DescriberKV{Key: "Events", Value: convertEventsSliceToTable(events, p.Clock)}) - - Describe(w, pairs) - return nil -} diff --git a/pkg/cli/printer/httproutes_test.go b/pkg/cli/printer/httproutes_test.go deleted file mode 100644 index eb525b430..000000000 --- a/pkg/cli/printer/httproutes_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestTablePrinter_printHTTPRoute(t *testing.T) { - options := PrinterOptions{} - p := &TablePrinter{PrinterOptions: options} - out := &bytes.Buffer{} - - for _, ns := range testData(t)[common.HTTPRouteGK] { - p.printHTTPRoute(ns, out) - p.Flush(out) - } - - wantOut := ` -NAMESPACE NAME HOSTNAMES PARENT REFS AGE -ns-1 http-route-1 foo.com,bar.com + 5 more 1 -` - - got := common.MultiLine(out.String()) - want := common.MultiLine(strings.TrimPrefix(wantOut, "\n")) - - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } -} diff --git a/pkg/cli/printer/main_test.go b/pkg/cli/printer/main_test.go deleted file mode 100644 index ae33da625..000000000 --- a/pkg/cli/printer/main_test.go +++ /dev/null @@ -1,238 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "flag" - "os" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/utils/ptr" - - "k8s.io/klog/v2" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -func TestMain(m *testing.M) { - fs := flag.NewFlagSet("mock-flags", flag.PanicOnError) - klog.InitFlags(fs) - fs.Set("v", "3") // Set klog verbosity. - - os.Exit(m.Run()) -} - -func testData(t *testing.T) map[schema.GroupKind][]*topology.Node { - ns1 := mustNewNode(t, &corev1.Namespace{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Namespace", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "ns-1", - }, - Status: corev1.NamespaceStatus{ - Phase: corev1.NamespaceActive, - }, - }) - - gatewayClass1 := mustNewNode(t, &gatewayv1.GatewayClass{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayv1.GroupVersion.String(), - Kind: "GatewayClass", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-class-1", - }, - Spec: gatewayv1.GatewayClassSpec{ - ControllerName: "foo.com/external-gateway-class", - }, - Status: gatewayv1.GatewayClassStatus{ - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: "True", - }, - }, - }, - }) - - gateway1 := mustNewNode(t, - &gatewayv1.Gateway{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayv1.GroupVersion.String(), - Kind: "Gateway", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-1", - Namespace: "ns-1", - }, - Spec: gatewayv1.GatewaySpec{ - GatewayClassName: "gateway-class-1", - Listeners: []gatewayv1.Listener{ - { - Name: gatewayv1.SectionName("http-80"), - Protocol: gatewayv1.HTTPProtocolType, - Port: gatewayv1.PortNumber(80), - }, - }, - }, - Status: gatewayv1.GatewayStatus{ - Addresses: []gatewayv1.GatewayStatusAddress{ - { - Value: "10.0.0.1", - }, - { - Value: "10.0.0.2", - }, - { - Value: "10.0.0.3", - }, - }, - Conditions: []metav1.Condition{ - { - Type: "Programmed", - Status: "True", - }, - }, - }, - }, - ) - - httpRoute1 := mustNewNode(t, - &gatewayv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayv1.GroupVersion.String(), - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "http-route-1", - Namespace: "ns-1", - }, - Spec: gatewayv1.HTTPRouteSpec{ - Hostnames: []gatewayv1.Hostname{"foo.com", "bar.com", "example.com", "example2.com", "example3.com", "example4.com", "example5.com"}, - CommonRouteSpec: gatewayv1.CommonRouteSpec{ - ParentRefs: []gatewayv1.ParentReference{ - { - Kind: ptr.To(gatewayv1.Kind("Gateway")), - Group: ptr.To(gatewayv1.Group("gateway.networking.k8s.io")), - Namespace: ptr.To(gatewayv1.Namespace("ns-1")), - Name: "gateway-1", - }, - }, - }, - Rules: []gatewayv1.HTTPRouteRule{ - { - BackendRefs: []gatewayv1.HTTPBackendRef{ - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8080)), - Name: gatewayv1.ObjectName("service-1"), - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns-1")), - }, - }, - }, - }, - }, - }, - }, - }, - ) - - service1 := mustNewNode(t, &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "svc-1", - Namespace: "ns-1", - }, - }) - - graph := &topology.Graph{} - graph.AddNode(ns1) - graph.AddNode(gatewayClass1) - graph.AddNode(gateway1) - graph.AddNode(httpRoute1) - graph.AddNode(service1) - - result := map[schema.GroupKind][]*topology.Node{} - for gk, nodes := range graph.Nodes { - for _, node := range nodes { - result[gk] = append(result[gk], node) - } - } - return result -} - -func testPoliciesData(t *testing.T) map[schema.GroupKind][]*topology.Node { - return map[schema.GroupKind][]*topology.Node{ - common.PolicyGK: { - mustNewPolicyNode(t, &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "policy-1", - "namespace": "ns-1", - }, - "spec": map[string]interface{}{ - "condition": "path=/abc", - "seconds": int64(30), - "targetRef": map[string]interface{}{ - "kind": "Namespace", - "name": "default", - }, - }, - }, - }, false), - }, - } -} - -func mustNewNode(t *testing.T, obj runtime.Object) *topology.Node { - u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) - if err != nil { - t.Fatal(err) - } - return &topology.Node{Object: &unstructured.Unstructured{Object: u}} -} - -func mustNewPolicyNode(t *testing.T, u *unstructured.Unstructured, inherited bool) *topology.Node { - policy, err := policymanager.ConstructPolicy(u, inherited) - if err != nil { - t.Fatal(err) - } - - return &topology.Node{ - Object: policy.Unstructured, - Metadata: map[string]any{ - common.PolicyGK.String(): policy, - }, - } -} diff --git a/pkg/cli/printer/namespace.go b/pkg/cli/printer/namespace.go deleted file mode 100644 index 3657f56b2..000000000 --- a/pkg/cli/printer/namespace.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "fmt" - "io" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/duration" - - "github.com/flomesh-io/fsm/pkg/cli/extension/directlyattachedpolicy" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -func (p *TablePrinter) printNamespace(namespaceNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("Namespace", w); err != nil { - return err - } - - if p.table == nil { - var columnNames []string - if p.OutputFormat == OutputFormatWide { - columnNames = []string{"NAME", "STATUS", "AGE", "POLICIES"} - } else { - columnNames = []string{"NAME", "STATUS", "AGE"} - } - p.table = &Table{ - ColumnNames: columnNames, - UseSeparator: false, - } - } - - ns := topology.MustAccessObject(namespaceNode, &corev1.Namespace{}) - - age := UnknownAge - creationTimestamp := ns.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - ns.Name, - string(ns.Status.Phase), - age, - } - if p.OutputFormat == OutputFormatWide { - policiesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - policiesCount := fmt.Sprintf("%d", len(policiesMap)) - row = append(row, policiesCount) - } - p.table.Rows = append(p.table.Rows, row) - return nil -} - -func (p *DescriptionPrinter) printNamespace(namespaceNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - namespace := topology.MustAccessObject(namespaceNode, &corev1.Namespace{}) - - metadata := namespace.ObjectMeta.DeepCopy() - metadata.Labels = nil - metadata.Annotations = nil - metadata.Name = "" - metadata.Namespace = "" - metadata.ManagedFields = nil - - pairs := []*DescriberKV{ - {Key: "Name", Value: namespace.GetName()}, - {Key: "Labels", Value: namespace.Labels}, - {Key: "Annotations", Value: namespace.Annotations}, - {Key: "Status", Value: &namespace.Status}, - } - - // DirectlyAttachedPolicies - policiesMap, err := directlyattachedpolicy.Access(namespaceNode) - if err != nil { - return err - } - policies := policymanager.ConvertPoliciesMapToSlice(policiesMap) - pairs = append(pairs, &DescriberKV{Key: "DirectlyAttachedPolicies", Value: convertPoliciesToRefsTable(policies, false)}) - - // Events - events, err := p.EventFetcher.FetchEventsFor(namespace) - if err != nil { - return err - } - pairs = append(pairs, &DescriberKV{Key: "Events", Value: convertEventsSliceToTable(events, p.Clock)}) - - Describe(w, pairs) - return nil -} diff --git a/pkg/cli/printer/namespace_test.go b/pkg/cli/printer/namespace_test.go deleted file mode 100644 index 4eaeff09c..000000000 --- a/pkg/cli/printer/namespace_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestTablePrinter_printNamespace(t *testing.T) { - options := PrinterOptions{} - p := &TablePrinter{PrinterOptions: options} - out := &bytes.Buffer{} - - for _, ns := range testData(t)[common.NamespaceGK] { - p.printNamespace(ns, out) - p.Flush(out) - } - - wantOut := ` -NAME STATUS AGE -ns-1 Active -` - - got := common.MultiLine(out.String()) - want := common.MultiLine(strings.TrimPrefix(wantOut, "\n")) - - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } -} diff --git a/pkg/cli/printer/policies.go b/pkg/cli/printer/policies.go deleted file mode 100644 index 4e0d8e4e4..000000000 --- a/pkg/cli/printer/policies.go +++ /dev/null @@ -1,186 +0,0 @@ -/* -Copyright 2023 The Kubernetes 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 printer - -import ( - "fmt" - "io" - - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/duration" - "k8s.io/klog/v2" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -func (p *TablePrinter) printPolicy(policyNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("Policy", w); err != nil { - return err - } - - if p.table == nil { - p.table = &Table{ - ColumnNames: []string{"NAME", "KIND", "TARGET NAME", "TARGET KIND", "POLICY TYPE", "AGE"}, - UseSeparator: false, - } - } - - var err error - var policy *policymanager.Policy - if policy, err = accessPolicyOrCRD[policymanager.Policy](policyNode, common.PolicyGK); err != nil { - return err - } - - policyType := "Direct" - if policy.IsInheritable() { - policyType = "Inherited" - } - - kind := fmt.Sprintf("%v.%v", policy.Unstructured.GroupVersionKind().Kind, policy.Unstructured.GroupVersionKind().Group) - - age := UnknownAge - creationTimestamp := policy.Unstructured.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - policy.Unstructured.GetName(), - kind, - policy.TargetRef.Name, - policy.TargetRef.Kind, - policyType, - age, - } - p.table.Rows = append(p.table.Rows, row) - - return nil -} - -func (p *TablePrinter) printPolicyCRD(policyCRDNode *topology.Node, w io.Writer) error { - if err := p.checkTypeChange("Policy", w); err != nil { - return err - } - - if p.table == nil { - p.table = &Table{ - ColumnNames: []string{"NAME", "POLICY TYPE", "SCOPE", "AGE"}, - UseSeparator: false, - } - } - - var err error - var policyCRD *policymanager.PolicyCRD - if policyCRD, err = accessPolicyOrCRD[policymanager.PolicyCRD](policyCRDNode, common.PolicyCRDGK); err != nil { - return err - } - - policyType := "Direct" - if policyCRD.IsInheritable() { - policyType = "Inherited" - } - - age := UnknownAge - creationTimestamp := policyCRD.CRD.GetCreationTimestamp() - if !creationTimestamp.IsZero() { - age = duration.HumanDuration(p.Clock.Since(creationTimestamp.Time)) - } - - row := []string{ - policyCRD.CRD.Name, - policyType, - string(policyCRD.CRD.Spec.Scope), - age, - } - p.table.Rows = append(p.table.Rows, row) - return nil -} - -func (p *DescriptionPrinter) printPolicy(policyNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - var err error - var policy *policymanager.Policy - if policy, err = accessPolicyOrCRD[policymanager.Policy](policyNode, common.PolicyGK); err != nil { - return err - } - - pairs := []*DescriberKV{ - {Key: "Name", Value: policy.Unstructured.GetName()}, - {Key: "Namespace", Value: policy.Unstructured.GetNamespace()}, - {Key: "Group", Value: policy.Unstructured.GroupVersionKind().Group}, - {Key: "Kind", Value: policy.Unstructured.GroupVersionKind().Kind}, - {Key: "Inherited", Value: fmt.Sprintf("%v", policy.IsInheritable())}, - {Key: "Spec", Value: policy.Spec()}, - } - - Describe(w, pairs) - return nil -} - -func (p *DescriptionPrinter) printPolicyCRD(policyCRDNode *topology.Node, w io.Writer) error { - if p.printSeparator { - fmt.Fprintf(w, "\n\n") - } - p.printSeparator = true - - var err error - var policyCRD *policymanager.PolicyCRD - if policyCRD, err = accessPolicyOrCRD[policymanager.PolicyCRD](policyCRDNode, common.PolicyCRDGK); err != nil { - return err - } - - crd := policyCRD.CRD - - metadata := crd.ObjectMeta.DeepCopy() - metadata.Labels = nil - metadata.Annotations = nil - metadata.Name = "" - metadata.Namespace = "" - - pairs := []*DescriberKV{ - {Key: "Name", Value: crd.Name}, - {Key: "Namespace", Value: crd.Namespace}, - {Key: "APIVersion", Value: crd.APIVersion}, - {Key: "Kind", Value: crd.Kind}, - {Key: "Labels", Value: crd.Labels}, - {Key: "Annotations", Value: crd.Annotations}, - {Key: "Metadata", Value: metadata}, - {Key: "Spec", Value: crd.Spec}, - {Key: "Status", Value: crd.Status}, - } - Describe(w, pairs) - return nil -} - -func accessPolicyOrCRD[T any](node *topology.Node, gk schema.GroupKind) (*T, error) { - rawData, ok := node.Metadata[gk.String()] - if !ok || rawData == nil { - klog.V(3).InfoS(fmt.Sprintf("no %v found in node", gk.String()), "node", node.GKNN()) - return nil, nil - } - data, ok := rawData.(*T) - if !ok { - return nil, fmt.Errorf("unable to perform type assertion to %v in node %v", gk.String(), node.GKNN()) - } - return data, nil -} diff --git a/pkg/cli/printer/policies_test.go b/pkg/cli/printer/policies_test.go deleted file mode 100644 index 128744081..000000000 --- a/pkg/cli/printer/policies_test.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestTablePrinter_printPolicy(t *testing.T) { - options := PrinterOptions{} - p := &TablePrinter{PrinterOptions: options} - out := &bytes.Buffer{} - - for _, ns := range testPoliciesData(t)[common.PolicyGK] { - p.printBackend(ns, out) - p.Flush(out) - } - - wantOut := ` -NAMESPACE NAME TYPE AGE -ns-1 policy-1 TimeoutPolicy -` - - got := common.MultiLine(out.String()) - want := common.MultiLine(strings.TrimPrefix(wantOut, "\n")) - - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } -} diff --git a/pkg/cli/printer/printer.go b/pkg/cli/printer/printer.go deleted file mode 100644 index 463b530e4..000000000 --- a/pkg/cli/printer/printer.go +++ /dev/null @@ -1,206 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "fmt" - "io" - - "k8s.io/cli-runtime/pkg/printers" - "k8s.io/utils/clock" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -type OutputFormat string - -const ( - OutputFormatWide OutputFormat = "wide" - OutputFormatJSON OutputFormat = "json" - OutputFormatYAML OutputFormat = "yaml" - OutputFormatGraph OutputFormat = "graph" - OutputFormatTable OutputFormat = "" -) - -func ValidateAndReturnOutputFormat(format string) (OutputFormat, error) { - switch format { - case "wide": - return OutputFormatWide, nil - case "json": - return OutputFormatJSON, nil - case "yaml": - return OutputFormatYAML, nil - case "graph": - return OutputFormatGraph, nil - case "": - return OutputFormatTable, nil - default: - var zero OutputFormat - return zero, fmt.Errorf("unknown format %s provided", format) - } -} - -func AllowedOutputFormatsForHelp() []string { - return []string{string(OutputFormatWide), string(OutputFormatJSON), string(OutputFormatYAML), string(OutputFormatGraph)} -} - -type PrinterOptions struct { //nolint:revive - OutputFormat OutputFormat - Description bool - Clock clock.Clock - EventFetcher eventFetcher -} - -type Printer interface { - PrintNode(node *topology.Node, w io.Writer) error - Flush(io.Writer) error -} - -func NewPrinter(options PrinterOptions) Printer { - switch { - case options.OutputFormat == OutputFormatJSON: - return NewJSONPrinter() - case options.OutputFormat == OutputFormatYAML: - return NewYAMLPrinter() - case options.Description: - return &DescriptionPrinter{PrinterOptions: options} - default: - return &TablePrinter{PrinterOptions: options} - } -} - -type TablePrinter struct { - PrinterOptions - - table *Table - unknownTablePrinter printers.ResourcePrinter - curType string -} - -func (p *TablePrinter) PrintNode(node *topology.Node, w io.Writer) error { - return parseAndPrint(node, w, p) -} - -func (p *TablePrinter) Flush(w io.Writer) error { - return p.checkTypeChange("", w) -} - -func (p *TablePrinter) printUnknown(node *topology.Node, w io.Writer) error { - if p.unknownTablePrinter == nil { - p.unknownTablePrinter = printers.NewTablePrinter(printers.PrintOptions{}) - } - return p.unknownTablePrinter.PrintObj(node.Object, w) -} - -func (p *TablePrinter) checkTypeChange(newType string, w io.Writer) error { - var err error - if p.curType != "" && p.curType != newType && p.table != nil { - err = p.table.Write(w, 0) - p.table = nil - } - p.curType = newType - return err -} - -type DescriptionPrinter struct { - PrinterOptions - - printSeparator bool -} - -func (p *DescriptionPrinter) PrintNode(node *topology.Node, w io.Writer) error { - return parseAndPrint(node, w, p) -} - -type typedPrinter interface { - printBackend(*topology.Node, io.Writer) error - printGatewayClass(*topology.Node, io.Writer) error - printGateway(*topology.Node, io.Writer) error - printHTTPRoute(*topology.Node, io.Writer) error - printNamespace(*topology.Node, io.Writer) error - printPolicy(*topology.Node, io.Writer) error - printPolicyCRD(*topology.Node, io.Writer) error - printUnknown(*topology.Node, io.Writer) error -} - -func parseAndPrint(node *topology.Node, w io.Writer, p typedPrinter) error { - if node.Metadata != nil && node.Metadata[common.PolicyGK.String()] != nil { - return p.printPolicy(node, w) - } - if node.Metadata != nil && node.Metadata[common.PolicyCRDGK.String()] != nil { - return p.printPolicyCRD(node, w) - } - - switch node.GKNN().GroupKind() { - case common.GatewayGK: - return p.printGateway(node, w) - case common.GatewayClassGK: - return p.printGatewayClass(node, w) - case common.HTTPRouteGK: - return p.printHTTPRoute(node, w) - case common.NamespaceGK: - return p.printNamespace(node, w) - case common.ServiceGK: - return p.printBackend(node, w) - default: - return p.printUnknown(node, w) - } -} - -func (p *DescriptionPrinter) Flush(io.Writer) error { return nil } - -func (p *DescriptionPrinter) printUnknown(node *topology.Node, w io.Writer) error { - printer := &printers.YAMLPrinter{} - return printer.PrintObj(node.Object, w) -} - -type JSONPrinter struct { - Delegate *printers.OmitManagedFieldsPrinter -} - -func NewJSONPrinter() *JSONPrinter { - return &JSONPrinter{ - Delegate: &printers.OmitManagedFieldsPrinter{ - Delegate: &printers.JSONPrinter{}, - }, - } -} - -func (p *JSONPrinter) PrintNode(node *topology.Node, w io.Writer) error { - return p.Delegate.PrintObj(node.Object, w) -} - -func (p *JSONPrinter) Flush(io.Writer) error { return nil } - -type YAMLPrinter struct { - Delegate *printers.OmitManagedFieldsPrinter -} - -func NewYAMLPrinter() *YAMLPrinter { - return &YAMLPrinter{ - Delegate: &printers.OmitManagedFieldsPrinter{ - Delegate: &printers.YAMLPrinter{}, - }, - } -} - -func (p *YAMLPrinter) PrintNode(node *topology.Node, w io.Writer) error { - return p.Delegate.PrintObj(node.Object, w) -} - -func (p *YAMLPrinter) Flush(io.Writer) error { return nil } diff --git a/pkg/cli/printer/types.go b/pkg/cli/printer/types.go deleted file mode 100644 index c2eda2a79..000000000 --- a/pkg/cli/printer/types.go +++ /dev/null @@ -1,5 +0,0 @@ -package printer - -const ( - UnknownAge string = "" -) diff --git a/pkg/cli/printer/utils.go b/pkg/cli/printer/utils.go deleted file mode 100644 index fc905d522..000000000 --- a/pkg/cli/printer/utils.go +++ /dev/null @@ -1,258 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "fmt" - "io" - "os" - "strings" - "text/tabwriter" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/duration" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/klog/v2" - "k8s.io/utils/clock" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/policymanager" -) - -// DescriberKV stores key-value pairs that are used with Describing a resource. -type DescriberKV struct { - Key string - Value any -} - -const ( - // Default indentation for Tables that are printed in the Describe view. - defaultDescribeTableIndentSpaces = 2 -) - -// Describe writes the key-value paris to the writer. It handles things like -// properly writing special data types like Tables. -func Describe(w io.Writer, pairs []*DescriberKV) { - for _, pair := range pairs { - // If the Value is of type Table, it needs special handling. - if table, ok := pair.Value.(*Table); ok { - if len(table.Rows) == 0 { - fmt.Fprintf(w, "%v: \n", pair.Key) - } else { - fmt.Fprintf(w, "%v:\n", pair.Key) - _ = table.Write(w, defaultDescribeTableIndentSpaces) - } - continue - } - - // If Value is NOT a Table, it can be handled through the yaml Marshaller. - data := map[string]any{pair.Key: pair.Value} - b, err := yaml.Marshal(data) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to marshal to yaml: %v\n", err) - os.Exit(1) - } - fmt.Fprint(w, string(b)) - } -} - -type Table struct { - ColumnNames []string - Rows [][]string - // UseSeparator indicates whether the header row and data rows will be - // separated through a separator. - UseSeparator bool -} - -// Write will write a formatted table to the writer. indent controls the -// number of spaces at the beginning of each row. -func (t *Table) Write(w io.Writer, indent int) error { - tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) - - // Print column names. - if len(t.ColumnNames) > 0 { - row := t.indentRow(t.ColumnNames, indent) - _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) - if err != nil { - return err - } - } - - // Optionally print a separator between header row and data rows. - if t.UseSeparator { - row := make([]string, len(t.ColumnNames)) - for i, value := range t.ColumnNames { - row[i] = strings.Repeat("-", len(value)) - } - row = t.indentRow(row, indent) - _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) - if err != nil { - return err - } - } - - // Print data rows. - for _, row := range t.Rows { - row = t.indentRow(row, indent) - _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")) - if err != nil { - return err - } - } - return tw.Flush() -} - -// indentRow will add 'indent' spaces to the beginning of the row. -func (t *Table) indentRow(row []string, indent int) []string { - if len(row) == 0 { - return row - } - - newRow := append([]string{}, row...) - newRow[0] = fmt.Sprintf("%s%s", strings.Repeat(" ", indent), newRow[0]) - return newRow -} - -func convertEventsSliceToTable(events []*corev1.Event, clock clock.Clock) *Table { - table := &Table{ - ColumnNames: []string{"Type", "Reason", "Age", "From", "Message"}, - UseSeparator: true, - } - for _, event := range events { - age := UnknownAge - if !event.FirstTimestamp.IsZero() { - age = duration.HumanDuration(clock.Since(event.FirstTimestamp.Time)) - } - - row := []string{ - event.Type, // Type - event.Reason, // Reason - age, // Age - event.Source.Component, // From - event.Message, // Message - } - table.Rows = append(table.Rows, row) - } - return table -} - -func convertPoliciesToRefsTable(policies []*policymanager.Policy, includeTarget bool) *Table { - table := &Table{ - ColumnNames: []string{"Type", "Name"}, - UseSeparator: true, - } - if includeTarget { - table.ColumnNames = append(table.ColumnNames, "Target Kind", "Target Name") - } - - for _, policy := range policies { - policyType := fmt.Sprintf("%v.%v", policy.Unstructured.GroupVersionKind().Kind, policy.Unstructured.GroupVersionKind().Group) - - policyName := policy.Unstructured.GetName() - if ns := policy.Unstructured.GetNamespace(); ns != "" { - policyName = fmt.Sprintf("%v/%v", ns, policyName) - } - - targetKind := policy.TargetRef.Kind - - targetName := policy.TargetRef.Name - if ns := policy.TargetRef.Namespace; ns != "" { - targetName = fmt.Sprintf("%v/%v", ns, targetName) - } - - row := []string{ - policyType, // Type - policyName, // Name - } - - if includeTarget { - row = append(row, - targetKind, // Target Kind - targetName, // Target Name - ) - } - - table.Rows = append(table.Rows, row) - } - return table -} - -func convertErrorsToString(errors []error) []string { - var result []string - for _, err := range errors { - result = append(result, err.Error()) - } - return result -} - -type eventFetcher interface { - FetchEventsFor(client.Object) ([]*corev1.Event, error) -} - -var _ eventFetcher = (*DefaultEventFetcher)(nil) - -type DefaultEventFetcher struct { - factory common.Factory -} - -func NewDefaultEventFetcher(factory common.Factory) *DefaultEventFetcher { - return &DefaultEventFetcher{factory: factory} -} - -func (d DefaultEventFetcher) FetchEventsFor(object client.Object) ([]*corev1.Event, error) { - eventGK := schema.GroupKind{Group: corev1.GroupName, Kind: "Event"} - - infos, err := d.factory.NewBuilder(). - WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). - Flatten(). - AllNamespaces(true). - ResourceTypeOrNameArgs(true, []string{fmt.Sprintf("%v.%v", eventGK.Kind, eventGK.Group)}...). - FieldSelectorParam(fmt.Sprintf("involvedObject.uid=%v", string(object.GetUID()))). - ContinueOnError(). - Do(). - Infos() - if err != nil { - return nil, err - } - - var result []*corev1.Event - for _, info := range infos { - eventObj, ok := info.Object.(*corev1.Event) - - if !ok { - obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) - if err != nil { - klog.V(3).ErrorS(nil, err.Error(), "info.Object", info.Object) - return nil, fmt.Errorf("failed to convert runtime.Object to *v1.Event") - } - - eventObj = &corev1.Event{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj, eventObj); err != nil { - klog.V(3).ErrorS(nil, err.Error(), "info.Object", info.Object) - return nil, fmt.Errorf("failed to convert runtime.Object to *v1.Event") - } - } - - result = append(result, eventObj) - } - - return result, nil -} diff --git a/pkg/cli/printer/utils_test.go b/pkg/cli/printer/utils_test.go deleted file mode 100644 index 607d110bc..000000000 --- a/pkg/cli/printer/utils_test.go +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 printer - -import ( - "bytes" - "strings" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" -) - -func TestDescribe(t *testing.T) { - pairs := []*DescriberKV{ - {Key: "Key1", Value: "string"}, - {Key: "Key2", Value: []any{"list-string1", 1234, "list-string-3"}}, - { - Key: "Key3-with-nested-structures", - Value: map[string]any{ - "a": "b", - "d": map[string]any{ - "e": []string{"v1", "v2", "v3"}, - }, - "c": 123, - }, - }, - { - Key: "Key4-table", - Value: &Table{ - ColumnNames: []string{"col1", "col2", "col3"}, - Rows: [][]string{ - {"row1-a", "row1-b", "row1-c"}, - {"row2-a", "row2-b", "row2-c"}, - {"row3-a", "row3-b", "row3-c"}, - }, - UseSeparator: true, - }, - }, - } - - writable := &bytes.Buffer{} - Describe(writable, pairs) - - got := writable.String() - want := `Key1: string -Key2: -- list-string1 -- 1234 -- list-string-3 -Key3-with-nested-structures: - a: b - c: 123 - d: - e: - - v1 - - v2 - - v3 -Key4-table: - col1 col2 col3 - ---- ---- ---- - row1-a row1-b row1-c - row2-a row2-b row2-c - row3-a row3-b row3-c -` - if diff := cmp.Diff(common.MultiLine(want), common.MultiLine(got), common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", common.MultiLine(got), common.MultiLine(want), common.MultiLine(diff)) - } -} - -func TestTable_writeTable(t *testing.T) { - testcases := []struct { - name string - table *Table - indent int - want string - }{ - { - name: "without separator", - table: &Table{ - ColumnNames: []string{"Kind", "Name"}, - Rows: [][]string{ - {"HTTPRoute", "default/my-httproute"}, - {"TCPRoute", "ns2/my-tcproute"}, - }, - }, - indent: 0, - want: ` -Kind Name -HTTPRoute default/my-httproute -TCPRoute ns2/my-tcproute -`, - }, - { - name: "with separator", - table: &Table{ - ColumnNames: []string{"Kind", "Name"}, - Rows: [][]string{ - {"HTTPRoute", "default/my-httproute"}, - {"TCPRoute", "ns2/my-tcproute"}, - }, - UseSeparator: true, - }, - indent: 0, - want: ` -Kind Name ----- ---- -HTTPRoute default/my-httproute -TCPRoute ns2/my-tcproute -`, - }, - { - name: "with indent and separator", - table: &Table{ - ColumnNames: []string{"Kind", "Name"}, - Rows: [][]string{ - {"HTTPRoute", "default/my-httproute"}, - {"TCPRoute", "ns2/my-tcproute"}, - }, - UseSeparator: true, - }, - indent: 3, // We want 3 spaces at the start of each row. - want: ` - Kind Name - ---- ---- - HTTPRoute default/my-httproute - TCPRoute ns2/my-tcproute -`, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - writable := &bytes.Buffer{} - tc.table.Write(writable, tc.indent) - - got := common.MultiLine(writable.String()) - want := common.MultiLine(strings.TrimPrefix(tc.want, "\n")) - if diff := cmp.Diff(want, got, common.MultiLineTransformer); diff != "" { - t.Fatalf("Unexpected diff:\n\ngot =\n\n%v\n\nwant =\n\n%v\n\ndiff (-want, +got) =\n\n%v", got, want, common.MultiLine(diff)) - } - }) - } -} diff --git a/pkg/cli/topology/gateway/gateway.go b/pkg/cli/topology/gateway/gateway.go deleted file mode 100644 index e3e7f4ebe..000000000 --- a/pkg/cli/topology/gateway/gateway.go +++ /dev/null @@ -1,299 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 gateway - -import ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/topology" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -var ( - AllRelations = []*topology.Relation{ - GatewayParentGatewayClassRelation, - HTTPRouteParentGatewaysRelation, - HTTPRouteChildBackendRefsRelation, - GatewayNamespace, - HTTPRouteNamespace, - BackendNamespace, - } - - // GatewayParentGatewayClassRelation returns GatewayClass for the Gateway. - GatewayParentGatewayClassRelation = &topology.Relation{ - From: common.GatewayGK, - To: common.GatewayClassGK, - Name: "GatewayClass", - NeighborFunc: func(u *unstructured.Unstructured) []common.GKNN { - gateway := &gatewayv1.Gateway{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), gateway); err != nil { - panic(fmt.Sprintf("failed to convert unstructured Gateway to structured: %v", err)) - } - return []common.GKNN{{ - Group: common.GatewayClassGK.Group, - Kind: common.GatewayClassGK.Kind, - Name: string(gateway.Spec.GatewayClassName), - }} - }, - } - - // HTTPRouteParentGatewayRelation returns Gateways which the HTTPRoute is - // attached to. - HTTPRouteParentGatewaysRelation = &topology.Relation{ - From: common.HTTPRouteGK, - To: common.GatewayGK, - Name: "ParentRef", - NeighborFunc: func(u *unstructured.Unstructured) []common.GKNN { - httpRoute := &gatewayv1.HTTPRoute{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), httpRoute); err != nil { - panic(fmt.Sprintf("failed to convert unstructured HTTPRoute to structured: %v", err)) - } - result := []common.GKNN{} - for _, gatewayRef := range httpRoute.Spec.ParentRefs { - namespace := httpRoute.GetNamespace() - if namespace == "" { - namespace = metav1.NamespaceDefault - } - if gatewayRef.Namespace != nil { - namespace = string(*gatewayRef.Namespace) - } - - result = append(result, common.GKNN{ - Group: common.GatewayGK.Group, - Kind: common.GatewayGK.Kind, - Namespace: namespace, - Name: string(gatewayRef.Name), - }) - } - return result - }, - } - - // HTTPRouteChildBackendRefsRelation returns Backends which the HTTPRoute - // references. - HTTPRouteChildBackendRefsRelation = &topology.Relation{ - From: common.HTTPRouteGK, - To: common.ServiceGK, - Name: "BackendRef", - NeighborFunc: func(u *unstructured.Unstructured) []common.GKNN { - httpRoute := &gatewayv1.HTTPRoute{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), httpRoute); err != nil { - panic(fmt.Sprintf("failed to convert unstructured HTTPRoute to structured: %v", err)) - } - // Aggregate all BackendRefs - var backendRefs []gatewayv1.BackendObjectReference - for _, rule := range httpRoute.Spec.Rules { - for _, backendRef := range rule.BackendRefs { - backendRefs = append(backendRefs, backendRef.BackendObjectReference) - } - for _, filter := range rule.Filters { - if filter.Type != gatewayv1.HTTPRouteFilterRequestMirror { - continue - } - if filter.RequestMirror == nil { - continue - } - backendRefs = append(backendRefs, filter.RequestMirror.BackendRef) - } - } - - // Convert each BackendRef to GKNN. GNKK does not use pointers and - // thus is easily comparable. - resultSet := make(map[common.GKNN]bool) - for _, backendRef := range backendRefs { - objRef := common.GKNN{ - Name: string(backendRef.Name), - // Assume namespace is unspecified in the backendRef and - // check later to override the default value. - Namespace: httpRoute.GetNamespace(), - } - if backendRef.Group != nil { - objRef.Group = string(*backendRef.Group) - } - if backendRef.Kind != nil { - objRef.Kind = string(*backendRef.Kind) - } else { - // Although for resources existing on the server, this value - // should have received a default before getting persisted. - // We still explicitly set this for the local analysis when - // the defaults do not get set automatically. - objRef.Kind = common.ServiceGK.Kind - } - if backendRef.Namespace != nil { - objRef.Namespace = string(*backendRef.Namespace) - } - resultSet[objRef] = true - } - - // Return unique objRefs - var result []common.GKNN - for objRef := range resultSet { - result = append(result, objRef) - } - return result - }, - } - - // GatewayNamespace returns the Namespace for the Gateway. - GatewayNamespace = &topology.Relation{ - From: common.GatewayGK, - To: common.NamespaceGK, - Name: "Namespace", - NeighborFunc: func(u *unstructured.Unstructured) []common.GKNN { - return []common.GKNN{{ - Group: common.NamespaceGK.Group, - Kind: common.NamespaceGK.Kind, - Name: u.GetNamespace(), - }} - }, - } - - // HTTPRouteNamespace returns the Namespace for the HTTPRoute. - HTTPRouteNamespace = &topology.Relation{ - From: common.HTTPRouteGK, - To: common.NamespaceGK, - Name: "Namespace", - NeighborFunc: func(u *unstructured.Unstructured) []common.GKNN { - return []common.GKNN{{ - Group: common.NamespaceGK.Group, - Kind: common.NamespaceGK.Kind, - Name: u.GetNamespace(), - }} - }, - } - - // BackendNamespace returns the Namespace for the Gateway. - BackendNamespace = &topology.Relation{ - From: common.ServiceGK, - To: common.NamespaceGK, - Name: "Namespace", - NeighborFunc: func(u *unstructured.Unstructured) []common.GKNN { - return []common.GKNN{{ - Group: common.NamespaceGK.Group, - Kind: common.NamespaceGK.Kind, - Name: u.GetNamespace(), - }} - }, - } -) - -type gatewayClassNode interface { - Gateways() map[common.GKNN]*topology.Node -} - -type gatewayNodeClassImpl struct { - node *topology.Node -} - -func GatewayClassNode(node *topology.Node) gatewayClassNode { //nolint:revive - return &gatewayNodeClassImpl{node: node} -} - -func (n *gatewayNodeClassImpl) Gateways() map[common.GKNN]*topology.Node { - return n.node.InNeighbors[GatewayParentGatewayClassRelation] -} - -type gatewayNode interface { - Namespace() *topology.Node - GatewayClass() *topology.Node - HTTPRoutes() map[common.GKNN]*topology.Node -} - -type gatewayNodeImpl struct { - node *topology.Node -} - -func GatewayNode(node *topology.Node) gatewayNode { //nolint:revive - return &gatewayNodeImpl{node: node} -} - -func (n *gatewayNodeImpl) Namespace() *topology.Node { - for _, namespaceNode := range n.node.OutNeighbors[GatewayNamespace] { - return namespaceNode - } - return nil -} - -func (n *gatewayNodeImpl) GatewayClass() *topology.Node { - for _, gatewayClassNode := range n.node.OutNeighbors[GatewayParentGatewayClassRelation] { - return gatewayClassNode - } - return nil -} - -func (n *gatewayNodeImpl) HTTPRoutes() map[common.GKNN]*topology.Node { - return n.node.InNeighbors[HTTPRouteParentGatewaysRelation] -} - -type httpRouteNode interface { - Namespace() *topology.Node - Gateways() map[common.GKNN]*topology.Node - Backends() map[common.GKNN]*topology.Node -} - -type httpRouteNodeImpl struct { - node *topology.Node -} - -func HTTPRouteNode(node *topology.Node) httpRouteNode { - return &httpRouteNodeImpl{node: node} -} - -func (n *httpRouteNodeImpl) Namespace() *topology.Node { - for _, namespaceNode := range n.node.OutNeighbors[HTTPRouteNamespace] { - return namespaceNode - } - return nil -} - -func (n *httpRouteNodeImpl) Gateways() map[common.GKNN]*topology.Node { - return n.node.OutNeighbors[HTTPRouteParentGatewaysRelation] -} - -func (n *httpRouteNodeImpl) Backends() map[common.GKNN]*topology.Node { - return n.node.OutNeighbors[HTTPRouteChildBackendRefsRelation] -} - -type backendNode interface { - Namespace() *topology.Node - HTTPRoutes() map[common.GKNN]*topology.Node -} - -type backendNodeImpl struct { - node *topology.Node -} - -func BackendNode(node *topology.Node) backendNode { - return &backendNodeImpl{node: node} -} - -func (n *backendNodeImpl) Namespace() *topology.Node { - for _, namespaceNode := range n.node.OutNeighbors[BackendNamespace] { - return namespaceNode - } - return nil -} - -func (n *backendNodeImpl) HTTPRoutes() map[common.GKNN]*topology.Node { - return n.node.InNeighbors[HTTPRouteChildBackendRefsRelation] -} diff --git a/pkg/cli/topology/gateway/graphviz.go b/pkg/cli/topology/gateway/graphviz.go deleted file mode 100644 index 8e73834c8..000000000 --- a/pkg/cli/topology/gateway/graphviz.go +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 gateway - -import ( - "bytes" - "context" - "log" - - graphviz "github.com/goccy/go-graphviz" - "github.com/goccy/go-graphviz/cgraph" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/flomesh-io/fsm/pkg/cli/topology" -) - -// TODO: -// - Show policy nodes. Attempt to group policy nodes along with their target -// nodes in a single subgraph so they get rendered closer together. -func ToDot(gwctlGraph *topology.Graph) ([]byte, error) { - ctx := context.TODO() - g, err := graphviz.New(ctx) - if err != nil { - return nil, err - } - cGraph, err := g.Graph() - if err != nil { - return nil, err - } - defer func() { - if err := cGraph.Close(); err != nil { - log.Fatal(err) - } - g.Close() - }() - cGraph.SetRankDir(cgraph.BTRank) - - cNodeMap := map[common.GKNN]*cgraph.Node{} - - // Create nodes. - for _, nodeMap := range gwctlGraph.Nodes { - for _, node := range nodeMap { - cNode, err := cGraph.CreateNodeByName(node.GKNN().String()) - if err != nil { - return nil, err - } - cNodeMap[node.GKNN()] = cNode - cNode.SetStyle(cgraph.FilledNodeStyle) - cNode.SetFillColor(nodeColor(node)) - - // Set the Node label - gk := node.GKNN().GroupKind() - if gk.Group == common.GatewayGK.Group { - gk.Group = "" - } - name := node.GKNN().NamespacedName().String() - if node.GKNN().Namespace == "" { - name = node.GKNN().Name - } - cNode.SetLabel(gk.String() + "\n" + name) - } - } - - // Create edges. - for fromNodeGKNN, cFromNode := range cNodeMap { - fromNode := gwctlGraph.Nodes[fromNodeGKNN.GroupKind()][fromNodeGKNN.NamespacedName()] - - for relation, outNodeMap := range fromNode.OutNeighbors { - for toNodeGKNN := range outNodeMap { - cToNode := cNodeMap[toNodeGKNN] - - // If this is an edge from an HTTPRoute to a Service, then - // reverse the direction of the edge (to affect the rank), and - // then reverse the display again to show the correct direction. - // The end result being that Services now get assigned the - // correct rank. - reverse := (fromNode.GKNN().GroupKind() == common.HTTPRouteGK && toNodeGKNN.GroupKind() == common.ServiceGK) || - (fromNode.GKNN().GroupKind() == common.GatewayGK && toNodeGKNN.GroupKind() == common.NamespaceGK) - u, v := cFromNode, cToNode - if reverse { - u, v = v, u - } - - e, err := cGraph.CreateEdgeByName(relation.Name, u, v) - if err != nil { - return nil, err - } - e.SetLabel(relation.Name) - if reverse { - e.SetDir(cgraph.BackDir) - } - // Create a dotted line for the relation to the namespace. - if toNodeGKNN.Kind == common.NamespaceGK.Kind { - e.SetStyle(cgraph.DottedEdgeStyle) - } - } - } - } - - var buf bytes.Buffer - if err := g.Render(ctx, cGraph, "dot", &buf); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func nodeColor(node *topology.Node) string { - switch node.GKNN().GroupKind() { - case common.NamespaceGK: - return "#d08770" - case common.GatewayClassGK: - return "#e5e9f0" - case common.GatewayGK: - return "#ebcb8b" - case common.HTTPRouteGK: - return "#a3be8c" - case common.ServiceGK: - return "#88c0d0" - } - return "#d8dee9" -} diff --git a/pkg/cli/topology/graph.go b/pkg/cli/topology/graph.go deleted file mode 100644 index 7dc255eb7..000000000 --- a/pkg/cli/topology/graph.go +++ /dev/null @@ -1,377 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 topology - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/klog/v2" - - "github.com/flomesh-io/fsm/pkg/cli/common" -) - -const ( - DefaultGraphMaxDepth = 3 -) - -type Graph struct { - Nodes map[schema.GroupKind]map[types.NamespacedName]*Node - Sources []*Node - // MaxDepth represents the value of the maximum depth parameter used while - // performing the BFS from Source nodes. Note that terminal nodes can have a - // depth equal to MaxDepth+1. Any validations that need to happen should be - // done for nodes <= MaxDepth. This ensures that any external references - // from nodes <= MaxDepth can be validated. - MaxDepth int - Relations []*Relation -} - -func (g *Graph) AddNode(node *Node) { - klog.V(3).InfoS("AddNode", "node", node.GKNN()) - if g.Nodes == nil { - g.Nodes = make(map[schema.GroupKind]map[types.NamespacedName]*Node) - } - if g.Nodes[node.GKNN().GroupKind()] == nil { - g.Nodes[node.GKNN().GroupKind()] = make(map[types.NamespacedName]*Node) - } - g.Nodes[node.GKNN().GroupKind()][node.GKNN().NamespacedName()] = node -} - -func (g *Graph) DeleteNode(node *Node) { - klog.V(3).InfoS("DeleteNode", "node", node.GKNN()) - if g.Nodes == nil { - return - } - if g.Nodes[node.GKNN().GroupKind()] == nil { - return - } - delete(g.Nodes[node.GKNN().GroupKind()], node.GKNN().NamespacedName()) - if len(g.Nodes[node.GKNN().GroupKind()]) == 0 { - delete(g.Nodes, node.GKNN().GroupKind()) - } -} - -func (g *Graph) DeleteNodeUsingGKNN(nodeGKNN common.GKNN) { - klog.V(3).InfoS("DeleteNodeUsingGKNN", "nodeGKNN", nodeGKNN) - if !g.HasNode(nodeGKNN) { - return - } - g.DeleteNode(g.Nodes[nodeGKNN.GroupKind()][nodeGKNN.NamespacedName()]) -} - -func (g *Graph) HasNode(nodeGKNN common.GKNN) bool { - if g.Nodes == nil { - return false - } - if g.Nodes[nodeGKNN.GroupKind()] == nil { - return false - } - return g.Nodes[nodeGKNN.GroupKind()][nodeGKNN.NamespacedName()] != nil -} - -func (g *Graph) AddEdge(from *Node, to *Node, relation *Relation) { - klog.V(3).InfoS("AddEdge", "from", from.GKNN(), "to", to.GKNN()) - if from.OutNeighbors == nil { - from.OutNeighbors = make(map[*Relation]map[common.GKNN]*Node) - } - if from.OutNeighbors[relation] == nil { - from.OutNeighbors[relation] = make(map[common.GKNN]*Node) - } - from.OutNeighbors[relation][to.GKNN()] = to - - if to.InNeighbors == nil { - to.InNeighbors = make(map[*Relation]map[common.GKNN]*Node) - } - if to.InNeighbors[relation] == nil { - to.InNeighbors[relation] = make(map[common.GKNN]*Node) - } - to.InNeighbors[relation][from.GKNN()] = from -} - -func (g *Graph) RemoveEdge(from *Node, to *Node, relation *Relation) { - klog.V(3).InfoS("RemoveEdge", "from", from.GKNN(), "to", to.GKNN()) - delete(from.OutNeighbors[relation], to.GKNN()) - if len(from.OutNeighbors[relation]) == 0 { - delete(from.OutNeighbors, relation) - } - - delete(to.InNeighbors[relation], from.GKNN()) - if len(to.InNeighbors[relation]) == 0 { - delete(to.InNeighbors, relation) - } -} - -func (g *Graph) RemoveMetadata(category string) { - for gk := range g.Nodes { - for nn := range g.Nodes[gk] { - node := g.Nodes[gk][nn] - if node.Metadata != nil { - delete(node.Metadata, category) - } - } - } -} - -type Node struct { - Object *unstructured.Unstructured - InNeighbors map[*Relation]map[common.GKNN]*Node - OutNeighbors map[*Relation]map[common.GKNN]*Node - Depth int - Metadata map[string]any -} - -func (n *Node) GKNN() common.GKNN { - return common.GKNN{ - Group: n.Object.GroupVersionKind().Group, - Kind: n.Object.GroupVersionKind().Kind, - Namespace: n.Object.GetNamespace(), - Name: n.Object.GetName(), - } -} - -func MustAccessObject[T any](node *Node, concreteObj T) T { - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(node.Object.UnstructuredContent(), concreteObj); err != nil { - panic(fmt.Sprintf("failed to convert unstructured %v to structured: %v", node.GKNN().GroupKind(), err)) - } - return concreteObj -} - -type NeighborFunc func(*unstructured.Unstructured) []common.GKNN - -type Relation struct { - From schema.GroupKind - To schema.GroupKind - Name string - NeighborFunc NeighborFunc -} - -type Builder struct { - Sources []*unstructured.Unstructured - Relations []*Relation - Fetcher common.GroupKindFetcher - MaxDepth int -} - -func NewBuilder(fetcher common.GroupKindFetcher) *Builder { - return &Builder{ - Fetcher: fetcher, - MaxDepth: DefaultGraphMaxDepth, - } -} - -func (b *Builder) StartFrom(sources []*unstructured.Unstructured) *Builder { - b.Sources = sources - return b -} - -func (b *Builder) UseRelationship(relation *Relation) *Builder { - b.Relations = append(b.Relations, relation) - return b -} - -func (b *Builder) UseRelationships(relations []*Relation) *Builder { - b.Relations = append(b.Relations, relations...) - return b -} - -func (b *Builder) WithMaxDepth(maxDepth int) *Builder { - b.MaxDepth = maxDepth - return b -} - -func (b *Builder) Build() (*Graph, error) { - graph := &Graph{ - MaxDepth: b.MaxDepth, - Relations: b.Relations, - } - - for _, obj := range b.Sources { - node := &Node{Object: obj} - graph.Sources = append(graph.Sources, node) - graph.AddNode(node) - } - - // Perform BFS from source GroupKinds to figure out distinct other - // GroupKinds that need to be fetched. - allGroupKinds := b.determineUniqueGroupKinds() - if len(allGroupKinds) == 1 { - // This case means there's only one GroupKind which is the same as the - // Sources, so nothing needs to be done. - return graph, nil - } - // Fetch all relevant resources and add them to the graph as Nodes. At a - // later point, we will remove the resources which are not relevant. - const inf = 100000000 - for _, groupKind := range allGroupKinds { - klog.V(3).InfoS("Fetching resources", "groupKind", groupKind) - resources, err := b.Fetcher.Fetch(groupKind) - if err != nil { - return nil, err - } - for _, resource := range resources { - node := &Node{Object: resource, Depth: inf} - if !graph.HasNode(node.GKNN()) { - graph.AddNode(node) - } - } - } - - // Connect related resources. - for _, relation := range b.Relations { - for _, fromNode := range graph.Nodes[relation.From] { - for _, toNodeGKNN := range relation.NeighborFunc(fromNode.Object) { - if _, ok := graph.Nodes[toNodeGKNN.GroupKind()]; !ok { - continue - } - toNode := graph.Nodes[toNodeGKNN.GroupKind()][toNodeGKNN.NamespacedName()] - if toNode != nil { - graph.AddEdge(fromNode, toNode, relation) - } - } - } - } - - // Perform BFS. - - q := []*Node{} // q is a Queue used in the BFS. - - // Initialize the sources for the BFS - for _, source := range b.Sources { - gknn := (&Node{Object: source}).GKNN() - node := graph.Nodes[gknn.GroupKind()][gknn.NamespacedName()] - node.Depth = 0 - q = append(q, node) - } - - for len(q) != 0 { - u := q[0] - q = q[1:] - - if u.Depth+1 > b.MaxDepth+1 { - break - } - - // Don't expand from Namespaces to other resources. - if u.GKNN().GroupKind() == common.NamespaceGK { - continue - } - - // Don't expand from GatewayClasses if it is not the source node. - // TODO: Find appropriate ways to encode this with the - // topology/relations. - if u.GKNN().GroupKind() == common.GatewayClassGK && u.Depth != 0 { - continue - } - - allNeighbors := []map[*Relation]map[common.GKNN]*Node{ - u.InNeighbors, - u.OutNeighbors, - } - - // For vertex u, find all adjacent vertices v. - for _, neighbor := range allNeighbors { - for _, nodes := range neighbor { - for _, v := range nodes { - visited := v.Depth < inf - if visited { - continue - } - v.Depth = u.Depth + 1 - q = append(q, v) - } - } - } - } - - // BFS is now complete. Delete all Nodes which still have infinite depth. - for gk, nodes := range graph.Nodes { - for _, u := range nodes { - if u.Depth < inf { - continue - } - - // For each vertex u, find all vertices v which have an outgoing - // edge to u (ie. u has an incoming edge v -> u) - for relation, neighbors := range u.InNeighbors { - for _, v := range neighbors { - graph.RemoveEdge(v, u, relation) - } - } - - graph.DeleteNode(u) - } - - if len(nodes) == 0 { - delete(graph.Nodes, gk) - } - } - - return graph, nil -} - -func (b *Builder) determineUniqueGroupKinds() []schema.GroupKind { - result := []schema.GroupKind{} // result is the set of unique GroupKinds having depth <= b.MaxDepth - - q := []schema.GroupKind{} // q is a Queue used in the BFS. - visited := map[schema.GroupKind]bool{} - depth := map[schema.GroupKind]int{} - - // Initialize the sources for the BFS - for _, source := range b.Sources { - gk := source.GroupVersionKind().GroupKind() - if !visited[gk] { - result = append(result, gk) - visited[gk] = true - q = append(q, gk) - depth[gk] = 0 - } - } - - for len(q) != 0 { - u := q[0] - q = q[1:] - - // For vertex u, find all adjacent vertices v. - for _, relation := range b.Relations { - if relation.From != u && relation.To != u { - continue - } - v := relation.To - if u == v { - v = relation.From - } - if visited[v] { - continue - } - - visited[v] = true - depth[v] = depth[u] + 1 - q = append(q, v) - if depth[v] <= b.MaxDepth { - result = append(result, v) - } else { - return result - } - } - } - - return result -} diff --git a/pkg/cli/topology/graph_test.go b/pkg/cli/topology/graph_test.go deleted file mode 100644 index 71f12f6a8..000000000 --- a/pkg/cli/topology/graph_test.go +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 topology - -import ( - "fmt" - "testing" - - "github.com/flomesh-io/fsm/pkg/cli/common" - "github.com/google/go-cmp/cmp" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" -) - -func TestGraph_AddNode(t *testing.T) { - graph := &Graph{} - - node1 := &Node{Object: buildUnstructured(common.GKNN{Group: "1", Kind: "2", Namespace: "3", Name: "4"})} - node2 := &Node{Object: buildUnstructured(common.GKNN{Group: "1", Kind: "2", Namespace: "3", Name: "5"})} - node3 := &Node{Object: buildUnstructured(common.GKNN{Group: "1", Kind: "2", Namespace: "6", Name: "7"})} - node4 := &Node{Object: buildUnstructured(common.GKNN{Group: "1", Kind: "8", Namespace: "3", Name: "4"})} - - graph.AddNode(node1) - graph.AddNode(node2) - graph.AddNode(node3) - graph.AddNode(node4) - - wantGraph := &Graph{ - Nodes: map[schema.GroupKind]map[types.NamespacedName]*Node{ - {Group: "1", Kind: "2"}: { - {Namespace: "3", Name: "4"}: node1, - {Namespace: "3", Name: "5"}: node2, - {Namespace: "6", Name: "7"}: node3, - }, - {Group: "1", Kind: "8"}: { - {Namespace: "3", Name: "4"}: node4, - }, - }, - } - - if diff := cmp.Diff(wantGraph, graph); diff != "" { - t.Fatalf("Unexpected diff in graph after AddNode operations: (-want, +got)\n%v", diff) - } -} - -func TestGraph_AddEdge(t *testing.T) { - graph := &Graph{} - - gknn1 := common.GKNN{Group: "1", Kind: "2", Namespace: "3", Name: "4"} - node1 := &Node{Object: buildUnstructured(gknn1)} - - gknn2 := common.GKNN{Group: "1", Kind: "2", Namespace: "3", Name: "5"} - node2 := &Node{Object: buildUnstructured(gknn2)} - - gknn3 := common.GKNN{Group: "1", Kind: "2", Namespace: "6", Name: "7"} - node3 := &Node{Object: buildUnstructured(gknn3)} - - gknn4 := common.GKNN{Group: "1", Kind: "8", Namespace: "3", Name: "4"} - node4 := &Node{Object: buildUnstructured(gknn4)} - - childRelation := &Relation{Name: "child"} - parentRelation := &Relation{Name: "parent"} - - graph.AddEdge(node1, node2, childRelation) - graph.AddEdge(node1, node3, childRelation) - graph.AddEdge(node1, node4, parentRelation) - - wantNode1 := &Node{ - Object: buildUnstructured(gknn1), - OutNeighbors: map[*Relation]map[common.GKNN]*Node{ - childRelation: { - gknn2: node2, - gknn3: node3, - }, - parentRelation: { - gknn4: node4, - }, - }, - } - wantNode2 := &Node{ - Object: buildUnstructured(gknn2), - InNeighbors: map[*Relation]map[common.GKNN]*Node{ - childRelation: { - gknn1: node1, - }, - }, - } - cmpopts := []cmp.Option{cmp.Transformer("NeighborsTransformer", NeighborsTransformer)} - if diff := cmp.Diff(wantNode1, node1, cmpopts...); diff != "" { - t.Errorf("Unexpected diff in node1 after AddEdge operations: (-want, +got)\n%v", diff) - } - if diff := cmp.Diff(wantNode2, node2, cmpopts...); diff != "" { - t.Errorf("Unexpected diff in node2 after AddEdge operations: (-want, +got)\n%v", diff) - } -} - -func NeighborsTransformer(neighbors map[*Relation]map[common.GKNN]*Node) map[*Relation]map[common.GKNN]bool { - result := make(map[*Relation]map[common.GKNN]bool) - for relation, nodeMap := range neighbors { - result[relation] = make(map[common.GKNN]bool) - for nodeGKNN := range nodeMap { - result[relation][nodeGKNN] = true - } - } - return result -} - -func TestBuilder(t *testing.T) { - gknn1 := common.GKNN{Group: "1", Kind: "1", Namespace: "1", Name: "1"} - gknn2 := common.GKNN{Group: "2", Kind: "2", Namespace: "2", Name: "2"} - gknn3 := common.GKNN{Group: "3", Kind: "3", Namespace: "3", Name: "3"} - gknn4 := common.GKNN{Group: "4", Kind: "4", Namespace: "4", Name: "4"} - gknn5 := common.GKNN{Group: "5", Kind: "5", Namespace: "5", Name: "5"} // Unreachable - gknn6 := common.GKNN{Group: "6", Kind: "6", Namespace: "6", Name: "6"} // Unreachable - - relation2To1 := &Relation{ - From: gknn2.GroupKind(), - To: gknn1.GroupKind(), - Name: "gk2_to_gk1", - NeighborFunc: func(*unstructured.Unstructured) []common.GKNN { - return []common.GKNN{gknn1} - }, - } - relation2To3 := &Relation{ - From: gknn2.GroupKind(), - To: gknn3.GroupKind(), - Name: "gk2_to_gk3", - NeighborFunc: func(*unstructured.Unstructured) []common.GKNN { - return []common.GKNN{gknn3} - }, - } - relation4To3 := &Relation{ - From: gknn4.GroupKind(), - To: gknn3.GroupKind(), - Name: "gk4_to_gk3", - NeighborFunc: func(*unstructured.Unstructured) []common.GKNN { - return []common.GKNN{gknn3} - }, - } - relation4To5 := &Relation{ - From: gknn4.GroupKind(), - To: gknn5.GroupKind(), - Name: "gk4_to_gk5", - NeighborFunc: func(*unstructured.Unstructured) []common.GKNN { return nil }, - } - relation6To4 := &Relation{ - From: gknn6.GroupKind(), - To: gknn4.GroupKind(), - Name: "gk6_to_gk4", - NeighborFunc: func(*unstructured.Unstructured) []common.GKNN { return nil }, - } - - u1 := buildUnstructured(gknn1) - u2 := buildUnstructured(gknn2) - u3 := buildUnstructured(gknn3) - u4 := buildUnstructured(gknn4) - fakeFetcher := &fakeGroupKindFetcher{ - data: map[schema.GroupKind][]*unstructured.Unstructured{ - gknn1.GroupKind(): {u1}, - gknn2.GroupKind(): {u2}, - gknn3.GroupKind(): {u3}, - gknn4.GroupKind(): {u4}, - }, - } - - sources := []*unstructured.Unstructured{buildUnstructured(gknn3)} - graph, err := NewBuilder(fakeFetcher). - StartFrom(sources). - UseRelationship(relation2To1). - UseRelationship(relation2To3). - UseRelationship(relation4To3). - UseRelationship(relation4To5). - UseRelationship(relation6To4). - Build() - if err != nil { - t.Fatalf("Builder...Build() failed with error %v; want no errors", err) - } - - wantGraph := &Graph{} - node1 := &Node{Object: u1, Depth: 2} - node2 := &Node{Object: u2, Depth: 1} - node3 := &Node{Object: u3, Depth: 0} - node4 := &Node{Object: u4, Depth: 1} - wantGraph.AddNode(node1) - wantGraph.AddNode(node2) - wantGraph.AddNode(node3) - wantGraph.AddNode(node4) - wantGraph.AddEdge(node2, node1, relation2To1) - wantGraph.AddEdge(node2, node3, relation2To3) - wantGraph.AddEdge(node4, node3, relation4To3) - - if diff := cmp.Diff(wantGraph.Nodes, graph.Nodes); diff != "" { - t.Fatalf("Builder...Build(): Unexpected diff in graph: (-want, +got)\n%v", diff) - } -} - -func buildUnstructured(gknn common.GKNN) *unstructured.Unstructured { - return &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": fmt.Sprintf("%v/v1", gknn.Group), - "kind": gknn.Kind, - "metadata": map[string]interface{}{ - "name": gknn.Name, - "namespace": gknn.Namespace, - }, - "spec": map[string]interface{}{ - "key": "value", - }, - }, - } -} - -type fakeGroupKindFetcher struct { - data map[schema.GroupKind][]*unstructured.Unstructured -} - -func (f *fakeGroupKindFetcher) Fetch(gk schema.GroupKind) ([]*unstructured.Unstructured, error) { - return f.data[gk], nil -} diff --git a/pkg/cli/topology/utils.go b/pkg/cli/topology/utils.go deleted file mode 100644 index 83c3dd0e3..000000000 --- a/pkg/cli/topology/utils.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2024 The Kubernetes 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 topology - -import ( - "sort" -) - -func SortedNodes(nodes []*Node) []*Node { - sort.Slice(nodes, func(i, j int) bool { - a := nodes[i].GKNN().String() - b := nodes[j].GKNN().String() - return a < b - }) - return nodes -}