1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package create
16
17 import (
18 "context"
19 "fmt"
20 "io/ioutil"
21 "path"
22 "regexp"
23 "sort"
24 "strconv"
25 "strings"
26 "sync"
27 "testing"
28 "time"
29
30 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/dynamic"
31 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
32 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
33 testcontroller "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/controller"
34 testgcp "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/gcp"
35 testvariable "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/resourcefixture/variable"
36 testyaml "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/yaml"
37 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/repo"
38
39 "github.com/ghodss/yaml"
40 "github.com/golang-collections/go-datastructures/queue"
41 "k8s.io/apimachinery/pkg/api/errors"
42 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
43 "k8s.io/apimachinery/pkg/util/wait"
44 "sigs.k8s.io/controller-runtime/pkg/log"
45 )
46
47 type Sample struct {
48 Name string
49 Resources []*unstructured.Unstructured
50 }
51
52 func sortSamplesInDescendingOrderByNumberOfResources(samples []Sample) {
53 sort.Slice(samples, func(i, j int) bool {
54 return len(samples[i].Resources) > len(samples[j].Resources)
55 })
56 }
57
58 func networksInSampleCount(sample Sample) int {
59 count := 0
60 for _, r := range sample.Resources {
61 if r.GetKind() == "ComputeNetwork" {
62 count += 1
63 }
64 }
65 return count
66 }
67
68 func SetupNamespacesAndApplyDefaults(t *Harness, samples []Sample, project testgcp.GCPProject) {
69 namespaceNames := getNamespaces(samples)
70 setupNamespaces(t, namespaceNames, project)
71 }
72
73 func setupNamespaces(t *Harness, namespaces []string, project testgcp.GCPProject) {
74 for _, n := range namespaces {
75 testcontroller.SetupNamespaceForProject(t.T, t.GetClient(), n, project.ProjectID)
76 }
77 }
78
79 func getNamespaces(samples []Sample) []string {
80 namespaces := make(map[string]bool)
81 for _, sample := range samples {
82 for _, unstruct := range sample.Resources {
83 namespaces[unstruct.GetNamespace()] = true
84 }
85 }
86 results := make([]string, 0, len(namespaces))
87 for k := range namespaces {
88 results = append(results, k)
89 }
90 return results
91 }
92
93 func RunCreateDeleteTest(t *Harness, unstructs []*unstructured.Unstructured, cleanupResources bool) {
94
95 for _, u := range unstructs {
96 if err := t.GetClient().Create(context.TODO(), u); err != nil {
97 t.Fatalf("error creating resource: %v", err)
98 }
99 }
100 waitForReady(t, unstructs)
101
102 if cleanupResources {
103 cleanup(t, unstructs)
104 }
105 }
106
107 func waitForReady(t *Harness, unstructs []*unstructured.Unstructured) {
108 var wg sync.WaitGroup
109 for _, u := range unstructs {
110 wg.Add(1)
111 go waitForReadySingleResource(t, &wg, u)
112 }
113 wg.Wait()
114 }
115
116 func waitForReadySingleResource(t *Harness, wg *sync.WaitGroup, u *unstructured.Unstructured) {
117 logger := log.FromContext(t.Ctx)
118
119 name := k8s.GetNamespacedName(u)
120 defer wg.Done()
121 err := wait.PollImmediate(15*time.Second, 35*time.Minute, func() (done bool, err error) {
122 done = true
123 logger.Info("Testing to see if resource is ready", "kind", u.GetKind(), "name", u.GetName())
124 err = t.GetClient().Get(t.Ctx, name, u)
125 if err != nil {
126 logger.Info("Error getting resource", "kind", u.GetKind(), "name", u.GetName(), "error", err)
127 return false, nil
128 }
129 if u.GetKind() == "Secret" {
130 return true, nil
131 }
132 if u.Object["status"] == nil ||
133 u.Object["status"].(map[string]interface{})["conditions"] == nil {
134 logger.Info("resource does not yet have status or conditions", "kind", u.GetKind(), "name", u.GetName())
135 return false, nil
136 }
137 conditions := dynamic.GetConditions(t.T, u)
138 for _, c := range conditions {
139 if c.Type == "Ready" && c.Status == "True" {
140 logger.Info("resource is ready", "kind", u.GetKind(), "name", u.GetName())
141 return true, nil
142 }
143 }
144
145 logger.Info("resource is not ready", "kind", u.GetKind(), "name", u.GetName(),
146 "conditions", conditions)
147 return false, nil
148 })
149 if err == nil {
150 return
151 }
152 if err != wait.ErrWaitTimeout {
153 t.Errorf("error while polling for ready on %v with name '%v': %v", u.GetKind(), u.GetName(), err)
154 return
155 }
156 baseMsg := fmt.Sprintf("timed out waiting for ready on %v with name '%v'", u.GetKind(), u.GetName())
157 if err := t.GetClient().Get(t.Ctx, name, u); err != nil {
158 t.Errorf("%v, error retrieving final status.conditions: %v", baseMsg, err)
159 return
160 }
161 conditions := dynamic.GetConditions(t.T, u)
162 if len(conditions) == 0 {
163 t.Errorf("%v, no conditions on resource", baseMsg)
164 return
165 }
166 t.Errorf("%v, final status.conditions: %v", baseMsg, conditions)
167 }
168
169 func cleanup(t *Harness, unstructs []*unstructured.Unstructured) {
170 logger := log.FromContext(t.Ctx)
171
172 for _, u := range unstructs {
173 logger.Info("Deleting resource", "kind", u.GetKind(), "name", u.GetName())
174 if err := t.GetClient().Delete(t.Ctx, u); err != nil {
175 t.Errorf("error deleting: %v", err)
176 }
177 }
178 var wg sync.WaitGroup
179 for _, u := range unstructs {
180 wg.Add(1)
181 go waitForDeleteToComplete(t, &wg, u)
182 }
183 wg.Wait()
184 }
185
186 func waitForDeleteToComplete(t *Harness, wg *sync.WaitGroup, u *unstructured.Unstructured) {
187 defer wg.Done()
188
189
190 err := wait.PollImmediate(15*time.Second, 30*time.Minute, func() (bool, error) {
191 if err := t.GetClient().Get(t.Ctx, k8s.GetNamespacedName(u), u); !errors.IsNotFound(err) {
192 return false, nil
193 }
194 return true, nil
195 })
196
197 if err != nil {
198 t.Errorf("error while polling for resource cleanup on %v with name '%v': %v; last seen status: %v", u.GetKind(), u.GetName(), err, u.Object["status"])
199 }
200 }
201
202
203 func LoadSamples(t *testing.T, project testgcp.GCPProject) []Sample {
204 matchEverything := regexp.MustCompile(".*")
205 return loadSamplesOntoUnstructs(t, matchEverything, project)
206 }
207
208 func loadSamplesOntoUnstructs(t *testing.T, regex *regexp.Regexp, project testgcp.GCPProject) []Sample {
209 t.Helper()
210
211 samples := make([]Sample, 0)
212 sampleNamesToFiles := mapSampleNamesToFilePaths(t, regex)
213 subVars := newSubstitutionVariables(t, project)
214 for sample, files := range sampleNamesToFiles {
215 resources := make([]*unstructured.Unstructured, 0)
216 for _, f := range files {
217 unstructs := readFileToUnstructs(t, f, subVars)
218 resources = append(resources, unstructs...)
219 }
220 s := Sample{
221 Name: sample,
222 Resources: resources,
223 }
224 samples = append(samples, s)
225 }
226 return samples
227 }
228
229 func mapSampleNamesToFilePaths(t *testing.T, regex *regexp.Regexp) map[string][]string {
230 t.Helper()
231 samples := make(map[string][]string)
232 q := queue.New(1)
233 q.Put(repo.GetResourcesSamplesPath())
234 for !q.Empty() {
235 items, err := q.Get(1)
236 if err != nil {
237 t.Fatalf("error retrieving an item from queue: %v", err)
238 }
239 dir := items[0].(string)
240 fileInfos, err := ioutil.ReadDir(dir)
241 if err != nil {
242 t.Fatalf("error reading directory '%v': %v", dir, err)
243 }
244 for _, fi := range fileInfos {
245 if fi.IsDir() {
246 q.Put(path.Join(dir, fi.Name()))
247 continue
248 }
249 if !strings.HasSuffix(fi.Name(), ".yaml") {
250 continue
251 }
252 sampleName := path.Base(dir)
253 if !regex.MatchString(sampleName) {
254 continue
255 }
256 filePath := path.Join(dir, fi.Name())
257 samples[sampleName] = append(samples[sampleName], filePath)
258 }
259 }
260 return samples
261 }
262
263 func newSubstitutionVariables(t *testing.T, project testgcp.GCPProject) map[string]string {
264 subs := make(map[string]string)
265 subs["${HOST_PROJECT_ID?}"] = project.ProjectID
266 subs["${PROJECT_ID?}"] = project.ProjectID
267 subs["${PROJECT_NUMBER?}"] = strconv.FormatInt(project.ProjectNumber, 10)
268 subs["${FOLDER_ID?}"] = testgcp.GetFolderID(t)
269 subs["${ORG_ID?}"] = testgcp.GetOrgID(t)
270 subs["${BILLING_ACCOUNT_ID?}"] = testgcp.GetBillingAccountID(t)
271 subs["${BILLING_ACCOUNT_ID_FOR_BILLING_RESOURCES?}"] = testgcp.GetTestBillingAccountIDForBillingResources(t)
272 subs["${GSA_EMAIL?}"] = getKCCServiceAccountEmail(t, project)
273 subs["${DLP_TEST_BUCKET?}"] = testgcp.GetDLPTestBucket(t)
274 return subs
275 }
276
277
278
279 func getKCCServiceAccountEmail(t *testing.T, project testgcp.GCPProject) string {
280
281
282
283 if sa, err := testgcp.FindDefaultServiceAccount(); err != nil {
284 t.Fatalf("error from FindDefaultServiceAccount: %v", err)
285 } else if sa != "" {
286 return sa
287 }
288
289
290 return fmt.Sprintf("cnrm-system@%v.iam.gserviceaccount.com", project.ProjectID)
291 }
292
293 func readFileToUnstructs(t *testing.T, fileName string, subVars map[string]string) []*unstructured.Unstructured {
294 t.Helper()
295 var returnUnstructs []*unstructured.Unstructured
296
297 b := testcontroller.ReadFileToBytes(t, fileName)
298 s := string(b)
299 for k, v := range subVars {
300 s = strings.ReplaceAll(s, k, v)
301 }
302 b = []byte(s)
303
304 yamls := testyaml.SplitYAML(t, b)
305 for _, b = range yamls {
306 u := test.ToUnstructWithNamespace(t, b, subVars["${PROJECT_ID?}"])
307 returnUnstructs = append(returnUnstructs, u)
308 }
309 return returnUnstructs
310 }
311
312 func replaceResourceNamesWithUniqueIDs(t *testing.T, unstructs []*unstructured.Unstructured) []*unstructured.Unstructured {
313 namesToBeReplaced := make([]string, 0)
314 for _, u := range unstructs {
315 namesToBeReplaced = append(namesToBeReplaced, u.GetName())
316 }
317
318
319
320
321
322
323 namesToBeReplaced = sortByDescendingLen(namesToBeReplaced)
324
325 namesToUniqueIDs := make(map[string]string)
326 idReg := regexp.MustCompile("[a-z]")
327 for _, n := range namesToBeReplaced {
328 namesToUniqueIDs[n] = testvariable.RandomIdGenerator(idReg, uint(len(n)))
329 }
330
331 newUnstructs := make([]*unstructured.Unstructured, 0)
332 for _, u := range unstructs {
333 b, err := yaml.Marshal(u)
334 if err != nil {
335 t.Fatalf("error marshalling unstruct to bytes: %v", err)
336 }
337 s := string(b)
338 for _, name := range namesToBeReplaced {
339 uniqueID := namesToUniqueIDs[name]
340 s = strings.ReplaceAll(s, name, uniqueID)
341 }
342 b = []byte(s)
343 newUnstruct := &unstructured.Unstructured{}
344 err = yaml.Unmarshal(b, newUnstruct)
345 if err != nil {
346 t.Fatalf("error unmarshalling bytes to unstruct: %v", err)
347 }
348
349 if newUnstruct.GetKind() == "Folder" {
350 newDisplayName, err := generateNewFolderDisplayName(u, idReg)
351 if err != nil {
352 t.Fatalf("error generating new spec.displayName value for Folder '%v': %v", u.GetName(), err)
353 }
354 unstructured.SetNestedField(newUnstruct.Object, newDisplayName, "spec", "displayName")
355 }
356 newUnstructs = append(newUnstructs, newUnstruct)
357 }
358 return newUnstructs
359 }
360
361
362
363
364
365 func generateNewFolderDisplayName(folderUnstruct *unstructured.Unstructured, idReg *regexp.Regexp) (string, error) {
366 newDisplayNamePrefix := "KCC "
367 uniqueIDLen := 10
368 minDisplayNameLen := len(newDisplayNamePrefix) + uniqueIDLen
369
370 displayName, err := getFolderDisplayName(folderUnstruct)
371 if err != nil {
372 return "", err
373 }
374
375 if len(displayName) < minDisplayNameLen {
376 return "", fmt.Errorf("Folder '%v' has a spec.displayName value of "+
377 "'%v' which is too short; please use a spec.displayName with at "+
378 "least '%v' characters", folderUnstruct.GetName(), displayName, minDisplayNameLen)
379 }
380
381 return newDisplayNamePrefix + testvariable.RandomIdGenerator(idReg, uint(len(displayName)-len(newDisplayNamePrefix))), nil
382 }
383
384 func getFolderDisplayName(folderUnstruct *unstructured.Unstructured) (string, error) {
385 displayName, ok, err := unstructured.NestedString(folderUnstruct.Object, "spec", "displayName")
386 if err != nil {
387 return "", fmt.Errorf("error getting spec.displayName of Folder unstruct: %v", err)
388 }
389 if !ok {
390 return "", fmt.Errorf("spec.displayName not found for Folder unstruct")
391 }
392 return displayName, nil
393 }
394
395 func sortByDescendingLen(strs []string) []string {
396 strsCopy := append(make([]string, 0), strs...)
397 sort.Slice(strsCopy, func(i, j int) bool {
398 return len(strsCopy[i]) > len(strsCopy[j])
399 })
400 return strsCopy
401 }
402
View as plain text