Skip to content

Commit

Permalink
Tolerate absence of resources in post-build subst.
Browse files Browse the repository at this point in the history
In a Kustomization's post-build substitution sources, introduce a new
"Optional" field to allow referencing a Kubernetes ConfigMap or Secret
that may not exist at time of reconciliation. Treat substitution when
the referenced object is missing as if the object had been present but
empty, lacking any variable bindings.

Retain the longstanding behavior of interpreting references to
Kubernetes objects being mandatory by default, such that
reconciliation fails if such a referenced object does not exist. Only
when the "Optional" field is set to true will reconciliation tolerate
finding the referenced object to be missing.
  • Loading branch information
seh committed Feb 13, 2022
1 parent 0a30937 commit 827484c
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 1 deletion.
9 changes: 8 additions & 1 deletion api/v1beta2/kustomization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ limitations under the License.
package v1beta2

import (
apimeta "k8s.io/apimachinery/pkg/api/meta"
"time"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

Expand Down Expand Up @@ -207,6 +207,13 @@ type SubstituteReference struct {
// +kubebuilder:validation:MaxLength=253
// +required
Name string `json:"name"`

// Optional indicates whether the referenced resource must exist, or whether to
// tolerate its absence. If true and the referenced resource is absent, proceed
// as if the resource was present but empty, without any variables defined.
// +kubebuilder:default:=false
// +optional
Optional bool `json:"optional,omitempty"`
}

// KustomizationStatus defines the observed state of a kustomization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,14 @@ spec:
maxLength: 253
minLength: 1
type: string
optional:
default: false
description: Optional indicates whether the referenced resource
must exist, or whether to tolerate its absence. If true
and the referenced resource is absent, proceed as if the
resource was present but empty, without any variables
defined.
type: boolean
required:
- kind
- name
Expand Down
7 changes: 7 additions & 0 deletions controllers/kustomization_varsub.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/drone/envsubst"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/resource"
Expand Down Expand Up @@ -48,6 +49,9 @@ func substituteVariables(
case "ConfigMap":
resource := &corev1.ConfigMap{}
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
if reference.Optional && apierrors.IsNotFound(err) {
continue
}
return nil, fmt.Errorf("substitute from 'ConfigMap/%s' error: %w", reference.Name, err)
}
for k, v := range resource.Data {
Expand All @@ -56,6 +60,9 @@ func substituteVariables(
case "Secret":
resource := &corev1.Secret{}
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
if reference.Optional && apierrors.IsNotFound(err) {
continue
}
return nil, fmt.Errorf("substitute from 'Secret/%s' error: %w", reference.Name, err)
}
for k, v := range resource.Data {
Expand Down
156 changes: 156 additions & 0 deletions controllers/kustomization_varsub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,159 @@ stringData:
g.Expect(resultSA.Labels[fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group)]).To(Equal(client.ObjectKeyFromObject(resultK).Namespace))
})
}

func TestKustomizationReconciler_VarsubOptional(t *testing.T) {
ctx := context.Background()

g := NewWithT(t)
id := "vars-" + randStringRunes(5)
revision := "v1.0.0/" + randStringRunes(7)

err := createNamespace(id)
g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")

err = createKubeConfigSecret(id)
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")

manifests := func(name string) []testserver.File {
return []testserver.File{
{
Name: "service-account.yaml",
Body: fmt.Sprintf(`
apiVersion: v1
kind: ServiceAccount
metadata:
name: %[1]s
namespace: %[1]s
labels:
color: "${color:=blue}"
shape: "${shape:=square}"
`, name),
},
}
}

artifact, err := testServer.ArtifactFromFiles(manifests(id))
g.Expect(err).NotTo(HaveOccurred())

repositoryName := types.NamespacedName{
Name: randStringRunes(5),
Namespace: id,
}

err = applyGitRepository(repositoryName, artifact, revision)
g.Expect(err).NotTo(HaveOccurred())

configName := types.NamespacedName{
Name: randStringRunes(5),
Namespace: id,
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configName.Name,
Namespace: configName.Namespace,
},
Data: map[string]string{"color": "\nred\n"},
}
g.Expect(k8sClient.Create(ctx, configMap)).Should(Succeed())

secretName := types.NamespacedName{
Name: randStringRunes(5),
Namespace: id,
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName.Name,
Namespace: secretName.Namespace,
},
StringData: map[string]string{"shape": "\ntriangle\n"},
}
g.Expect(k8sClient.Create(ctx, secret)).Should(Succeed())

inputK := &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: id,
Namespace: id,
},
Spec: kustomizev1.KustomizationSpec{
KubeConfig: &kustomizev1.KubeConfig{
SecretRef: meta.LocalObjectReference{
Name: "kubeconfig",
},
},
Interval: metav1.Duration{Duration: reconciliationInterval},
Path: "./",
Prune: true,
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: sourcev1.GitRepositoryKind,
Name: repositoryName.Name,
},
PostBuild: &kustomizev1.PostBuild{
Substitute: map[string]string{"var_substitution_enabled": "true"},
SubstituteFrom: []kustomizev1.SubstituteReference{
{
Kind: "ConfigMap",
Name: configName.Name,
Optional: true,
},
{
Kind: "Secret",
Name: secretName.Name,
Optional: true,
},
},
},
HealthChecks: []meta.NamespacedObjectKindReference{
{
APIVersion: "v1",
Kind: "ServiceAccount",
Name: id,
Namespace: id,
},
},
},
}
g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed())

resultSA := &corev1.ServiceAccount{}

ensureReconciles := func(nameSuffix string) {
t.Run("reconciles successfully"+nameSuffix, func(t *testing.T) {
g.Eventually(func() bool {
resultK := &kustomizev1.Kustomization{}
_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK)
for _, c := range resultK.Status.Conditions {
if c.Reason == meta.ReconciliationSucceededReason {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())

g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultSA)).Should(Succeed())
})
}

ensureReconciles(" with optional ConfigMap")
t.Run("replaces vars from optional ConfigMap", func(t *testing.T) {
g.Expect(resultSA.Labels["color"]).To(Equal("red"))
g.Expect(resultSA.Labels["shape"]).To(Equal("triangle"))
})

for _, o := range []client.Object{
configMap,
secret,
} {
g.Expect(k8sClient.Delete(ctx, o)).Should(Succeed())
}

// Force a second detectable reconciliation of the Kustomization.
g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), inputK)).Should(Succeed())
inputK.Status.Conditions = nil
g.Expect(k8sClient.Status().Update(ctx, inputK)).Should(Succeed())
ensureReconciles(" without optional ConfigMap")
t.Run("replaces vars tolerating absent ConfigMap", func(t *testing.T) {
g.Expect(resultSA.Labels["color"]).To(Equal("blue"))
g.Expect(resultSA.Labels["shape"]).To(Equal("square"))
})
}
14 changes: 14 additions & 0 deletions docs/api/kustomize.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,20 @@ string
referring resource.</p>
</td>
</tr>
<tr>
<td>
<code>optional</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>Optional indicates whether the referenced resource must exist, or whether to
tolerate its absence. If true and the referenced resource is absent, proceed
as if the resource was present but empty, without any variables defined.</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down

0 comments on commit 827484c

Please sign in to comment.