Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 581ec8e

Browse files
authored
Merge pull request #301 from KohlsTechnology/empty-dir
Allow user to pass empty TemplateSource ContextDir
2 parents 4e9c75a + 1b7e27b commit 581ec8e

File tree

7 files changed

+303
-17
lines changed

7 files changed

+303
-17
lines changed

pkg/controller/gitopsconfig/controller.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,8 @@ func (r *Reconciler) manageDeletion(instance *gitopsv1alpha1.GitOpsConfig) (reco
415415
// assume the GitOpsConfig never managed to successfully deploy, so we can
416416
// just delete the job, remove the finalizer, and be done (#216). It may be
417417
// either action=create or action=delete job.
418-
if len(jobs) == 1 {
418+
// If a job is blocked because of bad image, it only has one active pod
419+
if len(jobs) == 1 && jobs[0].Status.Succeeded == 0 && jobs[0].Status.Failed == 0 && jobs[0].Status.Active == 1 {
419420
status, err := jobContainerStatus(context.TODO(), r.client, &jobs[0])
420421
if err != nil {
421422
log.Error(err, "GitOpsConfig finalizer unable to get job pod's status", "instance", instance.Name)

template-processors/base/bin/gitClone.sh

+7
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,12 @@ function pullFromParametersRepo() {
7070

7171
echo Cloning Repositories
7272
pullFromTemplatesRepo
73+
# In git, if directory contains no files, it isn't tracked:
74+
# https://git.wiki.kernel.org/index.php/Git_FAQ#Can_I_add_empty_directories.3F
75+
if ! [[ -d "$CLONED_TEMPLATE_GIT_DIR" ]]; then
76+
echo "ERROR - directory ${CLONED_TEMPLATE_GIT_DIR#/git/templates/} does not exist in the remote repository.
77+
If you want an empty directory to be tracked by git, add a .gitkeep file inside" >&2
78+
exit 1
79+
fi
7380
pullFromParametersRepo
7481
mkdir -p "$MANIFEST_DIR"

template-processors/base/bin/processTemplates.sh

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,4 @@ set -euxo pipefail
1818

1919
echo Processing Templates
2020

21-
# shellcheck disable=SC2086
22-
cp -R "$CLONED_TEMPLATE_GIT_DIR/"* "$MANIFEST_DIR"/
21+
cp -R "${CLONED_TEMPLATE_GIT_DIR}/." "${MANIFEST_DIR}/"

template-processors/base/bin/resourceManager.sh

+11-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function addLabels() {
4444
local timestamp="$2"
4545
local tmpdir="$(mktemp -d)"
4646
# shellcheck disable=SC2044
47-
for file in $(find "$MANIFEST_DIR" -iregex '.*\.\(ya?ml\|json\)'); do
47+
for file in $(find "$MANIFEST_DIR" -regextype posix-extended -iregex '.*\.(ya?ml|json)'); do
4848
cat "$file" |
4949
yq -y -s "map(select(.!=null)|setpath([\"metadata\",\"labels\",\"$TAG_OWNER\"]; \"$owner\"))|.[]" |
5050
yq -y -s "map(select(.!=null)|setpath([\"metadata\",\"labels\",\"$TAG_APPLIED\"]; \"$timestamp\"))|.[]" \
@@ -79,6 +79,16 @@ function deleteByOldLabels() {
7979
function createUpdateResources() {
8080
local owner="$1"
8181
local timestamp="$(date +%s)"
82+
# Check if directory contains only hidden files like .gitkeep, or .gitignore.
83+
# This would mean that user purposefully wanted to track an empty directory in git.
84+
# https://git.wiki.kernel.org/index.php/Git_FAQ#Can_I_add_empty_directories.3F
85+
if [[ -z $(ls "${MANIFEST_DIR}") ]]; then
86+
echo "Manifest directory empty, skipping"
87+
return
88+
elif [[ -z $(find "$MANIFEST_DIR" -regextype posix-extended -iregex '.*\.(ya?ml|json)') ]]; then
89+
echo "ERROR - no files with .yaml, .yml, or .json extension in manifest directory"
90+
exit 1
91+
fi
8292
case "$CREATE_MODE" in
8393
Apply)
8494
addLabels "$owner" "$timestamp"

test/e2e/issue276_test.go

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// +build e2e
2+
3+
/*
4+
Copyright 2020 Kohl's Department Stores, Inc.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package e2e
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"os"
25+
"strings"
26+
"testing"
27+
"time"
28+
29+
framework "github.com/operator-framework/operator-sdk/pkg/test"
30+
apierrors "k8s.io/apimachinery/pkg/api/errors"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/util/wait"
33+
34+
"github.com/KohlsTechnology/eunomia/pkg/apis"
35+
gitopsv1alpha1 "github.com/KohlsTechnology/eunomia/pkg/apis/eunomia/v1alpha1"
36+
)
37+
38+
// TestIssue276NoTemplatesDir verifies that job's pod fails in such a
39+
// way that it prints a custom error message to output when user passes
40+
// nonexistent TemplateSource ContextDir. Then, the test also verifies
41+
// that the Custom Recource is successfully deleted
42+
func TestIssue276NoTemplatesDir(t *testing.T) {
43+
if testing.Short() {
44+
// FIXME: as of writing this test, "backoffLimit" in job.yaml is set to 4,
45+
// which means eunomia will wait to launch deletion Job until 5 Pod retries
46+
// fail, eventually triggering the origninal Job's failure; the back-off
47+
// time between the runs is unfortunately exponential and non-configurable,
48+
// which makes this test awfully long. Try to at least make it possible to
49+
// run in parallel with other tests.
50+
t.Skip("This test currently takes minutes to run, because of exponential backoff in kubernetes")
51+
}
52+
53+
ctx := framework.NewTestCtx(t)
54+
defer ctx.Cleanup()
55+
56+
namespace, err := ctx.GetNamespace()
57+
if err != nil {
58+
t.Fatalf("could not get namespace: %v", err)
59+
}
60+
if err = SetupRbacInNamespace(namespace); err != nil {
61+
t.Error(err)
62+
}
63+
64+
defer DumpJobsLogsOnError(t, framework.Global, namespace)
65+
err = framework.AddToFrameworkScheme(apis.AddToScheme, &gitopsv1alpha1.GitOpsConfigList{})
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
70+
eunomiaURI, found := os.LookupEnv("EUNOMIA_URI")
71+
if !found {
72+
eunomiaURI = "https://github.com/kohlstechnology/eunomia"
73+
}
74+
eunomiaRef, found := os.LookupEnv("EUNOMIA_REF")
75+
if !found {
76+
eunomiaRef = "master"
77+
}
78+
79+
// Step 1: create a CR with a nonexistent TemplateSource ContextDir
80+
81+
const noDir = "test/e2e/testdata/no-directory"
82+
gitops := &gitopsv1alpha1.GitOpsConfig{
83+
TypeMeta: metav1.TypeMeta{
84+
Kind: "GitOpsConfig",
85+
APIVersion: "eunomia.kohls.io/v1alpha1",
86+
},
87+
ObjectMeta: metav1.ObjectMeta{
88+
Name: "gitops-issue276",
89+
Namespace: namespace,
90+
Finalizers: []string{
91+
"gitopsconfig.eunomia.kohls.io/finalizer",
92+
},
93+
},
94+
Spec: gitopsv1alpha1.GitOpsConfigSpec{
95+
TemplateSource: gitopsv1alpha1.GitConfig{
96+
URI: eunomiaURI,
97+
Ref: eunomiaRef,
98+
ContextDir: noDir,
99+
},
100+
ParameterSource: gitopsv1alpha1.GitConfig{
101+
URI: eunomiaURI,
102+
Ref: eunomiaRef,
103+
ContextDir: "test/e2e/testdata/empty-yaml",
104+
},
105+
Triggers: []gitopsv1alpha1.GitOpsTrigger{
106+
{Type: "Change"},
107+
},
108+
TemplateProcessorImage: "quay.io/kohlstechnology/eunomia-base:dev",
109+
ResourceHandlingMode: "Apply",
110+
ResourceDeletionMode: "Delete",
111+
ServiceAccountRef: "eunomia-operator",
112+
},
113+
}
114+
gitops.Annotations = map[string]string{"gitopsconfig.eunomia.kohls.io/initialized": "true"}
115+
116+
err = framework.Global.Client.Create(context.TODO(), gitops, &framework.CleanupOptions{TestContext: ctx, Timeout: timeout, RetryInterval: retryInterval})
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
121+
// Step 2: Wait until Job's pod fails and check if it printed a clear error message
122+
123+
const name = "gitopsconfig-gitops-issue276-"
124+
err = wait.Poll(retryInterval, timeout, func() (done bool, err error) {
125+
pod, err := GetPod(namespace, name, "quay.io/kohlstechnology/eunomia-base:dev", framework.Global.KubeClient)
126+
switch {
127+
case apierrors.IsNotFound(err):
128+
t.Logf("Waiting for availability of %s pod", name)
129+
return false, nil
130+
case err != nil:
131+
return false, err
132+
case pod != nil && pod.Status.Phase == "Failed":
133+
logs, err := GetPodLogs(pod, framework.Global.KubeClient)
134+
if err != nil {
135+
t.Fatal(err)
136+
}
137+
if !strings.Contains(logs, fmt.Sprintf("ERROR - directory %s does not exist in the remote repository", noDir)) {
138+
t.Fatalf("Pod %s failed in an unexpected way; logs:\n%s", pod.Name, logs)
139+
}
140+
return true, nil
141+
case pod != nil:
142+
t.Logf("Waiting for error in pod %s; status: %s", pod.Name, debugJSON(pod.Status))
143+
return false, nil
144+
default:
145+
t.Logf("Waiting for error in pod %s", pod.Name)
146+
return false, nil
147+
}
148+
})
149+
if err != nil {
150+
t.Error(err)
151+
}
152+
153+
// Step 3: Delete GitOpsConfig and make sure that the deletion succeeded
154+
155+
t.Logf("Deleting CR")
156+
err = framework.Global.Client.Delete(context.TODO(), gitops)
157+
if err != nil {
158+
t.Fatal(err)
159+
}
160+
161+
// Three minutes timeout.
162+
err = WaitForPodAbsence(t, framework.Global, namespace, name, "quay.io/kohlstechnology/eunomia-base:dev", retryInterval, 3*time.Minute)
163+
if err != nil {
164+
t.Error(err)
165+
}
166+
}
167+
168+
// TestIssue276EmptyTemplatesDir verifies that job succeeds when user
169+
// passes a TemplateSource directory containing no resource files, but
170+
// only a single ".gitkeep" file
171+
func TestIssue276EmptyTemplatesDir(t *testing.T) {
172+
ctx := framework.NewTestCtx(t)
173+
defer ctx.Cleanup()
174+
175+
namespace, err := ctx.GetNamespace()
176+
if err != nil {
177+
t.Fatalf("could not get namespace: %v", err)
178+
}
179+
if err = SetupRbacInNamespace(namespace); err != nil {
180+
t.Error(err)
181+
}
182+
183+
defer DumpJobsLogsOnError(t, framework.Global, namespace)
184+
err = framework.AddToFrameworkScheme(apis.AddToScheme, &gitopsv1alpha1.GitOpsConfigList{})
185+
if err != nil {
186+
t.Fatal(err)
187+
}
188+
189+
eunomiaURI, found := os.LookupEnv("EUNOMIA_URI")
190+
if !found {
191+
eunomiaURI = "https://github.com/kohlstechnology/eunomia"
192+
}
193+
eunomiaRef, found := os.LookupEnv("EUNOMIA_REF")
194+
if !found {
195+
eunomiaRef = "master"
196+
}
197+
198+
// Step 1: create a CR with a TemplateSource ContextDir containing only a ".gitkeep" file
199+
200+
gitops := &gitopsv1alpha1.GitOpsConfig{
201+
TypeMeta: metav1.TypeMeta{
202+
Kind: "GitOpsConfig",
203+
APIVersion: "eunomia.kohls.io/v1alpha1",
204+
},
205+
ObjectMeta: metav1.ObjectMeta{
206+
Name: "gitops-issue276",
207+
Namespace: namespace,
208+
Finalizers: []string{
209+
"gitopsconfig.eunomia.kohls.io/finalizer",
210+
},
211+
},
212+
Spec: gitopsv1alpha1.GitOpsConfigSpec{
213+
TemplateSource: gitopsv1alpha1.GitConfig{
214+
URI: eunomiaURI,
215+
Ref: eunomiaRef,
216+
ContextDir: "test/e2e/testdata/empty-directory",
217+
},
218+
ParameterSource: gitopsv1alpha1.GitConfig{
219+
URI: eunomiaURI,
220+
Ref: eunomiaRef,
221+
ContextDir: "test/e2e/testdata/empty-yaml",
222+
},
223+
Triggers: []gitopsv1alpha1.GitOpsTrigger{
224+
{Type: "Change"},
225+
},
226+
TemplateProcessorImage: "quay.io/kohlstechnology/eunomia-base:dev",
227+
ResourceHandlingMode: "Apply",
228+
ResourceDeletionMode: "Delete",
229+
ServiceAccountRef: "eunomia-operator",
230+
},
231+
}
232+
gitops.Annotations = map[string]string{"gitopsconfig.eunomia.kohls.io/initialized": "true"}
233+
234+
err = framework.Global.Client.Create(context.TODO(), gitops, &framework.CleanupOptions{TestContext: ctx, Timeout: timeout, RetryInterval: retryInterval})
235+
if err != nil {
236+
t.Fatal(err)
237+
}
238+
239+
// Step 2: Wait until Job's pod succeeds
240+
241+
err = wait.Poll(retryInterval, timeout, func() (done bool, err error) {
242+
const name = "gitopsconfig-gitops-issue276-"
243+
pod, err := GetPod(namespace, name, "quay.io/kohlstechnology/eunomia-base:dev", framework.Global.KubeClient)
244+
switch {
245+
case apierrors.IsNotFound(err):
246+
t.Logf("Waiting for availability of %s pod", name)
247+
return false, nil
248+
case err != nil:
249+
return false, err
250+
case pod != nil && pod.Status.Phase == "Succeeded":
251+
return true, nil
252+
case pod != nil:
253+
t.Logf("Waiting for pod %s to succeed; status: %s", pod.Name, debugJSON(pod.Status))
254+
return false, nil
255+
default:
256+
t.Logf("Waiting for pod %s", name)
257+
return false, nil
258+
}
259+
})
260+
if err != nil {
261+
t.Error(err)
262+
}
263+
}

test/e2e/testdata/empty-directory/.gitkeep

Whitespace-only changes.

test/e2e/util.go

+19-13
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
goctx "context"
2424
"encoding/json"
2525
"fmt"
26-
"io"
26+
"io/ioutil"
2727
"os/exec"
2828
"path/filepath"
2929
"runtime"
@@ -61,6 +61,21 @@ func GetPod(namespace, namePrefix, containsImage string, kubeclient kubernetes.I
6161
return nil, nil
6262
}
6363

64+
// GetPodLogs retrieves logs of a given pod
65+
func GetPodLogs(pod *v1.Pod, kubeclient kubernetes.Interface) (string, error) {
66+
req := kubeclient.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &v1.PodLogOptions{Timestamps: true})
67+
logs, err := req.Stream()
68+
if err != nil {
69+
return "", xerrors.Errorf("could not get logs for pod %q: %w", pod.Name, err)
70+
}
71+
defer logs.Close()
72+
b, err := ioutil.ReadAll(logs)
73+
if err != nil {
74+
return "", xerrors.Errorf("could not get logs for pod %q: %w", pod.Name, err)
75+
}
76+
return string(b), nil
77+
}
78+
6479
// WaitForPod retrieves a specific pod with a known name and namespace and waits for it to be running and available
6580
func WaitForPod(t *testing.T, f *framework.Framework, namespace, name string, retryInterval, timeout time.Duration) error {
6681
err := wait.Poll(retryInterval, timeout, func() (done bool, err error) {
@@ -151,8 +166,7 @@ func DumpJobsLogsOnError(t *testing.T, f *framework.Framework, namespace string)
151166
if !t.Failed() {
152167
return
153168
}
154-
pods := f.KubeClient.CoreV1().Pods(namespace)
155-
podsList, err := pods.List(metav1.ListOptions{})
169+
podsList, err := f.KubeClient.CoreV1().Pods(namespace).List(metav1.ListOptions{})
156170
if err != nil {
157171
t.Logf("failed to list pods in namespace %s: %s", namespace, err)
158172
return
@@ -169,20 +183,12 @@ func DumpJobsLogsOnError(t *testing.T, f *framework.Framework, namespace string)
169183
continue
170184
}
171185
// Retrieve pod's logs
172-
req := pods.GetLogs(p.Name, &v1.PodLogOptions{Timestamps: true})
173-
logs, err := req.Stream()
174-
if err != nil {
175-
t.Logf("failed to retrieve logs for pod %s: %s", p.Name, err)
176-
continue
177-
}
178-
buf := &bytes.Buffer{}
179-
_, err = io.Copy(buf, logs)
180-
logs.Close()
186+
logs, err := GetPodLogs(&p, f.KubeClient)
181187
if err != nil {
182188
t.Logf("failed to retrieve logs for pod %s: %s", p.Name, err)
183189
continue
184190
}
185-
t.Logf("================ POD LOGS FOR %s ================\n%s\n\n", p.Name, buf.String())
191+
t.Logf("================ POD LOGS FOR %s ================\n%s\n\n", p.Name, logs)
186192
}
187193
}
188194

0 commit comments

Comments
 (0)