Skip to content

Commit

Permalink
kpt init command updates Kptfile inventory parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
seans3 committed Sep 25, 2020
1 parent 2466c0e commit 3c44efb
Show file tree
Hide file tree
Showing 3 changed files with 389 additions and 0 deletions.
183 changes: 183 additions & 0 deletions commands/initcmd.go
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
}
204 changes: 204 additions & 0 deletions commands/initcmd_test.go
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)
})
}
}
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,8 @@ sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbL
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/kustomize/cmd/config v0.8.2-0.20200924195921-3a5951563dec h1://OdIqwosbLerHd6Zk4/Y+BWSuFjlXmaEUmJU8gxyyM=
sigs.k8s.io/kustomize/cmd/config v0.8.2-0.20200924195921-3a5951563dec/go.mod h1:PlMvypgZFfP2rQKS5tOpYHnRb35tR1wJo7C+frqudWU=
sigs.k8s.io/kustomize/kyaml v0.8.1 h1:5GRanVGU6+iq3ERTiQD9VIfyGByFVB4z4GthP8NkRYE=
sigs.k8s.io/kustomize/kyaml v0.8.1/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw=
sigs.k8s.io/kustomize/kyaml v0.8.1/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw=
sigs.k8s.io/kustomize/kyaml v0.8.2-0.20200924195921-3a5951563dec h1:Nz39T2Tnj6mRG0Rhfz8YJHxrtMtPTQ1F9roBPUPMles=
sigs.k8s.io/kustomize/kyaml v0.8.2-0.20200924195921-3a5951563dec/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw=
Expand Down

0 comments on commit 3c44efb

Please sign in to comment.