change cluster schema (#2026)

* change cluster schema

* change cluster schema
This commit is contained in:
zryfish
2020-04-27 17:34:02 +08:00
committed by GitHub
parent 794f388306
commit 5a3eb651f3
123 changed files with 13582 additions and 1032 deletions

View File

@@ -0,0 +1,153 @@
/*
Copyright 2019 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 (
"k8s.io/apimachinery/pkg/runtime/schema"
fedv1b1 "sigs.k8s.io/kubefed/pkg/apis/core/v1beta1"
)
// Deprecated APIs removed in 1.16 will be served by current equivalent APIs
// https://kubernetes.io/blog/2019/07/18/api-deprecations-in-1-16/
//
// Only allow one of the equivalent APIs for federation to avoid the possibility
// of multiple sync controllers fighting to update the same resource
var equivalentAPIs = map[string][]schema.GroupVersion{
"deployments": {
{
Group: "apps",
Version: "v1",
},
{
Group: "apps",
Version: "v1beta1",
},
{
Group: "apps",
Version: "v1beta2",
},
{
Group: "extensions",
Version: "v1beta1",
},
},
"daemonsets": {
{
Group: "apps",
Version: "v1",
},
{
Group: "apps",
Version: "v1beta1",
},
{
Group: "apps",
Version: "v1beta2",
},
{
Group: "extensions",
Version: "v1beta1",
},
},
"statefulsets": {
{
Group: "apps",
Version: "v1",
},
{
Group: "apps",
Version: "v1beta1",
},
{
Group: "apps",
Version: "v1beta2",
},
},
"replicasets": {
{
Group: "apps",
Version: "v1",
},
{
Group: "apps",
Version: "v1beta1",
},
{
Group: "apps",
Version: "v1beta2",
},
{
Group: "extensions",
Version: "v1beta1",
},
},
"networkpolicies": {
{
Group: "networking.k8s.io",
Version: "v1",
},
{
Group: "extensions",
Version: "v1beta1",
},
},
"podsecuritypolicies": {
{
Group: "policy",
Version: "v1beta1",
},
{
Group: "extensions",
Version: "v1beta1",
},
},
"ingresses": {
{
Group: "networking.k8s.io",
Version: "v1beta1",
},
{
Group: "extensions",
Version: "v1beta1",
},
},
}
func IsEquivalentAPI(existingAPI, newAPI *fedv1b1.APIResource) bool {
if existingAPI.PluralName != newAPI.PluralName {
return false
}
apis, ok := equivalentAPIs[existingAPI.PluralName]
if !ok {
return false
}
for _, gv := range apis {
if gv.Group == existingAPI.Group && gv.Version == existingAPI.Version {
// skip exactly matched API from equivalent API list
continue
}
if gv.Group == newAPI.Group && gv.Version == newAPI.Version {
return true
}
}
return false
}

View File

@@ -0,0 +1,59 @@
/*
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 (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/kubefed/pkg/kubefedctl/options"
)
// EnableTypeDirectiveSpec defines the desired state of EnableTypeDirective.
type EnableTypeDirectiveSpec struct {
// The API version of the target type.
// +optional
TargetVersion string `json:"targetVersion,omitempty"`
// The name of the API group to use for generated federated types.
// +optional
FederatedGroup string `json:"federatedGroup,omitempty"`
// The API version to use for generated federated types.
// +optional
FederatedVersion string `json:"federatedVersion,omitempty"`
}
// TODO(marun) This should become a proper API type and drive enabling
// type federation via a controller. For now its only purpose is to
// enable loading of configuration from disk.
type EnableTypeDirective struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec EnableTypeDirectiveSpec `json:"spec,omitempty"`
}
func (ft *EnableTypeDirective) SetDefaults() {
ft.Spec.FederatedGroup = options.DefaultFederatedGroup
ft.Spec.FederatedVersion = options.DefaultFederatedVersion
}
func NewEnableTypeDirective() *EnableTypeDirective {
ft := &EnableTypeDirective{}
ft.SetDefaults()
return ft
}

View File

@@ -0,0 +1,428 @@
/*
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)
}

View File

@@ -0,0 +1,221 @@
/*
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 (
"fmt"
"github.com/pkg/errors"
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/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/kubectl/pkg/util/openapi"
)
type schemaAccessor interface {
templateSchema() map[string]apiextv1b1.JSONSchemaProps
}
func newSchemaAccessor(config *rest.Config, apiResource metav1.APIResource) (schemaAccessor, error) {
// Assume the resource may be a CRD, and fall back to OpenAPI if that is not the case.
crdAccessor, err := newCRDSchemaAccessor(config, apiResource)
if err != nil {
return nil, err
}
if crdAccessor != nil {
return crdAccessor, nil
}
return newOpenAPISchemaAccessor(config, apiResource)
}
type crdSchemaAccessor struct {
validation *apiextv1b1.CustomResourceValidation
}
func newCRDSchemaAccessor(config *rest.Config, apiResource metav1.APIResource) (schemaAccessor, error) {
// CRDs must have a group
if len(apiResource.Group) == 0 {
return nil, nil
}
// Check whether the target resource is a crd
crdClient, err := apiextv1b1client.NewForConfig(config)
if err != nil {
return nil, errors.Wrap(err, "Failed to create crd clientset")
}
crdName := fmt.Sprintf("%s.%s", apiResource.Name, apiResource.Group)
crd, err := crdClient.CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, errors.Wrapf(err, "Error attempting retrieval of crd %q", crdName)
}
return &crdSchemaAccessor{validation: crd.Spec.Validation}, nil
}
func (a *crdSchemaAccessor) templateSchema() map[string]apiextv1b1.JSONSchemaProps {
if a.validation != nil && a.validation.OpenAPIV3Schema != nil {
return a.validation.OpenAPIV3Schema.Properties
}
return nil
}
type openAPISchemaAccessor struct {
targetResource proto.Schema
}
func newOpenAPISchemaAccessor(config *rest.Config, apiResource metav1.APIResource) (schemaAccessor, error) {
client, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, errors.Wrap(err, "Error creating discovery client")
}
resources, err := openapi.NewOpenAPIGetter(client).Get()
if err != nil {
return nil, errors.Wrap(err, "Error loading openapi schema")
}
gvk := schema.GroupVersionKind{
Group: apiResource.Group,
Version: apiResource.Version,
Kind: apiResource.Kind,
}
targetResource := resources.LookupResource(gvk)
if targetResource == nil {
return nil, errors.Errorf("Unable to find openapi schema for %q", gvk)
}
return &openAPISchemaAccessor{
targetResource: targetResource,
}, nil
}
func (a *openAPISchemaAccessor) templateSchema() map[string]apiextv1b1.JSONSchemaProps {
var templateSchema *apiextv1b1.JSONSchemaProps
visitor := &jsonSchemaVistor{
collect: func(schema apiextv1b1.JSONSchemaProps) {
templateSchema = &schema
},
}
a.targetResource.Accept(visitor)
return templateSchema.Properties
}
// jsonSchemaVistor converts proto.Schema resources into json schema.
// A local visitor (and associated callback) is intended to be created
// whenever a function needs to recurse.
//
// TODO(marun) Generate more extensive schema if/when openapi schema
// provides more detail as per https://github.com/ant31/crd-validation
type jsonSchemaVistor struct {
collect func(schema apiextv1b1.JSONSchemaProps)
}
func (v *jsonSchemaVistor) VisitArray(a *proto.Array) {
arraySchema := apiextv1b1.JSONSchemaProps{
Type: "array",
Items: &apiextv1b1.JSONSchemaPropsOrArray{},
}
localVisitor := &jsonSchemaVistor{
collect: func(schema apiextv1b1.JSONSchemaProps) {
arraySchema.Items.Schema = &schema
},
}
a.SubType.Accept(localVisitor)
v.collect(arraySchema)
}
func (v *jsonSchemaVistor) VisitMap(m *proto.Map) {
mapSchema := apiextv1b1.JSONSchemaProps{
Type: "object",
AdditionalProperties: &apiextv1b1.JSONSchemaPropsOrBool{
Allows: true,
},
}
localVisitor := &jsonSchemaVistor{
collect: func(schema apiextv1b1.JSONSchemaProps) {
mapSchema.AdditionalProperties.Schema = &schema
},
}
m.SubType.Accept(localVisitor)
v.collect(mapSchema)
}
func (v *jsonSchemaVistor) VisitPrimitive(p *proto.Primitive) {
schema := schemaForPrimitive(p)
v.collect(schema)
}
func (v *jsonSchemaVistor) VisitKind(k *proto.Kind) {
kindSchema := apiextv1b1.JSONSchemaProps{
Type: "object",
Properties: make(map[string]apiextv1b1.JSONSchemaProps),
Required: k.RequiredFields,
}
for key, fieldSchema := range k.Fields {
// Status cannot be defined for a template
if key == "status" {
continue
}
localVisitor := &jsonSchemaVistor{
collect: func(schema apiextv1b1.JSONSchemaProps) {
kindSchema.Properties[key] = schema
},
}
fieldSchema.Accept(localVisitor)
}
v.collect(kindSchema)
}
func (v *jsonSchemaVistor) VisitReference(r proto.Reference) {
// Short-circuit the recursive definition of JSONSchemaProps (used for CRD validation)
//
// TODO(marun) Implement proper support for recursive schema
if r.Reference() == "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" {
v.collect(apiextv1b1.JSONSchemaProps{Type: "object"})
return
}
r.SubSchema().Accept(v)
}
func schemaForPrimitive(p *proto.Primitive) apiextv1b1.JSONSchemaProps {
schema := apiextv1b1.JSONSchemaProps{}
if p.Format == "int-or-string" {
schema.AnyOf = []apiextv1b1.JSONSchemaProps{
{
Type: "integer",
Format: "int32",
},
{
Type: "string",
},
}
return schema
}
if len(p.Format) > 0 {
schema.Format = p.Format
}
schema.Type = p.Type
return schema
}

View File

@@ -0,0 +1,199 @@
/*
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 (
"fmt"
"io"
"os"
"strings"
"github.com/pkg/errors"
apiextv1b1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"sigs.k8s.io/kubefed/pkg/apis/core/common"
"sigs.k8s.io/kubefed/pkg/apis/core/typeconfig"
)
func DecodeYAMLFromFile(filename string, obj interface{}) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
return DecodeYAML(f, obj)
}
func DecodeYAML(r io.Reader, obj interface{}) error {
decoder := yaml.NewYAMLToJSONDecoder(r)
return decoder.Decode(obj)
}
func CrdForAPIResource(apiResource metav1.APIResource, validation *apiextv1b1.CustomResourceValidation, shortNames []string) *apiextv1b1.CustomResourceDefinition {
scope := apiextv1b1.ClusterScoped
if apiResource.Namespaced {
scope = apiextv1b1.NamespaceScoped
}
return &apiextv1b1.CustomResourceDefinition{
// Explicitly including TypeMeta will ensure it will be
// serialized properly to yaml.
TypeMeta: metav1.TypeMeta{
Kind: "CustomResourceDefinition",
APIVersion: "apiextensions.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: typeconfig.GroupQualifiedName(apiResource),
},
Spec: apiextv1b1.CustomResourceDefinitionSpec{
Group: apiResource.Group,
Version: apiResource.Version,
Scope: scope,
Names: apiextv1b1.CustomResourceDefinitionNames{
Plural: apiResource.Name,
Kind: apiResource.Kind,
ShortNames: shortNames,
},
Validation: validation,
Subresources: &apiextv1b1.CustomResourceSubresources{
Status: &apiextv1b1.CustomResourceSubresourceStatus{},
},
},
}
}
func LookupAPIResource(config *rest.Config, key, targetVersion string) (*metav1.APIResource, error) {
resourceLists, err := GetServerPreferredResources(config)
if err != nil {
return nil, err
}
var targetResource *metav1.APIResource
var matchedResources []string
for _, resourceList := range resourceLists {
// The list holds the GroupVersion for its list of APIResources
gv, err := schema.ParseGroupVersion(resourceList.GroupVersion)
if err != nil {
return nil, errors.Wrap(err, "Error parsing GroupVersion")
}
if len(targetVersion) > 0 && gv.Version != targetVersion {
continue
}
for _, resource := range resourceList.APIResources {
group := gv.Group
if NameMatchesResource(key, resource, group) {
if targetResource == nil {
targetResource = resource.DeepCopy()
targetResource.Group = group
targetResource.Version = gv.Version
}
matchedResources = append(matchedResources, groupQualifiedName(resource.Name, gv.Group))
}
}
}
if len(matchedResources) > 1 {
return nil, errors.Errorf("Multiple resources are matched by %q: %s. A group-qualified plural name must be provided.", key, strings.Join(matchedResources, ", "))
}
if targetResource != nil {
return targetResource, nil
}
return nil, errors.Errorf("Unable to find api resource named %q.", key)
}
func NameMatchesResource(name string, apiResource metav1.APIResource, group string) bool {
lowerCaseName := strings.ToLower(name)
if lowerCaseName == apiResource.Name ||
lowerCaseName == apiResource.SingularName ||
lowerCaseName == strings.ToLower(apiResource.Kind) ||
lowerCaseName == fmt.Sprintf("%s.%s", apiResource.Name, group) {
return true
}
for _, shortName := range apiResource.ShortNames {
if lowerCaseName == strings.ToLower(shortName) {
return true
}
}
return false
}
func GetServerPreferredResources(config *rest.Config) ([]*metav1.APIResourceList, error) {
// TODO(marun) Consider using a caching scheme ala kubectl
client, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, errors.Wrap(err, "Error creating discovery client")
}
resourceLists, err := client.ServerPreferredResources()
if err != nil {
return nil, errors.Wrap(err, "Error listing api resources")
}
return resourceLists, nil
}
func NamespacedToScope(apiResource metav1.APIResource) apiextv1b1.ResourceScope {
if apiResource.Namespaced {
return apiextv1b1.NamespaceScoped
}
return apiextv1b1.ClusterScoped
}
func FederatedNamespacedToScope(apiResource metav1.APIResource) apiextv1b1.ResourceScope {
// Special-case the scope of federated namespace since it will
// hopefully be the only instance of the scope of a federated
// type differing from the scope of its target.
if typeconfig.GroupQualifiedName(apiResource) == common.NamespaceName {
// FederatedNamespace is namespaced to allow the control plane to run
// with only namespace-scoped permissions e.g. to determine placement.
return apiextv1b1.NamespaceScoped
}
return NamespacedToScope(apiResource)
}
func resourceKey(apiResource metav1.APIResource) string {
var group string
if len(apiResource.Group) == 0 {
group = "core"
} else {
group = apiResource.Group
}
var version string
if len(apiResource.Version) == 0 {
version = "v1"
} else {
version = apiResource.Version
}
return fmt.Sprintf("%s.%s/%s", apiResource.Name, group, version)
}
func groupQualifiedName(name, group string) string {
apiResource := metav1.APIResource{
Name: name,
Group: group,
}
return typeconfig.GroupQualifiedName(apiResource)
}

View File

@@ -0,0 +1,260 @@
/*
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 (
v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"sigs.k8s.io/kubefed/pkg/controller/util"
)
func federatedTypeValidationSchema(templateSchema map[string]v1beta1.JSONSchemaProps) *v1beta1.CustomResourceValidation {
schema := ValidationSchema(v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"placement": {
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
// References to one or more clusters allow a
// scheduling mechanism to explicitly indicate
// placement. If one or more clusters is provided,
// the clusterSelector field will be ignored.
"clusters": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"name": {
Type: "string",
},
},
Required: []string{
"name",
},
},
},
},
"clusterSelector": {
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"matchExpressions": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"key": {
Type: "string",
},
"operator": {
Type: "string",
},
"values": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "string",
},
},
},
},
Required: []string{
"key",
"operator",
},
},
},
},
"matchLabels": {
Type: "object",
AdditionalProperties: &v1beta1.JSONSchemaPropsOrBool{
Schema: &v1beta1.JSONSchemaProps{
Type: "string",
},
},
},
},
},
},
},
"overrides": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"clusterName": {
Type: "string",
},
"clusterOverrides": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"op": {
Type: "string",
Pattern: "^(add|remove|replace)?$",
},
"path": {
Type: "string",
},
"value": {
// Supporting the override of an arbitrary field
// precludes up-front validation. Errors in
// the definition of override values will need to
// be caught during propagation.
AnyOf: []v1beta1.JSONSchemaProps{
{
Type: "string",
},
{
Type: "integer",
},
{
Type: "boolean",
},
{
Type: "object",
},
{
Type: "array",
},
},
},
},
Required: []string{
"path",
},
},
},
},
},
},
},
},
},
})
if templateSchema != nil {
specProperties := schema.OpenAPIV3Schema.Properties["spec"].Properties
specProperties["template"] = v1beta1.JSONSchemaProps{
Type: "object",
}
// Add retainReplicas field to types that exposes a replicas
// field that could be targeted by HPA.
if templateSpec, ok := templateSchema["spec"]; ok {
// TODO: find a simpler way to detect that a resource is scalable than having to compute the entire schema.
if replicasField, ok := templateSpec.Properties["replicas"]; ok {
if replicasField.Type == "integer" && replicasField.Format == "int32" {
specProperties[util.RetainReplicasField] = v1beta1.JSONSchemaProps{
Type: "boolean",
}
}
}
}
}
return schema
}
func ValidationSchema(specProps v1beta1.JSONSchemaProps) *v1beta1.CustomResourceValidation {
return &v1beta1.CustomResourceValidation{
OpenAPIV3Schema: &v1beta1.JSONSchemaProps{
Properties: map[string]v1beta1.JSONSchemaProps{
"apiVersion": {
Type: "string",
},
"kind": {
Type: "string",
},
// TODO(marun) Add a comprehensive schema for metadata
"metadata": {
Type: "object",
},
"spec": specProps,
"status": {
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"conditions": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"type": {
Type: "string",
},
"status": {
Type: "string",
},
"reason": {
Type: "string",
},
"lastUpdateTime": {
Format: "date-time",
Type: "string",
},
"lastTransitionTime": {
Format: "date-time",
Type: "string",
},
},
Required: []string{
"type",
"status",
},
},
},
},
"clusters": {
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &v1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]v1beta1.JSONSchemaProps{
"name": {
Type: "string",
},
"status": {
Type: "string",
},
},
Required: []string{
"name",
},
},
},
},
"observedGeneration": {
Format: "int64",
Type: "integer",
},
},
},
},
// Require a spec (even if empty) as an aid to users
// manually creating federated configmaps or
// secrets. These target types do not include a spec,
// and the absence of the spec in a federated
// equivalent could indicate a malformed resource.
Required: []string{
"spec",
},
},
}
}