/* Copyright 2018 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 enable import ( "context" "fmt" "io" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" apiextv1b1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextv1b1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" pkgruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/klog" "sigs.k8s.io/kubefed/pkg/apis/core/typeconfig" fedv1b1 "sigs.k8s.io/kubefed/pkg/apis/core/v1beta1" genericclient "sigs.k8s.io/kubefed/pkg/client/generic" ctlutil "sigs.k8s.io/kubefed/pkg/controller/util" "sigs.k8s.io/kubefed/pkg/kubefedctl/options" "sigs.k8s.io/kubefed/pkg/kubefedctl/util" ) const ( federatedGroupUsage = "The name of the API group to use for the generated federated type." targetVersionUsage = "Optional, the API version of the target type." ) var ( enable_long = ` Enables a Kubernetes API type (including a CRD) to be propagated to clusters registered with a KubeFed control plane. A CRD for the federated type will be generated and a FederatedTypeConfig will be created to configure a sync controller. Current context is assumed to be a Kubernetes cluster hosting the kubefed control plane. Please use the --host-cluster-context flag otherwise.` enable_example = ` # Enable federation of Deployments kubefedctl enable deployments.apps --host-cluster-context=cluster1 # Enable federation of Deployments identified by name specified in # deployment.yaml kubefedctl enable -f deployment.yaml` ) type enableType struct { options.GlobalSubcommandOptions options.CommonEnableOptions enableTypeOptions } type enableTypeOptions struct { federatedVersion string output string outputYAML bool filename string enableTypeDirective *EnableTypeDirective } // Bind adds the join specific arguments to the flagset passed in as an // argument. func (o *enableTypeOptions) Bind(flags *pflag.FlagSet) { flags.StringVar(&o.federatedVersion, "federated-version", options.DefaultFederatedVersion, "The API version to use for the generated federated type.") flags.StringVarP(&o.output, "output", "o", "", "If provided, the resources that would be created in the API by the command are instead output to stdout in the provided format. Valid values are ['yaml'].") flags.StringVarP(&o.filename, "filename", "f", "", "If provided, the command will be configured from the provided yaml file. Only --output will be accepted from the command line") } // NewCmdTypeEnable defines the `enable` command that // enables federation of a Kubernetes API type. func NewCmdTypeEnable(cmdOut io.Writer, config util.FedConfig) *cobra.Command { opts := &enableType{} cmd := &cobra.Command{ Use: "enable (NAME | -f FILENAME)", Short: "Enables propagation of a Kubernetes API type", Long: enable_long, Example: enable_example, Run: func(cmd *cobra.Command, args []string) { err := opts.Complete(args) if err != nil { klog.Fatalf("Error: %v", err) } err = opts.Run(cmdOut, config) if err != nil { klog.Fatalf("Error: %v", err) } }, } flags := cmd.Flags() opts.GlobalSubcommandBind(flags) opts.CommonSubcommandBind(flags, federatedGroupUsage, targetVersionUsage) opts.Bind(flags) return cmd } // Complete ensures that options are valid and marshals them if necessary. func (j *enableType) Complete(args []string) error { j.enableTypeDirective = NewEnableTypeDirective() fd := j.enableTypeDirective if j.output == "yaml" { j.outputYAML = true } else if len(j.output) > 0 { return errors.Errorf("Invalid value for --output: %s", j.output) } if len(j.filename) > 0 { err := DecodeYAMLFromFile(j.filename, fd) if err != nil { return errors.Wrapf(err, "Failed to load yaml from file %q", j.filename) } return nil } if err := j.SetName(args); err != nil { return err } fd.Name = j.TargetName if len(j.TargetVersion) > 0 { fd.Spec.TargetVersion = j.TargetVersion } if len(j.FederatedGroup) > 0 { fd.Spec.FederatedGroup = j.FederatedGroup } if len(j.federatedVersion) > 0 { fd.Spec.FederatedVersion = j.federatedVersion } return nil } // Run is the implementation of the `enable` command. func (j *enableType) Run(cmdOut io.Writer, config util.FedConfig) error { hostConfig, err := config.HostConfig(j.HostClusterContext, j.Kubeconfig) if err != nil { return errors.Wrap(err, "Failed to get host cluster config") } resources, err := GetResources(hostConfig, j.enableTypeDirective) if err != nil { return err } if j.outputYAML { concreteTypeConfig := resources.TypeConfig.(*fedv1b1.FederatedTypeConfig) objects := []pkgruntime.Object{concreteTypeConfig, resources.CRD} err := writeObjectsToYAML(objects, cmdOut) if err != nil { return errors.Wrap(err, "Failed to write objects to YAML") } // -o yaml implies dry run return nil } return CreateResources(cmdOut, hostConfig, resources, j.KubeFedNamespace, j.DryRun) } type typeResources struct { TypeConfig typeconfig.Interface CRD *apiextv1b1.CustomResourceDefinition } func GetResources(config *rest.Config, enableTypeDirective *EnableTypeDirective) (*typeResources, error) { apiResource, err := LookupAPIResource(config, enableTypeDirective.Name, enableTypeDirective.Spec.TargetVersion) if err != nil { return nil, err } klog.V(2).Infof("Found type %q", resourceKey(*apiResource)) typeConfig := GenerateTypeConfigForTarget(*apiResource, enableTypeDirective) accessor, err := newSchemaAccessor(config, *apiResource) if err != nil { return nil, errors.Wrap(err, "Error initializing validation schema accessor") } shortNames := []string{} for _, shortName := range apiResource.ShortNames { shortNames = append(shortNames, fmt.Sprintf("f%s", shortName)) } crd := federatedTypeCRD(typeConfig, accessor, shortNames) return &typeResources{ TypeConfig: typeConfig, CRD: crd, }, nil } // TODO(marun) Allow updates to the configuration for a type that has // already been enabled for kubefed. This would likely involve // updating the version of the target type and the validation of the schema. func CreateResources(cmdOut io.Writer, config *rest.Config, resources *typeResources, namespace string, dryRun bool) error { write := func(data string) { if cmdOut != nil { if _, err := cmdOut.Write([]byte(data)); err != nil { klog.Fatalf("Unexpected err: %v\n", err) } } } hostClientset, err := util.HostClientset(config) if err != nil { return errors.Wrap(err, "Failed to create host clientset") } _, err = hostClientset.CoreV1().Namespaces().Get(namespace, metav1.GetOptions{}) if apierrors.IsNotFound(err) { return errors.Wrapf(err, "KubeFed system namespace %q does not exist", namespace) } else if err != nil { return errors.Wrapf(err, "Error attempting to determine whether KubeFed system namespace %q exists", namespace) } client, err := genericclient.New(config) if err != nil { return errors.Wrap(err, "Failed to get kubefed clientset") } concreteTypeConfig := resources.TypeConfig.(*fedv1b1.FederatedTypeConfig) existingTypeConfig := &fedv1b1.FederatedTypeConfig{} err = client.Get(context.TODO(), existingTypeConfig, namespace, concreteTypeConfig.Name) if err != nil && !apierrors.IsNotFound(err) { return errors.Wrapf(err, "Error retrieving FederatedTypeConfig %q", concreteTypeConfig.Name) } if err == nil { fedType := existingTypeConfig.GetFederatedType() target := existingTypeConfig.GetTargetType() concreteType := concreteTypeConfig.GetFederatedType() if fedType.Name != concreteType.Name || fedType.Version != concreteType.Version || fedType.Group != concreteType.Group { return errors.Errorf("Federation is already enabled for %q with federated type %q. Changing the federated type to %q is not supported.", qualifiedAPIResourceName(target), qualifiedAPIResourceName(fedType), qualifiedAPIResourceName(concreteType)) } } crdClient, err := apiextv1b1client.NewForConfig(config) if err != nil { return errors.Wrap(err, "Failed to create crd clientset") } existingCRD, err := crdClient.CustomResourceDefinitions().Get(resources.CRD.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { if !dryRun { _, err = crdClient.CustomResourceDefinitions().Create(resources.CRD) if err != nil { return errors.Wrapf(err, "Error creating CRD %q", resources.CRD.Name) } } write(fmt.Sprintf("customresourcedefinition.apiextensions.k8s.io/%s created\n", resources.CRD.Name)) } else if err != nil { return errors.Wrapf(err, "Error getting CRD %q", resources.CRD.Name) } else { ftcs := &fedv1b1.FederatedTypeConfigList{} err := client.List(context.TODO(), ftcs, namespace) if err != nil { return errors.Wrap(err, "Error getting FederatedTypeConfig list") } for _, ftc := range ftcs.Items { targetAPI := concreteTypeConfig.Spec.TargetType existingAPI := ftc.Spec.TargetType if IsEquivalentAPI(&existingAPI, &targetAPI) { existingName := qualifiedAPIResourceName(ftc.GetTargetType()) name := qualifiedAPIResourceName(concreteTypeConfig.GetTargetType()) qualifiedFTCName := ctlutil.QualifiedName{ Namespace: ftc.Namespace, Name: ftc.Name, } return errors.Errorf("Failed to enable %q. Federation of this type is already enabled for equivalent type %q by FederatedTypeConfig %q", name, existingName, qualifiedFTCName) } if concreteTypeConfig.Name == ftc.Name { continue } fedType := ftc.Spec.FederatedType name := typeconfig.GroupQualifiedName(metav1.APIResource{Name: fedType.PluralName, Group: fedType.Group}) if name == existingCRD.Name { return errors.Errorf("Failed to enable federation of %q due to the FederatedTypeConfig for %q already referencing a federated type CRD named %q. If these target types are distinct despite sharing the same kind, specifying a non-default --federated-group should allow %q to be enabled.", concreteTypeConfig.Name, ftc.Name, name, concreteTypeConfig.Name) } } existingCRD.Spec = resources.CRD.Spec if !dryRun { _, err = crdClient.CustomResourceDefinitions().Update(existingCRD) if err != nil { return errors.Wrapf(err, "Error updating CRD %q", resources.CRD.Name) } } write(fmt.Sprintf("customresourcedefinition.apiextensions.k8s.io/%s updated\n", resources.CRD.Name)) } concreteTypeConfig.Namespace = namespace err = client.Get(context.TODO(), existingTypeConfig, namespace, concreteTypeConfig.Name) createdOrUpdated := "created" if err != nil { if !apierrors.IsNotFound(err) { return errors.Wrapf(err, "Error retrieving FederatedTypeConfig %q", concreteTypeConfig.Name) } if !dryRun { err = client.Create(context.TODO(), concreteTypeConfig) if err != nil { return errors.Wrapf(err, "Error creating FederatedTypeConfig %q", concreteTypeConfig.Name) } } } else { existingTypeConfig.Spec = concreteTypeConfig.Spec if !dryRun { err = client.Update(context.TODO(), existingTypeConfig) if err != nil { return errors.Wrapf(err, "Error updating FederatedTypeConfig %q", concreteTypeConfig.Name) } } createdOrUpdated = "updated" } write(fmt.Sprintf("federatedtypeconfig.core.kubefed.io/%s %s in namespace %s\n", concreteTypeConfig.Name, createdOrUpdated, namespace)) return nil } func GenerateTypeConfigForTarget(apiResource metav1.APIResource, enableTypeDirective *EnableTypeDirective) typeconfig.Interface { spec := enableTypeDirective.Spec kind := apiResource.Kind pluralName := apiResource.Name typeConfig := &fedv1b1.FederatedTypeConfig{ // Explicitly including TypeMeta will ensure it will be // serialized properly to yaml. TypeMeta: metav1.TypeMeta{ Kind: "FederatedTypeConfig", APIVersion: "core.kubefed.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Name: typeconfig.GroupQualifiedName(apiResource), }, Spec: fedv1b1.FederatedTypeConfigSpec{ TargetType: fedv1b1.APIResource{ Version: apiResource.Version, Kind: kind, Scope: NamespacedToScope(apiResource), }, Propagation: fedv1b1.PropagationEnabled, FederatedType: fedv1b1.APIResource{ Group: spec.FederatedGroup, Version: spec.FederatedVersion, Kind: fmt.Sprintf("Federated%s", kind), PluralName: fmt.Sprintf("federated%s", pluralName), Scope: FederatedNamespacedToScope(apiResource), }, }, } // Set defaults that would normally be set by the api fedv1b1.SetFederatedTypeConfigDefaults(typeConfig) return typeConfig } func qualifiedAPIResourceName(resource metav1.APIResource) string { if resource.Group == "" { return fmt.Sprintf("%s/%s", resource.Name, resource.Version) } return fmt.Sprintf("%s.%s/%s", resource.Name, resource.Group, resource.Version) } func federatedTypeCRD(typeConfig typeconfig.Interface, accessor schemaAccessor, shortNames []string) *apiextv1b1.CustomResourceDefinition { templateSchema := accessor.templateSchema() schema := federatedTypeValidationSchema(templateSchema) return CrdForAPIResource(typeConfig.GetFederatedType(), schema, shortNames) } func writeObjectsToYAML(objects []pkgruntime.Object, w io.Writer) error { for _, obj := range objects { if _, err := w.Write([]byte("---\n")); err != nil { return errors.Wrap(err, "Error encoding object to yaml") } if err := writeObjectToYAML(obj, w); err != nil { return errors.Wrap(err, "Error encoding object to yaml") } } return nil } func writeObjectToYAML(obj pkgruntime.Object, w io.Writer) error { json, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(obj) if err != nil { return err } unstructuredObj := &unstructured.Unstructured{} if _, _, err := unstructured.UnstructuredJSONScheme.Decode(json, nil, unstructuredObj); err != nil { return err } return util.WriteUnstructuredToYaml(unstructuredObj, w) }