Skip to content

Commit

Permalink
refactor: move split from cmd to konf pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonTheLeg committed Oct 23, 2022
1 parent 577414b commit 6bbe5b1
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 189 deletions.
89 changes: 17 additions & 72 deletions cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"io"

"github.com/simontheleg/konf-go/konf"
log "github.com/simontheleg/konf-go/log"
Expand All @@ -20,8 +21,8 @@ type konfFile struct {
type importCmd struct {
fs afero.Fs

determineConfigs func(afero.Fs, string) ([]*konfFile, error)
writeConfig func(afero.Fs, *konfFile) error
determineConfigs func(io.Reader) ([]*konf.Config, error)
writeConfig func(afero.Fs, *konf.Config) error
deleteOriginalConfig func(afero.Fs, string) error

move bool
Expand All @@ -34,7 +35,7 @@ func newImportCmd() *importCmd {

ic := &importCmd{
fs: fs,
determineConfigs: determineConfigs,
determineConfigs: konf.KonfsFromKubeconfig,
writeConfig: writeConfig,
deleteOriginalConfig: deleteOriginalConfig,
}
Expand All @@ -59,21 +60,26 @@ contain a single context. Import will take care of splitting if necessary.`,
func (c *importCmd) importf(cmd *cobra.Command, args []string) error {
fpath := args[0] // safe, as we specify cobra.ExactArgs(1)

confs, err := c.determineConfigs(c.fs, fpath)
f, err := c.fs.Open(fpath)
if err != nil {
return err
}

if len(confs) == 0 {
konfs, err := c.determineConfigs(f)
if err != nil {
return err
}

if len(konfs) == 0 {
return fmt.Errorf("no contexts found in file %q", fpath)
}

for _, conf := range confs {
err = c.writeConfig(c.fs, conf)
for _, k := range konfs {
err = c.writeConfig(c.fs, k)
if err != nil {
return err
}
log.Info("Imported konf from %q successfully into %q\n", fpath, conf.FilePath)
log.Info("Imported konf from %q successfully into %q\n", fpath, k.StorePath)
}

if c.move {
Expand All @@ -86,74 +92,13 @@ func (c *importCmd) importf(cmd *cobra.Command, args []string) error {
return nil
}

// determineConfigs returns the individual configs from a konfigfile
// This is required as konfig requires each kubeconfig in its store to
// only contain a single context
// If more than one cluster is in a kubeconfig, determineConfig will split it up
// into multiple konfigFile and returns them as a slice
func determineConfigs(f afero.Fs, fpath string) ([]*konfFile, error) {

b, err := afero.ReadFile(f, fpath)
if err != nil {
return nil, err
}

var origConf k8s.Config
err = yaml.Unmarshal(b, &origConf)
if err != nil {
return nil, err
}

// basically should be as simple as
// 1. Loop through all the contexts
// 2. Find the corresponding cluster for each context
// 3. Find the corresponding user for each context
// 4. Create a new konfigFile for each context mapped to its cluster

var konfs = []*konfFile{}
for _, curCon := range origConf.Contexts {

cluster := k8s.NamedCluster{}
for _, curCl := range origConf.Clusters {
if curCl.Name == curCon.Context.Cluster {
cluster = curCl
break
}
}
user := k8s.NamedAuthInfo{}
for _, curU := range origConf.AuthInfos {
if curU.Name == curCon.Context.AuthInfo {
user = curU
break
}
}

var k konfFile
// TODO it might make sense to build in a duplicate detection here. This would ensure that the store is trustworthy, which in return makes it easy for
// TODO the set command as it does not need any verification
id := konf.IDFromClusterAndContext(cluster.Name, curCon.Name)
k.FilePath = id.StorePath()
k.Content.AuthInfos = append(k.Content.AuthInfos, user)
k.Content.Clusters = append(k.Content.Clusters, cluster)
k.Content.Contexts = append(k.Content.Contexts, curCon)

k.Content.APIVersion = origConf.APIVersion
k.Content.Kind = origConf.Kind
k.Content.CurrentContext = curCon.Name

konfs = append(konfs, &k)
}

return konfs, nil
}

func writeConfig(f afero.Fs, kf *konfFile) error {
b, err := yaml.Marshal(kf.Content)
func writeConfig(f afero.Fs, kf *konf.Config) error {
b, err := yaml.Marshal(kf.Kubeconfig)
if err != nil {
return err
}

err = afero.WriteFile(f, kf.FilePath, b, utils.KonfPerm)
err = afero.WriteFile(f, kf.StorePath, b, utils.KonfPerm)
if err != nil {
return err
}
Expand Down
125 changes: 8 additions & 117 deletions cmd/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package cmd
import (
"errors"
"fmt"
"io"
"io/fs"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/simontheleg/konf-go/konf"
"github.com/simontheleg/konf-go/testhelper"
"github.com/spf13/afero"
Expand All @@ -21,11 +21,11 @@ func TestImport(t *testing.T) {
var writeConfigCalledCount int
var deleteOriginalConfigCalled bool
// using just a wrapper here instead of a full mock, makes testing it slightly easier
var wrapDetermineConfig = func(f afero.Fs, fpath string) ([]*konfFile, error) {
var wrapDetermineConfig = func(r io.Reader) ([]*konf.Config, error) {
determineConfigsCalled = true
return determineConfigs(f, fpath)
return konf.KonfsFromKubeconfig(r)
}
var mockWriteConfig = func(afero.Fs, *konfFile) error { writeConfigCalledCount++; return nil }
var mockWriteConfig = func(afero.Fs, *konf.Config) error { writeConfigCalledCount++; return nil }
var mockDeleteOriginalConfig = func(afero.Fs, string) error { deleteOriginalConfigCalled = true; return nil }

type ExpCalls struct {
Expand Down Expand Up @@ -102,9 +102,9 @@ func TestImport(t *testing.T) {
}
}

var devEUControlGroup = &konfFile{
FilePath: konf.IDFromClusterAndContext("dev-eu-1", "dev-eu").StorePath(),
Content: k8s.Config{
var devEUControlGroup = &konf.Config{
StorePath: konf.IDFromClusterAndContext("dev-eu-1", "dev-eu").StorePath(),
Kubeconfig: k8s.Config{
APIVersion: "v1",
Kind: "Config",
CurrentContext: "dev-eu",
Expand Down Expand Up @@ -134,115 +134,6 @@ var devEUControlGroup = &konfFile{
},
}

var devASIAControlGroup = &konfFile{
FilePath: konf.IDFromClusterAndContext("dev-asia-1", "dev-asia").StorePath(),
Content: k8s.Config{
APIVersion: "v1",
Kind: "Config",
CurrentContext: "dev-asia",
Clusters: []k8s.NamedCluster{
{
Name: "dev-asia-1",
Cluster: k8s.Cluster{
Server: "https://192.168.0.1",
},
},
},
Contexts: []k8s.NamedContext{
{
Name: "dev-asia",
Context: k8s.Context{
Cluster: "dev-asia-1",
Namespace: "kube-system",
AuthInfo: "dev-asia",
},
},
},
AuthInfos: []k8s.NamedAuthInfo{
{
Name: "dev-asia",
AuthInfo: k8s.AuthInfo{},
},
},
},
}

func TestDetermineConfigs(t *testing.T) {
fm := testhelper.FilesystemManager{}

tt := map[string]struct {
FSCreator func() afero.Fs
konfpath string
ExpError error
ExpNumOfKonfigFile int
ExpKonfigFiles []*konfFile
}{
"SingleClusterSingleContext": {
FSCreator: testhelper.FSWithFiles(fm.StoreDir, fm.SingleClusterSingleContextEU),
konfpath: "./konf/store/dev-eu_dev-eu-1.yaml",
ExpError: nil,
ExpNumOfKonfigFile: 1,
ExpKonfigFiles: []*konfFile{
devEUControlGroup,
},
},
"multiClusterMultiContext": {
FSCreator: testhelper.FSWithFiles(fm.StoreDir, fm.MultiClusterMultiContext),
konfpath: "./konf/store/multi_multi_konf.yaml",
ExpError: nil,
ExpNumOfKonfigFile: 2,
ExpKonfigFiles: []*konfFile{
devASIAControlGroup,
devEUControlGroup,
},
},
"multiClusterSingleContext": {
FSCreator: testhelper.FSWithFiles(fm.StoreDir, fm.MultiClusterSingleContext),
konfpath: "./konf/store/multi_konf.yaml",
ExpError: nil,
ExpNumOfKonfigFile: 1,
ExpKonfigFiles: []*konfFile{
devASIAControlGroup,
},
},
"emptyConfig": {
FSCreator: testhelper.FSWithFiles(),
konfpath: "i-dont-exist.yaml",
ExpError: fmt.Errorf("open i-dont-exist.yaml: file does not exist"),
ExpNumOfKonfigFile: 0,
ExpKonfigFiles: nil,
},
// All for the coverage ;)
"invalidConfig": {
FSCreator: testhelper.FSWithFiles(fm.StoreDir, fm.InvalidYaml),
konfpath: "./konf/store/no-konf.yaml",
ExpError: fmt.Errorf("error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type v1.Config"),
ExpNumOfKonfigFile: 0,
ExpKonfigFiles: nil,
},
}

for name, tc := range tt {
t.Run(name, func(t *testing.T) {
res, err := determineConfigs(tc.FSCreator(), tc.konfpath)

if !testhelper.EqualError(err, tc.ExpError) {
t.Errorf("Want error '%s', got '%s'", tc.ExpError, err)
}

if len(tc.ExpKonfigFiles) != tc.ExpNumOfKonfigFile {
t.Errorf("Want %d, got %d kubeconfigs", tc.ExpNumOfKonfigFile, len(tc.ExpKonfigFiles))
}

if !cmp.Equal(tc.ExpKonfigFiles, res) {
t.Errorf("Exp and given KonfigFiles differ:\n'%s'", cmp.Diff(tc.ExpKonfigFiles, res))
}

})
}

}

func TestWriteConfig(t *testing.T) {
fm := testhelper.FilesystemManager{}
f := testhelper.FSWithFiles(fm.ActiveDir, fm.StoreDir)()
Expand Down Expand Up @@ -271,7 +162,7 @@ users:
t.Errorf("Exp err to be nil but got %q", err)
}

b, err := afero.ReadFile(f, devEUControlGroup.FilePath)
b, err := afero.ReadFile(f, devEUControlGroup.StorePath)
if err != nil {
t.Errorf("Exp read in file without any issues, but got %q", err)
}
Expand Down
11 changes: 11 additions & 0 deletions konf/konfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package konf

import (
k8s "k8s.io/client-go/tools/clientcmd/api/v1"
)

type Config struct {
Id KonfID
Kubeconfig k8s.Config
StorePath string
}
69 changes: 69 additions & 0 deletions konf/split.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package konf

import (
"io"

k8s "k8s.io/client-go/tools/clientcmd/api/v1"
"sigs.k8s.io/yaml"
)

// KonfsFromKubeconfig takes in the content of a kubeconfig and splits it into
// one or multiple konfs.
//
// No error is being returned if the kubeconfig contains no contexts, instead
// konfs is simply an empty slice
func KonfsFromKubeconfig(kubeconfig io.Reader) (konfs []*Config, err error) {
konfs = []*Config{}

b, err := io.ReadAll(kubeconfig)
if err != nil {
return nil, err
}

var origConf k8s.Config
err = yaml.Unmarshal(b, &origConf)
if err != nil {
return nil, err
}

// basically should be as simple as
// 1. Loop through all the contexts
// 2. Find the corresponding cluster for each context
// 3. Find the corresponding user for each context
// 4. Create a new konfigFile for each context mapped to its cluster

for _, curCon := range origConf.Contexts {

cluster := k8s.NamedCluster{}
for _, curCl := range origConf.Clusters {
if curCl.Name == curCon.Context.Cluster {
cluster = curCl
break
}
}
user := k8s.NamedAuthInfo{}
for _, curU := range origConf.AuthInfos {
if curU.Name == curCon.Context.AuthInfo {
user = curU
break
}
}

var k Config
id := IDFromClusterAndContext(cluster.Name, curCon.Name)
// TODO need to remove this. StorePath should only be setable by store pkg later on
k.StorePath = id.StorePath()
k.Id = id
k.Kubeconfig.AuthInfos = append(k.Kubeconfig.AuthInfos, user)
k.Kubeconfig.Clusters = append(k.Kubeconfig.Clusters, cluster)
k.Kubeconfig.Contexts = append(k.Kubeconfig.Contexts, curCon)

k.Kubeconfig.APIVersion = origConf.APIVersion
k.Kubeconfig.Kind = origConf.Kind
k.Kubeconfig.CurrentContext = curCon.Name

konfs = append(konfs, &k)
}

return konfs, nil
}
Loading

0 comments on commit 6bbe5b1

Please sign in to comment.