From d540a7d1c4324090adb1d39d2f1fa36c39b7c015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Wed, 22 Jan 2020 16:39:20 +0100 Subject: [PATCH 01/13] Allow for specifying the secret key of an implicit variable. This makes it possible to keep multiple values in one secret and reference them by their key, i.e. `((ssl/ca))` --- docs/from_bosh_to_kube.md | 27 +++++++++++++-- pkg/bosh/converter/resolver.go | 36 +++++++++++++++++--- pkg/bosh/converter/resolver_test.go | 51 +++++++++++++++++++++++++++++ pkg/bosh/manifest/manifest.go | 9 +++-- 4 files changed, 114 insertions(+), 9 deletions(-) diff --git a/docs/from_bosh_to_kube.md b/docs/from_bosh_to_kube.md index 45dc23abf..a49dada94 100644 --- a/docs/from_bosh_to_kube.md +++ b/docs/from_bosh_to_kube.md @@ -549,13 +549,17 @@ BOSH deployment manifests support two different types of variables, implicit and "Explicit" variables are declared in the `variables` section of the manifest and are generated automatically before the interpolation step. -"Implicit" variables just appear in the document within double parentheses without any declaration. These variables have to be provided by the user prior to creating the BOSH deployment. The variables have to be provided as a secret with the `value` key holding the variable content. The secret name has to follow the scheme +"Implicit" variables just appear in the document within double parentheses without any declaration. These variables have to be provided by the user prior to creating the BOSH deployment as a secret. The secret name has to follow the scheme ```text .var- ``` -Example: +By default the variable content is expected in the `value` key, e.g. + +``` +((system-domain)) +``` ```yaml --- @@ -568,6 +572,25 @@ stringData: value: example.com ``` +It is also possible to specify the key name after a `/` separator, e.g. + +``` +((ssl/ca)) +``` + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: nats-deployment.var-ssl +type: Opaque +stringData: + ca: ... + cert: ... + key: ... +``` + ### Pre_render_scripts Similar to what can be achieved in SCF v1, with the [patches](https://github.com/SUSE/scf/tree/develop/container-host-files/etc/scf/config/scripts/patches) scripts, the `cf-operator` is able to support this behaviour. Basically, it allows the user to execute a custom script during runtime of the job container for a specific `instance_group`. Because patching during runtime is always a great feature to have, for a variety of reasons, users can specify this via the `quarks.pre_render_scripts` key. diff --git a/pkg/bosh/converter/resolver.go b/pkg/bosh/converter/resolver.go index 943f400d1..15e0a53d6 100644 --- a/pkg/bosh/converter/resolver.go +++ b/pkg/bosh/converter/resolver.go @@ -125,8 +125,22 @@ func (r *ResolverImpl) WithOpsManifest(instance *bdv1.BOSHDeployment, namespace varSecrets := make([]string, len(vars)) for i, v := range vars { - varSecretName := names.DeploymentSecretName(names.DeploymentSecretTypeVariable, instance.GetName(), v) - varData, err := r.resourceData(namespace, bdv1.SecretReference, varSecretName, bdv1.ImplicitVariableKeyName) + varKeyName := "" + varSecretName := "" + if strings.Contains(v, "/") { + parts := strings.Split(v, "/") + if len(parts) != 2 { + return nil, []string{}, fmt.Errorf("expected one / separator for implicit variable/key name, have %d", len(parts)) + } + + varSecretName = names.DeploymentSecretName(names.DeploymentSecretTypeVariable, instance.GetName(), parts[0]) + varKeyName = parts[1] + } else { + varKeyName = bdv1.ImplicitVariableKeyName + varSecretName = names.DeploymentSecretName(names.DeploymentSecretTypeVariable, instance.GetName(), v) + } + + varData, err := r.resourceData(namespace, bdv1.SecretReference, varSecretName, varKeyName) if err != nil { return nil, varSecrets, errors.Wrapf(err, "failed to load secret for variable '%s'", v) } @@ -195,8 +209,22 @@ func (r *ResolverImpl) WithOpsManifestDetailed(instance *bdv1.BOSHDeployment, na varSecrets := make([]string, len(vars)) for i, v := range vars { - varSecretName := names.DeploymentSecretName(names.DeploymentSecretTypeVariable, instance.GetName(), v) - varData, err := r.resourceData(namespace, bdv1.SecretReference, varSecretName, bdv1.ImplicitVariableKeyName) + varKeyName := "" + varSecretName := "" + if strings.Contains(v, "/") { + parts := strings.Split(v, "/") + if len(parts) != 2 { + return nil, []string{}, fmt.Errorf("expected one / separator for implicit variable/key name, have %d", len(parts)) + } + + varSecretName = names.DeploymentSecretName(names.DeploymentSecretTypeVariable, instance.GetName(), parts[0]) + varKeyName = parts[1] + } else { + varKeyName = bdv1.ImplicitVariableKeyName + varSecretName = names.DeploymentSecretName(names.DeploymentSecretTypeVariable, instance.GetName(), v) + } + + varData, err := r.resourceData(namespace, bdv1.SecretReference, varSecretName, varKeyName) if err != nil { return nil, varSecrets, errors.Wrapf(err, "failed to load secret for variable '%s'", v) } diff --git a/pkg/bosh/converter/resolver_test.go b/pkg/bosh/converter/resolver_test.go index 350e7c05e..0e18f6da7 100644 --- a/pkg/bosh/converter/resolver_test.go +++ b/pkg/bosh/converter/resolver_test.go @@ -188,6 +188,23 @@ instance_groups: instances: 1 properties: host: 'foo.((system_domain))' +`}, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "manifest-with-multi-key-implicit-var", + Namespace: "default", + }, + Data: map[string]string{bdc.ManifestSpecName: `--- +name: foo +instance_groups: + - name: component1 + instances: 1 + properties: + ssl: + ca: '((ssl/ca))' + cert: '((ssl/cert))' + key: '((ssl/key))' `}, }, &corev1.Secret{ @@ -204,6 +221,17 @@ instance_groups: }, Data: map[string][]byte{"value": []byte("complicated\n'multiline'\nstring")}, }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-deployment.var-ssl", + Namespace: "default", + }, + Data: map[string][]byte{ + "ca": []byte("the-ca"), + "cert": []byte("the-cert"), + "key": []byte("the-key"), + }, + }, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "replace-ops", @@ -858,5 +886,28 @@ instance_groups: Expect(implicitVars[0]).To(Equal("foo-deployment.var-system-domain")) Expect(m.InstanceGroups[0].Properties.Properties["host"]).To(Equal("foo.example.com")) }) + + It("handles multi-key implicit variables", func() { + deployment := &bdc.BOSHDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-deployment", + }, + Spec: bdc.BOSHDeploymentSpec{ + Manifest: bdc.ResourceReference{ + Type: bdc.ConfigMapReference, + Name: "manifest-with-multi-key-implicit-var", + }, + Ops: []bdc.ResourceReference{}, + }, + } + m, implicitVars, err := resolver.WithOpsManifest(context.Background(), deployment, "default") + + sslProps := m.InstanceGroups[0].Properties.Properties["ssl"].(map[string]interface{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(implicitVars)).To(Equal(3)) + Expect(sslProps["ca"]).To(Equal("the-ca")) + Expect(sslProps["cert"]).To(Equal("the-cert")) + Expect(sslProps["key"]).To(Equal("the-key")) + }) }) }) diff --git a/pkg/bosh/manifest/manifest.go b/pkg/bosh/manifest/manifest.go index 806064dbc..66f93ad06 100644 --- a/pkg/bosh/manifest/manifest.go +++ b/pkg/bosh/manifest/manifest.go @@ -477,9 +477,12 @@ func (m *Manifest) ImplicitVariables() ([]string, error) { // Collect all variables varRegexp := regexp.MustCompile(`\(\((!?[-/\.\w\pL]+)\)\)`) for _, match := range varRegexp.FindAllStringSubmatch(rawManifest, -1) { - // Remove subfields from the match, e.g. ca.private_key -> ca - fieldRegexp := regexp.MustCompile(`[^\.]+`) - main := fieldRegexp.FindString(match[1]) + main := match[1] + if !strings.Contains(main, "/") { + // Remove subfields from explicit vars, e.g. ca.private_key -> ca + fieldRegexp := regexp.MustCompile(`[^\.]+`) + main = fieldRegexp.FindString(match[1]) + } varMap[main] = true } From a46d27a462dd7003ad6ce6b79de396f4cb99a05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Thu, 23 Jan 2020 08:55:17 +0100 Subject: [PATCH 02/13] Adapt to changes in master --- pkg/bosh/converter/resolver_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bosh/converter/resolver_test.go b/pkg/bosh/converter/resolver_test.go index 0e18f6da7..5f0d5f586 100644 --- a/pkg/bosh/converter/resolver_test.go +++ b/pkg/bosh/converter/resolver_test.go @@ -900,7 +900,7 @@ instance_groups: Ops: []bdc.ResourceReference{}, }, } - m, implicitVars, err := resolver.WithOpsManifest(context.Background(), deployment, "default") + m, implicitVars, err := resolver.WithOpsManifest(deployment, "default") sslProps := m.InstanceGroups[0].Properties.Properties["ssl"].(map[string]interface{}) Expect(err).ToNot(HaveOccurred()) From be83728680d2196653d78718982f4a0c2552a1a5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2020 11:16:51 +0000 Subject: [PATCH 03/13] Bump gopkg.in/yaml.v2 from 2.2.7 to 2.2.8 Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.7 to 2.2.8. - [Release notes](https://github.com/go-yaml/yaml/releases) - [Commits](https://github.com/go-yaml/yaml/compare/v2.2.7...v2.2.8) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 79b5a149e..9865be337 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( go.uber.org/zap v1.13.0 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 gomodules.xyz/jsonpatch/v2 v2.0.1 - gopkg.in/yaml.v2 v2.2.7 + gopkg.in/yaml.v2 v2.2.8 k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8 k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d diff --git a/go.sum b/go.sum index 7c0c87c2b..b5720e0ba 100644 --- a/go.sum +++ b/go.sum @@ -447,6 +447,8 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= From 290bcfbf016e70ffea87b9a1c7e622cc60bb6a52 Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Fri, 24 Jan 2020 13:53:23 +0100 Subject: [PATCH 04/13] QuarksSecret triggers on status updates If status.Generated is updated to false, the reconciler will trigger. This removes the need for a second update to the spec and the timestamp field, which was only added to trigger the update. [#169709469](https://www.pivotaltracker.com/story/show/169709469) --- pkg/kube/apis/quarkssecret/v1alpha1/types.go | 2 -- .../apis/quarkssecret/v1alpha1/zz_generated.deepcopy.go | 4 ---- .../controllers/quarkssecret/quarkssecret_controller.go | 2 +- .../controllers/quarkssecret/secret_rotation_reconciler.go | 7 ------- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/pkg/kube/apis/quarkssecret/v1alpha1/types.go b/pkg/kube/apis/quarkssecret/v1alpha1/types.go index b6c99714b..de0be8a8b 100644 --- a/pkg/kube/apis/quarkssecret/v1alpha1/types.go +++ b/pkg/kube/apis/quarkssecret/v1alpha1/types.go @@ -90,8 +90,6 @@ type QuarksSecretSpec struct { Type SecretType `json:"type"` Request Request `json:"request"` SecretName string `json:"secretName"` - // Rotation is used to trigger a new version of the generated secret - Rotation *metav1.Time `json:"rotation,omitempty"` } // QuarksSecretStatus defines the observed state of QuarksSecret diff --git a/pkg/kube/apis/quarkssecret/v1alpha1/zz_generated.deepcopy.go b/pkg/kube/apis/quarkssecret/v1alpha1/zz_generated.deepcopy.go index 259da36f6..cc2bfc6e1 100644 --- a/pkg/kube/apis/quarkssecret/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/kube/apis/quarkssecret/v1alpha1/zz_generated.deepcopy.go @@ -112,10 +112,6 @@ func (in *QuarksSecretList) DeepCopyObject() runtime.Object { func (in *QuarksSecretSpec) DeepCopyInto(out *QuarksSecretSpec) { *out = *in in.Request.DeepCopyInto(&out.Request) - if in.Rotation != nil { - in, out := &in.Rotation, &out.Rotation - *out = (*in).DeepCopy() - } return } diff --git a/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go b/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go index ec657d7d9..f9ac1459b 100644 --- a/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go +++ b/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go @@ -61,7 +61,7 @@ func AddQuarksSecret(ctx context.Context, config *config.Config, mgr manager.Man UpdateFunc: func(e event.UpdateEvent) bool { o := e.ObjectOld.(*qsv1a1.QuarksSecret) n := e.ObjectNew.(*qsv1a1.QuarksSecret) - if !reflect.DeepEqual(o.Spec, n.Spec) { + if !reflect.DeepEqual(o.Spec, n.Spec) || !n.Status.Generated { ctxlog.NewPredicateEvent(e.ObjectNew).Debug( ctx, e.MetaNew, "qsv1a1.QuarksSecret", fmt.Sprintf("Update predicate passed for '%s'", e.MetaNew.GetName()), diff --git a/pkg/kube/controllers/quarkssecret/secret_rotation_reconciler.go b/pkg/kube/controllers/quarkssecret/secret_rotation_reconciler.go index cef15629a..56d98c7f0 100644 --- a/pkg/kube/controllers/quarkssecret/secret_rotation_reconciler.go +++ b/pkg/kube/controllers/quarkssecret/secret_rotation_reconciler.go @@ -8,7 +8,6 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -89,12 +88,6 @@ func (r *ReconcileSecretRotation) Reconcile(request reconcile.Request) (reconcil if err != nil { return reconcile.Result{}, errors.Wrap(err, "Error updating QuarksSecret status") } - now := metav1.Now() - qsec.Spec.Rotation = &now - err = r.client.Update(ctx, qsec) - if err != nil { - return reconcile.Result{}, errors.Wrap(err, "Error updating QuarksSecret spec") - } } return reconcile.Result{}, nil From 02f62939b8749a8e6575d656565276cc19cbacaf Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Fri, 24 Jan 2020 12:55:41 +0100 Subject: [PATCH 05/13] Clean up mutator * check was never effective, because annotations were overwritten before * check was uneccessary because assignment of two equal values is not a change * check was uneccessary because update cannot modify status * fixed comments to match behavior --- pkg/kube/util/mutate/mutate.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/pkg/kube/util/mutate/mutate.go b/pkg/kube/util/mutate/mutate.go index 8edf2f8c4..9c187b3ff 100644 --- a/pkg/kube/util/mutate/mutate.go +++ b/pkg/kube/util/mutate/mutate.go @@ -55,21 +55,21 @@ func StatefulSetMutateFn(sfs *appsv1.StatefulSet) controllerutil.MutateFn { } // QuarksJobMutateFn returns MutateFn which mutates QuarksJob including: -// - labels, annotations +// - annotations and trigger strategy if empty +// - labels // - spec.output, spec.Template, spec.updateOnConfigChange func QuarksJobMutateFn(qJob *qjv1a1.QuarksJob) controllerutil.MutateFn { updated := qJob.DeepCopy() return func() error { qJob.Labels = updated.Labels - qJob.Annotations = updated.Annotations - // Does not reset Spec.Trigger.Strategy - if len(qJob.Spec.Trigger.Strategy) == 0 { - qJob.Spec.Trigger.Strategy = updated.Spec.Trigger.Strategy - } // Does not reset Annotations if qJob.ObjectMeta.Annotations == nil { qJob.ObjectMeta.Annotations = updated.ObjectMeta.Annotations } + // Does not reset Spec.Trigger.Strategy + if len(qJob.Spec.Trigger.Strategy) == 0 { + qJob.Spec.Trigger.Strategy = updated.Spec.Trigger.Strategy + } qJob.Spec.Output = updated.Spec.Output qJob.Spec.Template = updated.Spec.Template qJob.Spec.UpdateOnConfigChange = updated.Spec.UpdateOnConfigChange @@ -80,19 +80,12 @@ func QuarksJobMutateFn(qJob *qjv1a1.QuarksJob) controllerutil.MutateFn { // QuarksSecretMutateFn returns MutateFn which mutates QuarksSecret including: // - labels, annotations // - spec -// - status.generated func QuarksSecretMutateFn(qSec *qsv1a1.QuarksSecret) controllerutil.MutateFn { updated := qSec.DeepCopy() return func() error { qSec.Labels = updated.Labels qSec.Annotations = updated.Annotations - // Update only when spec or status has been changed - if !reflect.DeepEqual(qSec.Spec, updated.Spec) { - qSec.Spec = updated.Spec - } - if qSec.Status.Generated != updated.Status.Generated { - qSec.Status.Generated = updated.Status.Generated - } + qSec.Spec = updated.Spec return nil } From 54dbe38fe944c02c1f6e66ff230f163bf72eae7c Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Fri, 24 Jan 2020 12:54:40 +0100 Subject: [PATCH 06/13] Fix e2e update test for implicit secrets * implicit secret has to be used in manifest to trigger update * change to quarks secret, via implicit secret, was ignored. reconciler now sets generated to false when quarks secret changed [#169709469](https://www.pivotaltracker.com/story/show/169709469) --- ...nt-with-implicit-in-explicit-variable.yaml | 61 +++++++++++++++++++ ...boshdeployment-with-implicit-variable.yaml | 5 +- e2e/kube/examples_count_test.go | 2 +- e2e/kube/examples_test.go | 29 +++++++-- .../boshdeployment/deployment_reconciler.go | 15 ++++- 5 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 docs/examples/bosh-deployment/boshdeployment-with-implicit-in-explicit-variable.yaml diff --git a/docs/examples/bosh-deployment/boshdeployment-with-implicit-in-explicit-variable.yaml b/docs/examples/bosh-deployment/boshdeployment-with-implicit-in-explicit-variable.yaml new file mode 100644 index 000000000..972b911a9 --- /dev/null +++ b/docs/examples/bosh-deployment/boshdeployment-with-implicit-in-explicit-variable.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: nats-deployment.var-system-domain +type: Opaque +stringData: + value: foo.com +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nats-manifest +data: + manifest: | + --- + name: nats-deployment + releases: + - name: nats + version: "26" + url: docker.io/cfcontainerization + stemcell: + os: opensuse-42.3 + version: 30.g9c91e77-30.80-7.0.0_257.gb97ced55 + instance_groups: + - name: nats + instances: 1 + jobs: + - name: nats + release: nats + properties: + nats: + user: admin + password: ((nats_password)) + ca: ((nats_ca.certificate)) + variables: + - name: nats_password + type: password + - name: nats_ca + type: certificate + options: + is_ca: true + common_name: routerCA + - name: nats_cert + type: certificate + options: + ca: nats_ca + common_name: routerSSL + alternative_names: + - "((system_domain))" + - "*.((system_domain))" + +--- +apiVersion: quarks.cloudfoundry.org/v1alpha1 +kind: BOSHDeployment +metadata: + name: nats-deployment +spec: + manifest: + name: nats-manifest + type: configmap diff --git a/docs/examples/bosh-deployment/boshdeployment-with-implicit-variable.yaml b/docs/examples/bosh-deployment/boshdeployment-with-implicit-variable.yaml index 2248d4e37..04753eb6a 100644 --- a/docs/examples/bosh-deployment/boshdeployment-with-implicit-variable.yaml +++ b/docs/examples/bosh-deployment/boshdeployment-with-implicit-variable.yaml @@ -32,6 +32,7 @@ data: nats: user: admin password: ((nats_password)) + domain: ((system_domain)) variables: - name: nats_password type: password @@ -46,8 +47,8 @@ data: ca: nats_ca common_name: routerSSL alternative_names: - - "((system_domain))" - - "*.((system_domain))" + - "foo.bar" + - "*.foo.bar" --- apiVersion: quarks.cloudfoundry.org/v1alpha1 diff --git a/e2e/kube/examples_count_test.go b/e2e/kube/examples_count_test.go index 9b3e04da0..8810e4a0c 100644 --- a/e2e/kube/examples_count_test.go +++ b/e2e/kube/examples_count_test.go @@ -19,6 +19,6 @@ var _ = Describe("Examples Directory Files", func() { }) Expect(err).NotTo(HaveOccurred()) // If this testcase fails that means a test case is missing for an example in the docs folder - Expect(countFile).To(Equal(27)) + Expect(countFile).To(Equal(28)) }) }) diff --git a/e2e/kube/examples_test.go b/e2e/kube/examples_test.go index 591caa0cd..7eb18e37d 100644 --- a/e2e/kube/examples_test.go +++ b/e2e/kube/examples_test.go @@ -25,11 +25,11 @@ var _ = Describe("Examples Directory", func() { const pollInterval = 5 * time.Second podRestarted := func(podName string, startTime time.Time) { - wait.PollImmediate(pollInterval, kubectl.PollTimeout, func() (bool, error) { + err := wait.PollImmediate(pollInterval, kubectl.PollTimeout, func() (bool, error) { status, err := kubectl.PodStatus(namespace, podName) return ((err == nil) && status.StartTime.After(startTime)), err }) - podWait("pod/" + podName) + Expect(err).ToNot(HaveOccurred()) } JustBeforeEach(func() { @@ -220,15 +220,34 @@ var _ = Describe("Examples Directory", func() { }) }) - Context("bosh-deployment with a implicit variable example", func() { + Context("bosh-deployment with an implicit variable example", func() { BeforeEach(func() { example = "bosh-deployment/boshdeployment-with-implicit-variable.yaml" }) It("updates deployment when implicit variable changes", func() { + By("Checking for pods") + podWait("pod/nats-deployment-nats-0") + status, err := kubectl.PodStatus(namespace, "nats-deployment-nats-0") + Expect(err).ToNot(HaveOccurred(), "error getting pod status") + startTime := status.StartTime - Skip("Skipping this test as this is related to secret rotation and secret rotation is not yet supported in `cf-operator`.") + By("Updating implicit variable") + implicitVariablePath := examplesDir + "bosh-deployment/implicit-variable-updated.yaml" + err = cmdHelper.Apply(namespace, implicitVariablePath) + Expect(err).ToNot(HaveOccurred()) + + By("Checking for pod restart") + podRestarted("nats-deployment-nats-0", startTime.Time) + }) + }) + + Context("bosh-deployment with an implicit variable used by an explicit variable example", func() { + BeforeEach(func() { + example = "bosh-deployment/boshdeployment-with-implicit-in-explicit-variable.yaml" + }) + It("updates quarks secret when implicit variable changes, then deployment updates", func() { By("Checking for pods") podWait("pod/nats-deployment-nats-0") status, err := kubectl.PodStatus(namespace, "nats-deployment-nats-0") @@ -240,7 +259,7 @@ var _ = Describe("Examples Directory", func() { err = cmdHelper.Apply(namespace, implicitVariablePath) Expect(err).ToNot(HaveOccurred()) - By("Checking for new pod") + By("Checking for pod restart") podRestarted("nats-deployment-nats-0", startTime.Time) }) }) diff --git a/pkg/kube/controllers/boshdeployment/deployment_reconciler.go b/pkg/kube/controllers/boshdeployment/deployment_reconciler.go index aa5c4c686..bf5564d9e 100644 --- a/pkg/kube/controllers/boshdeployment/deployment_reconciler.go +++ b/pkg/kube/controllers/boshdeployment/deployment_reconciler.go @@ -412,12 +412,13 @@ func (r *ReconcileBOSHDeployment) listPodsFromSelector(namespace string, selecto // createQuarksSecrets create variables quarksSecrets func (r *ReconcileBOSHDeployment) createQuarksSecrets(ctx context.Context, manifestSecret *corev1.Secret, variables []qsv1a1.QuarksSecret) error { for _, variable := range variables { - log.Debugf(ctx, "Creating QuarksSecrets for explicit variable '%s'", variable.Name) + log.Debugf(ctx, "CreateOrUpdate QuarksSecrets for explicit variable '%s'", variable.Name) + // Set the "manifest with ops" secret as the owner for the QuarksSecrets // The "manifest with ops" secret is owned by the actual BOSHDeployment, so everything // should be garbage collected properly. if err := r.setReference(manifestSecret, &variable, r.scheme); err != nil { - err = log.WithEvent(manifestSecret, "OwnershipError").Errorf(ctx, "Failed to set ownership for %s: %v", variable.Name, err) + err = log.WithEvent(manifestSecret, "OwnershipError").Errorf(ctx, "failed to set ownership for %s: %v", variable.Name, err) return err } @@ -426,6 +427,16 @@ func (r *ReconcileBOSHDeployment) createQuarksSecrets(ctx context.Context, manif return errors.Wrapf(err, "creating or updating QuarksSecret '%s'", variable.Name) } + // Update does not update status. We only trigger quarks secret + // reconciler again if variable was updated by previous CreateOrUpdate + if op == controllerutil.OperationResultUpdated { + variable.Status.Generated = false + if err := r.client.Status().Update(ctx, &variable); err != nil { + log.WithEvent(&variable, "UpdateError").Errorf(ctx, "failed to update generated status on quarks secret '%s' (%v): %s", variable.Name, variable.ResourceVersion, err) + return err + } + } + log.Debugf(ctx, "QuarksSecret '%s' has been %s", variable.Name, op) } From 48f13b9fe92706b93a19a3c5b657c4a69e32e0e9 Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Mon, 27 Jan 2020 10:10:16 +0100 Subject: [PATCH 07/13] Update docs for QuarksSecret, clean up controller - docs were outdated - reconciler will skip if `.status.generated` is `true`, no need inspect `.spec` changes then. --- docs/controllers/quarks_gvc_and_esec_flow.png | Bin 50871 -> 0 bytes docs/controllers/quarks_secret.md | 46 +++++++++++------- .../quarkssecret/quarkssecret_controller.go | 4 +- 3 files changed, 30 insertions(+), 20 deletions(-) delete mode 100644 docs/controllers/quarks_gvc_and_esec_flow.png diff --git a/docs/controllers/quarks_gvc_and_esec_flow.png b/docs/controllers/quarks_gvc_and_esec_flow.png deleted file mode 100644 index 310a3c33d5ef5b1e8ee41c6b98681eba2c919b7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50871 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

{?3#ZT|eL^I&M4})NaEeK0D12L@$Ng`N4Xc-w~ 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 > You can find more details in the [BOSH docs](https://bosh.io/docs/variable-types). -##### Policies - -The developer can specify policies for rotation (e.g. automatic or not ) and how secrets are created (e.g. password complexity, certificate expiration date, etc.). - ##### Auto-approving Certificates -A certificate `QuarksSecret` can be signed by the Kube API Server. The **QuarksSecret** Controller is responsible for generating the certificate signing request: +A certificate `QuarksSecret` can be signed by the Kubernetes API Server. The **QuarksSecret** Controller is responsible for generating the certificate signing request: ```yaml apiVersion: certificates.k8s.io/v1beta1 @@ -89,24 +89,36 @@ spec: ![certsr-controller-flow](quarks_certsrcontroller_flow.png) *Fig. 3: The CertificateSigningRequest controller* -#### Watches in CSR controller +#### Watches in CSR Controller - `Certificate Signing Request`: Creation -#### Reconciliation in CSR controller +#### Reconciliation in CSR Controller - once the request is approved by Kubernetes API, will generate a certificate stored in a Kubernetes secret, that is recognized by the cluster. -#### Highlights in CSR controller +#### Highlights in CSR Controller The CertificateSigningRequest controller watches for `CertificateSigningRequest` and approves `QuarksSecret`-owned CSRs and persists the generated certificate. -## Relationship with the BDPL component +### **_SecretRotation Controller_** + +The secret rotation controller watches for a rotation config map and re-generates all the listed `QuarksSecrets`. + +#### Watches in Secret Rotation Controller + +- `ConfigMap`: Creation of a config map, which has the `secret-rotation` label. + +#### Reconciliation in Secret Rotation Controller + +- Will read the array of `QuarksSecret` names from the JSON under the config map key `secrets`. +- Skip `QuarksSecret` where `.status.generated` is `false`, as these might be under control of the user. +- Set `.status.generated` for each named `QuarksSecret` to `false`, to trigger re-creation of the corresponding secret. -![bdpl-qjob-relationship](quarks_gvc_and_esec_flow.png) -*Fig. 4: Relationship between the Generated V. controller and the QuarksSecret component* +## Relationship With the BDPL Component -Figure 4 illustrates the interaction of the **Generated Variables** Controller with the **QuarksSecret** Controller. When reconciling, the Generated Variables Controller lists all variables of a BOSH manifest(basically all BOSH variables) and generates an `QuarksSecret` instance per variable, which will trigger the **QuarksSecret** Controller. +All explicit variables of a BOSH manifest will be created as `QuarksSecret` instances, which will trigger the **QuarksSecret** Controller. +This will create corresponding secrets. If the user decides to change a secret, the `.status.generated` field in the corresponding `QuarksSecret` should be set to `false`, to protect against overwriting. ## `QuarksSecret` Examples diff --git a/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go b/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go index f9ac1459b..0d4199182 100644 --- a/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go +++ b/pkg/kube/controllers/quarkssecret/quarkssecret_controller.go @@ -3,7 +3,6 @@ package quarkssecret import ( "context" "fmt" - "reflect" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -59,9 +58,8 @@ func AddQuarksSecret(ctx context.Context, config *config.Config, mgr manager.Man DeleteFunc: func(e event.DeleteEvent) bool { return false }, GenericFunc: func(e event.GenericEvent) bool { return false }, UpdateFunc: func(e event.UpdateEvent) bool { - o := e.ObjectOld.(*qsv1a1.QuarksSecret) n := e.ObjectNew.(*qsv1a1.QuarksSecret) - if !reflect.DeepEqual(o.Spec, n.Spec) || !n.Status.Generated { + if !n.Status.Generated { ctxlog.NewPredicateEvent(e.ObjectNew).Debug( ctx, e.MetaNew, "qsv1a1.QuarksSecret", fmt.Sprintf("Update predicate passed for '%s'", e.MetaNew.GetName()), From ab1e98227f74b5e3452604b8d1b90af7515fb080 Mon Sep 17 00:00:00 2001 From: Svk Rohit Date: Thu, 23 Jan 2020 18:39:24 +0530 Subject: [PATCH 08/13] Add secret webhook validator --- pkg/kube/controllers/controllers.go | 1 + .../quarkssecret/validating_webhook.go | 131 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 pkg/kube/controllers/quarkssecret/validating_webhook.go diff --git a/pkg/kube/controllers/controllers.go b/pkg/kube/controllers/controllers.go index 697669457..9cebf25ac 100644 --- a/pkg/kube/controllers/controllers.go +++ b/pkg/kube/controllers/controllers.go @@ -64,6 +64,7 @@ var addToSchemes = runtime.SchemeBuilder{ var validatingHookFuncs = []func(*zap.SugaredLogger, *config.Config) *wh.OperatorWebhook{ boshdeployment.NewBOSHDeploymentValidator, + quarkssecret.NewSecretValidator, } var mutatingHookFuncs = []func(*zap.SugaredLogger, *config.Config) *wh.OperatorWebhook{ diff --git a/pkg/kube/controllers/quarkssecret/validating_webhook.go b/pkg/kube/controllers/quarkssecret/validating_webhook.go new file mode 100644 index 000000000..d73657fbc --- /dev/null +++ b/pkg/kube/controllers/quarkssecret/validating_webhook.go @@ -0,0 +1,131 @@ +package quarkssecret + +import ( + "context" + "fmt" + + "go.uber.org/zap" + + "k8s.io/api/admission/v1beta1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/runtime/inject" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + wh "code.cloudfoundry.org/cf-operator/pkg/kube/util/webhook" + "code.cloudfoundry.org/quarks-utils/pkg/config" + log "code.cloudfoundry.org/quarks-utils/pkg/ctxlog" + "code.cloudfoundry.org/quarks-utils/pkg/names" + vss "code.cloudfoundry.org/quarks-utils/pkg/versionedsecretstore" +) + +// NewSecretValidator creates a validating hook for Secret and adds it to the Manager +func NewSecretValidator(log *zap.SugaredLogger, config *config.Config) *wh.OperatorWebhook { + log.Info("Setting up validator for Secret") + + secretValidator := NewValidationHandler(log) + + globalScopeType := admissionregistrationv1beta1.ScopeType("*") + return &wh.OperatorWebhook{ + FailurePolicy: admissionregistrationv1beta1.Fail, + Rules: []admissionregistrationv1beta1.RuleWithOperations{ + { + Rule: admissionregistrationv1beta1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"secrets"}, + Scope: &globalScopeType, + }, + Operations: []admissionregistrationv1beta1.OperationType{ + "UPDATE", + }, + }, + }, + Path: "/validate-secret", + Name: "validate-secret." + names.GroupName, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "cf-operator-ns": config.Namespace, + }, + }, + Webhook: &admission.Webhook{ + Handler: secretValidator, + }, + } +} + +// ValidationHandler is a struct for secret validator object. +type ValidationHandler struct { + log *zap.SugaredLogger + client client.Client + decoder *admission.Decoder +} + +// NewValidationHandler returns a new ValidationHandler +func NewValidationHandler(log *zap.SugaredLogger) admission.Handler { + validationLog := log.Named("secret-validator") + validationLog.Info("Creating a validator for Secret") + return &ValidationHandler{ + log: validationLog, + } +} + +//Handle validates a Secret +func (v *ValidationHandler) Handle(_ context.Context, req admission.Request) admission.Response { + secret := &corev1.Secret{} + ctx := log.NewParentContext(v.log) + + err := v.decoder.Decode(req, secret) + if err != nil { + return admission.Response{ + AdmissionResponse: v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: fmt.Sprintf("Failed to decode secret: %s", err.Error()), + }, + }, + } + } + + // Checking if the secret is a versioned secret + ok := vss.IsVersionedSecret(*secret) + if ok { + log.Infof(ctx, "Denying updation of versioned secret '%s' as it is immutable.", secret.Name) + return admission.Response{ + AdmissionResponse: v1beta1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: fmt.Sprintf("Denying updation of versioned secret %s as it is immutable.", secret.GetName()), + }, + }, + } + } + + return admission.Response{ + AdmissionResponse: v1beta1.AdmissionResponse{ + Allowed: true, + }, + } +} + +// Validator implements inject.Client. +// A client will be automatically injected. +var _ inject.Client = &ValidationHandler{} + +// InjectClient injects the client. +func (v *ValidationHandler) InjectClient(c client.Client) error { + v.client = c + return nil +} + +// Validator implements inject.Decoder. +// A decoder will be automatically injected. +var _ admission.DecoderInjector = &ValidationHandler{} + +// InjectDecoder injects the decoder. +func (v *ValidationHandler) InjectDecoder(d *admission.Decoder) error { + v.decoder = d + return nil +} From 8db6a46519b62a30faf8887429873da95c5e7bd7 Mon Sep 17 00:00:00 2001 From: Svk Rohit Date: Thu, 23 Jan 2020 22:23:28 +0530 Subject: [PATCH 09/13] Update docs about secret validation webhook --- docs/testing.md | 4 +++- pkg/kube/controllers/quarkssecret/validating_webhook.go | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 81cf8cdd2..95b977596 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -92,7 +92,9 @@ is set to `true`. Quarks StatefulSet requires a k8s webhook to mutate the volumes of a pod. Kubernetes will call back to the operator for certain requests and use the modified pod manifest, which is returned. -CF-Operator also uses a validating webhook to check the BOSH deployment custom resource. +CF-Operator also uses a validating webhook to validate the BOSH deployment spec and the creation +of reference resources specified in the spec. Secret validation admission webhook restricts the +user from updating a versioned secret. The cf-operator integration tests use `CF_OPERATOR_WEBHOOK_SERVICE_PORT` as a base value to calculate the port number to listen to on `CF_OPERATOR_WEBHOOK_SERVICE_HOST`. diff --git a/pkg/kube/controllers/quarkssecret/validating_webhook.go b/pkg/kube/controllers/quarkssecret/validating_webhook.go index d73657fbc..18dad05df 100644 --- a/pkg/kube/controllers/quarkssecret/validating_webhook.go +++ b/pkg/kube/controllers/quarkssecret/validating_webhook.go @@ -21,7 +21,7 @@ import ( vss "code.cloudfoundry.org/quarks-utils/pkg/versionedsecretstore" ) -// NewSecretValidator creates a validating hook for Secret and adds it to the Manager +// NewSecretValidator creates a validating hook to deny updates to versioned secrets and adds it to the manager. func NewSecretValidator(log *zap.SugaredLogger, config *config.Config) *wh.OperatorWebhook { log.Info("Setting up validator for Secret") @@ -72,7 +72,7 @@ func NewValidationHandler(log *zap.SugaredLogger) admission.Handler { } } -//Handle validates a Secret +//Handle denies changes to all versioned secrets as they are immutable func (v *ValidationHandler) Handle(_ context.Context, req admission.Request) admission.Response { secret := &corev1.Secret{} ctx := log.NewParentContext(v.log) @@ -92,12 +92,12 @@ func (v *ValidationHandler) Handle(_ context.Context, req admission.Request) adm // Checking if the secret is a versioned secret ok := vss.IsVersionedSecret(*secret) if ok { - log.Infof(ctx, "Denying updation of versioned secret '%s' as it is immutable.", secret.Name) + log.Infof(ctx, "Denying update to versioned secret '%s' as it is immutable.", secret.Name) return admission.Response{ AdmissionResponse: v1beta1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ - Message: fmt.Sprintf("Denying updation of versioned secret %s as it is immutable.", secret.GetName()), + Message: fmt.Sprintf("Denying update to versioned secret '%s' as it is immutable.", secret.GetName()), }, }, } From b6782b849e54971af8bf3455e9f69230902127ca Mon Sep 17 00:00:00 2001 From: Svk Rohit Date: Thu, 23 Jan 2020 21:17:21 +0530 Subject: [PATCH 10/13] Add unit tests for secret validation webhook --- pkg/kube/controllers/controllers_test.go | 2 +- .../quarkssecret/validating_webhook_test.go | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 pkg/kube/controllers/quarkssecret/validating_webhook_test.go diff --git a/pkg/kube/controllers/controllers_test.go b/pkg/kube/controllers/controllers_test.go index 8c4f5e598..7c4031010 100644 --- a/pkg/kube/controllers/controllers_test.go +++ b/pkg/kube/controllers/controllers_test.go @@ -176,7 +176,7 @@ var _ = Describe("Controllers", func() { return nil case *admissionregistrationv1beta1.ValidatingWebhookConfiguration: Expect(config.Name).To(Equal("cf-operator-hook-" + config.Namespace)) - Expect(len(config.Webhooks)).To(Equal(1)) + Expect(len(config.Webhooks)).To(Equal(2)) wh := config.Webhooks[0] Expect(wh.Name).To(Equal("validate-boshdeployment.quarks.cloudfoundry.org")) diff --git a/pkg/kube/controllers/quarkssecret/validating_webhook_test.go b/pkg/kube/controllers/quarkssecret/validating_webhook_test.go new file mode 100644 index 000000000..e293ebd3f --- /dev/null +++ b/pkg/kube/controllers/quarkssecret/validating_webhook_test.go @@ -0,0 +1,93 @@ +package quarkssecret_test + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "go.uber.org/zap" + + "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/json" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "code.cloudfoundry.org/cf-operator/pkg/kube/controllers/quarkssecret" + "code.cloudfoundry.org/quarks-utils/pkg/ctxlog" + vss "code.cloudfoundry.org/quarks-utils/pkg/versionedsecretstore" + helper "code.cloudfoundry.org/quarks-utils/testing/testhelper" +) + +var _ = Describe("When the webhook handles update request of a secret", func() { + var ( + log *zap.SugaredLogger + ctx context.Context + decoder *admission.Decoder + validator admission.Handler + secretBytes []byte + validateSecret func() admission.Response + secret corev1.Secret + ) + + BeforeEach(func() { + _, log = helper.NewTestLogger() + ctx = ctxlog.NewParentContext(log) + + secret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mysecret", + Labels: map[string]string{ + vss.LabelSecretKind: "versionedSecret", + }, + }, + Data: map[string][]byte{ + "key": []byte("value"), + }, + } + }) + + JustBeforeEach(func() { + scheme := runtime.NewScheme() + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + decoder, _ = admission.NewDecoder(scheme) + validator = quarkssecret.NewValidationHandler(log) + validator.(admission.DecoderInjector).InjectDecoder(decoder) + + validateSecret = func() admission.Response { + response := validator.Handle(ctx, admission.Request{ + AdmissionRequest: v1beta1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: secretBytes, + }, + }, + }) + return response + } + }) + + Context("which is not a versioned type", func() { + BeforeEach(func() { + secret.SetLabels(map[string]string{}) + secretBytes, _ = json.Marshal(secret) + }) + + It("should allow", func() { + response := validateSecret() + Expect(response.AdmissionResponse.Allowed).To(BeTrue()) + }) + }) + + Context("which is a versioned type", func() { + BeforeEach(func() { + secretBytes, _ = json.Marshal(secret) + }) + + It("should not allow", func() { + response := validateSecret() + Expect(response.AdmissionResponse.Allowed).To(BeFalse()) + Expect(response.AdmissionResponse.Result.Message).To(Equal("Denying update to versioned secret 'mysecret' as it is immutable.")) + }) + }) +}) From c1abf810bdcf48765a867edafbd9428c2776f088 Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Tue, 14 Jan 2020 18:00:28 +0100 Subject: [PATCH 11/13] Apply helm chart guidelines * rename rbacEnable to rbac.create * only create rbac resources if rbac.create is true * only create service account if create is true * adapt documentation for service account * templates should use dashes https://helm.sh/docs/topics/chart_best_practices/rbac/ [#165014706](https://www.pivotaltracker.com/story/show/165014706) --- deploy/helm/cf-operator/Chart.yaml | 1 + deploy/helm/cf-operator/README.md | 8 ++--- .../helm/cf-operator/templates/_helpers.tpl | 6 ++-- .../{cluster_role.yaml => cluster-role.yaml} | 2 +- .../{role_binding.yaml => role-binding.yaml} | 2 ++ deploy/helm/cf-operator/templates/role.yaml | 2 ++ ....yaml => service-account-pull-secret.yaml} | 0 ...vice_account.yaml => service-account.yaml} | 2 ++ deploy/helm/cf-operator/values.yaml | 30 +++++++++---------- 9 files changed, 29 insertions(+), 24 deletions(-) rename deploy/helm/cf-operator/templates/{cluster_role.yaml => cluster-role.yaml} (95%) rename deploy/helm/cf-operator/templates/{role_binding.yaml => role-binding.yaml} (84%) rename deploy/helm/cf-operator/templates/{service_account_pull_secret.yaml => service-account-pull-secret.yaml} (100%) rename deploy/helm/cf-operator/templates/{service_account.yaml => service-account.yaml} (77%) diff --git a/deploy/helm/cf-operator/Chart.yaml b/deploy/helm/cf-operator/Chart.yaml index e504d5975..17e590ef8 100644 --- a/deploy/helm/cf-operator/Chart.yaml +++ b/deploy/helm/cf-operator/Chart.yaml @@ -1,4 +1,5 @@ apiVersion: v1 description: A Helm chart for cf-operator, the k8s operator for deploying BOSH releases +icon: https://www.cloudfoundry.org/wp-content/uploads/erini-quarks-wide.png name: cf-operator version: 0.0.1 diff --git a/deploy/helm/cf-operator/README.md b/deploy/helm/cf-operator/README.md index 1cb82000c..162220fd3 100644 --- a/deploy/helm/cf-operator/README.md +++ b/deploy/helm/cf-operator/README.md @@ -65,12 +65,12 @@ helm delete cf-operator --purge | `global.image.pullPolicy` | Kubernetes image pullPolicy | `IfNotPresent` | | `global.image.credentials` | Kubernetes image pull secret credentials (map with keys `servername`, `username`, and `password`) | `nil` | | `global.operator.watchNamespace` | Namespace the operator will watch for BOSH deployments | the release namespace | -| `global.rbacEnable` | Install required RBAC service account, roles and rolebindings | `true` | +| `global.rbac.create` | Install required RBAC service account, roles and rolebindings | `true` | | `operator.webhook.endpoint` | Hostname/IP under which the webhook server can be reached from the cluster | the IP of service `cf-operator-webhook` | | `operator.webhook.port` | Port the webhook server listens on | 2999 | | `global.operator.webhook.useServiceReference` | If true, the webhook server is addressed using a service reference instead of the IP | `true` | -| `serviceAccount.cfOperatorServiceAccount.create` | Will set the value of `cf-operator.serviceAccountName` to the current chart name | `true` | -| `serviceAccount.cfOperatorServiceAccount.name` | If the above is not set, it will set the `cf-operator.serviceAccountName` | | +| `serviceAccount.create` | If true, create a service account | `true` | +| `serviceAccount.name` | If not set and `create` is `true`, a name is generated using the fullname of the chart | | > **Note:** > @@ -92,5 +92,5 @@ By default, the helm chart will install RBAC ClusterRole and ClusterRoleBinding The RBAC resources are enable by default. To disable: ```bash -helm install --namespace cf-operator --name cf-operator https://s3.amazonaws.com/cf-operators/helm-charts/cf-operator-v0.2.2%2B47.g24492ea.tgz --set global.rbacEnable=false +helm install --namespace cf-operator --name cf-operator https://s3.amazonaws.com/cf-operators/helm-charts/cf-operator-v0.2.2%2B47.g24492ea.tgz --set global.rbac.create=false ``` diff --git a/deploy/helm/cf-operator/templates/_helpers.tpl b/deploy/helm/cf-operator/templates/_helpers.tpl index cff179563..8ff44e86c 100644 --- a/deploy/helm/cf-operator/templates/_helpers.tpl +++ b/deploy/helm/cf-operator/templates/_helpers.tpl @@ -35,9 +35,9 @@ Create chart name and version as used by the chart label. Create the name of the cf-operator service account to use */}} {{- define "cf-operator.serviceAccountName" -}} -{{- if .Values.serviceAccount.cfOperatorServiceAccount.create -}} - {{ default (include "cf-operator.fullname" .) .Values.serviceAccount.cfOperatorServiceAccount.name }} +{{- if .Values.serviceAccount.create -}} + {{ default (include "cf-operator.fullname" .) .Values.serviceAccount.name }} {{- else -}} - {{ default "default" .Values.serviceAccount.cfOperatorServiceAccount.name }} + {{ default "default" .Values.serviceAccount.name }} {{- end -}} {{- end -}} diff --git a/deploy/helm/cf-operator/templates/cluster_role.yaml b/deploy/helm/cf-operator/templates/cluster-role.yaml similarity index 95% rename from deploy/helm/cf-operator/templates/cluster_role.yaml rename to deploy/helm/cf-operator/templates/cluster-role.yaml index 4c721d914..41aeb1e29 100644 --- a/deploy/helm/cf-operator/templates/cluster_role.yaml +++ b/deploy/helm/cf-operator/templates/cluster-role.yaml @@ -1,4 +1,4 @@ -{{- if .Values.global.rbacEnable }} +{{- if .Values.global.rbac.create }} --- apiVersion: v1 kind: List diff --git a/deploy/helm/cf-operator/templates/role_binding.yaml b/deploy/helm/cf-operator/templates/role-binding.yaml similarity index 84% rename from deploy/helm/cf-operator/templates/role_binding.yaml rename to deploy/helm/cf-operator/templates/role-binding.yaml index 320066f36..d1bec7093 100644 --- a/deploy/helm/cf-operator/templates/role_binding.yaml +++ b/deploy/helm/cf-operator/templates/role-binding.yaml @@ -1,3 +1,4 @@ +{{- if .Values.global.rbac.create }} kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -10,3 +11,4 @@ roleRef: kind: Role name: cf-operator apiGroup: rbac.authorization.k8s.io +{{- end }} diff --git a/deploy/helm/cf-operator/templates/role.yaml b/deploy/helm/cf-operator/templates/role.yaml index daa8c5fd4..d45cc23dc 100644 --- a/deploy/helm/cf-operator/templates/role.yaml +++ b/deploy/helm/cf-operator/templates/role.yaml @@ -1,3 +1,4 @@ +{{- if .Values.global.rbac.create }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -47,3 +48,4 @@ rules: - jobs verbs: - '*' +{{- end }} diff --git a/deploy/helm/cf-operator/templates/service_account_pull_secret.yaml b/deploy/helm/cf-operator/templates/service-account-pull-secret.yaml similarity index 100% rename from deploy/helm/cf-operator/templates/service_account_pull_secret.yaml rename to deploy/helm/cf-operator/templates/service-account-pull-secret.yaml diff --git a/deploy/helm/cf-operator/templates/service_account.yaml b/deploy/helm/cf-operator/templates/service-account.yaml similarity index 77% rename from deploy/helm/cf-operator/templates/service_account.yaml rename to deploy/helm/cf-operator/templates/service-account.yaml index 7fc4471fa..490853f11 100644 --- a/deploy/helm/cf-operator/templates/service_account.yaml +++ b/deploy/helm/cf-operator/templates/service-account.yaml @@ -1,3 +1,4 @@ +{{- if or .Values.serviceAccount.create .Values.global.rbac.create }} apiVersion: v1 kind: ServiceAccount metadata: @@ -7,3 +8,4 @@ metadata: imagePullSecrets: - name: {{ template "cf-operator.serviceAccountName" . }}-pull-secret {{- end }} +{{- end }} diff --git a/deploy/helm/cf-operator/values.yaml b/deploy/helm/cf-operator/values.yaml index d34528c57..2a4eb128a 100644 --- a/deploy/helm/cf-operator/values.yaml +++ b/deploy/helm/cf-operator/values.yaml @@ -43,14 +43,13 @@ operator: # nameOverride overrides the chart name part of the release name nameOverride: "" +# serviceAccount contains the configuration +# values of the service account used by cf-operator. serviceAccount: - # cfOperatorServiceAccount contains the configuration - # values of the service account used by cf-operator. - cfOperatorServiceAccount: - # create is a boolean to control the creation of service account name. - create: true - # name of the service account. - name: + # create is a boolean to control the creation of service account name. + create: true + # name of the service account. + name: global: # Context Timeout for each K8's API request in seconds. @@ -70,17 +69,16 @@ global: # useServiceReference is a boolean to control the use of the # service reference in the webhook spec instead of a url. useServiceReference: true - # rbacEnable is a boolean to control the installation of quarks job cluster role template. - rbacEnable: true + + rbac: + # create is a boolean to control the installation of quarks job cluster role template. + create: true quarks-job: # createWatchNamespace is a boolean to control creation of watchnamespace. createWatchNamespace: false serviceAccount: - # quarksJobServiceAccount contains the configuration - # values of the service account used by quarks-job. - quarksJobServiceAccount: - # create is a boolean to control the creation of service account name. - create: true - # name of the service account. - name: + # create is a boolean to control the creation of service account name. + create: true + # name of the service account. + name: From f13462f9d6307b1f09baba4d2765dd828c0ea760 Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Thu, 16 Jan 2020 13:04:09 +0100 Subject: [PATCH 12/13] Add more information to helm chart --- bin/build-helm | 2 -- deploy/helm/cf-operator/Chart.yaml | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bin/build-helm b/bin/build-helm index 6f465dfa8..cb044eb9e 100755 --- a/bin/build-helm +++ b/bin/build-helm @@ -21,8 +21,6 @@ perl -pi -e "s|version: .*|version: ${ARTIFACT_VERSION}|g" "${output_dir}/cf-ope repo="https://cf-operators.s3.amazonaws.com/helm-charts/" qj="quarks-job-$QUARKS_JOB_HELM_VERSION.tgz" pushd "$output_dir/cf-operator" - #helm repo add quarks https://cf-operators.s3.amazonaws.com/helm-charts/ - #helm dependency update mkdir charts curl -LO "$repo$qj" tar xfz "$qj" -C charts diff --git a/deploy/helm/cf-operator/Chart.yaml b/deploy/helm/cf-operator/Chart.yaml index 17e590ef8..7c7aab360 100644 --- a/deploy/helm/cf-operator/Chart.yaml +++ b/deploy/helm/cf-operator/Chart.yaml @@ -1,5 +1,18 @@ apiVersion: v1 -description: A Helm chart for cf-operator, the k8s operator for deploying BOSH releases -icon: https://www.cloudfoundry.org/wp-content/uploads/erini-quarks-wide.png name: cf-operator version: 0.0.1 +description: A Helm chart for cf-operator, the k8s operator for deploying BOSH releases +home: https://github.com/cloudfoundry-incubator/cf-operator +icon: https://cloudfoundry-incubator.github.io/quarks-helm/logo.png +keywords: +- cloudfoundry +- bosh +- quarks +- deployment +sources: +- https://github.com/cloudfoundry-incubator/cf-operator +- https://github.com/cloudfoundry-incubator/quarks-job +- https://github.com/cfcontainerizationbot/cf-operator-base +maintainers: +- name: project-quarks + email: project-quarks@googlegroups.com From 86710ccdcf30b1bc44c79e893d201189c1961dba Mon Sep 17 00:00:00 2001 From: Mario Manno Date: Wed, 22 Jan 2020 16:49:02 +0100 Subject: [PATCH 13/13] Use valid semver in helm chart * spec doesn't allow v in actual version * helm repo doesn't list invalid versions * version has to be part of the charts filename --- bin/build-helm | 7 +++++-- bin/include/dependencies | 4 ++-- deploy/helm/cf-operator/Chart.yaml | 3 ++- go.mod | 2 +- go.sum | 16 ++-------------- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/bin/build-helm b/bin/build-helm index cb044eb9e..0d46c19dd 100755 --- a/bin/build-helm +++ b/bin/build-helm @@ -8,15 +8,18 @@ GIT_ROOT=${GIT_ROOT:-$(git rev-parse --show-toplevel)} . "${GIT_ROOT}/bin/include/dependencies" output_dir=${GIT_ROOT}/helm -filename="${output_dir}/cf-operator-${ARTIFACT_VERSION}.tgz" +version=$(echo "$ARTIFACT_VERSION" | sed 's/^v//') +filename="${output_dir}/cf-operator-${version}.tgz" [ -d "${output_dir}" ] && rm -r "${output_dir}" cp -r "${GIT_ROOT}/deploy/helm" "${output_dir}" + perl -pi -e "s|repository: .*|repository: ${DOCKER_IMAGE_REPOSITORY}|g" "${output_dir}/cf-operator/values.yaml" perl -pi -e "s|org: .*|org: ${DOCKER_IMAGE_ORG}|g" "${output_dir}/cf-operator/values.yaml" perl -pi -e "s|tag: .*|tag: ${DOCKER_IMAGE_TAG}|g" "${output_dir}/cf-operator/values.yaml" -perl -pi -e "s|version: .*|version: ${ARTIFACT_VERSION}|g" "${output_dir}/cf-operator/Chart.yaml" +perl -pi -e "s|version: .*|version: ${version}|g" "${output_dir}/cf-operator/Chart.yaml" +perl -pi -e "s|appVersion: .*|appVersion: ${version}|g" "${output_dir}/cf-operator/Chart.yaml" repo="https://cf-operators.s3.amazonaws.com/helm-charts/" qj="quarks-job-$QUARKS_JOB_HELM_VERSION.tgz" diff --git a/bin/include/dependencies b/bin/include/dependencies index af9d03f04..bc19c41d6 100644 --- a/bin/include/dependencies +++ b/bin/include/dependencies @@ -1,6 +1,6 @@ #!/bin/bash -git_sha="e0ed198" +git_sha="6a177e8" quarks_job_release="v0.0.0-0.g$git_sha" # QUARKS_JOB_IMAGE_TAG is used for integration tests @@ -11,6 +11,6 @@ fi # QUARKS_JOB_HELM_VERSION is used to build helm charts including sub-charts if [ -z ${QUARKS_JOB_HELM_VERSION+x} ]; then - QUARKS_JOB_HELM_VERSION="$quarks_job_release" + QUARKS_JOB_HELM_VERSION=$(echo "$quarks_job_release" | sed 's/^v//') export QUARKS_JOB_HELM_VERSION fi diff --git a/deploy/helm/cf-operator/Chart.yaml b/deploy/helm/cf-operator/Chart.yaml index 7c7aab360..f4316985f 100644 --- a/deploy/helm/cf-operator/Chart.yaml +++ b/deploy/helm/cf-operator/Chart.yaml @@ -1,6 +1,7 @@ apiVersion: v1 name: cf-operator -version: 0.0.1 +version: x.x.x +appVersion: x.x.x description: A Helm chart for cf-operator, the k8s operator for deploying BOSH releases home: https://github.com/cloudfoundry-incubator/cf-operator icon: https://cloudfoundry-incubator.github.io/quarks-helm/logo.png diff --git a/go.mod b/go.mod index 9865be337..a11e870f9 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module code.cloudfoundry.org/cf-operator require ( - code.cloudfoundry.org/quarks-job v0.0.0-20200117020937-e0ed19868499 + code.cloudfoundry.org/quarks-job v0.0.0-20200127101209-6a177e8e364d code.cloudfoundry.org/quarks-utils v0.0.0-20200121122630-31020afe6ac7 github.com/bmatcuk/doublestar v1.1.1 // indirect github.com/charlievieth/fs v0.0.0-20170613215519-7dc373669fa1 // indirect diff --git a/go.sum b/go.sum index b5720e0ba..e632637fd 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -code.cloudfoundry.org/quarks-job v0.0.0-20200109094705-4db12e77f5e6 h1:ShATyI/tega1MxJYZmz9kU8BoORkLS0N4OrXZWk5NUk= -code.cloudfoundry.org/quarks-job v0.0.0-20200109094705-4db12e77f5e6/go.mod h1:UoFtaj5zIi4N2q/tE6kqTK6+XbaBguaIFohTdenU5lE= -code.cloudfoundry.org/quarks-job v0.0.0-20200117020937-e0ed19868499 h1:Re0OA9Ptk4zKjJvYDKQf4L5ZIwALln3Enwn3Ty8MJTE= -code.cloudfoundry.org/quarks-job v0.0.0-20200117020937-e0ed19868499/go.mod h1:UoFtaj5zIi4N2q/tE6kqTK6+XbaBguaIFohTdenU5lE= +code.cloudfoundry.org/quarks-job v0.0.0-20200127101209-6a177e8e364d h1:OFJ43ZiR1Tp1S1p9pqA1XorkYSxqFNLECdaVezbOjgU= +code.cloudfoundry.org/quarks-job v0.0.0-20200127101209-6a177e8e364d/go.mod h1:UoFtaj5zIi4N2q/tE6kqTK6+XbaBguaIFohTdenU5lE= code.cloudfoundry.org/quarks-utils v0.0.0-20191220014113-dab838a1c0be h1:e0cGMd6GdQ01b4eAcYJIycsohlUhrO2v0Nq9dKCQyNI= code.cloudfoundry.org/quarks-utils v0.0.0-20191220014113-dab838a1c0be/go.mod h1:H6fVNegFsTZqOU2Cggvue8X5vfAdeGDKemikmwc7RBc= -code.cloudfoundry.org/quarks-utils v0.0.0-20200113103742-673f14c44002 h1:0OAvT5uLXXeDWeiF7+83/QJqm4JdyFf5HzeJbTxhDYc= -code.cloudfoundry.org/quarks-utils v0.0.0-20200113103742-673f14c44002/go.mod h1:H6fVNegFsTZqOU2Cggvue8X5vfAdeGDKemikmwc7RBc= code.cloudfoundry.org/quarks-utils v0.0.0-20200121122630-31020afe6ac7 h1:tqlhWIbFp1J68eyVpzp7A54YlqZWdMNGpx2SruUa1qg= code.cloudfoundry.org/quarks-utils v0.0.0-20200121122630-31020afe6ac7/go.mod h1:H6fVNegFsTZqOU2Cggvue8X5vfAdeGDKemikmwc7RBc= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= @@ -93,8 +89,6 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc= github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= @@ -106,8 +100,6 @@ github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -298,8 +290,6 @@ github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= -github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -445,8 +435,6 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=