diff --git a/pkg/action/install.go b/pkg/action/install.go index 580b8a0cb..fdb8ad903 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -449,7 +449,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st if _, err := i.cfg.KubeClient.Create( resourceList, - kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) { + kube.ClientCreateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts)); err != nil && !apierrors.IsAlreadyExists(err) { return nil, err } } @@ -518,7 +518,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource if len(toBeAdopted) == 0 && len(resources) > 0 { _, err = i.cfg.KubeClient.Create( resources, - kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)) + kube.ClientCreateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts)) } else if len(resources) > 0 { updateThreeWayMergeForUnstructured := i.TakeOwnership && !i.ServerSideApply // Use three-way merge when taking ownership (and not using server-side apply) _, err = i.cfg.KubeClient.Update( diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 05ca9a75e..e0efa9c5c 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -27,6 +27,7 @@ import ( "net/url" "os" "path/filepath" + "reflect" "regexp" "strings" "testing" @@ -177,6 +178,67 @@ func installAction(t *testing.T) *Install { return instAction } +type installCreateOptionRecorder struct { + *kubefake.FailingKubeClient + serverSideApply bool + forceConflicts bool + sawCreate bool +} + +func (r *installCreateOptionRecorder) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) { + serverSideApply, forceConflicts, err := parseCreateOptions(options) + if err != nil { + return nil, err + } + r.serverSideApply = serverSideApply + r.forceConflicts = forceConflicts + r.sawCreate = true + return r.FailingKubeClient.Create(resources, options...) +} + +func parseCreateOptions(options []kube.ClientCreateOption) (bool, bool, error) { + var serverSideApply bool + var forceConflicts bool + sawServerSideApply := false + sawForceConflicts := false + + for _, option := range options { + fn := reflect.ValueOf(option) + if fn.Kind() != reflect.Func { + return false, false, fmt.Errorf("create option is not a function: %T", option) + } + argType := fn.Type().In(0) + if argType.Kind() != reflect.Ptr { + return false, false, fmt.Errorf("unexpected create option argument type: %s", argType) + } + arg := reflect.New(argType.Elem()) + results := fn.Call([]reflect.Value{arg}) + if len(results) > 0 && !results[0].IsNil() { + return false, false, results[0].Interface().(error) + } + + serverSideApplyField := arg.Elem().FieldByName("serverSideApply") + if !serverSideApplyField.IsValid() { + return false, false, errors.New("client create options missing serverSideApply field") + } + forceConflictsField := arg.Elem().FieldByName("forceConflicts") + if !forceConflictsField.IsValid() { + return false, false, errors.New("client create options missing forceConflicts field") + } + + serverSideApply = serverSideApplyField.Bool() + forceConflicts = forceConflictsField.Bool() + sawServerSideApply = true + sawForceConflicts = true + } + + if !sawServerSideApply || !sawForceConflicts { + return false, false, errors.New("expected server-side apply create options were not provided") + } + + return serverSideApply, forceConflicts, nil +} + func TestInstallRelease(t *testing.T) { is := assert.New(t) req := require.New(t) @@ -220,6 +282,24 @@ func TestInstallRelease(t *testing.T) { is.Equal(lrel.Info.Status, rcommon.StatusDeployed) } +func TestPerformInstallCreatePathPassesForceConflicts(t *testing.T) { + config := actionConfigFixtureWithDummyResources(t, nil) + baseClient := config.KubeClient.(*kubefake.FailingKubeClient) + recorder := &installCreateOptionRecorder{FailingKubeClient: baseClient} + config.KubeClient = recorder + + instAction := installActionWithConfig(config) + instAction.DisableHooks = true + instAction.ServerSideApply = true + instAction.ForceConflicts = true + + _, err := instAction.performInstall(releaseStub(), nil, createDummyResourceList(false)) + require.NoError(t, err) + require.True(t, recorder.sawCreate, "expected performInstall to use the create path") + assert.True(t, recorder.serverSideApply) + assert.True(t, recorder.forceConflicts) +} + func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) { // This test will test checking ownership of a resource // returned by the fake client. If the resource is not