Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/action/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
85 changes: 85 additions & 0 deletions pkg/action/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,41 @@ func createDummyCRDList(owned bool) kube.ResourceList {
return resourceList
}

func createNewResourceList() kube.ResourceList {
obj := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "dummyName",
Namespace: "spaced",
},
}

resInfo := resource.Info{
Name: "dummyName",
Namespace: "spaced",
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Scope: meta.RESTScopeNamespace,
},
Object: obj,
}

resInfo.Client = &fake.RESTClient{
GroupVersion: schema.GroupVersion{Group: "apps", Version: "v1"},
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
Client: fake.CreateHTTPClient(func(_ *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusNotFound,
Header: http.Header{"Content-Type": []string{kuberuntime.ContentTypeJSON}},
Body: io.NopCloser(bytes.NewReader([]byte(`{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","reason":"NotFound","code":404}`))),
}, nil
}),
}
var resourceList kube.ResourceList
resourceList.Append(&resInfo)
return resourceList
}

func installActionWithConfig(config *Configuration) *Install {
instAction := NewInstall(config)
instAction.Namespace = "spaced"
Expand Down Expand Up @@ -1297,3 +1332,53 @@ func TestInstallRelease_WaitOptionsPassedDownstream(t *testing.T) {
// Verify that WaitOptions were passed to GetWaiter
is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter")
}

func TestInstallRelease_ForceConflictsPassedToCreate(t *testing.T) {
is := assert.New(t)

// Use resources whose REST client returns NotFound so they go through
// the Create path (toBeAdopted is empty) in performInstall.
config := actionConfigFixtureWithDummyResources(t, createNewResourceList())
instAction := installActionWithConfig(config)
instAction.ServerSideApply = true
instAction.ForceConflicts = true

failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)

vals := map[string]any{}
_, err := instAction.Run(buildChart(), vals)
is.NoError(err)

is.NotEmpty(failer.RecordedCreateCalls, "Create calls should be recorded")
var foundForceConflicts bool
for _, call := range failer.RecordedCreateCalls {
resolved, err := kube.ResolveCreateOptions(call...)
is.NoError(err)
if resolved.ForceConflicts {
foundForceConflicts = true
}
}
is.True(foundForceConflicts, "ForceConflicts should be passed through on the Create path")
}

func TestInstallRelease_ForceConflictsFalseByDefault(t *testing.T) {
is := assert.New(t)

config := actionConfigFixtureWithDummyResources(t, createNewResourceList())
instAction := installActionWithConfig(config)
instAction.ServerSideApply = true
instAction.ForceConflicts = false

failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)

vals := map[string]any{}
_, err := instAction.Run(buildChart(), vals)
is.NoError(err)

is.NotEmpty(failer.RecordedCreateCalls, "Create calls should be recorded")
for _, call := range failer.RecordedCreateCalls {
resolved, err := kube.ResolveCreateOptions(call...)
is.NoError(err)
is.False(resolved.ForceConflicts, "ForceConflicts should be false when not set")
}
}
22 changes: 22 additions & 0 deletions pkg/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,28 @@ func ClientCreateOptionFieldValidationDirective(fieldValidationDirective FieldVa
}
}

// ResolvedCreateOptions holds the resolved values from ClientCreateOption functions.
// This is exported for testing purposes only.
type ResolvedCreateOptions struct {
ServerSideApply bool
ForceConflicts bool
}

// ResolveCreateOptions applies the given ClientCreateOptions and returns the resolved values.
// This is exported for testing purposes only.
func ResolveCreateOptions(opts ...ClientCreateOption) (ResolvedCreateOptions, error) {
o := clientCreateOptions{}
for _, opt := range opts {
if err := opt(&o); err != nil {
return ResolvedCreateOptions{}, err
}
}
return ResolvedCreateOptions{
ServerSideApply: o.serverSideApply,
ForceConflicts: o.forceConflicts,
}, nil
}

func (c *Client) makeCreateApplyFunc(serverSideApply, forceConflicts, dryRun bool, fieldValidationDirective FieldValidationDirective) CreateApplyFunc {
if serverSideApply {
c.Logger().Debug(
Expand Down
3 changes: 3 additions & 0 deletions pkg/kube/fake/failing_kube_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type FailingKubeClient struct {
WaitDuration time.Duration
// RecordedWaitOptions stores the WaitOptions passed to GetWaiter for testing
RecordedWaitOptions []kube.WaitOption
// RecordedCreateCalls stores the ClientCreateOptions for each call to Create for testing
RecordedCreateCalls [][]kube.ClientCreateOption
}

var _ kube.Interface = &FailingKubeClient{}
Expand All @@ -65,6 +67,7 @@ type FailingKubeWaiter struct {

// Create returns the configured error if set or prints
func (f *FailingKubeClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) {
f.RecordedCreateCalls = append(f.RecordedCreateCalls, options)
if f.CreateError != nil {
return nil, f.CreateError
}
Expand Down