Add phase tree command in package module

* This commit is the smaller patchset from earlier work:
  https://review.opendev.org/#/c/750449/

* This is the first series of patchset

Change-Id: Ib1ff74eba65de7c7c59cf8f4fd26b15e388ba368
Signed-off-by: bijayasharma <vetbijaya@gmail.com>
Relates-To: #296
This commit is contained in:
bijayasharma 2020-11-11 14:02:23 -05:00 committed by Bijaya Sharma
parent 957abd5432
commit f9378e74b0
11 changed files with 637 additions and 0 deletions

View File

@ -37,3 +37,6 @@ const (
ClusterctlMetadataVersion = "v1alpha3"
ClusterctlMetadataGroup = "clusterctl.cluster.x-k8s.io"
)
// KustomizationFile is used for kustomization file
const KustomizationFile = "kustomization.yaml"

View File

@ -0,0 +1,4 @@
inventory:
path: "manifests/site/inventory"
phase:
path: "no_plan_site/phases"

View File

@ -0,0 +1,10 @@
# Site-level, phase-specific lists of hosts to generate
# This is used by the hostgenerator-m3 function to narrow down the site-level
# host-catalogue to just the hosts needed for a particular phase.
apiVersion: airshipit.org/v1alpha1
kind: VariableCatalogue
metadata:
name: host-generation-catalogue
hosts:
m3:
- node03

View File

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- host-generation.yaml

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- nodes
generators:
- hostgenerator

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
commonLabels:
airshipit.org/k8s-role: controlplane-host

186
pkg/document/tree.go Normal file
View File

