diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go index 5264f1a2a77..a1a528671fd 100644 --- a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go @@ -20,20 +20,51 @@ import ( "fmt" "path" + "github.com/spf13/pflag" "k8s.io/gengo/args" + "k8s.io/gengo/types" codegenutil "k8s.io/code-generator/pkg/util" ) +// ClientGenArgs is a wrapper for arguments to applyconfiguration-gen. +type CustomArgs struct { + // ExternalApplyConfigurations provides the locations of externally generated + // apply configuration types for types referenced by the go structs provided as input. + // Locations are provided as a comma separated list of .: + // entries. + // + // E.g. if a type references appsv1.Deployment, the location of its apply configuration should + // be provided: + // k8s.io/api/apps/v1.Deployment:k8s.io/client-go/applyconfigurations/apps/v1 + // + // meta/v1 types (TypeMeta and ObjectMeta) are always included and do not need to be passed in. + ExternalApplyConfigurations map[types.Name]string +} + // NewDefaults returns default arguments for the generator. -func NewDefaults() *args.GeneratorArgs { +func NewDefaults() (*args.GeneratorArgs, *CustomArgs) { genericArgs := args.Default().WithoutDefaultFlagParsing() + customArgs := &CustomArgs{ + ExternalApplyConfigurations: map[types.Name]string{ + // Always include TypeMeta and ObjectMeta. They are sufficient for the vast majority of use cases. + {Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "TypeMeta"}: "k8s.io/client-go/applyconfigurations/meta/v1", + {Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "ObjectMeta"}: "k8s.io/client-go/applyconfigurations/meta/v1", + }, + } + genericArgs.CustomArgs = customArgs if pkg := codegenutil.CurrentPackage(); len(pkg) != 0 { genericArgs.OutputPackagePath = path.Join(pkg, "pkg/client/applyconfigurations") } - return genericArgs + return genericArgs, customArgs +} + +func (ca *CustomArgs) AddFlags(fs *pflag.FlagSet, inputBase string) { + pflag.Var(NewExternalApplyConfigurationValue(&ca.ExternalApplyConfigurations, nil), "external-applyconfigurations", + "list of comma separated external apply configurations locations in .: form."+ + "For example: k8s.io/api/apps/v1.Deployment:k8s.io/client-go/applyconfigurations/apps/v1") } // Validate checks the given arguments. diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/externaltypes.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/externaltypes.go new file mode 100644 index 00000000000..87a6badd71a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/externaltypes.go @@ -0,0 +1,125 @@ +/* +Copyright 2021 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 args + +import ( + "bytes" + "encoding/csv" + "flag" + "fmt" + "strings" + + "k8s.io/gengo/types" +) + +type externalApplyConfigurationValue struct { + externals *map[types.Name]string + changed bool +} + +func NewExternalApplyConfigurationValue(externals *map[types.Name]string, def []string) *externalApplyConfigurationValue { + val := new(externalApplyConfigurationValue) + val.externals = externals + if def != nil { + if err := val.set(def); err != nil { + panic(err) + } + } + return val +} + +var _ flag.Value = &externalApplyConfigurationValue{} + +func (s *externalApplyConfigurationValue) set(vs []string) error { + if !s.changed { + *s.externals = map[types.Name]string{} + } + + for _, input := range vs { + typ, pkg, err := parseExternalMapping(input) + if err != nil { + return err + } + if _, ok := (*s.externals)[typ]; ok { + return fmt.Errorf("duplicate type found in --external-applyconfigurations: %v", typ) + } + (*s.externals)[typ] = pkg + } + + return nil +} + +func (s *externalApplyConfigurationValue) Set(val string) error { + vs, err := readAsCSV(val) + if err != nil { + return err + } + if err := s.set(vs); err != nil { + return err + } + return nil +} + +func (s *externalApplyConfigurationValue) Type() string { + return "string" +} + +func (s *externalApplyConfigurationValue) String() string { + var strs []string + for k, v := range *s.externals { + strs = append(strs, fmt.Sprintf("%s.%s:%s", k.Package, k.Name, v)) + } + str, _ := writeAsCSV(strs) + return "[" + str + "]" +} + +func readAsCSV(val string) ([]string, error) { + if val == "" { + return []string{}, nil + } + stringReader := strings.NewReader(val) + csvReader := csv.NewReader(stringReader) + return csvReader.Read() +} + +func writeAsCSV(vals []string) (string, error) { + b := &bytes.Buffer{} + w := csv.NewWriter(b) + err := w.Write(vals) + if err != nil { + return "", err + } + w.Flush() + return strings.TrimSuffix(b.String(), "\n"), nil +} + +func parseExternalMapping(mapping string) (typ types.Name, pkg string, err error) { + parts := strings.Split(mapping, ":") + if len(parts) != 2 { + return types.Name{}, "", fmt.Errorf("expected string of the form .: but got %s", mapping) + } + packageTypeStr := parts[0] + pkg = parts[1] + ptParts := strings.Split(packageTypeStr, ".") + if len(ptParts) != 2 { + return types.Name{}, "", fmt.Errorf("expected package and type of the form # but got %s", packageTypeStr) + } + structPkg := ptParts[0] + structType := ptParts[1] + + return types.Name{Package: structPkg, Name: structType}, pkg, nil +} diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go index bb794f73c38..d4de946fc03 100644 --- a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go @@ -28,6 +28,7 @@ import ( "k8s.io/gengo/types" "k8s.io/klog/v2" + applygenargs "k8s.io/code-generator/cmd/applyconfiguration-gen/args" clientgentypes "k8s.io/code-generator/cmd/client-gen/types" ) @@ -59,7 +60,8 @@ func Packages(context *generator.Context, arguments *args.GeneratorArgs) generat } pkgTypes := packageTypesForInputDirs(context, arguments.InputDirs, arguments.OutputPackagePath) - refs := refGraphForReachableTypes(pkgTypes) + initialTypes := arguments.CustomArgs.(*applygenargs.CustomArgs).ExternalApplyConfigurations + refs := refGraphForReachableTypes(pkgTypes, initialTypes) groupVersions := make(map[string]clientgentypes.GroupVersions) groupGoNames := make(map[string]string) diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go index 600012bd79c..cae4fce7dbf 100644 --- a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go @@ -28,8 +28,8 @@ type refGraph map[types.Name]string // refGraphForReachableTypes returns a refGraph that contains all reachable types from // the root clientgen types of the provided packages. -func refGraphForReachableTypes(pkgTypes map[string]*types.Package) refGraph { - refs := refGraph{} +func refGraphForReachableTypes(pkgTypes map[string]*types.Package, initialTypes map[types.Name]string) refGraph { + var refs refGraph = initialTypes // Include only types that are reachable from the root clientgen types. // We don't want to generate apply configurations for types that are not reachable from a root diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go index af8d5f23e67..ba204def226 100644 --- a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go @@ -32,9 +32,10 @@ import ( func main() { klog.InitFlags(nil) - genericArgs := generatorargs.NewDefaults() + genericArgs, customArgs := generatorargs.NewDefaults() genericArgs.GoHeaderFilePath = filepath.Join(args.DefaultSourceTree(), util.BoilerplatePath()) genericArgs.AddFlags(pflag.CommandLine) + customArgs.AddFlags(pflag.CommandLine, "k8s.io/kubernetes/pkg/apis") // TODO: move this input path out of client-gen if err := flag.Set("logtostderr", "true"); err != nil { klog.Fatalf("Error: %v", err) }