From 5db1a4c3df9b6aa37aa69b3e712306bffa75851b Mon Sep 17 00:00:00 2001 From: SergioLangaritaBenitez Date: Wed, 20 May 2026 13:47:13 +0200 Subject: [PATCH] feat: add federation commands --- cmd/federation.go | 44 ++++++++++++++ cmd/federation_create.go | 78 +++++++++++++++++++++++++ cmd/federation_delete.go | 57 ++++++++++++++++++ cmd/federation_get.go | 89 ++++++++++++++++++++++++++++ cmd/federation_update.go | 78 +++++++++++++++++++++++++ cmd/root.go | 1 + pkg/service/federation.go | 119 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 466 insertions(+) create mode 100644 cmd/federation.go create mode 100644 cmd/federation_create.go create mode 100644 cmd/federation_delete.go create mode 100644 cmd/federation_get.go create mode 100644 cmd/federation_update.go create mode 100644 pkg/service/federation.go diff --git a/cmd/federation.go b/cmd/federation.go new file mode 100644 index 0000000..e780823 --- /dev/null +++ b/cmd/federation.go @@ -0,0 +1,44 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 cmd + +import ( + "github.com/spf13/cobra" +) + +func federationFunc(cmd *cobra.Command, args []string) { + cmd.Help() +} + +func makeFederationCmd() *cobra.Command { + federationCmd := &cobra.Command{ + Use: "federation", + Short: "Manage service federation replicas", + Args: cobra.NoArgs, + Run: federationFunc, + } + + federationCmd.PersistentFlags().StringP("cluster", "c", "", "set the cluster") + federationCmd.PersistentFlags().StringVar(&configPath, "config", defaultConfigPath, "set the location of the config file (YAML or JSON)") + + federationCmd.AddCommand(makeFederationGetCmd()) + federationCmd.AddCommand(makeFederationCreateCmd()) + federationCmd.AddCommand(makeFederationUpdateCmd()) + federationCmd.AddCommand(makeFederationDeleteCmd()) + + return federationCmd +} diff --git a/cmd/federation_create.go b/cmd/federation_create.go new file mode 100644 index 0000000..87ba32c --- /dev/null +++ b/cmd/federation_create.go @@ -0,0 +1,78 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 cmd + +import ( + "fmt" + + "github.com/grycap/oscar-cli/pkg/config" + "github.com/grycap/oscar-cli/pkg/service" + "github.com/grycap/oscar/v4/pkg/types" + "github.com/spf13/cobra" +) + +func federationCreateFunc(cmd *cobra.Command, args []string) error { + conf, err := config.ReadConfig(configPath) + if err != nil { + return err + } + + clusterName, err := getCluster(cmd, conf) + if err != nil { + return err + } + + replicaType, _ := cmd.Flags().GetString("type") + clusterID, _ := cmd.Flags().GetString("cluster-id") + serviceName, _ := cmd.Flags().GetString("service-name") + url, _ := cmd.Flags().GetString("url") + priority, _ := cmd.Flags().GetUint("priority") + + replica := types.Replica{ + Type: replicaType, + ClusterID: clusterID, + ServiceName: serviceName, + URL: url, + SSLVerify: true, + Priority: priority, + } + + if err := service.CreateFederation(conf.Oscar[clusterName], args[0], []types.Replica{replica}); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "Federation replica created for service %q\n", args[0]) + + return nil +} + +func makeFederationCreateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create SERVICE_NAME", + Short: "Create a federation replica for a service", + Args: cobra.ExactArgs(1), + RunE: federationCreateFunc, + } + + cmd.Flags().String("type", "oscar", "replica type (oscar or endpoint)") + cmd.Flags().String("cluster-id", "", "cluster ID (for oscar type)") + cmd.Flags().String("service-name", "", "service name in the replica cluster (for oscar type)") + cmd.Flags().String("url", "", "endpoint URL (for endpoint type)") + cmd.Flags().Uint("priority", 0, "delegation priority (0 = highest)") + + return cmd +} diff --git a/cmd/federation_delete.go b/cmd/federation_delete.go new file mode 100644 index 0000000..4c59c2a --- /dev/null +++ b/cmd/federation_delete.go @@ -0,0 +1,57 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 cmd + +import ( + "fmt" + + "github.com/grycap/oscar-cli/pkg/config" + "github.com/grycap/oscar-cli/pkg/service" + "github.com/spf13/cobra" +) + +func federationDeleteFunc(cmd *cobra.Command, args []string) error { + conf, err := config.ReadConfig(configPath) + if err != nil { + return err + } + + clusterName, err := getCluster(cmd, conf) + if err != nil { + return err + } + + if err := service.DeleteFederation(conf.Oscar[clusterName], args[0]); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "Federation replicas deleted for service %q\n", args[0]) + + return nil +} + +func makeFederationDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete SERVICE_NAME", + Short: "Delete federation replicas of a service", + Args: cobra.ExactArgs(1), + Aliases: []string{"rm"}, + RunE: federationDeleteFunc, + } + + return cmd +} diff --git a/cmd/federation_get.go b/cmd/federation_get.go new file mode 100644 index 0000000..f1d7a5a --- /dev/null +++ b/cmd/federation_get.go @@ -0,0 +1,89 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 cmd + +import ( + "encoding/json" + "fmt" + "text/tabwriter" + + "github.com/grycap/oscar-cli/pkg/config" + "github.com/grycap/oscar-cli/pkg/service" + "github.com/spf13/cobra" +) + +func federationGetFunc(cmd *cobra.Command, args []string) error { + conf, err := config.ReadConfig(configPath) + if err != nil { + return err + } + + clusterName, err := getCluster(cmd, conf) + if err != nil { + return err + } + + replicas, err := service.GetFederation(conf.Oscar[clusterName], args[0]) + if err != nil { + return err + } + + output, _ := cmd.Flags().GetString("output") + switch output { + case "json": + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetIndent("", " ") + return encoder.Encode(replicas) + case "table": + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 8, 2, ' ', 0) + fmt.Fprintln(w, "TYPE\tCLUSTER ID\tSERVICE NAME\tURL\tPRIORITY") + for _, r := range replicas { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", r.Type, r.ClusterID, r.ServiceName, r.URL, r.Priority) + } + w.Flush() + if len(replicas) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No federation replicas found") + } + default: + for _, r := range replicas { + fmt.Fprintf(cmd.OutOrStdout(), "Type: %s\n", r.Type) + fmt.Fprintf(cmd.OutOrStdout(), "Cluster ID: %s\n", r.ClusterID) + fmt.Fprintf(cmd.OutOrStdout(), "Service Name: %s\n", r.ServiceName) + fmt.Fprintf(cmd.OutOrStdout(), "URL: %s\n", r.URL) + fmt.Fprintf(cmd.OutOrStdout(), "Priority: %d\n\n", r.Priority) + } + if len(replicas) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No federation replicas found") + } + } + + return nil +} + +func makeFederationGetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get SERVICE_NAME", + Short: "Get federated replicas of a service", + Args: cobra.ExactArgs(1), + Aliases: []string{"g"}, + RunE: federationGetFunc, + } + + cmd.Flags().StringP("output", "o", "text", "output format (text, json, table)") + + return cmd +} diff --git a/cmd/federation_update.go b/cmd/federation_update.go new file mode 100644 index 0000000..5787f0f --- /dev/null +++ b/cmd/federation_update.go @@ -0,0 +1,78 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 cmd + +import ( + "fmt" + + "github.com/grycap/oscar-cli/pkg/config" + "github.com/grycap/oscar-cli/pkg/service" + "github.com/grycap/oscar/v4/pkg/types" + "github.com/spf13/cobra" +) + +func federationUpdateFunc(cmd *cobra.Command, args []string) error { + conf, err := config.ReadConfig(configPath) + if err != nil { + return err + } + + clusterName, err := getCluster(cmd, conf) + if err != nil { + return err + } + + replicaType, _ := cmd.Flags().GetString("type") + clusterID, _ := cmd.Flags().GetString("cluster-id") + serviceName, _ := cmd.Flags().GetString("service-name") + url, _ := cmd.Flags().GetString("url") + priority, _ := cmd.Flags().GetUint("priority") + + replica := types.Replica{ + Type: replicaType, + ClusterID: clusterID, + ServiceName: serviceName, + URL: url, + SSLVerify: true, + Priority: priority, + } + + if err := service.UpdateFederation(conf.Oscar[clusterName], args[0], []types.Replica{replica}); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "Federation replica updated for service %q\n", args[0]) + + return nil +} + +func makeFederationUpdateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update SERVICE_NAME", + Short: "Update a federation replica for a service", + Args: cobra.ExactArgs(1), + RunE: federationUpdateFunc, + } + + cmd.Flags().String("type", "oscar", "replica type (oscar or endpoint)") + cmd.Flags().String("cluster-id", "", "cluster ID (for oscar type)") + cmd.Flags().String("service-name", "", "service name in the replica cluster (for oscar type)") + cmd.Flags().String("url", "", "endpoint URL (for endpoint type)") + cmd.Flags().Uint("priority", 0, "delegation priority (0 = highest)") + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index 3063622..e0f8160 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -53,6 +53,7 @@ func newRootCommand() *cobra.Command { cmd.AddCommand(makeInteractiveCmd()) cmd.AddCommand(makeDeleteCmd()) cmd.AddCommand(makeHealthCmd()) + cmd.AddCommand(makeFederationCmd()) return cmd } diff --git a/pkg/service/federation.go b/pkg/service/federation.go new file mode 100644 index 0000000..51767a0 --- /dev/null +++ b/pkg/service/federation.go @@ -0,0 +1,119 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 service + +import ( + "bytes" + "encoding/json" + "net/http" + "net/url" + "path" + + "github.com/grycap/oscar-cli/pkg/cluster" + "github.com/grycap/oscar/v4/pkg/types" +) + +const federationPath = "/system/federation" + +// GetFederation returns the federated replicas of a service. +func GetFederation(c *cluster.Cluster, serviceName string) ([]types.Replica, error) { + getURL, err := url.Parse(c.Endpoint) + if err != nil { + return nil, cluster.ErrParsingEndpoint + } + getURL.Path = path.Join(getURL.Path, federationPath, serviceName) + + req, err := http.NewRequest(http.MethodGet, getURL.String(), nil) + if err != nil { + return nil, cluster.ErrMakingRequest + } + + client, err := c.GetClientSafe() + if err != nil { + return nil, err + } + + res, err := client.Do(req) + if err != nil { + return nil, cluster.ErrSendingRequest + } + defer res.Body.Close() + + if err := cluster.CheckStatusCode(res); err != nil { + return nil, err + } + + var replicas []types.Replica + if err := json.NewDecoder(res.Body).Decode(&replicas); err != nil { + return nil, err + } + + return replicas, nil +} + +// CreateFederation creates federated replicas for a service. +func CreateFederation(c *cluster.Cluster, serviceName string, replicas []types.Replica) error { + return federationRequest(c, http.MethodPost, serviceName, replicas) +} + +// UpdateFederation updates federated replicas for a service. +func UpdateFederation(c *cluster.Cluster, serviceName string, replicas []types.Replica) error { + return federationRequest(c, http.MethodPut, serviceName, replicas) +} + +// DeleteFederation deletes federated replicas for a service. +func DeleteFederation(c *cluster.Cluster, serviceName string) error { + return federationRequest(c, http.MethodDelete, serviceName, nil) +} + +func federationRequest(c *cluster.Cluster, method, serviceName string, replicas []types.Replica) error { + reqURL, err := url.Parse(c.Endpoint) + if err != nil { + return cluster.ErrParsingEndpoint + } + reqURL.Path = path.Join(reqURL.Path, federationPath, serviceName) + + var body *bytes.Buffer + if replicas != nil { + bodyBytes, err := json.Marshal(replicas) + if err != nil { + return err + } + body = bytes.NewBuffer(bodyBytes) + } + + req, err := http.NewRequest(method, reqURL.String(), body) + if err != nil { + return cluster.ErrMakingRequest + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + client, err := c.GetClientSafe() + if err != nil { + return err + } + + res, err := client.Do(req) + if err != nil { + return cluster.ErrSendingRequest + } + defer res.Body.Close() + + return cluster.CheckStatusCode(res) +}