@ -0,0 +1,186 @@
/*
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 document
import (
"fmt"
"io"
"path/filepath"
"sigs.k8s.io/kustomize/api/types"
"opendev.org/airship/airshipctl/pkg/fs"
)
// KustomNode is used to create name and data to display tree structure
type KustomNode struct {
Name string // name used for display purposes (cli)
Data string // this could be a Kustomization object, or a string containing a file path
Children []KustomNode
Writer io.Writer
}
// BuildKustomTree creates a tree based on entrypoint
func BuildKustomTree(entrypoint string, writer io.Writer, manifestsDir string) (KustomNode, error) {
fs := fs.NewDocumentFs()
name, err := filepath.Rel(manifestsDir, entrypoint)
if err != nil {
name = entrypoint
}
root := KustomNode{
Name: name,
Data: entrypoint,
Children: []KustomNode{},
Writer: writer,
}
resMap, err := MakeResMap(fs, entrypoint)
if err != nil {
return KustomNode{}, err
}
for sourceType, sources := range resMap {
n := KustomNode{
Name: sourceType,
Writer: writer,
}
for _, s := range sources {
if !fs.IsDir(s) {
name, err := filepath.Rel(manifestsDir, s)
if err != nil {
name = s
}
n.Children = append(n.Children, KustomNode{
Name: name,
Data: s,
})
} else {
s = filepath.Join(s, KustomizationFile)
child, err := BuildKustomTree(s, writer, "")
if err != nil {
return KustomNode{}, err
}
n.Children = append(n.Children, child)
}
}
root.Children = append(root.Children, n)
}
return root, nil
}
//MakeResMap creates resmap based of kustomize types
func MakeResMap(fs fs.FileSystem, kfile string) (map[string][]string, error) {
if fs == nil {
return nil, fmt.Errorf("received nil filesystem")
}
bytes, err := fs.ReadFile(kfile)
if err != nil {
return nil, err
}
k := types.Kustomization{}
err = k.Unmarshal(bytes)
if err != nil {
return nil, err
}
basedir := filepath.Dir(kfile)
var resMap = make(map[string][]string)
for _, p := range k.Resources {
path := filepath.Join(basedir, p)
resMap["Resources"] = append(resMap["Resources"], path)
}
for _, p := range k.Crds {
path := filepath.Join(basedir, p)
resMap["Crds"] = append(resMap["Crds"], path)
}
buildConfigMapAndSecretGenerator(k, basedir, resMap)
for _, p := range k.Generators {
path := filepath.Join(basedir, p)
resMap["Generators"] = append(resMap["Generators"], path)
}
for _, p := range k.Transformers {
path := filepath.Join(basedir, p)
resMap["Transformers"] = append(resMap["Transformers"], path)
}
return resMap, nil
}
func buildConfigMapAndSecretGenerator(k types.Kustomization, basedir string, resMap map[string][]string) {
for _, p := range k.SecretGenerator {
for _, s := range p.FileSources {
path := filepath.Join(basedir, s)
resMap["SecretGenerator"] = append(resMap["SecretGenerator"], path)
}
}
for _, p := range k.ConfigMapGenerator {
for _, s := range p.FileSources {
path := filepath.Join(basedir, s)
resMap["ConfigMapGenerator"] = append(resMap["ConfigMapGenerator"], path)
}
}
}
// PrintTree prints tree view of phase
func (k KustomNode) PrintTree(prefix string) {
if prefix == "" {
basedir := filepath.Dir(k.Name)
dir := filepath.Base(basedir)
fmt.Fprintf(k.Writer, "%s [%s]\n", dir, basedir)
}
for i, child := range k.Children {
var subprefix string
knodes := GetKustomChildren(child)
if len(knodes) > 0 {
// we found a kustomize file, so print the subtree name first
if i == len(k.Children)-1 {
fmt.Fprintf(k.Writer, "%s└── %s\n", prefix, child.Name)
subprefix = " "
} else {
fmt.Fprintf(k.Writer, "%s├── %s\n", prefix, child.Name)
subprefix = "│ "
}
}
for j, c := range knodes {
bd := filepath.Dir(c.Name)
d := filepath.Base(bd)
name := fmt.Sprintf("%s [%s]", d, bd)
if j == len(knodes)-1 {
fmt.Printf("%s%s└── %s\n", prefix, subprefix, name)
c.PrintTree(fmt.Sprintf("%s%s ", prefix, subprefix))
} else {
fmt.Printf("%s%s├── %s\n", prefix, subprefix, name)
c.PrintTree(fmt.Sprintf("%s%s│ ", prefix, subprefix))
}
}
}
}
// GetKustomChildren returns children nodes of kustomnode
func GetKustomChildren(k KustomNode) []KustomNode {
nodes := []KustomNode{}
for _, c := range k.Children {
if len(c.Children) > 0 {
nodes = append(nodes, c)
}
}
return nodes
}

297
pkg/document/tree_test.go Normal file
View File

@ -0,0 +1,297 @@
/*
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
https://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 document_test
import (
"bytes"
"io"
"io/ioutil"
"os"
"testing"
"bufio"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/fs"
)
func KustomNodeTestdata(writer io.Writer) document.KustomNode {
return document.KustomNode{
Name: "workers-targetphase/kustomization.yaml",
Data: "testdata/workers-targetphase/kustomization.yaml",
Children: []document.KustomNode{
{
Name: "Resources",
Data: "",
Children: []document.KustomNode{
{
Name: "workers-targetphase/nodes/kustomization.yaml",
Data: "testdata/workers-targetphase/nodes/kustomization.yaml",
Children: []document.KustomNode{},
},
},
Writer: writer,
},
{
Name: "Generators",
Data: "",
Children: []document.KustomNode{
{
Name: "workers-targetphase/hostgenerator/kustomization.yaml",
Data: "testdata/workers-targetphase/hostgenerator/kustomization.yaml",
Children: []document.KustomNode{{
Name: "Resources",
Data: "",
Children: []document.KustomNode{{
Name: "workers-targetphase/hostgenerator/host-generation.yaml",
Data: "testdata/workers-targetphase/hostgenerator/host-generation.yaml",
},
},
Writer: writer,
},
},
},
},
Writer: writer,
},
},
Writer: writer,
}
}
func TestBuildKustomTree(t *testing.T) {
var b bytes.Buffer
w := bufio.NewWriter(&b)
type args struct {
entrypoint string
}
tests := []struct {
name string
args func(t *testing.T) args
want1 document.KustomNode
errContains string
}{
{
name: "success build tree",
args: func(t *testing.T) args {
return args{entrypoint: "testdata/workers-targetphase/kustomization.yaml"}
},
want1: KustomNodeTestdata(w),
},
{
name: "entrypoint doesn't exist",
args: func(t *testing.T) args {
return args{entrypoint: "tdata/kustomization.yaml"}
},
want1: KustomNodeTestdata(w),
errContains: "no such file or directory",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tArgs := tt.args(t)
manifestsDir := "testdata"
got1, actualErr := document.BuildKustomTree(tArgs.entrypoint, w, manifestsDir)
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.Equal(t, got1.Name, tt.want1.Name)
assert.Equal(t, len(got1.Children), len(tt.want1.Children))
}
})
}
}
func Test_makeResMap(t *testing.T) {
type args struct {
kfile string
fs fs.FileSystem
}
tests := []struct {
name string
args func(t *testing.T) args
want1 map[string][]string
errContains string
}{
{
args: func(t *testing.T) args {
return args{kfile: "testdata/workers-targetphase/kustomization.yaml", fs: fs.NewDocumentFs()}
},
name: "success resmap",
want1: map[string][]string{
"Generators": {
"testdata/workers-targetphase/hostgenerator",
},
"Resources": {
"testdata/workers-targetphase/nodes",
},
},
},
{
args: func(t *testing.T) args {
return args{kfile: "testdata/no_plan_site/phases/kustomization.yaml"}
},
name: "nil case",
want1: map[string][]string{},
errContains: "received nil filesystem",
},
{
args: func(t *testing.T) args {
return args{kfile: "t/workers-targetphase/kustomization.yaml", fs: fs.NewDocumentFs()}
},
name: "fail resmap,entrypoint not found",
want1: map[string][]string{
"Resources": {
"testdata/workers-targetphase/nodes",
},
},
errContains: "no such file or directory",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tArgs := tt.args(t)
got1, actualErr := document.MakeResMap(tArgs.fs, tArgs.kfile)
if tt.errContains != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errContains)
} else {
require.NoError(t, actualErr)
assert.Equal(t, got1, tt.want1)
}
})
}
}
func TestKustomNode_PrintTree(t *testing.T) {
var b bytes.Buffer
writer := bufio.NewWriter(&b)
type args struct {
prefix string
}
tests := []struct {
name string
init func(t *testing.T) document.KustomNode
want string
args func(t *testing.T) args
}{
{
name: "valid print tree",
args: func(t *testing.T) args {
return args{prefix: ""}
},
init: func(t *testing.T) document.KustomNode {
return KustomNodeTestdata(writer)
},
want: " └── hostgenerator [workers-targetphase/hostgenerator]\n",
},
}
rescueStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
require.Error(t, err)
}
os.Stdout = w
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tArgs := tt.args(t)
receiver := tt.init(t)
receiver.PrintTree(tArgs.prefix)
w.Close()
out, err := ioutil.ReadAll(r)
if err != nil {
require.Error(t, err)
}
os.Stdout = rescueStdout
assert.Equal(t, string(out), tt.want)
})
}
}
func Test_getKustomChildren(t *testing.T) {
type args struct {
k document.KustomNode
}
tests := []struct {
name string
args func(t *testing.T) args
want1 []document.KustomNode
}{
{
name: "success getkustomchildren",
args: func(t *testing.T) args {
return args{k: document.KustomNode{
Name: "Generators",
Data: "",
Children: []document.KustomNode{
{
Name: "workers-targetphase/hostgenerator/kustomization.yaml",
Data: "testdata/workers-targetphase/hostgenerator/kustomization.yaml",
Children: []document.KustomNode{{
Name: "workers-targetphase/hostgenerator/host-generation.yaml",
Data: "testdata/workers-targetphase/hostgenerator/host-generation.yaml",
},
},
},
},
},
}
},
want1: []document.KustomNode{
{
Name: "workers-targetphase/hostgenerator/kustomization.yaml",
Data: "testdata/workers-targetphase/hostgenerator/kustomization.yaml",
Children: []document.KustomNode{{
Name: "workers-targetphase/hostgenerator/host-generation.yaml",
Data: "testdata/workers-targetphase/hostgenerator/host-generation.yaml",
},
},
},
},
},
{
name: "no children nodes",
args: func(t *testing.T) args {
return args{k: document.KustomNode{
Name: "Transformers",
Data: "",
}}
},
want1: []document.KustomNode{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tArgs := tt.args(t)
got1 := document.GetKustomChildren(tArgs.k)
assert.Equal(t, got1, tt.want1)
})
}
}

View File

@ -16,9 +16,13 @@ package phase
import (
"io"
"os"
"path/filepath"
"strings"
"time"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
@ -84,3 +88,54 @@ func (c *PlanCommand) RunE() error {
return PrintPlan(plan, c.Writer)
}
// TreeCommand plan command
type TreeCommand struct {
Factory config.Factory
PhaseID ifc.ID
Writer io.Writer
Argument string
}
// RunE runs the phase tree command
func (c *TreeCommand) RunE() error {
var entrypoint string
cfg, err := c.Factory()
if err != nil {
return err
}
helper, err := NewHelper(cfg)
if err != nil {
return err
}
client := NewClient(helper)
var manifestsDir string
// check if its a relative path
if _, err = os.Stat(c.Argument); err == nil {
// capture manifests directory from phase relative path
manifestsDir = strings.SplitAfter(c.Argument, "/manifests")[0]
entrypoint = filepath.Join(c.Argument, document.KustomizationFile)
} else {
c.PhaseID.Name = c.Argument
manifestsDir = filepath.Join(helper.TargetPath(), helper.PhaseRepoDir())
var phase ifc.Phase
phase, err = client.PhaseByID(c.PhaseID)
if err != nil {
return err
}
var rootPath string
rootPath, err = phase.DocumentRoot()
if err != nil {
return err
}
entrypoint = filepath.Join(rootPath, document.KustomizationFile)
}
t, err := document.BuildKustomTree(entrypoint, c.Writer, manifestsDir)
if err != nil {
return err
}
t.PrintTree("")
return nil
}

View File

@ -169,3 +169,70 @@ func TestPlanCommand(t *testing.T) {
})
}
}
func TestTreeCommand(t *testing.T) {
tests := []struct {
name string
errContains string
factory config.Factory
}{
{
name: "Error config factory",
factory: func() (*config.Config, error) {
return nil, fmt.Errorf(testFactoryErr)
},
errContains: testFactoryErr,
},
{
name: "Error new helper",
factory: func() (*config.Config, error) {
return &config.Config{
CurrentContext: "does not exist",
Contexts: make(map[string]*config.Context),
}, nil
},
errContains: testNewHelperErr,
},
{
name: "Error phase by id",
factory: func() (*config.Config, error) {
conf := config.NewConfig()
conf.Manifests = map[string]*config.Manifest{
"manifest": {
MetadataPath: "broken_metadata.yaml",
TargetPath: "testdata",
PhaseRepositoryName: config.DefaultTestPhaseRepo,
Repositories: map[string]*config.Repository{
config.DefaultTestPhaseRepo: {
URLString: "",
},
},
},
}
conf.CurrentContext = "context"
conf.Contexts = map[string]*config.Context{
"context": {
Manifest: "manifest",
},
}
return conf, nil
},
errContains: testNoBundlePath,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
command := phase.TreeCommand{
Factory: tt.factory,
}
err := command.RunE()
if tt.errContains != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
} else {
assert.NoError(t, err)
}
})
}
}