1package k8s_test
2
3import (
4	"encoding/json"
5
6	. "github.com/concourse/concourse/topgun"
7	. "github.com/onsi/ginkgo"
8	. "github.com/onsi/gomega"
9)
10
11var _ = Describe("Kubernetes credential management", func() {
12	var (
13		atc                Endpoint
14		username, password = "test", "test"
15	)
16
17	BeforeEach(func() {
18		setReleaseNameAndNamespace("k8s-cm")
19	})
20
21	AfterEach(func() {
22		atc.Close()
23		cleanupReleases()
24	})
25
26	JustBeforeEach(func() {
27		atc = waitAndLogin(namespace, releaseName+"-web")
28	})
29
30	Context("/api/v1/info/creds", func() {
31		var parsedResponse struct {
32			Kubernetes struct {
33				ConfigPath      string `json:"config_path"`
34				InClusterConfig bool   `json:"in_cluster_config"`
35				NamespaceConfig string `json:"namespace_config"`
36			} `json:"kubernetes"`
37		}
38
39		BeforeEach(func() {
40			deployConcourseChart(releaseName, "--set=worker.replicas=1")
41		})
42
43		It("Contains kubernetes config", func() {
44			token, err := FetchToken("http://"+atc.Address(), username, password)
45			Expect(err).ToNot(HaveOccurred())
46
47			body, err := RequestCredsInfo("http://"+atc.Address(), token.AccessToken)
48			Expect(err).ToNot(HaveOccurred())
49
50			err = json.Unmarshal(body, &parsedResponse)
51			Expect(err).ToNot(HaveOccurred())
52
53			Expect(parsedResponse.Kubernetes.ConfigPath).To(BeEmpty())
54			Expect(parsedResponse.Kubernetes.InClusterConfig).To(BeTrue())
55			Expect(parsedResponse.Kubernetes.NamespaceConfig).To(Equal(releaseName + "-"))
56		})
57	})
58
59	Context("Consuming k8s credentials", func() {
60		var cachingSetup = func() {
61			deployConcourseChart(releaseName, "--set=worker.replicas=1",
62				"--set=concourse.web.secretCacheEnabled=true",
63				"--set=concourse.web.secretCacheDuration=600s",
64			)
65		}
66
67		var disableTeamNamespaces = func() {
68			By("creating a namespace made by the user instead of the chart")
69			Run(nil, "kubectl", "create", "namespace", releaseName+"-main")
70
71			By("binding the role that grants access to the secrets in our newly created namespace ")
72			Run(nil,
73				"kubectl", "create",
74				"--namespace", releaseName+"-main",
75				"rolebinding", "rb",
76				"--clusterrole", releaseName+"-web",
77				"--serviceaccount", releaseName+":"+releaseName+"-web",
78			)
79
80			deployConcourseChart(releaseName, "--set=worker.replicas=1",
81				"--set=concourse.web.secretCacheEnabled=true",
82				"--set=concourse.web.secretCacheDuration=600s",
83				"--set=concourse.web.kubernetes.createTeamNamespaces=false",
84			)
85		}
86
87		Context("using per-team credentials", func() {
88
89			const (
90				secretNameFoo = "foo"
91				secretNameCaz = "caz"
92			)
93
94			Context("using the default namespace created by the chart", func() {
95				BeforeEach(func() {
96					deployConcourseChart(releaseName, "--set=worker.replicas=1")
97				})
98
99				It("succeeds", func() {
100					runsBuildWithCredentialsResolved(secretNameFoo, secretNameCaz)
101				})
102			})
103
104			Context("with caching enabled", func() {
105				BeforeEach(cachingSetup)
106
107				It("gets cached credentials", func() {
108					runGetsCachedCredentials(secretNameFoo, secretNameCaz)
109				})
110			})
111
112			Context("using a user-provided namespace", func() {
113				BeforeEach(disableTeamNamespaces)
114
115				It("succeeds", func() {
116					runsBuildWithCredentialsResolved(secretNameFoo, secretNameCaz)
117				})
118
119				AfterEach(func() {
120					Run(nil, "kubectl", "delete", "namespace", releaseName+"-main", "--wait=false")
121				})
122			})
123
124		})
125
126		Context("using per-pipeline credentials", func() {
127
128			const (
129				secretNameFoo = "pipeline.foo"
130				secretNameCaz = "pipeline.caz"
131			)
132
133			Context("using the default namespace created by the chart", func() {
134				BeforeEach(func() {
135					deployConcourseChart(releaseName, "--set=worker.replicas=1")
136				})
137
138				It("succeeds", func() {
139					runsBuildWithCredentialsResolved(secretNameFoo, secretNameCaz)
140				})
141			})
142
143			Context("with caching enabled", func() {
144				BeforeEach(cachingSetup)
145
146				It("gets cached credentials", func() {
147					runGetsCachedCredentials(secretNameFoo, secretNameCaz)
148				})
149			})
150
151			Context("using a user-provided namespace", func() {
152				BeforeEach(disableTeamNamespaces)
153
154				It("succeeds", func() {
155					runsBuildWithCredentialsResolved(secretNameFoo, secretNameCaz)
156				})
157
158				AfterEach(func() {
159					Run(nil, "kubectl", "delete", "namespace", releaseName+"-main", "--wait=false")
160				})
161			})
162		})
163	})
164
165	Context("one-off build", func() {
166		BeforeEach(func() {
167			deployConcourseChart(releaseName, "--set=worker.replicas=1")
168		})
169
170		It("runs the one-off build successfully", func() {
171			By("creating the secret in the main team")
172			createCredentialSecret(releaseName, "some-secret", "main", map[string]string{"value": "mysecret"})
173
174			By("successfully running the one-off build")
175			fly.Run("execute",
176				"-c", "tasks/simple-secret.yml")
177		})
178
179		It("one-off build fails", func() {
180			By("not creating the secret")
181			sess := fly.Start("execute",
182				"-c", "tasks/simple-secret.yml")
183			<-sess.Exited
184			Expect(sess.ExitCode()).NotTo(Equal(0))
185		})
186	})
187
188})
189
190func deleteSecret(releaseName, team, secretName string) {
191	Run(nil, "kubectl", "--namespace="+releaseName+"-main", "delete", "secret", secretName)
192}
193
194func createCredentialSecret(releaseName, secretName, team string, kv map[string]string) {
195	args := []string{
196		"create",
197		"secret",
198		"generic",
199		secretName,
200		"--namespace=" + releaseName + "-" + team,
201	}
202
203	for key, value := range kv {
204		args = append(args, "--from-literal="+key+"="+value)
205	}
206
207	Run(nil, "kubectl", args...)
208}
209
210func runsBuildWithCredentialsResolved(normalSecret string, specialKeySecret string) {
211	By("creating credentials in k8s credential manager")
212	createCredentialSecret(releaseName, normalSecret, "main", map[string]string{"value": "bar"})
213	createCredentialSecret(releaseName, specialKeySecret, "main", map[string]string{"baz": "zaz"})
214
215	fly.Run("set-pipeline", "-n",
216		"-c", "pipelines/minimal-credential-management.yml",
217		"-p", "pipeline",
218	)
219
220	fly.Run("unpause-pipeline", "-p", "pipeline")
221
222	session := fly.Start("trigger-job", "-j", "pipeline/unit", "-w")
223	Wait(session)
224
225	By("seeing the credentials were resolved by concourse")
226	Expect(string(session.Out.Contents())).To(ContainSubstring("bar"))
227	Expect(string(session.Out.Contents())).To(ContainSubstring("zaz"))
228}
229
230func runGetsCachedCredentials(secretNameFoo string, secretNameCaz string) {
231	runsBuildWithCredentialsResolved(secretNameFoo, secretNameCaz)
232	deleteSecret(releaseName, "main", secretNameFoo)
233	deleteSecret(releaseName, "main", secretNameCaz)
234	By("seeing that concourse uses the cached credentials")
235	runsBuildWithCredentialsResolved(secretNameFoo, secretNameCaz)
236}
237