1
2
3
4 package config
5
6 import (
7 "fmt"
8 "os"
9 "path/filepath"
10 "regexp"
11 "strings"
12 "time"
13
14 "github.com/google/uuid"
15 "k8s.io/cli-runtime/pkg/genericclioptions"
16 "k8s.io/klog/v2"
17 cmdutil "k8s.io/kubectl/pkg/cmd/util"
18 "sigs.k8s.io/cli-utils/pkg/common"
19 "sigs.k8s.io/cli-utils/pkg/inventory/configmap"
20 "sigs.k8s.io/kustomize/kyaml/kio"
21 "sigs.k8s.io/kustomize/kyaml/kio/filters"
22 "sigs.k8s.io/kustomize/kyaml/openapi"
23 )
24
25 const (
26 manifestFilename = "inventory-template.yaml"
27 )
28
29
30
31 type InitOptions struct {
32 factory cmdutil.Factory
33
34 ioStreams genericclioptions.IOStreams
35
36 Template string
37
38 Dir string
39
40 Namespace string
41
42 InventoryID string
43 }
44
45 func NewInitOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *InitOptions {
46 return &InitOptions{
47 factory: f,
48 ioStreams: ioStreams,
49 Template: configmap.ConfigMapTemplate,
50 }
51 }
52
53
54
55
56 func (i *InitOptions) Complete(args []string) error {
57 if len(args) != 1 {
58 return fmt.Errorf("need one 'directory' arg; have %d", len(args))
59 }
60 dir, err := NormalizeDir(args[0])
61 if err != nil {
62 return err
63 }
64 i.Dir = dir
65 klog.V(4).Infof("init directory: %s", i.Dir)
66
67 ns, err := FindNamespace(i.factory.ToRawKubeConfigLoader(), i.Dir)
68 if err != nil {
69 return err
70 }
71 i.Namespace = ns
72
73
74 if len(i.InventoryID) == 0 {
75 inventoryID, err := i.defaultInventoryID()
76 if err != nil {
77 return err
78 }
79 i.InventoryID = inventoryID
80 }
81 if !validateInventoryID(i.InventoryID) {
82 return fmt.Errorf("invalid group name: %s", i.InventoryID)
83 }
84
85 fmt.Fprintf(i.ioStreams.Out, "namespace: %s is used for inventory object\n", i.Namespace)
86 return nil
87 }
88
89 type namespaceLoader interface {
90 Namespace() (string, bool, error)
91 }
92
93
94
95
96
97
98
99 func FindNamespace(loader namespaceLoader, dir string) (string, error) {
100 namespace, enforceNamespace, err := loader.Namespace()
101 if err != nil {
102 return "", err
103 }
104 if enforceNamespace {
105 klog.V(6).Infof("enforcing namespace: %s", namespace)
106 return namespace, nil
107 }
108
109 ns, allInSameNs, err := allInSameNamespace(dir)
110 if err != nil {
111 return "", err
112 }
113 if allInSameNs {
114 klog.V(6).Infof("all in same namespace: %s", ns)
115 return ns, nil
116 }
117 klog.V(6).Infof("returning namespace: %s", namespace)
118 return namespace, nil
119 }
120
121
122
123
124
125 func NormalizeDir(dirPath string) (string, error) {
126 if !common.IsDir(dirPath) {
127 return "", fmt.Errorf("invalid directory argument: %s", dirPath)
128 }
129 return filepath.Abs(dirPath)
130 }
131
132
133
134
135
136
137 func allInSameNamespace(packageDir string) (string, bool, error) {
138 r := kio.LocalPackageReader{PackagePath: packageDir}
139 nodes, err := r.Read()
140 if err != nil {
141 return "", false, err
142 }
143
144
145 nodes, err = (&filters.IsLocalConfig{}).Filter(nodes)
146 if err != nil {
147 return "", false, err
148 }
149
150 var ns string
151 for _, node := range nodes {
152 rm, err := node.GetMeta()
153 if err != nil {
154 return "", false, err
155 }
156
157 namespaced, found := openapi.IsNamespaceScoped(rm.TypeMeta)
158 if found && !namespaced {
159 klog.V(6).Infof("cluster-scoped resource %s--skip namespace calc", rm.TypeMeta)
160 continue
161 }
162 if rm.Namespace == "" {
163 klog.V(6).Infof("one resource missing namespace (%s): return empty namespace", rm.Name)
164 return "", false, nil
165 }
166 if ns == "" {
167 ns = rm.Namespace
168 } else if rm.Namespace != ns {
169 klog.V(6).Infof("two namespaces not same: %s versus %s", rm.Namespace, ns)
170 return "", false, nil
171 }
172 }
173 if ns != "" {
174 klog.V(6).Infof("returning empty namespace")
175 return ns, true, nil
176 }
177 return "", false, nil
178 }
179
180
181
182 func (i *InitOptions) defaultInventoryID() (string, error) {
183 u, err := uuid.NewRandom()
184 if err != nil {
185 return "", err
186 }
187 return u.String(), nil
188 }
189
190
191
192
193 const inventoryIDRegexp = `^[a-zA-Z0-9][a-zA-Z0-9\-\_\.]+[a-zA-Z0-9]$`
194
195
196
197
198
199 func validateInventoryID(inventoryID string) bool {
200 if len(inventoryID) == 0 || len(inventoryID) > 63 {
201 return false
202 }
203 re := regexp.MustCompile(inventoryIDRegexp)
204 return re.MatchString(inventoryID)
205 }
206
207
208
209 func fileExists(path string) bool {
210 f, err := os.Stat(path)
211 if os.IsNotExist(err) {
212 return false
213 }
214 return !f.IsDir()
215 }
216
217
218
219
220 func (i *InitOptions) fillInValues() string {
221 now := time.Now()
222 nowStr := now.Format("2006-01-02 15:04:05 MST")
223 randomSuffix := common.RandomStr()
224 manifestStr := i.Template
225 klog.V(4).Infof("namespace/inventory-id: %s/%s", i.Namespace, i.InventoryID)
226 manifestStr = strings.ReplaceAll(manifestStr, "<DATETIME>", nowStr)
227 manifestStr = strings.ReplaceAll(manifestStr, "<NAMESPACE>", i.Namespace)
228 manifestStr = strings.ReplaceAll(manifestStr, "<RANDOMSUFFIX>", randomSuffix)
229 manifestStr = strings.ReplaceAll(manifestStr, "<INVENTORYID>", i.InventoryID)
230 return manifestStr
231 }
232
233 func (i *InitOptions) Run() error {
234 manifestFilePath := filepath.Join(i.Dir, manifestFilename)
235 if fileExists(manifestFilePath) {
236 return fmt.Errorf("inventory object template file already exists: %s", manifestFilePath)
237 }
238 klog.V(4).Infof("creating manifest filename: %s", manifestFilePath)
239 f, err := os.Create(manifestFilePath)
240 if err != nil {
241 return fmt.Errorf("unable to create inventory object template file: %s", err)
242 }
243 defer f.Close()
244 _, err = f.WriteString(i.fillInValues())
245 if err != nil {
246 return fmt.Errorf("unable to write inventory object template file: %s", manifestFilePath)
247 }
248 fmt.Fprintf(i.ioStreams.Out, "Initialized: %s\n", manifestFilePath)
249 return nil
250 }
251
View as plain text