From 2a68351c952952af7fa4702c69c3b410fca7881d Mon Sep 17 00:00:00 2001 From: ymqytw Date: Fri, 17 Nov 2017 15:37:09 -0800 Subject: [PATCH] implement initial kinflate --- cmd/kinflate/{main.go => kinflate.go} | 9 + .../{main_test.go => kinflate_test.go} | 0 pkg/kinflate/kinflate.go | 189 ++++++++++++++++++ pkg/kinflate/util.go | 92 +++++---- 4 files changed, 243 insertions(+), 47 deletions(-) rename cmd/kinflate/{main.go => kinflate.go} (82%) rename cmd/kinflate/{main_test.go => kinflate_test.go} (100%) create mode 100644 pkg/kinflate/kinflate.go diff --git a/cmd/kinflate/main.go b/cmd/kinflate/kinflate.go similarity index 82% rename from cmd/kinflate/main.go rename to cmd/kinflate/kinflate.go index 1bd51ced2..ab84badc0 100644 --- a/cmd/kinflate/main.go +++ b/cmd/kinflate/kinflate.go @@ -18,6 +18,9 @@ package main import ( "fmt" + "os" + + "k8s.io/kubectl/pkg/kinflate" ) // TestableMain allows test coverage for main. @@ -28,4 +31,10 @@ func TestableMain() error { func main() { TestableMain() + cmd := kinflate.NewCmdKinflate(os.Stdout, os.Stderr) + err := cmd.Execute() + if err != nil { + os.Exit(1) + } + os.Exit(0) } diff --git a/cmd/kinflate/main_test.go b/cmd/kinflate/kinflate_test.go similarity index 100% rename from cmd/kinflate/main_test.go rename to cmd/kinflate/kinflate_test.go diff --git a/pkg/kinflate/kinflate.go b/pkg/kinflate/kinflate.go new file mode 100644 index 000000000..fd80efdcc --- /dev/null +++ b/pkg/kinflate/kinflate.go @@ -0,0 +1,189 @@ +/* +Copyright 2017 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 kinflate + +import ( + "fmt" + "io" + "io/ioutil" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/kubectl/pkg/scheme" +) + +type kinflateOptions struct { + manifestDir string + namespace string +} + +type groupVersionKindName struct { + gvk schema.GroupVersionKind + // name of the resource. + name string +} + +// NewCmdKinflate creates a new kinflate command. +func NewCmdKinflate(out, errOut io.Writer) *cobra.Command { + var o kinflateOptions + + cmd := &cobra.Command{ + Use: "kinflate -f [path]", + Short: "Use a Manifest file to generate a set of api resources", + Long: "Use a Manifest file to generate a set of api resources", + Example: ` + # Use the Kube-manifest.yaml file under somedir/ to generate a set of api resources. + kinflate -f somedir/`, + Run: func(cmd *cobra.Command, args []string) { + err := o.Validate(cmd, args) + if err != nil { + panic(err) + } + err = o.Complete(cmd, args) + if err != nil { + panic(err) + } + err = o.RunKinflate(cmd, out, errOut) + if err != nil { + panic(err) + } + }, + } + + cmd.Flags().StringVarP(&o.manifestDir, "filename", "f", "", "Pass in directory that contains the Kube-manifest.yaml file.") + cmd.MarkFlagRequired("filename") + cmd.Flags().StringVarP(&o.namespace, "namespace", "o", "yaml", "Output mode. Support json or yaml.") + return cmd +} + +// Validate validates kinflate command. +func (o *kinflateOptions) Validate(cmd *cobra.Command, args []string) error { + return nil +} + +// Complete completes kinflate command. +func (o *kinflateOptions) Complete(cmd *cobra.Command, args []string) error { + return nil +} + +// RunKinflate runs kinflate command (do real work). +func (o *kinflateOptions) RunKinflate(cmd *cobra.Command, out, errOut io.Writer) error { + decoder := unstructured.UnstructuredJSONScheme + + baseFiles, overlayFiles, overlayPkg, err := loadBaseAndOverlayPkg(o.manifestDir) + if err != nil { + return err + } + + // This func will build a visitor given filenameOptions. + // It will visit each info and populate the map. + populateResourceMap := func(files []string, m map[groupVersionKindName][]byte) error { + for _, file := range files { + content, err := ioutil.ReadFile(file) + if err != nil { + return err + } + + // try converting to json, if there is a error, probably because the content is already json. + jsoncontent, err := yaml.YAMLToJSON(content) + if err != nil { + fmt.Fprintf(errOut, "error when trying to convert yaml to json: %v\n", err) + } else { + content = jsoncontent + } + + obj, gvk, err := decoder.Decode(content, nil, nil) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + name := accessor.GetName() + gvkn := groupVersionKindName{gvk: *gvk, name: name} + if err != nil { + return err + } + if _, found := m[gvkn]; found { + return fmt.Errorf("unexpected same groupVersionKindName: %#v", gvkn) + } + m[gvkn] = content + } + return nil + } + + // map from GroupVersionKind to marshaled json bytes + overlayResouceMap := map[groupVersionKindName][]byte{} + err = populateResourceMap(overlayFiles, overlayResouceMap) + if err != nil { + return err + } + + // map from GroupVersionKind to marshaled json bytes + baseResouceMap := map[groupVersionKindName][]byte{} + err = populateResourceMap(baseFiles, baseResouceMap) + if err != nil { + return err + } + + // Strategic merge the resources exist in both base and overlay. + for gvkn, base := range baseResouceMap { + // Merge overlay with base resource. + if overlay, found := overlayResouceMap[gvkn]; found { + versionedObj, err := scheme.Scheme.New(gvkn.gvk) + if err != nil { + switch { + case runtime.IsNotRegisteredError(err): + return fmt.Errorf("CRD and TPR are not supported now: %v", err) + default: + return err + } + } + merged, err := strategicpatch.StrategicMergePatch(base, overlay, versionedObj) + if err != nil { + return err + } + baseResouceMap[gvkn] = merged + delete(overlayResouceMap, gvkn) + } + } + + // If there are resources in overlay that are not defined in base, just add it to base. + if len(overlayResouceMap) > 0 { + for gvkn, jsonObj := range overlayResouceMap { + baseResouceMap[gvkn] = jsonObj + } + } + + // Inject the labels, annotations and name prefix. + // Then print the object. + for _, jsonObj := range baseResouceMap { + yamlObj, err := updateMetadata(jsonObj, overlayPkg) + if err != nil { + return err + } + fmt.Fprintf(out, "---\n%s", yamlObj) + } + return nil +} diff --git a/pkg/kinflate/util.go b/pkg/kinflate/util.go index 68bbfbb4e..f17b14036 100644 --- a/pkg/kinflate/util.go +++ b/pkg/kinflate/util.go @@ -14,62 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package manifest +package kinflate import ( + "errors" "io/ioutil" + "path" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" ) const kubeManifestFileName = "Kube-manifest.yaml" -// // loadBaseAndOverlayPkg returns: -// // - List of FilenameOptions, each FilenameOptions contains all the files and whether recursive for each base defined in overlay kube-manifest.yaml. -// // - Fileoptions for overlay. -// // - Package object for overlay. -// // - A potential error. -// func loadBaseAndOverlayPkg(f string) ([]resource.FilenameOptions, resource.FilenameOptions, *manifest.Manifest, error) { -// overlay, err := loadManifestPkg(path.Join(f, kubeManifestFileName)) -// if err != nil { -// return nil, resource.FilenameOptions{}, nil, err -// } -// overlayFileOptions := resource.FilenameOptions{ -// // TODO: support `recursive` when we figure out what its behavior should be. -// // Recursive: overlay.Recursive -// } -// for _, o := range overlay.Patches { -// overlayFileOptions.Filenames = append(overlayFileOptions.Filenames, path.Join(f, o)) -// } +// loadBaseAndOverlayPkg returns: +// - List of FilenameOptions, each FilenameOptions contains all the files and whether recursive for each base defined in overlay kube-manifest.yaml. +// - Fileoptions for overlay. +// - Package object for overlay. +// - A potential error. +func loadBaseAndOverlayPkg(f string) ([]string, []string, *manifest.Manifest, error) { + overlay, err := loadManifestPkg(path.Join(f, kubeManifestFileName)) + if err != nil { + return nil, nil, nil, err + } -// if len(overlay.Resources) == 0 { -// return nil, resource.FilenameOptions{}, nil, errors.New("expect at least one base, but got 0") -// } + // TODO: support `recursive` when we figure out what its behavior should be. + // Recursive: overlay.Recursive + overlayFiles := []string{} -// var baseFileOptionsList []resource.FilenameOptions -// for _, base := range overlay.Resources { -// var baseFilenames []string -// baseManifest, err := loadManifestPkg(path.Join(f, base, kubeManifestFileName)) -// if err != nil { -// return nil, resource.FilenameOptions{}, nil, err -// } -// for _, filename := range baseManifest.Resources { -// baseFilenames = append(baseFilenames, path.Join(f, base, filename)) -// } -// baseFileOptions := resource.FilenameOptions{ -// Filenames: baseFilenames, -// // TODO: support `recursive` when we figure out what its behavior should be. -// // Recursive: baseManifest.Recursive, -// } -// baseFileOptionsList = append(baseFileOptionsList, baseFileOptions) -// } + for _, o := range overlay.Patches { + overlayFiles = append(overlayFiles, path.Join(f, o)) + } -// return baseFileOptionsList, overlayFileOptions, overlay, nil -// } + if len(overlay.Resources) == 0 { + return nil, nil, nil, errors.New("expect at least one base, but got 0") + } + + var baseFiles []string + for _, base := range overlay.Resources { + baseManifest, err := loadManifestPkg(path.Join(f, base, kubeManifestFileName)) + if err != nil { + return nil, nil, nil, err + } + for _, filename := range baseManifest.Resources { + baseFiles = append(baseFiles, path.Join(f, base, filename)) + } + } + + return baseFiles, overlayFiles, overlay, nil +} // loadManifestPkg loads a manifest file and parse it in to the Package object. func loadManifestPkg(filename string) (*manifest.Manifest, error) { @@ -84,14 +80,16 @@ func loadManifestPkg(filename string) (*manifest.Manifest, error) { } // updateMetadata will inject the labels and annotations and add name prefix. -func updateMetadata(obj runtime.Object, overlayPkg *manifest.Manifest) error { - if overlayPkg == nil { - return nil +func updateMetadata(jsonObj []byte, overlayPkg *manifest.Manifest) ([]byte, error) { + if len(jsonObj) == 0 || overlayPkg == nil { + return nil, nil } + obj, _, err := unstructured.UnstructuredJSONScheme.Decode(jsonObj, nil, nil) + accessor, err := meta.Accessor(obj) if err != nil { - return err + return nil, err } accessor.SetName(overlayPkg.NamePrefix + accessor.GetName()) @@ -114,5 +112,5 @@ func updateMetadata(obj runtime.Object, overlayPkg *manifest.Manifest) error { } accessor.SetAnnotations(annotations) - return nil + return yaml.Marshal(obj) }