-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
kpt init command updates Kptfile inventory parameters
- Loading branch information
Showing
3 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
// Copyright 2020 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package commands | ||
|
||
import ( | ||
"crypto/sha1" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/GoogleContainerTools/kpt/pkg/kptfile" | ||
"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" | ||
"github.com/spf13/cobra" | ||
"k8s.io/cli-runtime/pkg/genericclioptions" | ||
cmdutil "k8s.io/kubectl/pkg/cmd/util" | ||
"k8s.io/kubectl/pkg/util/i18n" | ||
"sigs.k8s.io/cli-utils/pkg/common" | ||
"sigs.k8s.io/cli-utils/pkg/config" | ||
) | ||
|
||
const defaultInventoryName = "inventory" | ||
|
||
var invExistsError = `ResourceGroup configuration has already been created. Changing | ||
them after a package has been applied to the cluster can lead to | ||
undesired results. Use the --force flag to suppress this error. | ||
` | ||
|
||
// KptInitOptions encapsulates fields for kpt init command. This init command | ||
// fills in inventory values in the Kptfile. | ||
type KptInitOptions struct { | ||
factory cmdutil.Factory | ||
ioStreams genericclioptions.IOStreams | ||
dir string // Directory with Kptfile | ||
force bool // Set inventory values even if already set in Kptfile | ||
name string // Inventory object name | ||
namespace string // Inventory object namespace | ||
inventoryID string // Inventory object unique identifier label | ||
} | ||
|
||
// NewKptInitOptions returns a pointer to an initial KptInitOptions structure. | ||
func NewKptInitOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *KptInitOptions { | ||
return &KptInitOptions{ | ||
factory: f, | ||
ioStreams: ioStreams, | ||
} | ||
} | ||
|
||
// Complete fills in fields for KptInitOptions based on the passed "args". | ||
func (io *KptInitOptions) Run(args []string) error { | ||
// Set the init options directory. | ||
if len(args) != 1 { | ||
return fmt.Errorf("need one 'directory' arg; have %d", len(args)) | ||
} | ||
dir, err := config.NormalizeDir(args[0]) | ||
if err != nil { | ||
return err | ||
} | ||
io.dir = dir | ||
// Set the init options inventory object namespace. | ||
ns, err := config.FindNamespace(io.factory.ToRawKubeConfigLoader(), io.dir) | ||
if err != nil { | ||
return err | ||
} | ||
io.namespace = strings.TrimSpace(ns) | ||
fmt.Fprintf(io.ioStreams.Out, "namespace: %s is used for inventory object\n", io.namespace) | ||
// Set the init options default inventory object name, if not set by flag. | ||
if io.name == "" { | ||
randomSuffix := common.RandomStr(time.Now().UTC().UnixNano()) | ||
io.name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix) | ||
} | ||
// Set the init options inventory id label. | ||
id, err := generateID(io.name, io.namespace, time.Now()) | ||
if err != nil { | ||
return err | ||
} | ||
io.inventoryID = id | ||
// Finally, update these values in the Inventory section of the Kptfile. | ||
return io.updateKptfile() | ||
} | ||
|
||
// generateID returns the string which is a SHA1 hash of the passed namespace | ||
// and name, with the unix timestamp string concatenated. Returns an error | ||
// if either the namespace or name are empty. | ||
func generateID(namespace string, name string, t time.Time) (string, error) { | ||
hashStr, err := generateHash(namespace, name) | ||
if err != nil { | ||
return "", err | ||
} | ||
timeStr := strconv.FormatInt(t.UTC().UnixNano(), 10) | ||
return fmt.Sprintf("%s-%s", hashStr, timeStr), nil | ||
} | ||
|
||
// generateHash returns the SHA1 hash of the concatenated "namespace:name" string, | ||
// or an error if either namespace or name is empty. | ||
func generateHash(namespace string, name string) (string, error) { | ||
if len(namespace) == 0 || len(name) == 0 { | ||
return "", fmt.Errorf("can not generate hash with empty namespace or name") | ||
} | ||
str := fmt.Sprintf("%s:%s", namespace, name) | ||
h := sha1.New() | ||
if _, err := h.Write([]byte(str)); err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("%x", (h.Sum(nil))), nil | ||
} | ||
|
||
// Run fills in the inventory object values into the Kptfile. | ||
func (io *KptInitOptions) updateKptfile() error { | ||
// Read the Kptfile io io.dir | ||
kf, err := kptfileutil.ReadFile(io.dir) | ||
if err != nil { | ||
return err | ||
} | ||
// Validate the inventory values don't already exist | ||
isEmpty := kptfileInventoryEmpty(kf.Inventory) | ||
if !isEmpty && !io.force { | ||
return fmt.Errorf(invExistsError) | ||
} | ||
// Check the new inventory values are valid. | ||
if err := io.validate(); err != nil { | ||
return err | ||
} | ||
// Finally, set the inventory parameters in the Kptfile and write it. | ||
kf.Inventory = kptfile.Inventory{ | ||
Namespace: io.namespace, | ||
Name: io.name, | ||
InventoryID: io.inventoryID, | ||
} | ||
if err := kptfileutil.WriteFile(io.dir, kf); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// validate ensures the inventory object parameters are valid. | ||
func (io *KptInitOptions) validate() error { | ||
// name is required | ||
if len(io.name) == 0 { | ||
return fmt.Errorf("inventory name is missing") | ||
} | ||
// namespace is required | ||
if len(io.namespace) == 0 { | ||
return fmt.Errorf("inventory namespace is missing") | ||
} | ||
// inventoryID is required | ||
if len(io.inventoryID) == 0 { | ||
return fmt.Errorf("inventoryID is missing") | ||
} | ||
return nil | ||
} | ||
|
||
// kptfileInventoryEmpty returns true if the Inventory structure | ||
// in the Kptfile is empty; false otherwise. | ||
func kptfileInventoryEmpty(inv kptfile.Inventory) bool { | ||
if len(inv.Name) > 0 { | ||
return false | ||
} | ||
if len(inv.Namespace) > 0 { | ||
return false | ||
} | ||
if len(inv.InventoryID) > 0 { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
// NewCmdInit returns the cobra command for the init command. | ||
func NewCmdInit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { | ||
io := NewKptInitOptions(f, ioStreams) | ||
cmd := &cobra.Command{ | ||
Use: "init DIRECTORY", | ||
DisableFlagsInUseLine: true, | ||
Short: i18n.T("Initialize inventory parameters into Kptfile"), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return io.Run(args) | ||
}, | ||
} | ||
cmd.Flags().StringVar(&io.name, "name", "", "Inventory object name") | ||
cmd.Flags().BoolVar(&io.force, "force", false, "Set inventory values even if already set in Kptfile") | ||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
// Copyright 2020 The Kubernetes Authors. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package commands | ||
|
||
import ( | ||
"io/ioutil" | ||
"path/filepath" | ||
"testing" | ||
"time" | ||
|
||
"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" | ||
"github.com/stretchr/testify/assert" | ||
"k8s.io/cli-runtime/pkg/genericclioptions" | ||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" | ||
) | ||
|
||
var ( | ||
inventoryName = "inventory-obj-name" | ||
inventoryNamespace = "test-namespace" | ||
inventoryID = "XXXXXXX-OOOOOOOOOO-XXXX" | ||
) | ||
|
||
var kptFile = ` | ||
apiVersion: kpt.dev/v1alpha1 | ||
kind: Kptfile | ||
metadata: | ||
name: test1 | ||
upstream: | ||
type: git | ||
git: | ||
commit: 786b898857bd7e9647c229d5f39b0be4de86c915 | ||
repo: git@github.com:seans3/blueprint-helloworld | ||
directory: / | ||
ref: master | ||
` | ||
|
||
var kptFileWithInventory = ` | ||
apiVersion: kpt.dev/v1alpha1 | ||
kind: Kptfile | ||
metadata: | ||
name: test1 | ||
upstream: | ||
type: git | ||
git: | ||
commit: 786b898857bd7e9647c229d5f39b0be4de86c915 | ||
repo: git@github.com:seans3/blueprint-helloworld | ||
directory: / | ||
ref: master | ||
inventory: | ||
name: foo | ||
namespace: bar | ||
inventoryID: SSSSSSSSSS-RRRRR | ||
` | ||
|
||
var testTime = time.Unix(5555555, 66666666) | ||
|
||
func TestKptInitOptions_generateID(t *testing.T) { | ||
testCases := map[string]struct { | ||
namespace string | ||
name string | ||
t time.Time | ||
expected string | ||
isError bool | ||
}{ | ||
"Empty inventory namespace is an error": { | ||
name: inventoryName, | ||
namespace: "", | ||
t: testTime, | ||
isError: true, | ||
}, | ||
"Empty inventory name is an error": { | ||
name: "", | ||
namespace: inventoryNamespace, | ||
t: testTime, | ||
isError: true, | ||
}, | ||
"Namespace/name hash is valid": { | ||
name: inventoryName, | ||
namespace: inventoryNamespace, | ||
t: testTime, | ||
expected: "fa6dc0d39b0465b90f101c2ad50d50e9b4022f23-5555555066666666", | ||
isError: false, | ||
}, | ||
} | ||
|
||
for tn, tc := range testCases { | ||
t.Run(tn, func(t *testing.T) { | ||
actual, err := generateID(tc.namespace, tc.name, tc.t) | ||
// Check if there should be an error | ||
if tc.isError { | ||
if err == nil { | ||
t.Fatalf("expected error but received none") | ||
} | ||
return | ||
} | ||
assert.NoError(t, err) | ||
if tc.expected != actual { | ||
t.Errorf("expecting generated id (%s), got (%s)", tc.expected, actual) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestKptInitOptions_updateKptfile(t *testing.T) { | ||
testCases := map[string]struct { | ||
kptfile string | ||
name string | ||
namespace string | ||
inventoryID string | ||
force bool | ||
isError bool | ||
}{ | ||
"Empty inventory name is an error": { | ||
kptfile: kptFile, | ||
name: "", | ||
namespace: inventoryNamespace, | ||
inventoryID: inventoryID, | ||
force: false, | ||
isError: true, | ||
}, | ||
"Empty inventory namespace is an error": { | ||
kptfile: kptFile, | ||
name: inventoryName, | ||
namespace: "", | ||
inventoryID: inventoryID, | ||
force: false, | ||
isError: true, | ||
}, | ||
"Empty inventory id is an error": { | ||
kptfile: kptFile, | ||
name: inventoryName, | ||
namespace: inventoryNamespace, | ||
inventoryID: "", | ||
force: false, | ||
isError: true, | ||
}, | ||
"Kptfile with inventory already set is error": { | ||
kptfile: kptFileWithInventory, | ||
name: inventoryName, | ||
namespace: inventoryNamespace, | ||
inventoryID: inventoryID, | ||
force: false, | ||
isError: true, | ||
}, | ||
"KptInitOptions default": { | ||
kptfile: kptFile, | ||
name: inventoryName, | ||
namespace: inventoryNamespace, | ||
inventoryID: inventoryID, | ||
force: false, | ||
isError: false, | ||
}, | ||
"KptInitOptions force sets inventory values when already set": { | ||
kptfile: kptFileWithInventory, | ||
name: inventoryName, | ||
namespace: inventoryNamespace, | ||
inventoryID: inventoryID, | ||
force: true, | ||
isError: false, | ||
}, | ||
} | ||
|
||
for tn, tc := range testCases { | ||
t.Run(tn, func(t *testing.T) { | ||
// Set up fake test factory | ||
tf := cmdtesting.NewTestFactory().WithNamespace("test-ns") | ||
defer tf.Cleanup() | ||
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled | ||
|
||
// Set up temp directory with Ktpfile | ||
dir, err := ioutil.TempDir("", "kpt-init-options-test") | ||
assert.NoError(t, err) | ||
p := filepath.Join(dir, "Kptfile") | ||
err = ioutil.WriteFile(p, []byte(tc.kptfile), 0600) | ||
assert.NoError(t, err) | ||
|
||
// Create KptInitOptions and call Run() | ||
initOptions := NewKptInitOptions(tf, ioStreams) | ||
initOptions.dir = dir | ||
initOptions.force = tc.force | ||
initOptions.name = tc.name | ||
initOptions.namespace = tc.namespace | ||
initOptions.inventoryID = tc.inventoryID | ||
err = initOptions.updateKptfile() | ||
|
||
// Check if there should be an error | ||
if tc.isError { | ||
if err == nil { | ||
t.Fatalf("expected error but received none") | ||
} | ||
return | ||
} | ||
|
||
// Otherwise, validate the kptfile values | ||
assert.NoError(t, err) | ||
kf, err := kptfileutil.ReadFile(initOptions.dir) | ||
assert.NoError(t, err) | ||
assert.Equal(t, inventoryName, kf.Inventory.Name) | ||
assert.Equal(t, inventoryNamespace, kf.Inventory.Namespace) | ||
assert.Equal(t, inventoryID, kf.Inventory.InventoryID) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters