Skip to content

Commit

Permalink
Support recreating objects on immutable field updates
Browse files Browse the repository at this point in the history
Allow passing --force to kubectl apply. Useful when dealing with
immutable field changes in resources.

Signed-off-by: Aurel Canciu <aurelcanciu@gmail.com>
  • Loading branch information
relu committed Feb 15, 2021
1 parent 527af26 commit 274d108
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 5 deletions.
9 changes: 8 additions & 1 deletion api/v1beta1/kustomization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ limitations under the License.
package v1beta1

import (
"time"

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

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -127,6 +128,12 @@ type KustomizationSpec struct {
// +kubebuilder:validation:Enum=none;client;server
// +optional
Validation string `json:"validation,omitempty"`

// Force instructs the controller to recreate resources in the situation
// when dealing with immutable field changes.
// +kubebuilder:default:=false
// +optional
Force bool `json:"force,omitempty"`
}

// Decryption defines how decryption is handled for Kubernetes manifests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ spec:
- name
type: object
type: array
force:
default: false
description: Force instructs the controller to recreate resources
in the situation when dealing with immutable field changes.
type: boolean
healthChecks:
description: A list of resources to be included in the health assessment.
items:
Expand Down
15 changes: 11 additions & 4 deletions controllers/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,15 @@ func (r *KustomizationReconciler) validate(ctx context.Context, kustomization ku
applyCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

cmd := fmt.Sprintf("cd %s && kubectl apply -f %s.yaml --timeout=%s --dry-run=%s --cache-dir=/tmp",
dirPath, kustomization.GetUID(), kustomization.GetTimeout().String(), kustomization.Spec.Validation)
validation := kustomization.Spec.Validation
if validation == "server" && kustomization.Spec.Force {
// Use client-side validation with force
validation = "client"
(logr.FromContext(ctx)).Info(fmt.Sprintf("Server-side validation is configured, falling-back to client-side validation since 'force' is enabled"))
}

cmd := fmt.Sprintf("cd %s && kubectl apply -f %s.yaml --timeout=%s --dry-run=%s --cache-dir=/tmp --force=%t",
dirPath, kustomization.GetUID(), kustomization.GetTimeout().String(), validation, kustomization.Spec.Force)

if kustomization.Spec.KubeConfig != nil {
kubeConfig, err := imp.WriteKubeConfig(ctx)
Expand Down Expand Up @@ -589,8 +596,8 @@ func (r *KustomizationReconciler) apply(ctx context.Context, kustomization kusto
defer cancel()
fieldManager := "kustomize-controller"

cmd := fmt.Sprintf("cd %s && kubectl apply --field-manager=%s -f %s.yaml --timeout=%s --cache-dir=/tmp",
dirPath, fieldManager, kustomization.GetUID(), kustomization.Spec.Interval.Duration.String())
cmd := fmt.Sprintf("cd %s && kubectl apply --field-manager=%s -f %s.yaml --timeout=%s --cache-dir=/tmp --force=%t",
dirPath, fieldManager, kustomization.GetUID(), kustomization.Spec.Interval.Duration.String(), kustomization.Spec.Force)

if kustomization.Spec.KubeConfig != nil {
kubeConfig, err := imp.WriteKubeConfig(ctx)
Expand Down
135 changes: 135 additions & 0 deletions controllers/kustomization_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ var _ = Describe("KustomizationReconciler", func() {
Suspend: false,
Timeout: nil,
Validation: "client",
Force: false,
PostBuild: &kustomizev1.PostBuild{
Substitute: map[string]string{"region": "eu-central-1"},
},
Expand Down Expand Up @@ -244,5 +245,139 @@ metadata:
expectRevision: "branch/commit1",
}),
)

Describe("Kustomization resource replacement", func() {
cmManifest := func(namespace, value string) string {
return fmt.Sprintf(`---
kind: ConfigMap
apiVersion: v1
metadata:
name: test
namespace: %s
data:
example: %q
immutable: true
`,
namespace, value)
}

It("should replace immutable field resource using force", func() {
manifests := []testserver.File{
{
Name: "cm.yaml",
Body: cmManifest(namespace.Name, "v1"),
},
}
artifact, err := httpServer.ArtifactFromFiles(manifests)
Expect(err).NotTo(HaveOccurred())

url := fmt.Sprintf("%s/%s", httpServer.URL(), artifact)

repositoryName := types.NamespacedName{
Name: fmt.Sprintf("%s", randStringRunes(5)),
Namespace: namespace.Name,
}
repository := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: repositoryName.Name,
Namespace: repositoryName.Namespace,
},
Spec: sourcev1.GitRepositorySpec{
URL: "https://github.com/test/repository",
Interval: metav1.Duration{Duration: reconciliationInterval},
},
Status: sourcev1.GitRepositoryStatus{
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: sourcev1.GitOperationSucceedReason,
},
},
URL: url,
Artifact: &sourcev1.Artifact{
Path: url,
URL: url,
Revision: "v1",
LastUpdateTime: metav1.Now(),
},
},
}
Expect(k8sClient.Create(context.Background(), repository)).Should(Succeed())
Expect(k8sClient.Status().Update(context.Background(), repository)).Should(Succeed())
defer k8sClient.Delete(context.Background(), repository)

kName := types.NamespacedName{
Name: fmt.Sprintf("%s", randStringRunes(5)),
Namespace: namespace.Name,
}
k := &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: kName.Name,
Namespace: kName.Namespace,
},
Spec: kustomizev1.KustomizationSpec{
KubeConfig: kubeconfig,
Interval: metav1.Duration{Duration: reconciliationInterval},
Path: "./",
Prune: true,
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: sourcev1.GitRepositoryKind,
Name: repository.Name,
},
Suspend: false,
Timeout: &metav1.Duration{Duration: 60 * time.Second},
Validation: "client",
Force: true,
},
}
Expect(k8sClient.Create(context.Background(), k)).Should(Succeed())
defer k8sClient.Delete(context.Background(), k)

got := &kustomizev1.Kustomization{}
Eventually(func() bool {
_ = k8sClient.Get(context.Background(), kName, got)
c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition)
return c != nil && c.Reason == meta.ReconciliationSucceededReason
}, timeout, interval).Should(BeTrue())
Expect(got.Status.LastAppliedRevision).To(Equal("v1"))

cm := &corev1.ConfigMap{}
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: namespace.Name}, cm)).Should(Succeed())
Expect(cm.Data["example"]).To(Equal("v1"))

manifests = []testserver.File{
{
Name: "cm.yaml",
Body: cmManifest(namespace.Name, "v2"),
},
}

artifact, err = httpServer.ArtifactFromFiles(manifests)
Expect(err).NotTo(HaveOccurred())

url = fmt.Sprintf("%s/%s", httpServer.URL(), artifact)

repository.Status.URL = url
repository.Status.Artifact = &sourcev1.Artifact{
Path: url,
URL: url,
Revision: "v2",
LastUpdateTime: metav1.Now(),
}
Expect(k8sClient.Status().Update(context.Background(), repository)).Should(Succeed())

lastAppliedRev := got.Status.LastAppliedRevision
Eventually(func() bool {
_ = k8sClient.Get(context.Background(), kName, got)
return apimeta.IsStatusConditionTrue(got.Status.Conditions, meta.ReadyCondition) && got.Status.LastAppliedRevision != lastAppliedRev
}, timeout, interval).Should(BeTrue())
Expect(got.Status.LastAppliedRevision).To(Equal("v2"))

Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: namespace.Name}, cm)).Should(Succeed())
Expect(cm.Data["example"]).To(Equal("v2"))
})
})
})
})
26 changes: 26 additions & 0 deletions docs/api/kustomize.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ string
The validation strategy can be &lsquo;client&rsquo; (local dry-run), &lsquo;server&rsquo; (APIServer dry-run) or &lsquo;none&rsquo;.</p>
</td>
</tr>
<tr>
<td>
<code>force</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>Force instructs the controller to recreate resources in the situation
when dealing with immutable field changes.</p>
</td>
</tr>
</table>
</td>
</tr>
Expand Down Expand Up @@ -763,6 +776,19 @@ string
The validation strategy can be &lsquo;client&rsquo; (local dry-run), &lsquo;server&rsquo; (APIServer dry-run) or &lsquo;none&rsquo;.</p>
</td>
</tr>
<tr>
<td>
<code>force</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>Force instructs the controller to recreate resources in the situation
when dealing with immutable field changes.</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down

0 comments on commit 274d108

Please sign in to comment.